From ca9b0e7c46adb6dde0e6238acea388b97bb7397b Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Wed, 29 Apr 2026 15:29:46 -0500 Subject: [PATCH 01/32] fix: add enclosure for sql identifiers --- src/Database/Alias.php | 12 +- src/Database/Clause.php | 34 +- src/Database/Clauses/BasicWhereClause.php | 5 + src/Database/Clauses/DateWhereClause.php | 60 ++++ src/Database/Clauses/RowWhereClause.php | 61 ++++ src/Database/Concerns/HasDriver.php | 24 ++ src/Database/Concerns/Query/BuildsQuery.php | 24 +- src/Database/Concerns/Query/HasJoinClause.php | 25 +- .../Concerns/Query/HasWhereClause.php | 2 + .../Concerns/Query/HasWhereDateClause.php | 32 +- .../Concerns/Query/HasWhereRowClause.php | 42 +-- .../Dialects/Compilers/ClauseCompiler.php | 12 + .../Dialects/Compilers/DeleteCompiler.php | 3 +- .../Dialects/Compilers/ExistsCompiler.php | 6 +- .../Dialects/Compilers/InsertCompiler.php | 5 +- .../Dialects/Compilers/SelectCompiler.php | 89 +++-- .../Dialects/Compilers/UpdateCompiler.php | 12 +- .../Dialects/Compilers/WhereCompiler.php | 23 +- .../Dialects/Mysql/Compilers/Insert.php | 7 +- .../Dialects/Mysql/Compilers/Update.php | 6 +- .../Dialects/Mysql/Compilers/Where.php | 56 ++- .../Dialects/Postgres/Compilers/Insert.php | 17 +- .../Dialects/Postgres/Compilers/Update.php | 6 +- .../Dialects/Postgres/Compilers/Where.php | 50 ++- .../Dialects/Sqlite/Compilers/Delete.php | 11 +- .../Dialects/Sqlite/Compilers/Insert.php | 9 +- .../Dialects/Sqlite/Compilers/Update.php | 6 +- src/Database/Functions.php | 41 ++- src/Database/Grammar.php | 17 +- src/Database/Having.php | 10 +- src/Database/Join.php | 27 +- src/Database/QueryAst.php | 3 + src/Database/SelectCase.php | 87 +++-- src/Database/Subquery.php | 4 +- src/Database/Value.php | 8 +- src/Database/Wrapper.php | 64 ++++ .../Models/Postgres/DatabaseModelTest.php | 4 +- tests/Unit/Database/QueryBuilderTest.php | 4 +- .../QueryGenerator/DeleteStatementTest.php | 4 +- .../QueryGenerator/GroupByStatementTest.php | 31 +- .../QueryGenerator/HavingClauseTest.php | 40 ++- .../InsertIntoStatementTest.php | 20 +- .../QueryGenerator/JoinClausesTest.php | 65 ++-- .../Database/QueryGenerator/PaginateTest.php | 6 +- .../Postgres/DeleteStatementTest.php | 26 +- .../Postgres/GroupByStatementTest.php | 53 +-- .../Postgres/HavingClauseTest.php | 60 +++- .../Postgres/InsertIntoStatementTest.php | 24 +- .../Postgres/JoinClausesTest.php | 81 +++-- .../QueryGenerator/Postgres/PaginateTest.php | 12 +- .../Postgres/SelectColumnsTest.php | 109 +++--- .../QueryGenerator/SelectColumnsTest.php | 327 +++--------------- .../QueryGenerator/UpdateStatementTest.php | 4 +- .../QueryGenerator/WhereClausesTest.php | 74 ++-- .../QueryGenerator/WhereDateClausesTest.php | 27 +- 55 files changed, 1129 insertions(+), 742 deletions(-) create mode 100644 src/Database/Clauses/DateWhereClause.php create mode 100644 src/Database/Clauses/RowWhereClause.php create mode 100644 src/Database/Concerns/HasDriver.php create mode 100644 src/Database/Dialects/Compilers/ClauseCompiler.php create mode 100644 src/Database/Wrapper.php diff --git a/src/Database/Alias.php b/src/Database/Alias.php index c3a903a3..aa0a372c 100644 --- a/src/Database/Alias.php +++ b/src/Database/Alias.php @@ -4,10 +4,13 @@ namespace Phenix\Database; +use Phenix\Database\Concerns\HasDriver; use Stringable; class Alias implements Stringable { + use HasDriver; + protected string $alias; public function __construct(protected readonly string $name) @@ -24,7 +27,14 @@ public function as(string $alias): self public function __toString(): string { - return "{$this->name} AS {$this->alias}"; + $parts = array_map( + fn (string $part): string => (string) Wrapper::of($this->getDriver(), $part), + explode('.', $this->name) + ); + + $alias = Wrapper::of($this->getDriver(), $this->alias); + + return implode('.', $parts) . " AS {$alias}"; } public static function of(string $name): self diff --git a/src/Database/Clause.php b/src/Database/Clause.php index d2597cef..4ea6f58f 100644 --- a/src/Database/Clause.php +++ b/src/Database/Clause.php @@ -6,6 +6,7 @@ use Closure; use Phenix\Database\Clauses\BasicWhereClause; +use Phenix\Database\Clauses\RowWhereClause; use Phenix\Database\Clauses\SubqueryWhereClause; use Phenix\Database\Clauses\WhereClause; use Phenix\Database\Concerns\Query\HasWhereClause; @@ -54,7 +55,8 @@ protected function whereSubquery( LogicalConnector $logicalConnector = LogicalConnector::AND ): void { $builder = new Subquery($this->driver); - $builder->select(['*']); + $builder->setDriver($this->driver); + $builder->selectAllColumns(); $subquery($builder); @@ -74,6 +76,36 @@ protected function whereSubquery( $this->arguments = array_merge($this->arguments, $arguments); } + /** + * @param array $columns + */ + protected function whereRowSubquery( + Closure $subquery, + Operator $comparisonOperator, + array $columns, + LogicalConnector $logicalConnector = LogicalConnector::AND + ): void { + $builder = new Subquery($this->driver); + $builder->setDriver($this->driver); + $builder->selectAllColumns(); + + $subquery($builder); + + [$dml, $arguments] = $builder->toSql(); + + $connector = count($this->clauses) === 0 ? null : $logicalConnector; + + $this->clauses[] = new RowWhereClause( + columns: $columns, + comparisonOperator: $comparisonOperator, + sql: trim($dml, '()'), + params: $arguments, + connector: $connector + ); + + $this->arguments = array_merge($this->arguments, $arguments); + } + protected function pushWhereWithArgs( string $column, Operator $operator, diff --git a/src/Database/Clauses/BasicWhereClause.php b/src/Database/Clauses/BasicWhereClause.php index 7746a37b..34dbbe70 100644 --- a/src/Database/Clauses/BasicWhereClause.php +++ b/src/Database/Clauses/BasicWhereClause.php @@ -78,4 +78,9 @@ public function isInOperator(): bool { return $this->operator === Operator::IN || $this->operator === Operator::NOT_IN; } + + public function usesPlaceholder(): bool + { + return $this->usePlaceholder; + } } diff --git a/src/Database/Clauses/DateWhereClause.php b/src/Database/Clauses/DateWhereClause.php new file mode 100644 index 00000000..aaa8bb77 --- /dev/null +++ b/src/Database/Clauses/DateWhereClause.php @@ -0,0 +1,60 @@ +column = $column; + $this->operator = $operator; + $this->function = $function; + $this->value = $value; + $this->connector = $connector; + } + + public function getColumn(): string + { + return $this->column; + } + + public function getOperator(): Operator + { + return $this->operator; + } + + public function getFunction(): DatabaseFunction + { + return $this->function; + } + + public function getValue(): string|int + { + return $this->value; + } + + public function renderValue(): string + { + return SQL::PLACEHOLDER->value; + } +} diff --git a/src/Database/Clauses/RowWhereClause.php b/src/Database/Clauses/RowWhereClause.php new file mode 100644 index 00000000..e9613c5f --- /dev/null +++ b/src/Database/Clauses/RowWhereClause.php @@ -0,0 +1,61 @@ + $columns + * @param array $params + */ + public function __construct( + protected array $columns, + protected Operator $comparisonOperator, + protected string $sql, + protected array $params, + LogicalConnector|null $connector = null + ) { + $this->connector = $connector; + } + + /** + * @return array + */ + public function getColumns(): array + { + return $this->columns; + } + + public function getColumn(): null + { + return null; + } + + public function getOperator(): Operator + { + return $this->comparisonOperator; + } + + public function getSql(): string + { + return $this->sql; + } + + /** + * @return array + */ + public function getParams(): array + { + return $this->params; + } + + public function renderValue(): string + { + return "({$this->sql})"; + } +} diff --git a/src/Database/Concerns/HasDriver.php b/src/Database/Concerns/HasDriver.php new file mode 100644 index 00000000..ec98b03d --- /dev/null +++ b/src/Database/Concerns/HasDriver.php @@ -0,0 +1,24 @@ +driver = $driver; + + return $this; + } + + public function getDriver(): Driver + { + return $this->driver; + } +} diff --git a/src/Database/Concerns/Query/BuildsQuery.php b/src/Database/Concerns/Query/BuildsQuery.php index 24ac37e9..3525a3a4 100644 --- a/src/Database/Concerns/Query/BuildsQuery.php +++ b/src/Database/Concerns/Query/BuildsQuery.php @@ -14,7 +14,8 @@ use Phenix\Database\QueryAst; use Phenix\Database\SelectCase; use Phenix\Database\Subquery; -use Phenix\Util\Arr; + +use function is_string; trait BuildsQuery { @@ -29,6 +30,7 @@ public function from(Closure|string $table): static { if ($table instanceof Closure) { $builder = new Subquery($this->driver); + $builder->setDriver($this->driver); $builder->selectAllColumns(); $table($builder); @@ -64,12 +66,11 @@ public function selectAllColumns(): static public function groupBy(Functions|array|string $column): static { - $column = match (true) { - $column instanceof Functions => (string) $column, - default => $column, - }; + if ($column instanceof Functions || is_string($column)) { + $column = [$column]; + } - $this->groupBy = [Operator::GROUP_BY->value, Arr::implodeDeeply((array) $column, ', ')]; + $this->groupBy = $column; return $this; } @@ -77,6 +78,7 @@ public function groupBy(Functions|array|string $column): static public function having(Closure $clause): static { $having = new Having(); + $having->setDriver($this->driver); $clause($having); @@ -91,12 +93,11 @@ public function having(Closure $clause): static public function orderBy(SelectCase|array|string $column, Order $order = Order::DESC): static { - $column = match (true) { - $column instanceof SelectCase => '(' . $column . ')', - default => $column, - }; + if ($column instanceof SelectCase || is_string($column)) { + $column = [$column]; + } - $this->orderBy = [Operator::ORDER_BY->value, Arr::implodeDeeply((array) $column, ', '), $order->value]; + $this->orderBy = [$column, $order->value]; return $this; } @@ -135,6 +136,7 @@ public function toSql(): array protected function buildAst(): QueryAst { $ast = new QueryAst(); + $ast->driver = $this->driver; $ast->action = $this->action; $ast->table = $this->table; $ast->columns = $this->columns; diff --git a/src/Database/Concerns/Query/HasJoinClause.php b/src/Database/Concerns/Query/HasJoinClause.php index 12c807eb..0fe5f1a4 100644 --- a/src/Database/Concerns/Query/HasJoinClause.php +++ b/src/Database/Concerns/Query/HasJoinClause.php @@ -5,75 +5,76 @@ namespace Phenix\Database\Concerns\Query; use Closure; +use Phenix\Database\Alias; use Phenix\Database\Constants\JoinType; use Phenix\Database\Join; trait HasJoinClause { - public function innerJoin(string $relationship, Closure $callback): static + public function innerJoin(Alias|string $relationship, Closure $callback): static { $this->jointIt($relationship, $callback, JoinType::INNER); return $this; } - public function innerJoinOnEqual(string $relationship, string $column, string $value): static + public function innerJoinOnEqual(Alias|string $relationship, string $column, string $value): static { $this->jointFrom($relationship, $column, $value, JoinType::INNER); return $this; } - public function leftJoin(string $relationship, Closure $callback): static + public function leftJoin(Alias|string $relationship, Closure $callback): static { $this->jointIt($relationship, $callback, JoinType::LEFT); return $this; } - public function leftJoinOnEqual(string $relationship, string $column, string $value): static + public function leftJoinOnEqual(Alias|string $relationship, string $column, string $value): static { $this->jointFrom($relationship, $column, $value, JoinType::LEFT); return $this; } - public function leftOuterJoin(string $relationship, Closure $callback): static + public function leftOuterJoin(Alias|string $relationship, Closure $callback): static { $this->jointIt($relationship, $callback, JoinType::LEFT_OUTER); return $this; } - public function rightJoin(string $relationship, Closure $callback): static + public function rightJoin(Alias|string $relationship, Closure $callback): static { $this->jointIt($relationship, $callback, JoinType::RIGHT); return $this; } - public function rightJoinOnEqual(string $relationship, string $column, string $value): static + public function rightJoinOnEqual(Alias|string $relationship, string $column, string $value): static { $this->jointFrom($relationship, $column, $value, JoinType::RIGHT); return $this; } - public function rightOuterJoin(string $relationship, Closure $callback): static + public function rightOuterJoin(Alias|string $relationship, Closure $callback): static { $this->jointIt($relationship, $callback, JoinType::RIGHT_OUTER); return $this; } - public function crossJoin(string $relationship, Closure $callback): static + public function crossJoin(Alias|string $relationship, Closure $callback): static { $this->jointIt($relationship, $callback, JoinType::CROSS); return $this; } - protected function jointIt(string $relationship, Closure $callback, JoinType $joinType): void + protected function jointIt(Alias|string $relationship, Closure $callback, JoinType $joinType): void { $join = new Join($relationship, $joinType); @@ -82,7 +83,7 @@ protected function jointIt(string $relationship, Closure $callback, JoinType $jo $this->pushJoin($join); } - protected function jointFrom(string $relationship, string $column, string $value, JoinType $joinType): void + protected function jointFrom(Alias|string $relationship, string $column, string $value, JoinType $joinType): void { $join = new Join($relationship, $joinType); $join->onEqual($column, $value); @@ -92,6 +93,8 @@ protected function jointFrom(string $relationship, string $column, string $value protected function pushJoin(Join $join): void { + $join->setDriver($this->driver); + [$dml, $arguments] = $join->toSql(); $this->joins[] = $dml; diff --git a/src/Database/Concerns/Query/HasWhereClause.php b/src/Database/Concerns/Query/HasWhereClause.php index a0e0fc8d..78d6db39 100644 --- a/src/Database/Concerns/Query/HasWhereClause.php +++ b/src/Database/Concerns/Query/HasWhereClause.php @@ -12,6 +12,8 @@ use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; +use function count; + trait HasWhereClause { use HasWhereAllClause; diff --git a/src/Database/Concerns/Query/HasWhereDateClause.php b/src/Database/Concerns/Query/HasWhereDateClause.php index 60ecf2cf..d3417cb0 100644 --- a/src/Database/Concerns/Query/HasWhereDateClause.php +++ b/src/Database/Concerns/Query/HasWhereDateClause.php @@ -5,9 +5,10 @@ namespace Phenix\Database\Concerns\Query; use Carbon\CarbonInterface; +use Phenix\Database\Clauses\DateWhereClause; +use Phenix\Database\Constants\DatabaseFunction; use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; -use Phenix\Database\Functions; trait HasWhereDateClause { @@ -231,12 +232,7 @@ protected function pushDateClause( $value = $value->format('Y-m-d'); } - $this->pushTimeClause( - Functions::date($column), - $operator, - $value, - $logicalConnector - ); + $this->pushTimeClause($column, $operator, DatabaseFunction::DATE, $value, $logicalConnector); } protected function pushMonthClause( @@ -249,12 +245,7 @@ protected function pushMonthClause( $value = (int) $value->format('m'); } - $this->pushTimeClause( - Functions::month($column), - $operator, - $value, - $logicalConnector - ); + $this->pushTimeClause($column, $operator, DatabaseFunction::MONTH, $value, $logicalConnector); } protected function pushYearClause( @@ -267,20 +258,17 @@ protected function pushYearClause( $value = (int) $value->format('Y'); } - $this->pushTimeClause( - Functions::year($column), - $operator, - $value, - $logicalConnector - ); + $this->pushTimeClause($column, $operator, DatabaseFunction::YEAR, $value, $logicalConnector); } protected function pushTimeClause( - Functions $function, + string $column, Operator $operator, - CarbonInterface|string|int $value, + DatabaseFunction $function, + string|int $value, LogicalConnector $logicalConnector = LogicalConnector::AND ): void { - $this->pushWhereWithArgs((string) $function, $operator, $value, $logicalConnector); + $this->pushClause(new DateWhereClause($column, $operator, $function, $value), $logicalConnector); + $this->arguments = array_merge($this->arguments, [$value]); } } diff --git a/src/Database/Concerns/Query/HasWhereRowClause.php b/src/Database/Concerns/Query/HasWhereRowClause.php index 23d30116..1cf2b8cb 100644 --- a/src/Database/Concerns/Query/HasWhereRowClause.php +++ b/src/Database/Concerns/Query/HasWhereRowClause.php @@ -5,80 +5,76 @@ namespace Phenix\Database\Concerns\Query; use Closure; +use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; trait HasWhereRowClause { public function whereRowEqual(array $columns, Closure $subquery): static { - $this->whereSubquery($subquery, Operator::EQUAL, $this->prepareRowFields($columns)); + $this->pushRowClause($columns, $subquery, Operator::EQUAL); return $this; } public function whereRowNotEqual(array $columns, Closure $subquery): static { - $this->whereSubquery($subquery, Operator::NOT_EQUAL, $this->prepareRowFields($columns)); + $this->pushRowClause($columns, $subquery, Operator::NOT_EQUAL); return $this; } public function whereRowGreaterThan(array $columns, Closure $subquery): static { - $this->whereSubquery( - $subquery, - Operator::GREATER_THAN, - $this->prepareRowFields($columns) - ); + $this->pushRowClause($columns, $subquery, Operator::GREATER_THAN); return $this; } public function whereRowGreaterThanOrEqual(array $columns, Closure $subquery): static { - $this->whereSubquery( - $subquery, - Operator::GREATER_THAN_OR_EQUAL, - $this->prepareRowFields($columns) - ); + $this->pushRowClause($columns, $subquery, Operator::GREATER_THAN_OR_EQUAL); return $this; } public function whereRowLessThan(array $columns, Closure $subquery): static { - $this->whereSubquery($subquery, Operator::LESS_THAN, $this->prepareRowFields($columns)); + $this->pushRowClause($columns, $subquery, Operator::LESS_THAN); return $this; } public function whereRowLessThanOrEqual(array $columns, Closure $subquery): static { - $this->whereSubquery( - $subquery, - Operator::LESS_THAN_OR_EQUAL, - $this->prepareRowFields($columns) - ); + $this->pushRowClause($columns, $subquery, Operator::LESS_THAN_OR_EQUAL); return $this; } public function whereRowIn(array $columns, Closure $subquery): static { - $this->whereSubquery($subquery, Operator::IN, $this->prepareRowFields($columns)); + $this->pushRowClause($columns, $subquery, Operator::IN); return $this; } public function whereRowNotIn(array $columns, Closure $subquery): static { - $this->whereSubquery($subquery, Operator::NOT_IN, $this->prepareRowFields($columns)); + $this->pushRowClause($columns, $subquery, Operator::NOT_IN); return $this; } - private function prepareRowFields(array $fields) - { - return 'ROW(' . $this->prepareColumns($fields) . ')'; + /** + * @param array $columns + */ + private function pushRowClause( + array $columns, + Closure $subquery, + Operator $operator, + LogicalConnector $logicalConnector = LogicalConnector::AND + ): void { + $this->whereRowSubquery($subquery, $operator, $columns, $logicalConnector); } } diff --git a/src/Database/Dialects/Compilers/ClauseCompiler.php b/src/Database/Dialects/Compilers/ClauseCompiler.php new file mode 100644 index 00000000..748073ad --- /dev/null +++ b/src/Database/Dialects/Compilers/ClauseCompiler.php @@ -0,0 +1,12 @@ +table; + $parts[] = Wrapper::of($ast->driver, $ast->table); if (! empty($ast->wheres)) { $whereCompiled = $this->whereCompiler->compile($ast->wheres); diff --git a/src/Database/Dialects/Compilers/ExistsCompiler.php b/src/Database/Dialects/Compilers/ExistsCompiler.php index 312c2bbb..22c291e0 100644 --- a/src/Database/Dialects/Compilers/ExistsCompiler.php +++ b/src/Database/Dialects/Compilers/ExistsCompiler.php @@ -7,7 +7,7 @@ use Phenix\Database\Contracts\ClauseCompiler; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\QueryAst; -use Phenix\Database\Value; +use Phenix\Database\Wrapper; use Phenix\Util\Arr; abstract class ExistsCompiler implements ClauseCompiler @@ -24,7 +24,7 @@ public function compile(QueryAst $ast): CompiledClause $subquery = []; $subquery[] = 'SELECT 1 FROM'; - $subquery[] = $ast->table; + $subquery[] = Wrapper::of($ast->driver, $ast->table); if (! empty($ast->wheres)) { $whereCompiled = $this->whereCompiler->compile($ast->wheres); @@ -35,7 +35,7 @@ public function compile(QueryAst $ast): CompiledClause $parts[] = '(' . Arr::implodeDeeply($subquery) . ')'; $parts[] = 'AS'; - $parts[] = Value::from('exists'); + $parts[] = Wrapper::column($ast->driver, 'exists'); $sql = Arr::implodeDeeply($parts); diff --git a/src/Database/Dialects/Compilers/InsertCompiler.php b/src/Database/Dialects/Compilers/InsertCompiler.php index 45a3f57c..73affacc 100644 --- a/src/Database/Dialects/Compilers/InsertCompiler.php +++ b/src/Database/Dialects/Compilers/InsertCompiler.php @@ -7,6 +7,7 @@ use Phenix\Database\Contracts\ClauseCompiler; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\QueryAst; +use Phenix\Database\Wrapper; use Phenix\Util\Arr; abstract class InsertCompiler implements ClauseCompiler @@ -19,10 +20,10 @@ public function compile(QueryAst $ast): CompiledClause // INSERT [IGNORE] INTO $parts[] = $this->compileInsertClause($ast); - $parts[] = $ast->table; + $parts[] = Wrapper::of($ast->driver, $ast->table); // (column1, column2, ...) - $parts[] = '(' . Arr::implodeDeeply($ast->columns, ', ') . ')'; + $parts[] = '(' . Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->columns), ', ') . ')'; // VALUES (...), (...) or raw statement if ($ast->rawStatement !== null) { diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index ff18f24a..32bf5940 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -5,30 +5,36 @@ namespace Phenix\Database\Dialects\Compilers; use Phenix\Database\Alias; -use Phenix\Database\Contracts\ClauseCompiler; +use Phenix\Database\Constants\Driver; +use Phenix\Database\Constants\Operator; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Exceptions\QueryErrorException; use Phenix\Database\Functions; use Phenix\Database\QueryAst; use Phenix\Database\SelectCase; use Phenix\Database\Subquery; +use Phenix\Database\Wrapper; use Phenix\Util\Arr; use function is_string; -abstract class SelectCompiler implements ClauseCompiler +abstract class SelectCompiler extends ClauseCompiler { - protected $whereCompiler; + protected array $params = []; + + abstract protected function compileLock(QueryAst $ast): string; public function compile(QueryAst $ast): CompiledClause { + $this->params = $ast->params; + $columns = empty($ast->columns) ? ['*'] : $ast->columns; $sql = [ 'SELECT', - $this->compileColumns($columns, $ast->params), + $this->compileColumns($columns, $ast->driver), 'FROM', - $ast->table, + $this->compileTable($ast->table, $ast->driver), ]; if (! empty($ast->joins)) { @@ -49,11 +55,13 @@ public function compile(QueryAst $ast): CompiledClause } if (! empty($ast->groups)) { - $sql[] = Arr::implodeDeeply($ast->groups); + $sql[] = Operator::GROUP_BY->value; + $sql[] = $this->compileGroups($ast->groups, $ast->driver); } if (! empty($ast->orders)) { - $sql[] = Arr::implodeDeeply($ast->orders); + $sql[] = Operator::ORDER_BY->value; + $sql[] = $this->compileOrders($ast->orders, $ast->driver); } if ($ast->limit !== null) { @@ -74,51 +82,88 @@ public function compile(QueryAst $ast): CompiledClause return new CompiledClause( Arr::implodeDeeply($sql), - $ast->params + $this->params ); } /** - * @param QueryAst $ast + * @param array $columns * @return string */ - abstract protected function compileLock(QueryAst $ast): string; + protected function compileColumns(array $columns, Driver $driver): string + { + $compiled = Arr::map($columns, function (string|Alias|Functions|SelectCase|Subquery $value, int|string $key) use ($driver): string { + return match (true) { + is_string($key) => (string) Alias::of($key)->as($value)->setDriver($driver), + $value instanceof Alias => (string) $value->setDriver($driver), + $value instanceof Functions => (string) $value->setDriver($driver), + $value instanceof SelectCase => (string) $value->setDriver($driver), + $value instanceof Subquery => $this->compileSubquery($value, $driver), + default => (string) Wrapper::column($driver, (string) $value), + }; + }); + + return Arr::implodeDeeply($compiled, ', '); + } /** - * @param array $columns - * @param array $params Reference to params array for subqueries + * @param array $groups * @return string */ - protected function compileColumns(array $columns, array &$params): string + protected function compileGroups(array $groups, Driver $driver): string { - $compiled = Arr::map($columns, function (string|Functions|SelectCase|Subquery $value, int|string $key) use (&$params): string { + $compiled = Arr::map($groups, function (string|Functions $value) use ($driver): string { return match (true) { - is_string($key) => (string) Alias::of($key)->as($value), - $value instanceof Functions => (string) $value, - $value instanceof SelectCase => (string) $value, - $value instanceof Subquery => $this->compileSubquery($value, $params), - default => $value, + $value instanceof Functions => (string) $value->setDriver($driver), + default => (string) Wrapper::column($driver, $value), }; }); return Arr::implodeDeeply($compiled, ', '); } + protected function compileOrders(array $orders, Driver $driver): string + { + [$columns, $order] = $orders; + + $compiled = Arr::map($columns, function (string|Functions|SelectCase $value) use ($driver): string { + return match (true) { + $value instanceof Functions => (string) $value->setDriver($driver), + $value instanceof SelectCase => '(' . (string) $value->setDriver($driver) . ')', + default => (string) Wrapper::column($driver, $value), + }; + }); + + return Arr::implodeDeeply([Arr::implodeDeeply($compiled, ', '), $order]); + } + /** * @param Subquery $subquery - * @param array $params Reference to params array * @return string */ - private function compileSubquery(Subquery $subquery, array &$params): string + private function compileSubquery(Subquery $subquery, Driver $driver): string { + $subquery->setDriver($driver); + [$dml, $arguments] = $subquery->toSql(); if (! str_contains($dml, 'LIMIT 1')) { throw new QueryErrorException('The subquery must be limited to one record'); } - $params = array_merge($params, $arguments); + $this->params = [...$this->params, ...$arguments]; return $dml; } + + private function compileTable(string $table, Driver $driver): string + { + $trimmed = trim($table); + + if ($trimmed !== '' && str_starts_with($trimmed, '(')) { + return $table; + } + + return (string) Wrapper::of($driver, $table); + } } diff --git a/src/Database/Dialects/Compilers/UpdateCompiler.php b/src/Database/Dialects/Compilers/UpdateCompiler.php index dcd6861f..84992398 100644 --- a/src/Database/Dialects/Compilers/UpdateCompiler.php +++ b/src/Database/Dialects/Compilers/UpdateCompiler.php @@ -4,11 +4,15 @@ namespace Phenix\Database\Dialects\Compilers; +use Phenix\Database\Constants\Driver; use Phenix\Database\Contracts\ClauseCompiler; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\QueryAst; +use Phenix\Database\Wrapper; use Phenix\Util\Arr; +use function count; + abstract class UpdateCompiler implements ClauseCompiler { protected $whereCompiler; @@ -19,7 +23,7 @@ public function compile(QueryAst $ast): CompiledClause $params = []; $parts[] = 'UPDATE'; - $parts[] = $ast->table; + $parts[] = Wrapper::of($ast->driver, $ast->table); // SET col1 = ?, col2 = ? // Extract params from values (these are actual values, not placeholders) @@ -27,7 +31,7 @@ public function compile(QueryAst $ast): CompiledClause foreach ($ast->values as $column => $value) { $params[] = $value; - $columns[] = $this->compileSetClause($column, count($params)); + $columns[] = $this->compileSetClause($ast->driver, $column, count($params)); } $parts[] = 'SET'; @@ -44,7 +48,7 @@ public function compile(QueryAst $ast): CompiledClause if (! empty($ast->returning)) { $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply($ast->returning, ', '); + $parts[] = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->returning), ', '); } $sql = Arr::implodeDeeply($parts); @@ -56,5 +60,5 @@ public function compile(QueryAst $ast): CompiledClause * Compile the SET clause for a column assignment * This is dialect-specific for placeholder syntax */ - abstract protected function compileSetClause(string $column, int $paramIndex): string; + abstract protected function compileSetClause(Driver $driver, string $column, int $paramIndex): string; } diff --git a/src/Database/Dialects/Compilers/WhereCompiler.php b/src/Database/Dialects/Compilers/WhereCompiler.php index 1b2c2626..026b0cb6 100644 --- a/src/Database/Dialects/Compilers/WhereCompiler.php +++ b/src/Database/Dialects/Compilers/WhereCompiler.php @@ -8,7 +8,9 @@ use Phenix\Database\Clauses\BetweenWhereClause; use Phenix\Database\Clauses\BooleanWhereClause; use Phenix\Database\Clauses\ColumnWhereClause; +use Phenix\Database\Clauses\DateWhereClause; use Phenix\Database\Clauses\NullWhereClause; +use Phenix\Database\Clauses\RowWhereClause; use Phenix\Database\Clauses\SubqueryWhereClause; use Phenix\Database\Clauses\WhereClause; use Phenix\Database\Dialects\CompiledClause; @@ -39,9 +41,11 @@ protected function compileClause(WhereClause $clause): string { return match (true) { $clause instanceof BasicWhereClause => $this->compileBasicClause($clause), + $clause instanceof DateWhereClause => $this->compileDateClause($clause), $clause instanceof NullWhereClause => $this->compileNullClause($clause), $clause instanceof BooleanWhereClause => $this->compileBooleanClause($clause), $clause instanceof BetweenWhereClause => $this->compileBetweenClause($clause), + $clause instanceof RowWhereClause => $this->compileRowClause($clause), $clause instanceof SubqueryWhereClause => $this->compileSubqueryClause($clause), $clause instanceof ColumnWhereClause => $this->compileColumnClause($clause), default => '', @@ -50,22 +54,17 @@ protected function compileClause(WhereClause $clause): string abstract protected function compileBasicClause(BasicWhereClause $clause): string; - protected function compileNullClause(NullWhereClause $clause): string - { - return "{$clause->getColumn()} {$clause->getOperator()->value}"; - } + abstract protected function compileDateClause(DateWhereClause $clause): string; - protected function compileBooleanClause(BooleanWhereClause $clause): string - { - return "{$clause->getColumn()} {$clause->getOperator()->value}"; - } + abstract protected function compileNullClause(NullWhereClause $clause): string; + + abstract protected function compileBooleanClause(BooleanWhereClause $clause): string; abstract protected function compileBetweenClause(BetweenWhereClause $clause): string; + abstract protected function compileRowClause(RowWhereClause $clause): string; + abstract protected function compileSubqueryClause(SubqueryWhereClause $clause): string; - protected function compileColumnClause(ColumnWhereClause $clause): string - { - return "{$clause->getColumn()} {$clause->getOperator()->value} {$clause->getCompareColumn()}"; - } + abstract protected function compileColumnClause(ColumnWhereClause $clause): string; } diff --git a/src/Database/Dialects/Mysql/Compilers/Insert.php b/src/Database/Dialects/Mysql/Compilers/Insert.php index 0ce6c441..0254b5ac 100644 --- a/src/Database/Dialects/Mysql/Compilers/Insert.php +++ b/src/Database/Dialects/Mysql/Compilers/Insert.php @@ -6,6 +6,7 @@ use Phenix\Database\Dialects\Compilers\InsertCompiler; use Phenix\Database\QueryAst; +use Phenix\Database\Wrapper; use Phenix\Util\Arr; class Insert extends InsertCompiler @@ -18,7 +19,11 @@ protected function compileInsertIgnore(): string protected function compileUpsert(QueryAst $ast): string { $columns = array_map( - fn (string $column): string => "{$column} = VALUES({$column})", + function (string $column) use ($ast): string { + $column = Wrapper::column($ast->driver, $column); + + return "{$column} = VALUES({$column})"; + }, $ast->uniqueColumns ); diff --git a/src/Database/Dialects/Mysql/Compilers/Update.php b/src/Database/Dialects/Mysql/Compilers/Update.php index 8a99ffed..3242fd48 100644 --- a/src/Database/Dialects/Mysql/Compilers/Update.php +++ b/src/Database/Dialects/Mysql/Compilers/Update.php @@ -4,7 +4,9 @@ namespace Phenix\Database\Dialects\Mysql\Compilers; +use Phenix\Database\Constants\Driver; use Phenix\Database\Dialects\Compilers\UpdateCompiler; +use Phenix\Database\Wrapper; class Update extends UpdateCompiler { @@ -13,8 +15,10 @@ public function __construct() $this->whereCompiler = new Where(); } - protected function compileSetClause(string $column, int $paramIndex): string + protected function compileSetClause(Driver $driver, string $column, int $paramIndex): string { + $column = Wrapper::column($driver, $column); + return "{$column} = ?"; } } diff --git a/src/Database/Dialects/Mysql/Compilers/Where.php b/src/Database/Dialects/Mysql/Compilers/Where.php index 72e94248..ad44832e 100644 --- a/src/Database/Dialects/Mysql/Compilers/Where.php +++ b/src/Database/Dialects/Mysql/Compilers/Where.php @@ -6,26 +6,52 @@ use Phenix\Database\Clauses\BasicWhereClause; use Phenix\Database\Clauses\BetweenWhereClause; +use Phenix\Database\Clauses\BooleanWhereClause; +use Phenix\Database\Clauses\ColumnWhereClause; +use Phenix\Database\Clauses\DateWhereClause; +use Phenix\Database\Clauses\NullWhereClause; +use Phenix\Database\Clauses\RowWhereClause; use Phenix\Database\Clauses\SubqueryWhereClause; +use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\SQL; use Phenix\Database\Dialects\Compilers\WhereCompiler; +use Phenix\Database\Wrapper; class Where extends WhereCompiler { protected function compileBasicClause(BasicWhereClause $clause): string { + $column = Wrapper::column(Driver::MYSQL, $clause->getColumn()); + if ($clause->isInOperator()) { $placeholders = str_repeat(SQL::PLACEHOLDER->value . ', ', $clause->getValueCount() - 1) . SQL::PLACEHOLDER->value; - return "{$clause->getColumn()} {$clause->getOperator()->value} ({$placeholders})"; + return "{$column} {$clause->getOperator()->value} ({$placeholders})"; } - return "{$clause->getColumn()} {$clause->getOperator()->value} " . SQL::PLACEHOLDER->value; + return "{$column} {$clause->getOperator()->value} " . SQL::PLACEHOLDER->value; + } + + protected function compileDateClause(DateWhereClause $clause): string + { + $column = Wrapper::column(Driver::MYSQL, $clause->getColumn()); + $function = $clause->getFunction()->name; + + return "{$function}({$column}) {$clause->getOperator()->value} {$clause->renderValue()}"; } protected function compileBetweenClause(BetweenWhereClause $clause): string { - return "{$clause->getColumn()} {$clause->getOperator()->value} {$clause->renderValue()}"; + $column = Wrapper::column(Driver::MYSQL, $clause->getColumn()); + + return "{$column} {$clause->getOperator()->value} {$clause->renderValue()}"; + } + + protected function compileRowClause(RowWhereClause $clause): string + { + $columns = implode(', ', Wrapper::columnList(Driver::MYSQL, $clause->getColumns())); + + return "ROW({$columns}) {$clause->getOperator()->value} {$clause->renderValue()}"; } protected function compileSubqueryClause(SubqueryWhereClause $clause): string @@ -33,7 +59,7 @@ protected function compileSubqueryClause(SubqueryWhereClause $clause): string $parts = []; if ($clause->getColumn() !== null) { - $parts[] = $clause->getColumn(); + $parts[] = Wrapper::column(Driver::MYSQL, $clause->getColumn()); } $parts[] = $clause->getOperator()->value; @@ -43,4 +69,26 @@ protected function compileSubqueryClause(SubqueryWhereClause $clause): string return implode(' ', $parts); } + + protected function compileColumnClause(ColumnWhereClause $clause): string + { + $column = Wrapper::column(Driver::MYSQL, $clause->getColumn()); + $compareColumn = Wrapper::column(Driver::MYSQL, $clause->getCompareColumn()); + + return "{$column} {$clause->getOperator()->value} {$compareColumn}"; + } + + protected function compileNullClause(NullWhereClause $clause): string + { + $column = Wrapper::column(Driver::MYSQL, $clause->getColumn()); + + return "{$column} {$clause->getOperator()->value}"; + } + + protected function compileBooleanClause(BooleanWhereClause $clause): string + { + $column = Wrapper::column(Driver::MYSQL, $clause->getColumn()); + + return "{$column} {$clause->getOperator()->value}"; + } } diff --git a/src/Database/Dialects/Postgres/Compilers/Insert.php b/src/Database/Dialects/Postgres/Compilers/Insert.php index 23d44c01..07543c20 100644 --- a/src/Database/Dialects/Postgres/Compilers/Insert.php +++ b/src/Database/Dialects/Postgres/Compilers/Insert.php @@ -8,8 +8,11 @@ use Phenix\Database\Dialects\Compilers\InsertCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; use Phenix\Database\QueryAst; +use Phenix\Database\Wrapper; use Phenix\Util\Arr; +use function sprintf; + /** * Supports: * - INSERT ... ON CONFLICT DO NOTHING (ignore conflicts) @@ -26,9 +29,11 @@ protected function compileInsertIgnore(): string protected function compileUpsert(QueryAst $ast): string { - $conflictColumns = Arr::implodeDeeply($ast->uniqueColumns, ', '); + $conflictColumns = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->uniqueColumns), ', '); + + $updateColumns = array_map(function (string $column) use ($ast): string { + $column = Wrapper::column($ast->driver, $column); - $updateColumns = array_map(function (string $column): string { return "{$column} = EXCLUDED.{$column}"; }, $ast->uniqueColumns); @@ -44,8 +49,8 @@ public function compile(QueryAst $ast): CompiledClause if ($ast->ignore && empty($ast->uniqueColumns)) { $parts = []; $parts[] = 'INSERT INTO'; - $parts[] = $ast->table; - $parts[] = '(' . Arr::implodeDeeply($ast->columns, ', ') . ')'; + $parts[] = Wrapper::of($ast->driver, $ast->table); + $parts[] = '(' . Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->columns), ', ') . ')'; if ($ast->rawStatement !== null) { $parts[] = $ast->rawStatement; @@ -63,7 +68,7 @@ public function compile(QueryAst $ast): CompiledClause if (! empty($ast->returning)) { $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply($ast->returning, ', '); + $parts[] = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->returning), ', '); } $sql = Arr::implodeDeeply($parts); @@ -77,7 +82,7 @@ public function compile(QueryAst $ast): CompiledClause if (! empty($ast->returning)) { $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply($ast->returning, ', '); + $parts[] = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->returning), ', '); } return new CompiledClause( diff --git a/src/Database/Dialects/Postgres/Compilers/Update.php b/src/Database/Dialects/Postgres/Compilers/Update.php index 8da8aafb..dd69fcdf 100644 --- a/src/Database/Dialects/Postgres/Compilers/Update.php +++ b/src/Database/Dialects/Postgres/Compilers/Update.php @@ -4,10 +4,12 @@ namespace Phenix\Database\Dialects\Postgres\Compilers; +use Phenix\Database\Constants\Driver; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\UpdateCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; use Phenix\Database\QueryAst; +use Phenix\Database\Wrapper; use function count; @@ -20,8 +22,10 @@ public function __construct() $this->whereCompiler = new Where(); } - protected function compileSetClause(string $column, int $paramIndex): string + protected function compileSetClause(Driver $driver, string $column, int $paramIndex): string { + $column = Wrapper::column($driver, $column); + return "{$column} = $" . $paramIndex; } diff --git a/src/Database/Dialects/Postgres/Compilers/Where.php b/src/Database/Dialects/Postgres/Compilers/Where.php index 59fb5cb7..ce3995c7 100644 --- a/src/Database/Dialects/Postgres/Compilers/Where.php +++ b/src/Database/Dialects/Postgres/Compilers/Where.php @@ -6,15 +6,22 @@ use Phenix\Database\Clauses\BasicWhereClause; use Phenix\Database\Clauses\BetweenWhereClause; +use Phenix\Database\Clauses\BooleanWhereClause; +use Phenix\Database\Clauses\ColumnWhereClause; +use Phenix\Database\Clauses\DateWhereClause; +use Phenix\Database\Clauses\NullWhereClause; +use Phenix\Database\Clauses\RowWhereClause; use Phenix\Database\Clauses\SubqueryWhereClause; +use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\SQL; use Phenix\Database\Dialects\Compilers\WhereCompiler; +use Phenix\Database\Wrapper; class Where extends WhereCompiler { protected function compileBasicClause(BasicWhereClause $clause): string { - $column = $clause->getColumn(); + $column = Wrapper::column(Driver::POSTGRESQL, $clause->getColumn()); $operator = $clause->getOperator(); if ($clause->isInOperator()) { @@ -26,20 +33,35 @@ protected function compileBasicClause(BasicWhereClause $clause): string return "{$column} {$operator->value} " . SQL::PLACEHOLDER->value; } + protected function compileDateClause(DateWhereClause $clause): string + { + $column = Wrapper::column(Driver::POSTGRESQL, $clause->getColumn()); + $function = $clause->getFunction()->name; + + return "{$function}({$column}) {$clause->getOperator()->value} {$clause->renderValue()}"; + } + protected function compileBetweenClause(BetweenWhereClause $clause): string { - $column = $clause->getColumn(); + $column = Wrapper::column(Driver::POSTGRESQL, $clause->getColumn()); $operator = $clause->getOperator(); return "{$column} {$operator->value} {$clause->renderValue()}"; } + protected function compileRowClause(RowWhereClause $clause): string + { + $columns = implode(', ', Wrapper::columnList(Driver::POSTGRESQL, $clause->getColumns())); + + return "ROW({$columns}) {$clause->getOperator()->value} {$clause->renderValue()}"; + } + protected function compileSubqueryClause(SubqueryWhereClause $clause): string { $parts = []; if ($clause->getColumn() !== null) { - $parts[] = $clause->getColumn(); + $parts[] = Wrapper::column(Driver::POSTGRESQL, $clause->getColumn()); } $parts[] = $clause->getOperator()->value; @@ -53,4 +75,26 @@ protected function compileSubqueryClause(SubqueryWhereClause $clause): string return implode(' ', $parts); } + + protected function compileColumnClause(ColumnWhereClause $clause): string + { + $column = Wrapper::column(Driver::POSTGRESQL, $clause->getColumn()); + $compareColumn = Wrapper::column(Driver::POSTGRESQL, $clause->getCompareColumn()); + + return "{$column} {$clause->getOperator()->value} {$compareColumn}"; + } + + protected function compileNullClause(NullWhereClause $clause): string + { + $column = Wrapper::column(Driver::POSTGRESQL, $clause->getColumn()); + + return "{$column} {$clause->getOperator()->value}"; + } + + protected function compileBooleanClause(BooleanWhereClause $clause): string + { + $column = Wrapper::column(Driver::POSTGRESQL, $clause->getColumn()); + + return "{$column} {$clause->getOperator()->value}"; + } } diff --git a/src/Database/Dialects/Sqlite/Compilers/Delete.php b/src/Database/Dialects/Sqlite/Compilers/Delete.php index 0fe24bdf..836245f8 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Delete.php +++ b/src/Database/Dialects/Sqlite/Compilers/Delete.php @@ -4,9 +4,11 @@ namespace Phenix\Database\Dialects\Sqlite\Compilers; +use Phenix\Database\Constants\Driver; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\DeleteCompiler; use Phenix\Database\QueryAst; +use Phenix\Database\Wrapper; use Phenix\Util\Arr; class Delete extends DeleteCompiler @@ -21,7 +23,7 @@ public function compile(QueryAst $ast): CompiledClause $parts = []; $parts[] = 'DELETE FROM'; - $parts[] = $ast->table; + $parts[] = Wrapper::of($ast->driver, $ast->table); if (! empty($ast->wheres)) { $whereCompiled = $this->whereCompiler->compile($ast->wheres); @@ -32,11 +34,16 @@ public function compile(QueryAst $ast): CompiledClause if (! empty($ast->returning)) { $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply($ast->returning, ', '); + $parts[] = Arr::implodeDeeply($this->wrapColumns($ast->returning), ', '); } $sql = Arr::implodeDeeply($parts); return new CompiledClause($sql, $ast->params); } + + protected function wrapColumns(array $columns): array + { + return array_map(fn (string $col): string => Wrapper::column(Driver::SQLITE, $col), $columns); + } } diff --git a/src/Database/Dialects/Sqlite/Compilers/Insert.php b/src/Database/Dialects/Sqlite/Compilers/Insert.php index cdad0799..6158bccd 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Insert.php +++ b/src/Database/Dialects/Sqlite/Compilers/Insert.php @@ -7,6 +7,7 @@ use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\InsertCompiler; use Phenix\Database\QueryAst; +use Phenix\Database\Wrapper; use Phenix\Util\Arr; /** @@ -29,9 +30,11 @@ protected function compileInsertIgnore(): string */ protected function compileUpsert(QueryAst $ast): string { - $conflictColumns = Arr::implodeDeeply($ast->uniqueColumns, ', '); + $conflictColumns = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->uniqueColumns), ', '); + + $updateColumns = array_map(function (string $column) use ($ast): string { + $column = Wrapper::column($ast->driver, $column); - $updateColumns = array_map(function (string $column): string { return "{$column} = excluded.{$column}"; }, $ast->uniqueColumns); @@ -49,7 +52,7 @@ public function compile(QueryAst $ast): CompiledClause if (! empty($ast->returning)) { $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply($ast->returning, ', '); + $parts[] = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->returning), ', '); } return new CompiledClause( diff --git a/src/Database/Dialects/Sqlite/Compilers/Update.php b/src/Database/Dialects/Sqlite/Compilers/Update.php index 67d05255..327fd19f 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Update.php +++ b/src/Database/Dialects/Sqlite/Compilers/Update.php @@ -4,7 +4,9 @@ namespace Phenix\Database\Dialects\Sqlite\Compilers; +use Phenix\Database\Constants\Driver; use Phenix\Database\Dialects\Compilers\UpdateCompiler; +use Phenix\Database\Wrapper; class Update extends UpdateCompiler { @@ -13,8 +15,10 @@ public function __construct() $this->whereCompiler = new Where(); } - protected function compileSetClause(string $column, int $paramIndex): string + protected function compileSetClause(Driver $driver, string $column, int $paramIndex): string { + $column = Wrapper::column($driver, $column); + return "{$column} = ?"; } } diff --git a/src/Database/Functions.php b/src/Database/Functions.php index d12139a6..905ae1f7 100644 --- a/src/Database/Functions.php +++ b/src/Database/Functions.php @@ -4,11 +4,14 @@ namespace Phenix\Database; +use Phenix\Database\Concerns\HasDriver; use Phenix\Database\Constants\DatabaseFunction; use Stringable; class Functions implements Stringable { + use HasDriver; + protected string $alias; public function __construct( @@ -18,24 +21,6 @@ public function __construct( // .. } - public function as(string $alias): self - { - $this->alias = $alias; - - return $this; - } - - public function __toString(): string - { - $function = $this->function->name . '(' . $this->column . ')'; - - if (isset($this->alias)) { - $function .= ' AS ' . $this->alias; - } - - return $function; - } - public static function avg(string $column): self { return new self(DatabaseFunction::AVG, $column); @@ -80,4 +65,24 @@ public static function case(): SelectCase { return new SelectCase(); } + + public function as(string $alias): self + { + $this->alias = $alias; + + return $this; + } + + public function __toString(): string + { + $column = Wrapper::column($this->driver, $this->column); + + $function = $this->function->name . '(' . $column . ')'; + + if (isset($this->alias)) { + $function .= ' AS ' . Wrapper::column($this->driver, $this->alias); + } + + return $function; + } } diff --git a/src/Database/Grammar.php b/src/Database/Grammar.php index 1857db3b..8cca4224 100644 --- a/src/Database/Grammar.php +++ b/src/Database/Grammar.php @@ -7,32 +7,21 @@ use Amp\Mysql\MysqlConnectionPool; use Amp\Postgres\PostgresConnectionPool; use Amp\Sql\SqlConnection; +use Phenix\Database\Concerns\HasDriver; use Phenix\Database\Constants\Driver; use Phenix\Facades\Config; use Phenix\Sqlite\SqliteConnection; abstract class Grammar { - protected Driver $driver; - - public function setDriver(Driver $driver): static - { - $this->driver = $driver; - - return $this; - } - - public function getDriver(): Driver - { - return $this->driver; - } + use HasDriver; protected function resolveDriver(SqlConnection $connection): void { $driver = $this->resolveDriverFromConnection($connection); $driver ??= $this->resolveDriverFromConfig(); - $this->driver = $driver; + $this->setDriver($driver); } protected function resolveDriverFromConfig(): Driver diff --git a/src/Database/Having.php b/src/Database/Having.php index be4b309e..0136cbeb 100644 --- a/src/Database/Having.php +++ b/src/Database/Having.php @@ -4,6 +4,7 @@ namespace Phenix\Database; +use Phenix\Database\Clauses\DateWhereClause; use Phenix\Database\Constants\SQL; class Having extends Clause @@ -19,7 +20,14 @@ public function toSql(): array $sql = []; foreach ($this->clauses as $clause) { - $clauseSql = "{$clause->getColumn()} {$clause->getOperator()->value} " . SQL::PLACEHOLDER->value; + $column = Wrapper::column($this->driver, $clause->getColumn()); + + if ($clause instanceof DateWhereClause) { + $function = $clause->getFunction()->name; + $column = "{$function}({$column})"; + } + + $clauseSql = "{$column} {$clause->getOperator()->value} " . SQL::PLACEHOLDER->value; if ($connector = $clause->getConnector()) { $clauseSql = "{$connector->value} {$clauseSql}"; diff --git a/src/Database/Join.php b/src/Database/Join.php index 1648b30b..534f39dc 100644 --- a/src/Database/Join.php +++ b/src/Database/Join.php @@ -5,6 +5,7 @@ namespace Phenix\Database; use Phenix\Database\Clauses\BasicWhereClause; +use Phenix\Database\Clauses\DateWhereClause; use Phenix\Database\Constants\JoinType; use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; @@ -56,8 +57,21 @@ public function toSql(): array $connector = $clause->getConnector(); $column = $clause->getColumn(); + $column = $column ? Wrapper::column($this->driver, $clause->getColumn()) : null; + $operator = $clause->getOperator(); - $value = $clause->renderValue(); + + if ($clause instanceof DateWhereClause) { + $function = $clause->getFunction()->name; + $column = "{$function}({$column})"; + $value = $clause->renderValue(); + } else { + $value = $clause->renderValue(); + + if (! $clause instanceof BasicWhereClause || ! $clause->usesPlaceholder()) { + $value = Wrapper::column($this->driver, $value); + } + } $clauseSql = "{$column} {$operator->value} {$value}"; @@ -69,8 +83,17 @@ public function toSql(): array } return [ - "{$this->type->value} {$this->relationship} ON " . implode(' ', $sql), + "{$this->type->value} {$this->prepareRelationship()} ON " . implode(' ', $sql), $this->arguments, ]; } + + protected function prepareRelationship(): string + { + if ($this->relationship instanceof Alias) { + return (string) $this->relationship->setDriver($this->driver); + } + + return (string) Wrapper::of($this->driver, $this->relationship); + } } diff --git a/src/Database/QueryAst.php b/src/Database/QueryAst.php index 16d40cb1..8ffef80d 100644 --- a/src/Database/QueryAst.php +++ b/src/Database/QueryAst.php @@ -6,10 +6,13 @@ use Phenix\Database\Clauses\WhereClause; use Phenix\Database\Constants\Action; +use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Lock; class QueryAst { + public Driver $driver; + public Action $action; public string $table; diff --git a/src/Database/SelectCase.php b/src/Database/SelectCase.php index d60735c2..90df3d20 100644 --- a/src/Database/SelectCase.php +++ b/src/Database/SelectCase.php @@ -4,14 +4,22 @@ namespace Phenix\Database; +use Phenix\Database\Concerns\HasDriver; use Phenix\Database\Constants\Operator; +use Phenix\Database\Contracts\RawValue; use Phenix\Util\Arr; use Stringable; +use function is_int; + class SelectCase implements Stringable { + use HasDriver; + protected array $cases; - protected Value|string $default; + + protected RawValue|string|int $default; + protected string $alias; public function __construct() @@ -19,7 +27,7 @@ public function __construct() $this->cases = []; } - public function whenEqual(Functions|string $column, Value|string|int $value, Value|string $result): self + public function whenEqual(Functions|string $column, RawValue|string|int $value, RawValue|string|int $result): self { $this->pushCase( $column, @@ -31,7 +39,7 @@ public function whenEqual(Functions|string $column, Value|string|int $value, Val return $this; } - public function whenNotEqual(Functions|string $column, Value|string|int $value, Value|string $result): self + public function whenNotEqual(Functions|string $column, RawValue|string|int $value, RawValue|string|int $result): self { $this->pushCase( $column, @@ -43,7 +51,7 @@ public function whenNotEqual(Functions|string $column, Value|string|int $value, return $this; } - public function whenGreaterThan(Functions|string $column, Value|string|int $value, Value|string $result): self + public function whenGreaterThan(Functions|string $column, RawValue|string|int $value, RawValue|string|int $result): self { $this->pushCase( $column, @@ -57,8 +65,8 @@ public function whenGreaterThan(Functions|string $column, Value|string|int $valu public function whenGreaterThanOrEqual( Functions|string $column, - Value|string|int $value, - Value|string $result + RawValue|string|int $value, + RawValue|string|int $result ): self { $this->pushCase( $column, @@ -70,7 +78,7 @@ public function whenGreaterThanOrEqual( return $this; } - public function whenLessThan(Functions|string $column, Value|string|int $value, Value|string $result): self + public function whenLessThan(Functions|string $column, RawValue|string|int $value, RawValue|string|int $result): self { $this->pushCase( $column, @@ -82,7 +90,7 @@ public function whenLessThan(Functions|string $column, Value|string|int $value, return $this; } - public function whenLessThanOrEqual(Functions|string $column, Value|string|int $value, Value|string $result): self + public function whenLessThanOrEqual(Functions|string $column, RawValue|string|int $value, RawValue|string|int $result): self { $this->pushCase( $column, @@ -94,7 +102,7 @@ public function whenLessThanOrEqual(Functions|string $column, Value|string|int $ return $this; } - public function whenNull(string $column, Value|string $result): self + public function whenNull(string $column, RawValue|string|int $result): self { $this->pushCase( $column, @@ -105,7 +113,7 @@ public function whenNull(string $column, Value|string $result): self return $this; } - public function whenNotNull(string $column, Value|string $result): self + public function whenNotNull(string $column, RawValue|string|int $result): self { $this->pushCase( $column, @@ -116,7 +124,7 @@ public function whenNotNull(string $column, Value|string $result): self return $this; } - public function whenTrue(string $column, Value|string $result): self + public function whenTrue(string $column, RawValue|string|int $result): self { $this->pushCase( $column, @@ -127,7 +135,7 @@ public function whenTrue(string $column, Value|string $result): self return $this; } - public function whenFalse(string $column, Value|string $result): self + public function whenFalse(string $column, RawValue|string|int $result): self { $this->pushCase( $column, @@ -138,7 +146,7 @@ public function whenFalse(string $column, Value|string $result): self return $this; } - public function defaultResult(Value|string|int $value): self + public function defaultResult(RawValue|string|int $value): self { $this->default = $value; @@ -154,25 +162,20 @@ public function as(string $alias): self public function __toString(): string { - $cases = array_map(function (array $case): array { - return array_map(function (Operator|string $item): string { - return match (true) { - $item instanceof Operator => $item->value, - default => (string) $item, - }; - }, $case); - }, $this->cases); + $cases = array_map($this->compileCase(...), $this->cases); if (isset($this->default)) { - $cases[] = ['ELSE ' . strval($this->default)]; + $cases[] = 'ELSE ' . $this->renderOperand($this->default); } $cases[] = 'END'; - $dml = 'CASE ' . Arr::implodeDeeply($cases); + $dml = 'CASE ' . Arr::implodeDeeply($cases, ' '); if (isset($this->alias)) { - $dml = '(' . $dml . ') AS ' . $this->alias; + $alias = Wrapper::of($this->getDriver(), $this->alias); + + $dml = "({$dml}) AS {$alias}"; } return $dml; @@ -181,11 +184,41 @@ public function __toString(): string protected function pushCase( Functions|string $column, Operator $operators, - Value|string $result, - Value|string|int|null $value = null + RawValue|string|int $result, + RawValue|string|int|null $value = null ): void { - $condition = array_filter([$column, $operators, $value]); + $condition = array_filter([$column, $operators, $value], static fn (mixed $item): bool => $item !== null); $this->cases[] = ['WHEN', ...$condition, 'THEN', $result]; } + + protected function compileCase(array $case): string + { + $column = $this->compileColumn($case[1]); + $operator = $case[2] instanceof Operator ? $case[2]->value : (string) $case[2]; + + if (($case[3] ?? null) === 'THEN') { + return "WHEN {$column} {$operator} THEN " . $this->renderOperand($case[4]); + } + + return "WHEN {$column} {$operator} " . $this->renderOperand($case[3]) . " THEN " . $this->renderOperand($case[5]); + } + + protected function compileColumn(Functions|string $column): string + { + if ($column instanceof Functions) { + return (string) $column->setDriver($this->getDriver()); + } + + return Wrapper::column($this->getDriver(), $column); + } + + protected function renderOperand(RawValue|string|int $value): string + { + if ($value instanceof RawValue || is_int($value)) { + return (string) $value; + } + + return (string) Value::from($value); + } } diff --git a/src/Database/Subquery.php b/src/Database/Subquery.php index 9ad78e88..1d0e1fbb 100644 --- a/src/Database/Subquery.php +++ b/src/Database/Subquery.php @@ -20,7 +20,9 @@ public function toSql(): array [$dml, $arguments] = parent::toSql(); if (isset($this->alias)) { - return ["({$dml}) AS {$this->alias}", $arguments]; + $alias = Wrapper::column($this->driver, $this->alias); + + return ["({$dml}) AS {$alias}", $arguments]; } return ["({$dml})", $arguments]; diff --git a/src/Database/Value.php b/src/Database/Value.php index eee01eaf..d58a5a80 100644 --- a/src/Database/Value.php +++ b/src/Database/Value.php @@ -6,6 +6,8 @@ use Phenix\Database\Contracts\RawValue; +use function is_int; + class Value implements RawValue { public function __construct( @@ -16,7 +18,11 @@ public function __construct( public function __toString(): string { - return "'" . $this->value . "'"; + if (is_int($this->value)) { + return (string) $this->value; + } + + return "'" . str_replace("'", "''", $this->value) . "'"; } public static function from(string|int $value): self diff --git a/src/Database/Wrapper.php b/src/Database/Wrapper.php new file mode 100644 index 00000000..923e5f5e --- /dev/null +++ b/src/Database/Wrapper.php @@ -0,0 +1,64 @@ +value) || $this->value === '*' || $this->value === SQL::PLACEHOLDER->value) { + return $this->value; + } + + return $this->encloser + . str_replace($this->encloser, $this->encloser . $this->encloser, $this->value) + . $this->encloser; + } + + public static function doubleQuote(string $value): self + { + return new self($value, '"'); + } + + public static function backtick(string $value): self + { + return new self($value, '`'); + } + + public static function of(Driver $driver, string $value): self + { + return match ($driver) { + Driver::MYSQL => self::backtick($value), + Driver::POSTGRESQL, Driver::SQLITE => self::doubleQuote($value), + default => self::backtick($value), + }; + } + + public static function column(Driver $driver, string $column): string + { + $parts = array_map( + fn (string $part): string => (string) self::of($driver, $part), + explode('.', $column) + ); + + return implode('.', $parts); + } + + public static function columnList(Driver $driver, array $columns): array + { + return array_map(fn (string $column): string => self::column($driver, $column), $columns); + } +} diff --git a/tests/Feature/Database/Models/Postgres/DatabaseModelTest.php b/tests/Feature/Database/Models/Postgres/DatabaseModelTest.php index 542182a2..2498ac65 100644 --- a/tests/Feature/Database/Models/Postgres/DatabaseModelTest.php +++ b/tests/Feature/Database/Models/Postgres/DatabaseModelTest.php @@ -40,7 +40,7 @@ expect($model->save())->toBeTrue(); expect($model->id)->toBe(77); expect($model->isExisting())->toBeTrue(); - expect($capturedSql)->toContain('RETURNING id'); + expect($capturedSql)->toContain('RETURNING "id"'); }); it('creates a new model on postgresql using its mapped key column', function (): void { @@ -68,5 +68,5 @@ expect($model->id)->toBe(88); expect($model->isExisting())->toBeTrue(); - expect($capturedSql)->toContain('RETURNING id'); + expect($capturedSql)->toContain('RETURNING "id"'); }); diff --git a/tests/Unit/Database/QueryBuilderTest.php b/tests/Unit/Database/QueryBuilderTest.php index 7f28c86a..b24605c5 100644 --- a/tests/Unit/Database/QueryBuilderTest.php +++ b/tests/Unit/Database/QueryBuilderTest.php @@ -119,7 +119,7 @@ $insertedId = $query->table('users')->insertGetId(['name' => 'Tony']); expect($insertedId)->toBe(123); - expect($capturedSql)->toContain('RETURNING id'); + expect($capturedSql)->toContain('RETURNING "id"'); }); it('inserts row and returns generated id on mysql', function () { @@ -160,7 +160,7 @@ ->insertGetId(['name' => 'Tony'], 'user_id'); expect($insertedId)->toBe(456); - expect($capturedSql)->toContain('RETURNING user_id'); + expect($capturedSql)->toContain('RETURNING "user_id"'); }); it('fails on insert records', function () { diff --git a/tests/Unit/Database/QueryGenerator/DeleteStatementTest.php b/tests/Unit/Database/QueryGenerator/DeleteStatementTest.php index c7716339..99bc1009 100644 --- a/tests/Unit/Database/QueryGenerator/DeleteStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/DeleteStatementTest.php @@ -13,7 +13,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE id = ?"; + $expected = "DELETE FROM `users` WHERE `id` = ?"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -27,7 +27,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users"; + $expected = "DELETE FROM `users`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); diff --git a/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php index e9a2eded..8be1ee7a 100644 --- a/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php @@ -6,7 +6,7 @@ use Phenix\Database\Join; use Phenix\Database\QueryGenerator; -it('generates a grouped query', function (Functions|string $column, Functions|array|string $groupBy, string $rawGroup) { +it('generates a grouped query', function (Functions|string $column, Functions|array|string $groupBy, string $rawGroup, string $rawColumn) { $query = new QueryGenerator(); $sql = $query->select([ @@ -23,23 +23,24 @@ [$dml, $params] = $sql; - $expected = "SELECT {$column}, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " + $expected = "SELECT {$rawColumn}, `products`.`category_id`, `categories`.`description` " + . "FROM `products` " + . "LEFT JOIN `categories` ON `products`.`category_id` = `categories`.`id` " . "GROUP BY {$rawGroup}"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', 'category_id'], - ['location_id', ['category_id', 'location_id'], 'category_id, location_id'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT(products.id)'], + [Functions::count('products.id'), 'category_id', '`category_id`', 'COUNT(`products`.`id`)'], + ['location_id', ['category_id', 'location_id'], '`category_id`, `location_id`', '`location_id`'], + [Functions::count('products.id'), Functions::count('products.id'), 'COUNT(`products`.`id`)', 'COUNT(`products`.`id`)'], ]); it('generates a grouped and ordered query', function ( Functions|string $column, Functions|array|string $groupBy, - string $rawGroup + string $rawGroup, + string $rawColumn ) { $query = new QueryGenerator(); @@ -58,16 +59,16 @@ [$dml, $params] = $sql; - $expected = "SELECT {$column}, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " + $expected = "SELECT {$rawColumn}, `products`.`category_id`, `categories`.`description` " + . "FROM `products` " + . "LEFT JOIN `categories` ON `products`.`category_id` = `categories`.`id` " . "GROUP BY {$rawGroup} " - . "ORDER BY products.id DESC"; + . "ORDER BY `products`.`id` DESC"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', 'category_id'], - ['location_id', ['category_id', 'location_id'], 'category_id, location_id'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT(products.id)'], + [Functions::count('products.id'), 'category_id', '`category_id`', 'COUNT(`products`.`id`)'], + ['location_id', ['category_id', 'location_id'], '`category_id`, `location_id`', '`location_id`'], + [Functions::count('products.id'), Functions::count('products.id'), 'COUNT(`products`.`id`)', 'COUNT(`products`.`id`)'], ]); diff --git a/tests/Unit/Database/QueryGenerator/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/HavingClauseTest.php index c2f6d75e..5e0ff38c 100644 --- a/tests/Unit/Database/QueryGenerator/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/HavingClauseTest.php @@ -27,10 +27,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS identifiers, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " - . "HAVING identifiers > ? GROUP BY products.category_id"; + $expected = "SELECT COUNT(`products`.`id`) AS `identifiers`, `products`.`category_id`, `categories`.`description` " + . "FROM `products` " + . "LEFT JOIN `categories` ON `products`.`category_id` = `categories`.`id` " + . "HAVING `identifiers` > ? GROUP BY `products`.`category_id`"; expect($dml)->toBe($expected); expect($params)->toBe([5]); @@ -57,11 +57,35 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS identifiers, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " - . "HAVING identifiers > ? AND products.category_id > ? GROUP BY products.category_id"; + $expected = "SELECT COUNT(`products`.`id`) AS `identifiers`, `products`.`category_id`, `categories`.`description` " + . "FROM `products` " + . "LEFT JOIN `categories` ON `products`.`category_id` = `categories`.`id` " + . "HAVING `identifiers` > ? AND `products`.`category_id` > ? GROUP BY `products`.`category_id`"; expect($dml)->toBe($expected); expect($params)->toBe([5, 10]); }); + +it('generates a query using having with date clause', function () { + $query = new QueryGenerator(); + + $sql = $query->select([ + Functions::count('products.id')->as('product_count'), + 'products.created_at', + ]) + ->from('products') + ->groupBy('products.created_at') + ->having(function (Having $having): void { + $having->whereDateEqual('products.created_at', '2026-01-15'); + }) + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT COUNT(`products`.`id`) AS `product_count`, `products`.`created_at` " + . "FROM `products` " + . "HAVING DATE(`products`.`created_at`) = ? GROUP BY `products`.`created_at`"; + + expect($dml)->toBe($expected); + expect($params)->toBe(['2026-01-15']); +}); diff --git a/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php b/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php index 4af01758..94a660dd 100644 --- a/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php @@ -21,7 +21,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES (?, ?)"; + $expected = "INSERT INTO `users` (`email`, `name`) VALUES (?, ?)"; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -47,7 +47,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES (?, ?), (?, ?)"; + $expected = "INSERT INTO `users` (`email`, `name`) VALUES (?, ?), (?, ?)"; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name, $email, $name]); @@ -67,7 +67,7 @@ [$dml, $params] = $sql; - $expected = "INSERT IGNORE INTO users (email, name) VALUES (?, ?)"; + $expected = "INSERT IGNORE INTO `users` (`email`, `name`) VALUES (?, ?)"; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -87,8 +87,8 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES (?, ?) " - . "ON DUPLICATE KEY UPDATE name = VALUES(name)"; + $expected = "INSERT INTO `users` (`email`, `name`) VALUES (?, ?) " + . "ON DUPLICATE KEY UPDATE `name` = VALUES(`name`)"; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -108,8 +108,8 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name, username) VALUES (?, ?, ?) " - . "ON DUPLICATE KEY UPDATE name = VALUES(name), username = VALUES(username)"; + $expected = "INSERT INTO `users` (`email`, `name`, `username`) VALUES (?, ?, ?) " + . "ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `username` = VALUES(`username`)"; \ksort($data); @@ -130,7 +130,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (name, email) SELECT name, email FROM customers WHERE verified_at IS NOT NULL"; + $expected = "INSERT INTO `users` (`name`, `email`) SELECT `name`, `email` FROM `customers` WHERE `verified_at` IS NOT NULL"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -148,8 +148,8 @@ [$dml, $params] = $sql; - $expected = "INSERT IGNORE INTO users (name, email) " - . "SELECT name, email FROM customers WHERE verified_at IS NOT NULL"; + $expected = "INSERT IGNORE INTO `users` (`name`, `email`) " + . "SELECT `name`, `email` FROM `customers` WHERE `verified_at` IS NOT NULL"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); diff --git a/tests/Unit/Database/QueryGenerator/JoinClausesTest.php b/tests/Unit/Database/QueryGenerator/JoinClausesTest.php index 961669a2..5e55437d 100644 --- a/tests/Unit/Database/QueryGenerator/JoinClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/JoinClausesTest.php @@ -22,10 +22,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "{$joinType} categories " - . "ON products.category_id = categories.id"; + $expected = "SELECT `products`.`id`, `products`.`description`, `categories`.`description` " + . "FROM `products` " + . "{$joinType} `categories` " + . "ON `products`.`category_id` = `categories`.`id`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -54,10 +54,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "INNER JOIN categories " - . "ON products.category_id != categories.id"; + $expected = "SELECT `products`.`id`, `products`.`description`, `categories`.`description` " + . "FROM `products` " + . "INNER JOIN `categories` " + . "ON `products`.`category_id` != `categories`.`id`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -85,10 +85,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "INNER JOIN categories " - . "ON products.category_id = categories.id {$clause}"; + $expected = "SELECT `products`.`id`, `products`.`description`, `categories`.`description` " + . "FROM `products` " + . "INNER JOIN `categories` " + . "ON `products`.`category_id` = `categories`.`id` {$clause}"; expect($dml)->toBe($expected); expect($params)->toBe($joinParams); @@ -96,25 +96,25 @@ [ 'orOnEqual', ['products.location_id', 'categories.location_id'], - 'OR products.location_id = categories.location_id', + 'OR `products`.`location_id` = `categories`.`location_id`', [], ], [ 'whereEqual', ['categories.name', 'php'], - 'AND categories.name = ?', + 'AND `categories`.`name` = ?', ['php'], ], [ 'orOnNotEqual', ['products.location_id', 'categories.location_id'], - 'OR products.location_id != categories.location_id', + 'OR `products`.`location_id` != `categories`.`location_id`', [], ], [ 'orWhereEqual', ['categories.name', 'php'], - 'OR categories.name = ?', + 'OR `categories`.`name` = ?', ['php'], ], ]); @@ -133,10 +133,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "{$joinType} categories " - . "ON products.category_id = categories.id"; + $expected = "SELECT `products`.`id`, `products`.`description`, `categories`.`description` " + . "FROM `products` " + . "{$joinType} `categories` " + . "ON `products`.`category_id` = `categories`.`id`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -145,3 +145,28 @@ ['leftJoinOnEqual', JoinType::LEFT->value], ['rightJoinOnEqual', JoinType::RIGHT->value], ]); + +it('generates query with join date clause', function () { + $query = new QueryGenerator(); + + $sql = $query->select([ + 'products.id', + 'categories.description', + ]) + ->from('products') + ->innerJoin('categories', function (Join $join) { + $join->onEqual('products.category_id', 'categories.id') + ->whereDateEqual('categories.created_at', '2026-01-15'); + }) + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT `products`.`id`, `categories`.`description` " + . "FROM `products` " + . "INNER JOIN `categories` " + . "ON `products`.`category_id` = `categories`.`id` AND DATE(`categories`.`created_at`) = ?"; + + expect($dml)->toBe($expected); + expect($params)->toBe(['2026-01-15']); +}); diff --git a/tests/Unit/Database/QueryGenerator/PaginateTest.php b/tests/Unit/Database/QueryGenerator/PaginateTest.php index c09c314f..00d3a8ee 100644 --- a/tests/Unit/Database/QueryGenerator/PaginateTest.php +++ b/tests/Unit/Database/QueryGenerator/PaginateTest.php @@ -13,7 +13,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 15 OFFSET 0'); + expect($dml)->toBe('SELECT * FROM `users` LIMIT 15 OFFSET 0'); expect($params)->toBeEmpty(); }); @@ -26,7 +26,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 15 OFFSET 30'); + expect($dml)->toBe('SELECT * FROM `users` LIMIT 15 OFFSET 30'); expect($params)->toBeEmpty(); }); @@ -40,6 +40,6 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 15 OFFSET 15'); + expect($dml)->toBe('SELECT * FROM `users` LIMIT 15 OFFSET 15'); expect($params)->toBeEmpty(); }); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/DeleteStatementTest.php b/tests/Unit/Database/QueryGenerator/Postgres/DeleteStatementTest.php index 0e9f6f56..2bf239f0 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/DeleteStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/DeleteStatementTest.php @@ -14,7 +14,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE id = $1"; + $expected = "DELETE FROM \"users\" WHERE \"id\" = $1"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -28,7 +28,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users"; + $expected = "DELETE FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -44,7 +44,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE status = $1 AND role = $2"; + $expected = "DELETE FROM \"users\" WHERE \"status\" = $1 AND \"role\" = $2"; expect($dml)->toBe($expected); expect($params)->toBe(['inactive', 'user']); @@ -59,7 +59,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE id IN ($1, $2, $3)"; + $expected = "DELETE FROM \"users\" WHERE \"id\" IN ($1, $2, $3)"; expect($dml)->toBe($expected); expect($params)->toBe([1, 2, 3]); @@ -74,7 +74,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE status != $1"; + $expected = "DELETE FROM \"users\" WHERE \"status\" != $1"; expect($dml)->toBe($expected); expect($params)->toBe(['active']); @@ -89,7 +89,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE age > $1"; + $expected = "DELETE FROM \"users\" WHERE \"age\" > $1"; expect($dml)->toBe($expected); expect($params)->toBe([18]); @@ -104,7 +104,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE age < $1"; + $expected = "DELETE FROM \"users\" WHERE \"age\" < $1"; expect($dml)->toBe($expected); expect($params)->toBe([65]); @@ -119,7 +119,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE deleted_at IS NULL"; + $expected = "DELETE FROM \"users\" WHERE \"deleted_at\" IS NULL"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -134,7 +134,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE email IS NOT NULL"; + $expected = "DELETE FROM \"users\" WHERE \"email\" IS NOT NULL"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -150,7 +150,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE id = $1 RETURNING id, name, email"; + $expected = "DELETE FROM \"users\" WHERE \"id\" = $1 RETURNING \"id\", \"name\", \"email\""; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -166,7 +166,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE status IN ($1, $2) RETURNING *"; + $expected = "DELETE FROM \"users\" WHERE \"status\" IN ($1, $2) RETURNING *"; expect($dml)->toBe($expected); expect($params)->toBe(['inactive', 'deleted']); @@ -181,7 +181,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users RETURNING id, email"; + $expected = "DELETE FROM \"users\" RETURNING \"id\", \"email\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -198,7 +198,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE status = $1 AND created_at > $2 RETURNING id, name, status, created_at"; + $expected = "DELETE FROM \"users\" WHERE \"status\" = $1 AND \"created_at\" > $2 RETURNING \"id\", \"name\", \"status\", \"created_at\""; expect($dml)->toBe($expected); expect($params)->toBe(['inactive', '2024-01-01']); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php index f4223bb5..e3ddb443 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php @@ -8,7 +8,7 @@ use Phenix\Database\Join; use Phenix\Database\QueryGenerator; -it('generates a grouped query', function (Functions|string $column, Functions|array|string $groupBy, string $rawGroup): void { +it('generates a grouped query', function (Functions|string $column, Functions|array|string $groupBy, string $rawGroup, string $rawColumn): void { $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ @@ -25,23 +25,24 @@ [$dml, $params] = $sql; - $expected = "SELECT {$column}, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " + $expected = "SELECT {$rawColumn}, \"products\".\"category_id\", \"categories\".\"description\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " . "GROUP BY {$rawGroup}"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', 'category_id'], - ['location_id', ['category_id', 'location_id'], 'category_id, location_id'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT(products.id)'], + [Functions::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], + [Functions::count('products.id'), Functions::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped and ordered query', function ( Functions|string $column, Functions|array|string $groupBy, - string $rawGroup + string $rawGroup, + string $rawColumn ) { $query = new QueryGenerator(Driver::POSTGRESQL); @@ -60,18 +61,18 @@ [$dml, $params] = $sql; - $expected = "SELECT {$column}, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " + $expected = "SELECT {$rawColumn}, \"products\".\"category_id\", \"categories\".\"description\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " . "GROUP BY {$rawGroup} " - . "ORDER BY products.id DESC"; + . "ORDER BY \"products\".\"id\" DESC"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', 'category_id'], - ['location_id', ['category_id', 'location_id'], 'category_id, location_id'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT(products.id)'], + [Functions::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], + [Functions::count('products.id'), Functions::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped query with where clause', function (): void { @@ -88,10 +89,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id), products.category_id " - . "FROM products " - . "WHERE products.status = $1 " - . "GROUP BY category_id"; + $expected = "SELECT COUNT(\"products\".\"id\"), \"products\".\"category_id\" " + . "FROM \"products\" " + . "WHERE \"products\".\"status\" = $1 " + . "GROUP BY \"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe(['active']); @@ -113,10 +114,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS product_count, products.category_id " - . "FROM products " - . "HAVING product_count > $1 " - . "GROUP BY category_id"; + $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " + . "FROM \"products\" " + . "HAVING \"product_count\" > $1 " + . "GROUP BY \"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe([5]); @@ -137,9 +138,9 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id), SUM(products.price), AVG(products.price), products.category_id " - . "FROM products " - . "GROUP BY category_id"; + $expected = "SELECT COUNT(\"products\".\"id\"), SUM(\"products\".\"price\"), AVG(\"products\".\"price\"), \"products\".\"category_id\" " + . "FROM \"products\" " + . "GROUP BY \"category_id\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php index 3a1ec837..b50340cd 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php @@ -28,10 +28,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS identifiers, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " - . "HAVING identifiers > $1 GROUP BY products.category_id"; + $expected = "SELECT COUNT(\"products\".\"id\") AS \"identifiers\", \"products\".\"category_id\", \"categories\".\"description\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " + . "HAVING \"identifiers\" > $1 GROUP BY \"products\".\"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe([5]); @@ -58,10 +58,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS identifiers, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " - . "HAVING identifiers > $1 AND products.category_id > $2 GROUP BY products.category_id"; + $expected = "SELECT COUNT(\"products\".\"id\") AS \"identifiers\", \"products\".\"category_id\", \"categories\".\"description\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " + . "HAVING \"identifiers\" > $1 AND \"products\".\"category_id\" > $2 GROUP BY \"products\".\"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe([5, 10]); @@ -84,10 +84,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS product_count, products.category_id " - . "FROM products " - . "WHERE products.status = $1 " - . "HAVING product_count > $2 GROUP BY products.category_id"; + $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " + . "FROM \"products\" " + . "WHERE \"products\".\"status\" = $1 " + . "HAVING \"product_count\" > $2 GROUP BY \"products\".\"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe(['active', 3]); @@ -109,9 +109,9 @@ [$dml, $params] = $sql; - $expected = "SELECT SUM(orders.total) AS total_sales, orders.customer_id " - . "FROM orders " - . "HAVING total_sales < $1 GROUP BY orders.customer_id"; + $expected = "SELECT SUM(\"orders\".\"total\") AS \"total_sales\", \"orders\".\"customer_id\" " + . "FROM \"orders\" " + . "HAVING \"total_sales\" < $1 GROUP BY \"orders\".\"customer_id\""; expect($dml)->toBe($expected); expect($params)->toBe([1000]); @@ -133,10 +133,34 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS product_count, products.category_id " - . "FROM products " - . "HAVING product_count = $1 GROUP BY products.category_id"; + $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " + . "FROM \"products\" " + . "HAVING \"product_count\" = $1 GROUP BY \"products\".\"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe([10]); }); + +it('generates a query using having with date clause', function () { + $query = new QueryGenerator(Driver::POSTGRESQL); + + $sql = $query->select([ + Functions::count('products.id')->as('product_count'), + 'products.created_at', + ]) + ->from('products') + ->groupBy('products.created_at') + ->having(function (Having $having): void { + $having->whereDateEqual('products.created_at', '2026-01-15'); + }) + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"created_at\" " + . "FROM \"products\" " + . "HAVING DATE(\"products\".\"created_at\") = $1 GROUP BY \"products\".\"created_at\""; + + expect($dml)->toBe($expected); + expect($params)->toBe(['2026-01-15']); +}); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/InsertIntoStatementTest.php b/tests/Unit/Database/QueryGenerator/Postgres/InsertIntoStatementTest.php index 4204a7fc..d536408c 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/InsertIntoStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/InsertIntoStatementTest.php @@ -22,7 +22,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES ($1, $2)"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\") VALUES ($1, $2)"; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -48,7 +48,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES ($1, $2), ($3, $4)"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\") VALUES ($1, $2), ($3, $4)"; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name, $email, $name]); @@ -68,7 +68,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES ($1, $2) ON CONFLICT DO NOTHING"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\") VALUES ($1, $2) ON CONFLICT DO NOTHING"; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -89,7 +89,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES ($1, $2) RETURNING id"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\") VALUES ($1, $2) RETURNING \"id\""; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -110,7 +110,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING id, email"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\") VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING \"id\", \"email\""; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -130,8 +130,8 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES ($1, $2) " - . "ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\") VALUES ($1, $2) " + . "ON CONFLICT (\"name\") DO UPDATE SET \"name\" = EXCLUDED.\"name\""; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -151,8 +151,8 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name, username) VALUES ($1, $2, $3) " - . "ON CONFLICT (name, username) DO UPDATE SET name = EXCLUDED.name, username = EXCLUDED.username"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\", \"username\") VALUES ($1, $2, $3) " + . "ON CONFLICT (\"name\", \"username\") DO UPDATE SET \"name\" = EXCLUDED.\"name\", \"username\" = EXCLUDED.\"username\""; \ksort($data); @@ -172,7 +172,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (name, email) SELECT name, email FROM customers WHERE verified_at IS NOT NULL"; + $expected = "INSERT INTO \"users\" (\"name\", \"email\") SELECT \"name\", \"email\" FROM \"customers\" WHERE \"verified_at\" IS NOT NULL"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -190,8 +190,8 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (name, email) " - . "SELECT name, email FROM customers WHERE verified_at IS NOT NULL " + $expected = "INSERT INTO \"users\" (\"name\", \"email\") " + . "SELECT \"name\", \"email\" FROM \"customers\" WHERE \"verified_at\" IS NOT NULL " . "ON CONFLICT DO NOTHING"; expect($dml)->toBe($expected); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php b/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php index ecfdc834..0e40b803 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php @@ -23,10 +23,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "{$joinType} categories " - . "ON products.category_id = categories.id"; + $expected = "SELECT \"products\".\"id\", \"products\".\"description\", \"categories\".\"description\" " + . "FROM \"products\" " + . "{$joinType} \"categories\" " + . "ON \"products\".\"category_id\" = \"categories\".\"id\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -55,10 +55,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "INNER JOIN categories " - . "ON products.category_id != categories.id"; + $expected = "SELECT \"products\".\"id\", \"products\".\"description\", \"categories\".\"description\" " + . "FROM \"products\" " + . "INNER JOIN \"categories\" " + . "ON \"products\".\"category_id\" != \"categories\".\"id\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -86,10 +86,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "INNER JOIN categories " - . "ON products.category_id = categories.id {$clause}"; + $expected = "SELECT \"products\".\"id\", \"products\".\"description\", \"categories\".\"description\" " + . "FROM \"products\" " + . "INNER JOIN \"categories\" " + . "ON \"products\".\"category_id\" = \"categories\".\"id\" {$clause}"; expect($dml)->toBe($expected); expect($params)->toBe($joinParams); @@ -97,25 +97,25 @@ [ 'orOnEqual', ['products.location_id', 'categories.location_id'], - 'OR products.location_id = categories.location_id', + 'OR "products"."location_id" = "categories"."location_id"', [], ], [ 'whereEqual', ['categories.name', 'php'], - 'AND categories.name = $1', + 'AND "categories"."name" = $1', ['php'], ], [ 'orOnNotEqual', ['products.location_id', 'categories.location_id'], - 'OR products.location_id != categories.location_id', + 'OR "products"."location_id" != "categories"."location_id"', [], ], [ 'orWhereEqual', ['categories.name', 'php'], - 'OR categories.name = $1', + 'OR "categories"."name" = $1', ['php'], ], ]); @@ -134,10 +134,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "{$joinType} categories " - . "ON products.category_id = categories.id"; + $expected = "SELECT \"products\".\"id\", \"products\".\"description\", \"categories\".\"description\" " + . "FROM \"products\" " + . "{$joinType} \"categories\" " + . "ON \"products\".\"category_id\" = \"categories\".\"id\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -147,6 +147,31 @@ ['rightJoinOnEqual', JoinType::RIGHT->value], ]); +it('generates query with join date clause', function () { + $query = new QueryGenerator(Driver::POSTGRESQL); + + $sql = $query->select([ + 'products.id', + 'categories.description', + ]) + ->from('products') + ->innerJoin('categories', function (Join $join) { + $join->onEqual('products.category_id', 'categories.id') + ->whereDateEqual('categories.created_at', '2026-01-15'); + }) + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT \"products\".\"id\", \"categories\".\"description\" " + . "FROM \"products\" " + . "INNER JOIN \"categories\" " + . "ON \"products\".\"category_id\" = \"categories\".\"id\" AND DATE(\"categories\".\"created_at\") = $1"; + + expect($dml)->toBe($expected); + expect($params)->toBe(['2026-01-15']); +}); + it('generates query with multiple joins', function () { $query = new QueryGenerator(Driver::POSTGRESQL); @@ -166,10 +191,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, categories.name, suppliers.name " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " - . "LEFT JOIN suppliers ON products.supplier_id = suppliers.id"; + $expected = "SELECT \"products\".\"id\", \"categories\".\"name\", \"suppliers\".\"name\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " + . "LEFT JOIN \"suppliers\" ON \"products\".\"supplier_id\" = \"suppliers\".\"id\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -191,10 +216,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, categories.name " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " - . "WHERE products.status = $1"; + $expected = "SELECT \"products\".\"id\", \"categories\".\"name\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " + . "WHERE \"products\".\"status\" = $1"; expect($dml)->toBe($expected); expect($params)->toBe(['active']); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/PaginateTest.php b/tests/Unit/Database/QueryGenerator/Postgres/PaginateTest.php index 6f782397..6de35d34 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/PaginateTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/PaginateTest.php @@ -15,7 +15,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 15 OFFSET 0'); + expect($dml)->toBe('SELECT * FROM "users" LIMIT 15 OFFSET 0'); expect($params)->toBeEmpty(); }); @@ -28,7 +28,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 15 OFFSET 30'); + expect($dml)->toBe('SELECT * FROM "users" LIMIT 15 OFFSET 30'); expect($params)->toBeEmpty(); }); @@ -42,7 +42,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 15 OFFSET 15'); + expect($dml)->toBe('SELECT * FROM "users" LIMIT 15 OFFSET 15'); expect($params)->toBeEmpty(); }); @@ -56,7 +56,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users WHERE status = $1 LIMIT 15 OFFSET 15'); + expect($dml)->toBe('SELECT * FROM "users" WHERE "status" = $1 LIMIT 15 OFFSET 15'); expect($params)->toBe(['active']); }); @@ -70,7 +70,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users ORDER BY created_at ASC LIMIT 15 OFFSET 0'); + expect($dml)->toBe('SELECT * FROM "users" ORDER BY "created_at" ASC LIMIT 15 OFFSET 0'); expect($params)->toBeEmpty(); }); @@ -83,6 +83,6 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 25 OFFSET 25'); + expect($dml)->toBe('SELECT * FROM "users" LIMIT 25 OFFSET 25'); expect($params)->toBeEmpty(); }); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php index 1bb05d0d..1f10b284 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php @@ -10,7 +10,6 @@ use Phenix\Database\Functions; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; -use Phenix\Database\Value; it('generates query to select all columns of table', function () { $query = new QueryGenerator(Driver::POSTGRESQL); @@ -22,7 +21,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users'); + expect($dml)->toBe('SELECT * FROM "users"'); expect($params)->toBeEmpty(); }); @@ -36,7 +35,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users'); + expect($dml)->toBe('SELECT * FROM "users"'); expect($params)->toBeEmpty(); }); @@ -49,14 +48,14 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT {$rawFunction} FROM products"); + expect($dml)->toBe("SELECT {$rawFunction} FROM \"products\""); expect($params)->toBeEmpty(); })->with([ - ['avg', 'price', 'AVG(price)'], - ['sum', 'price', 'SUM(price)'], - ['min', 'price', 'MIN(price)'], - ['max', 'price', 'MAX(price)'], - ['count', 'id', 'COUNT(id)'], + ['avg', 'price', 'AVG("price")'], + ['sum', 'price', 'SUM("price")'], + ['min', 'price', 'MIN("price")'], + ['max', 'price', 'MAX("price")'], + ['count', 'id', 'COUNT("id")'], ]); it('generates a query using sql functions with alias', function ( @@ -73,14 +72,14 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT {$rawFunction} FROM products"); + expect($dml)->toBe("SELECT {$rawFunction} FROM \"products\""); expect($params)->toBeEmpty(); })->with([ - ['avg', 'price', 'value', 'AVG(price) AS value'], - ['sum', 'price', 'value', 'SUM(price) AS value'], - ['min', 'price', 'value', 'MIN(price) AS value'], - ['max', 'price', 'value', 'MAX(price) AS value'], - ['count', 'id', 'value', 'COUNT(id) AS value'], + ['avg', 'price', 'value', 'AVG("price") AS "value"'], + ['sum', 'price', 'value', 'SUM("price") AS "value"'], + ['min', 'price', 'value', 'MIN("price") AS "value"'], + ['max', 'price', 'value', 'MAX("price") AS "value"'], + ['count', 'id', 'value', 'COUNT("id") AS "value"'], ]); it('selects field from subquery', function () { @@ -96,7 +95,7 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, email FROM (SELECT * FROM users WHERE verified_at = $1)"; + $expected = "SELECT \"id\", \"name\", \"email\" FROM (SELECT * FROM \"users\" WHERE \"verified_at\" = $1)"; expect($dml)->toBe($expected); expect($params)->toBe([$date]); @@ -120,8 +119,8 @@ [$dml, $params] = $sql; - $subquery = "SELECT name FROM countries WHERE users.country_id = countries.id LIMIT 1"; - $expected = "SELECT id, name, ({$subquery}) AS country_name FROM users"; + $subquery = "SELECT \"name\" FROM \"countries\" WHERE \"users\".\"country_id\" = \"countries\".\"id\" LIMIT 1"; + $expected = "SELECT \"id\", \"name\", ({$subquery}) AS \"country_name\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -156,7 +155,7 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name AS full_name FROM users"; + $expected = "SELECT \"id\", \"name\" AS \"full_name\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -174,7 +173,7 @@ [$dml, $params] = $sql; - $expected = "SELECT id AS model_id, name AS full_name FROM users"; + $expected = "SELECT \"id\" AS \"model_id\", \"name\" AS \"full_name\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -188,8 +187,6 @@ ) { [$column, $value, $result] = $data; - $value = Value::from($value); - $query = new QueryGenerator(Driver::POSTGRESQL); $case = Functions::case() @@ -207,8 +204,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, description, (CASE WHEN {$column} {$operator} {$value} " - . "THEN {$result} ELSE $defaultResult END) AS type FROM products"; + $expected = "SELECT \"id\", \"description\", (CASE WHEN \"{$column}\" {$operator} {$value} " + . "THEN '{$result}' ELSE '{$defaultResult}' END) AS \"type\" FROM \"products\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -246,8 +243,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, (CASE WHEN {$column} {$operator} " - . "THEN {$result} ELSE $defaultResult END) AS status FROM users"; + $expected = "SELECT \"id\", \"name\", (CASE WHEN \"{$column}\" {$operator} " + . "THEN '{$result}' ELSE '{$defaultResult}' END) AS \"status\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -264,9 +261,9 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $case = Functions::case() - ->whenNull('created_at', Value::from('inactive')) - ->whenGreaterThan('created_at', Value::from($date), Value::from('new user')) - ->defaultResult(Value::from('old user')) + ->whenNull('created_at', 'inactive') + ->whenGreaterThan('created_at', $date, 'new user') + ->defaultResult('old user') ->as('status'); $sql = $query->select([ @@ -279,8 +276,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, (CASE WHEN created_at IS NULL THEN 'inactive' " - . "WHEN created_at > '{$date}' THEN 'new user' ELSE 'old user' END) AS status FROM users"; + $expected = "SELECT \"id\", \"name\", (CASE WHEN \"created_at\" IS NULL THEN 'inactive' " + . "WHEN \"created_at\" > '{$date}' THEN 'new user' ELSE 'old user' END) AS \"status\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -292,8 +289,8 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $case = Functions::case() - ->whenNull('created_at', Value::from('inactive')) - ->whenGreaterThan('created_at', Value::from($date), Value::from('new user')) + ->whenNull('created_at', 'inactive') + ->whenGreaterThan('created_at', $date, 'new user') ->as('status'); $sql = $query->select([ @@ -306,8 +303,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, (CASE WHEN created_at IS NULL THEN 'inactive' " - . "WHEN created_at > '{$date}' THEN 'new user' END) AS status FROM users"; + $expected = "SELECT \"id\", \"name\", (CASE WHEN \"created_at\" IS NULL THEN 'inactive' " + . "WHEN \"created_at\" > '{$date}' THEN 'new user' END) AS \"status\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -317,8 +314,8 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $case = Functions::case() - ->whenGreaterThanOrEqual(Functions::avg('price'), 4, Value::from('expensive')) - ->defaultResult(Value::from('cheap')) + ->whenGreaterThanOrEqual(Functions::avg('price'), 4, 'expensive') + ->defaultResult('cheap') ->as('message'); $sql = $query->select([ @@ -332,8 +329,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, description, price, (CASE WHEN AVG(price) >= 4 THEN 'expensive' ELSE 'cheap' END) " - . "AS message FROM products"; + $expected = "SELECT \"id\", \"description\", \"price\", (CASE WHEN AVG(\"price\") >= 4 THEN 'expensive' ELSE 'cheap' END) " + . "AS \"message\" FROM \"products\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -346,7 +343,7 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(*) FROM products"; + $expected = "SELECT COUNT(*) FROM \"products\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -362,7 +359,7 @@ [$dml, $params] = $sql; $expected = "SELECT EXISTS" - . " (SELECT 1 FROM products WHERE id = $1) AS 'exists'"; + . " (SELECT 1 FROM \"products\" WHERE \"id\" = $1) AS \"exists\""; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -378,7 +375,7 @@ [$dml, $params] = $sql; $expected = "SELECT NOT EXISTS" - . " (SELECT 1 FROM products WHERE id = $1) AS 'exists'"; + . " (SELECT 1 FROM \"products\" WHERE \"id\" = $1) AS \"exists\""; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -393,7 +390,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM products WHERE id = $1 LIMIT 1"; + $expected = "SELECT * FROM \"products\" WHERE \"id\" = $1 LIMIT 1"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -408,7 +405,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users'); + expect($dml)->toBe('SELECT * FROM "users"'); expect($params)->toBeEmpty(); }); @@ -422,7 +419,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR UPDATE"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR UPDATE"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -438,7 +435,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR SHARE"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR SHARE"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -454,7 +451,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR UPDATE SKIP LOCKED"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR UPDATE SKIP LOCKED"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -470,7 +467,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR UPDATE NOWAIT"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR UPDATE NOWAIT"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -486,7 +483,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR UPDATE SKIP LOCKED"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR UPDATE SKIP LOCKED"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -504,7 +501,7 @@ [$dml, $params] = $builder->get(); - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -520,7 +517,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR NO KEY UPDATE"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR NO KEY UPDATE"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -536,7 +533,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR KEY SHARE"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR KEY SHARE"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -552,7 +549,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR SHARE SKIP LOCKED"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR SHARE SKIP LOCKED"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -568,7 +565,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR SHARE NOWAIT"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR SHARE NOWAIT"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -584,7 +581,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR NO KEY UPDATE SKIP LOCKED"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR NO KEY UPDATE SKIP LOCKED"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -600,7 +597,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR NO KEY UPDATE NOWAIT"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL FOR NO KEY UPDATE NOWAIT"; expect($dml)->toBe($expected); expect($params)->toBe([]); diff --git a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php index 541b5047..e9341b00 100644 --- a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php @@ -3,16 +3,13 @@ declare(strict_types=1); use Phenix\Database\Alias; -use Phenix\Database\Constants\Driver; -use Phenix\Database\Constants\Lock; use Phenix\Database\Constants\Operator; use Phenix\Database\Exceptions\QueryErrorException; use Phenix\Database\Functions; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; -use Phenix\Database\Value; -it('generates query to select all columns of table', function () { +it('generates query to select all columns of table', function (): void { $query = new QueryGenerator(); $sql = $query->table('users') @@ -22,11 +19,11 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users'); + expect($dml)->toBe('SELECT * FROM `users`'); expect($params)->toBeEmpty(); }); -it('generates query to select all columns from table', function () { +it('generates query to select all columns from table', function (): void { $query = new QueryGenerator(); $sql = $query->from('users') @@ -36,7 +33,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users'); + expect($dml)->toBe('SELECT * FROM `users`'); expect($params)->toBeEmpty(); }); @@ -49,14 +46,14 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT {$rawFunction} FROM products"); + expect($dml)->toBe("SELECT {$rawFunction} FROM `products`"); expect($params)->toBeEmpty(); })->with([ - ['avg', 'price', 'AVG(price)'], - ['sum', 'price', 'SUM(price)'], - ['min', 'price', 'MIN(price)'], - ['max', 'price', 'MAX(price)'], - ['count', 'id', 'COUNT(id)'], + ['avg', 'price', 'AVG(`price`)'], + ['sum', 'price', 'SUM(`price`)'], + ['min', 'price', 'MIN(`price`)'], + ['max', 'price', 'MAX(`price`)'], + ['count', 'id', 'COUNT(`id`)'], ]); it('generates a query using sql functions with alias', function ( @@ -73,14 +70,14 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT {$rawFunction} FROM products"); + expect($dml)->toBe("SELECT {$rawFunction} FROM `products`"); expect($params)->toBeEmpty(); })->with([ - ['avg', 'price', 'value', 'AVG(price) AS value'], - ['sum', 'price', 'value', 'SUM(price) AS value'], - ['min', 'price', 'value', 'MIN(price) AS value'], - ['max', 'price', 'value', 'MAX(price) AS value'], - ['count', 'id', 'value', 'COUNT(id) AS value'], + ['avg', 'price', 'value', 'AVG(`price`) AS `value`'], + ['sum', 'price', 'value', 'SUM(`price`) AS `value`'], + ['min', 'price', 'value', 'MIN(`price`) AS `value`'], + ['max', 'price', 'value', 'MAX(`price`) AS `value`'], + ['count', 'id', 'value', 'COUNT(`id`) AS `value`'], ]); it('selects field from subquery', function () { @@ -96,7 +93,7 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, email FROM (SELECT * FROM users WHERE verified_at = ?)"; + $expected = "SELECT `id`, `name`, `email` FROM (SELECT * FROM `users` WHERE `verified_at` = ?)"; expect($dml)->toBe($expected); expect($params)->toBe([$date]); @@ -120,8 +117,8 @@ [$dml, $params] = $sql; - $subquery = "SELECT name FROM countries WHERE users.country_id = countries.id LIMIT 1"; - $expected = "SELECT id, name, ({$subquery}) AS country_name FROM users"; + $subquery = "SELECT `name` FROM `countries` WHERE `users`.`country_id` = `countries`.`id` LIMIT 1"; + $expected = "SELECT `id`, `name`, ({$subquery}) AS `country_name` FROM `users`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -156,7 +153,7 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name AS full_name FROM users"; + $expected = "SELECT `id`, `name` AS `full_name` FROM `users`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -174,7 +171,7 @@ [$dml, $params] = $sql; - $expected = "SELECT id AS model_id, name AS full_name FROM users"; + $expected = "SELECT `id` AS `model_id`, `name` AS `full_name` FROM `users`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -188,8 +185,6 @@ ) { [$column, $value, $result] = $data; - $value = Value::from($value); - $query = new QueryGenerator(); $case = Functions::case() @@ -207,8 +202,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, description, (CASE WHEN {$column} {$operator} {$value} " - . "THEN {$result} ELSE $defaultResult END) AS type FROM products"; + $expected = "SELECT `id`, `description`, (CASE WHEN `{$column}` {$operator} {$value} " + . "THEN '{$result}' ELSE '{$defaultResult}' END) AS `type` FROM `products`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -246,8 +241,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, (CASE WHEN {$column} {$operator} " - . "THEN {$result} ELSE $defaultResult END) AS status FROM users"; + $expected = "SELECT `id`, `name`, (CASE WHEN `{$column}` {$operator} " + . "THEN '{$result}' ELSE '{$defaultResult}' END) AS `status` FROM `users`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -264,9 +259,9 @@ $query = new QueryGenerator(); $case = Functions::case() - ->whenNull('created_at', Value::from('inactive')) - ->whenGreaterThan('created_at', Value::from($date), Value::from('new user')) - ->defaultResult(Value::from('old user')) + ->whenNull('created_at', 'inactive') + ->whenGreaterThan('created_at', $date, 'new user') + ->defaultResult('old user') ->as('status'); $sql = $query->select([ @@ -279,8 +274,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, (CASE WHEN created_at IS NULL THEN 'inactive' " - . "WHEN created_at > '{$date}' THEN 'new user' ELSE 'old user' END) AS status FROM users"; + $expected = "SELECT `id`, `name`, (CASE WHEN `created_at` IS NULL THEN 'inactive' " + . "WHEN `created_at` > '{$date}' THEN 'new user' ELSE 'old user' END) AS `status` FROM `users`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -292,8 +287,8 @@ $query = new QueryGenerator(); $case = Functions::case() - ->whenNull('created_at', Value::from('inactive')) - ->whenGreaterThan('created_at', Value::from($date), Value::from('new user')) + ->whenNull('created_at', 'inactive') + ->whenGreaterThan('created_at', $date, 'new user') ->as('status'); $sql = $query->select([ @@ -306,8 +301,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, (CASE WHEN created_at IS NULL THEN 'inactive' " - . "WHEN created_at > '{$date}' THEN 'new user' END) AS status FROM users"; + $expected = "SELECT `id`, `name`, (CASE WHEN `created_at` IS NULL THEN 'inactive' " + . "WHEN `created_at` > '{$date}' THEN 'new user' END) AS `status` FROM `users`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -317,8 +312,8 @@ $query = new QueryGenerator(); $case = Functions::case() - ->whenGreaterThanOrEqual(Functions::avg('price'), 4, Value::from('expensive')) - ->defaultResult(Value::from('cheap')) + ->whenGreaterThanOrEqual(Functions::avg('price'), 4, 'expensive') + ->defaultResult('cheap') ->as('message'); $sql = $query->select([ @@ -332,8 +327,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, description, price, (CASE WHEN AVG(price) >= 4 THEN 'expensive' ELSE 'cheap' END) " - . "AS message FROM products"; + $expected = "SELECT `id`, `description`, `price`, (CASE WHEN AVG(`price`) >= 4 THEN 'expensive' ELSE 'cheap' END) " + . "AS `message` FROM `products`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -346,7 +341,7 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(*) FROM products"; + $expected = "SELECT COUNT(*) FROM `products`"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -362,7 +357,7 @@ [$dml, $params] = $sql; $expected = "SELECT EXISTS" - . " (SELECT 1 FROM products WHERE id = ?) AS 'exists'"; + . " (SELECT 1 FROM `products` WHERE `id` = ?) AS `exists`"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -378,7 +373,7 @@ [$dml, $params] = $sql; $expected = "SELECT NOT EXISTS" - . " (SELECT 1 FROM products WHERE id = ?) AS 'exists'"; + . " (SELECT 1 FROM `products` WHERE `id` = ?) AS `exists`"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -393,7 +388,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM products WHERE id = ? LIMIT 1"; + $expected = "SELECT * FROM `products` WHERE `id` = ? LIMIT 1"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -408,7 +403,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users'); + expect($dml)->toBe('SELECT * FROM `users`'); expect($params)->toBeEmpty(); }); @@ -422,7 +417,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR UPDATE"; + $expected = "SELECT * FROM `tasks` WHERE `reserved_at` IS NULL FOR UPDATE"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -438,7 +433,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR SHARE"; + $expected = "SELECT * FROM `tasks` WHERE `reserved_at` IS NULL FOR SHARE"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -454,237 +449,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR UPDATE SKIP LOCKED"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for update for postgresql', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForUpdate() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR UPDATE"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for update no wait', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForUpdateNoWait() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR UPDATE NOWAIT"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for update skip locked for postgresql', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForUpdateSkipLocked() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR UPDATE SKIP LOCKED"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for update skip locked for postgresql using constants', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lock(Lock::FOR_UPDATE_SKIP_LOCKED) - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR UPDATE SKIP LOCKED"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('remove locks from query', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $builder = $query->from('tasks') - ->whereNull('reserved_at') - ->lock(Lock::FOR_UPDATE_SKIP_LOCKED) - ->unlock(); - - expect($builder->isLocked())->toBeFalse(); - - [$dml, $params] = $builder->get(); - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for share for postgresql', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForShare() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR SHARE"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for no key update for postgresql', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForNoKeyUpdate() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR NO KEY UPDATE"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for key share for postgresql', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForKeyShare() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR KEY SHARE"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for share skip locked for postgresql', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForShareSkipLocked() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR SHARE SKIP LOCKED"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for share no wait for postgresql', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForShareNoWait() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR SHARE NOWAIT"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for no key update skip locked for postgresql', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForNoKeyUpdateSkipLocked() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR NO KEY UPDATE SKIP LOCKED"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('generate query with lock for no key update no wait for postgresql', function () { - $query = new QueryGenerator(Driver::POSTGRESQL); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForNoKeyUpdateNoWait() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL FOR NO KEY UPDATE NOWAIT"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('tries to generate lock using sqlite', function () { - $query = new QueryGenerator(Driver::SQLITE); - - expect($query->getDriver())->toBe(Driver::SQLITE); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lockForNoKeyUpdateNoWait() - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL"; - - expect($dml)->toBe($expected); - expect($params)->toBe([]); -}); - -it('tries to generate lock using sqlite with constants', function () { - $query = new QueryGenerator(Driver::SQLITE); - - expect($query->getDriver())->toBe(Driver::SQLITE); - - $sql = $query->from('tasks') - ->whereNull('reserved_at') - ->lock(Lock::FOR_NO_KEY_UPDATE) - ->get(); - - [$dml, $params] = $sql; - - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL"; + $expected = "SELECT * FROM `tasks` WHERE `reserved_at` IS NULL FOR UPDATE SKIP LOCKED"; expect($dml)->toBe($expected); expect($params)->toBe([]); diff --git a/tests/Unit/Database/QueryGenerator/UpdateStatementTest.php b/tests/Unit/Database/QueryGenerator/UpdateStatementTest.php index 7f22f06c..e1ed9b5b 100644 --- a/tests/Unit/Database/QueryGenerator/UpdateStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/UpdateStatementTest.php @@ -17,7 +17,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = ? WHERE id = ?"; + $expected = "UPDATE `users` SET `name` = ? WHERE `id` = ?"; expect($dml)->toBe($expected); expect($params)->toBe([$name, 1]); @@ -35,7 +35,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = ?, active = ? WHERE verified_at IS NOT NULL AND role_id = ?"; + $expected = "UPDATE `users` SET `name` = ?, `active` = ? WHERE `verified_at` IS NOT NULL AND `role_id` = ?"; expect($dml)->toBe($expected); expect($params)->toBe([$name, true, 2]); diff --git a/tests/Unit/Database/QueryGenerator/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/WhereClausesTest.php index 3b1d8d01..32c1b5bf 100644 --- a/tests/Unit/Database/QueryGenerator/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/WhereClausesTest.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use Phenix\Database\Clauses\RowWhereClause; +use Phenix\Database\Clauses\SubqueryWhereClause; use Phenix\Database\Constants\Operator; use Phenix\Database\Constants\Order; use Phenix\Database\Functions; @@ -19,7 +21,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users WHERE id = ?'); + expect($dml)->toBe('SELECT * FROM `users` WHERE `id` = ?'); expect($params)->toBe([1]); }); @@ -36,7 +38,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users WHERE username = ? AND email = ? AND document = ?'); + expect($dml)->toBe('SELECT * FROM `users` WHERE `username` = ? AND `email` = ? AND `document` = ?'); expect($params)->toBe(['john', 'john@mail.com', 123456]); }); @@ -54,7 +56,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE {$column} {$operator} ?"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `{$column}` {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['whereNotEqual', 'id', Operator::NOT_EQUAL->value, 1], @@ -74,7 +76,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT id, name, email FROM users WHERE id = ?'); + expect($dml)->toBe('SELECT `id`, `name`, `email` FROM `users` WHERE `id` = ?'); expect($params)->toBe([1]); }); @@ -88,7 +90,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE id {$operator} (?, ?, ?)"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `id` {$operator} (?, ?, ?)"); expect($params)->toBe([1, 2, 3]); })->with([ ['whereIn', Operator::IN->value], @@ -110,8 +112,8 @@ $date = date('Y-m-d'); - $expected = "SELECT * FROM users WHERE id {$operator} " - . "(SELECT id FROM users WHERE created_at >= ?)"; + $expected = "SELECT * FROM `users` WHERE `id` {$operator} " + . "(SELECT `id` FROM `users` WHERE `created_at` >= ?)"; expect($dml)->toBe($expected); expect($params)->toBe([$date]); @@ -129,7 +131,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE verified_at {$operator}"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `verified_at` {$operator}"); expect($params)->toBe([]); })->with([ ['whereNull', Operator::IS_NULL->value], @@ -148,7 +150,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > ? OR verified_at {$operator}"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `created_at` > ? OR `verified_at` {$operator}"); expect($params)->toBe([$date]); })->with([ ['orWhereNull', Operator::IS_NULL->value], @@ -164,7 +166,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE enabled {$operator}"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `enabled` {$operator}"); expect($params)->toBe([]); })->with([ ['whereTrue', Operator::IS_TRUE->value], @@ -183,7 +185,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > ? OR enabled {$operator}"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `created_at` > ? OR `enabled` {$operator}"); expect($params)->toBe([$date]); })->with([ ['orWhereTrue', Operator::IS_TRUE->value], @@ -205,7 +207,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE verified_at IS NOT NULL AND created_at > ? OR updated_at < ?"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `verified_at` IS NOT NULL AND `created_at` > ? OR `updated_at` < ?"); expect($params)->toBe([$date, $date]); }); @@ -224,7 +226,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > ? OR updated_at < ? AND verified_at IS NOT NULL"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `created_at` > ? OR `updated_at` < ? AND `verified_at` IS NOT NULL"); expect($params)->toBe([$date, $date]); }); @@ -253,7 +255,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE verified_at IS NOT NULL OR {$column} {$operator} {$placeholders}"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `verified_at` IS NOT NULL OR `{$column}` {$operator} {$placeholders}"); expect($params)->toBe([...(array)$value]); })->with([ ['orWhereLessThan', 'updated_at', date('Y-m-d'), Operator::LESS_THAN->value], @@ -276,7 +278,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE age {$operator} ? AND ?"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `age` {$operator} ? AND ?"); expect($params)->toBe([20, 30]); })->with([ ['whereBetween', Operator::BETWEEN->value], @@ -297,7 +299,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > ? OR updated_at {$operator} ? AND ?"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `created_at` > ? OR `updated_at` {$operator} ? AND ?"); expect($params)->toBe([$date, $startDate, $endDate]); })->with([ ['orWhereBetween', Operator::BETWEEN->value], @@ -315,9 +317,12 @@ $operator = Operator::ORDER_BY->value; - $column = implode(', ', (array) $column); + $column = implode(', ', array_map( + fn (string $column): string => '`' . str_replace('.', '`.`', $column) . '`', + (array) $column + )); - expect($dml)->toBe("SELECT * FROM users {$operator} {$column} {$order}"); + expect($dml)->toBe("SELECT * FROM `users` {$operator} {$column} {$order}"); expect($params)->toBe($params); })->with([ ['id', Order::ASC->value], @@ -339,7 +344,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users ORDER BY (CASE WHEN city IS NULL THEN country ELSE city END) ASC"); + expect($dml)->toBe("SELECT * FROM `users` ORDER BY (CASE WHEN `city` IS NULL THEN 'country' ELSE 'city' END) ASC"); expect($params)->toBe($params); }); @@ -356,9 +361,12 @@ $operator = Operator::ORDER_BY->value; - $column = implode(', ', (array) $column); + $column = implode(', ', array_map( + fn (string $column): string => '`' . str_replace('.', '`.`', $column) . '`', + (array) $column + )); - expect($dml)->toBe("SELECT * FROM users WHERE id = ? {$operator} {$column} {$order} LIMIT 1"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `id` = ? {$operator} {$column} {$order} LIMIT 1"); expect($params)->toBe([1]); })->with([ ['id', Order::ASC->value], @@ -381,8 +389,8 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM users WHERE {$operator} " - . "(SELECT * FROM user_role WHERE user_id = ? AND role_id = ? LIMIT 1)"; + $expected = "SELECT * FROM `users` WHERE {$operator} " + . "(SELECT * FROM `user_role` WHERE `user_id` = ? AND `role_id` = ? LIMIT 1)"; expect($dml)->toBe($expected); expect($params)->toBe([1, 9]); @@ -408,8 +416,8 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM users WHERE is_admin IS TRUE OR {$operator} " - . "(SELECT * FROM user_role WHERE user_id = ? LIMIT 1)"; + $expected = "SELECT * FROM `users` WHERE `is_admin` IS TRUE OR {$operator} " + . "(SELECT * FROM `user_role` WHERE `user_id` = ? LIMIT 1)"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -433,8 +441,8 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM products WHERE {$column} {$operator} " - . '(SELECT ' . Functions::max('price') . ' FROM products)'; + $expected = "SELECT * FROM `products` WHERE `{$column}` {$operator} " + . '(SELECT MAX(`price`) FROM `products`)'; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -465,8 +473,8 @@ [$dml, $params] = $sql; - $expected = "SELECT description FROM products WHERE id {$comparisonOperator} {$operator}" - . "(SELECT product_id FROM orders WHERE quantity > ?)"; + $expected = "SELECT `description` FROM `products` WHERE `id` {$comparisonOperator} {$operator}" + . "(SELECT `product_id` FROM `orders` WHERE `quantity` > ?)"; expect($dml)->toBe($expected); expect($params)->toBe([10]); @@ -498,7 +506,7 @@ $sql = $query->table('employees') ->{$method}(['manager_id', 'department_id'], function (Subquery $subquery) { - $subquery->select(['id, department_id']) + $subquery->select(['id', 'department_id']) ->from('managers') ->whereEqual('location_id', 1); }) @@ -507,10 +515,10 @@ [$dml, $params] = $sql; - $subquery = 'SELECT id, department_id FROM managers WHERE location_id = ?'; + $subquery = 'SELECT `id`, `department_id` FROM `managers` WHERE `location_id` = ?'; - $expected = "SELECT name FROM employees " - . "WHERE ROW(manager_id, department_id) {$operator} ({$subquery})"; + $expected = "SELECT `name` FROM `employees` " + . "WHERE ROW(`manager_id`, `department_id`) {$operator} ({$subquery})"; expect($dml)->toBe($expected); expect($params)->toBe([1]); diff --git a/tests/Unit/Database/QueryGenerator/WhereDateClausesTest.php b/tests/Unit/Database/QueryGenerator/WhereDateClausesTest.php index d8944ac8..f8a5dde8 100644 --- a/tests/Unit/Database/QueryGenerator/WhereDateClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/WhereDateClausesTest.php @@ -4,6 +4,8 @@ use Carbon\Carbon; use Carbon\CarbonInterface; +use Phenix\Database\Clauses\BasicWhereClause; +use Phenix\Database\Clauses\DateWhereClause; use Phenix\Database\Constants\Operator; use Phenix\Database\QueryGenerator; @@ -23,7 +25,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE DATE(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM `users` WHERE DATE(`created_at`) {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['whereDateEqual', Carbon::now(), Carbon::now()->format('Y-m-d'), Operator::EQUAL->value], @@ -51,7 +53,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE active IS FALSE OR DATE(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `active` IS FALSE OR DATE(`created_at`) {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['orWhereDateEqual', date('Y-m-d'), date('Y-m-d'), Operator::EQUAL->value], @@ -77,7 +79,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE MONTH(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM `users` WHERE MONTH(`created_at`) {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['whereMonthEqual', Carbon::now(), Carbon::now()->format('m'), Operator::EQUAL->value], @@ -105,7 +107,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE active IS FALSE OR MONTH(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `active` IS FALSE OR MONTH(`created_at`) {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['orWhereMonthEqual', Carbon::now(), Carbon::now()->format('m'), Operator::EQUAL->value], @@ -132,7 +134,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE YEAR(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM `users` WHERE YEAR(`created_at`) {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['whereYearEqual', Carbon::now(), Carbon::now()->format('Y'), Operator::EQUAL->value], @@ -160,7 +162,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE active IS FALSE OR YEAR(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM `users` WHERE `active` IS FALSE OR YEAR(`created_at`) {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['orWhereYearEqual', Carbon::now(), Carbon::now()->format('Y'), Operator::EQUAL->value], @@ -170,3 +172,16 @@ ['orWhereYearLessThan', date('Y'), date('Y'), Operator::LESS_THAN->value], ['orWhereYearLessThanOrEqual', date('Y'), date('Y'), Operator::LESS_THAN_OR_EQUAL->value], ]); + +it('stores date where clauses as DateWhereClause instances', function () { + $query = new QueryGenerator(); + $query->table('users')->whereDateEqual('created_at', '2026-01-15'); + + $reflection = new ReflectionClass($query); + $property = $reflection->getProperty('clauses'); + $clauses = $property->getValue($query); + + expect($clauses)->toHaveCount(1); + expect($clauses[0])->toBeInstanceOf(DateWhereClause::class); + expect($clauses[0])->not->toBeInstanceOf(BasicWhereClause::class); +}); From 65e28ef27339e67aba5543b62624c54be05c0864 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Wed, 29 Apr 2026 15:30:16 -0500 Subject: [PATCH 02/32] fix: add enclosure for sql identifiers --- .../Postgres/UpdateStatementTest.php | 32 ++--- .../Postgres/WhereClausesTest.php | 72 +++++----- .../Postgres/WhereDateClausesTest.php | 12 +- .../Sqlite/DeleteStatementTest.php | 26 ++-- .../Sqlite/GroupByStatementTest.php | 53 +++---- .../Sqlite/HavingClauseTest.php | 60 +++++--- .../Sqlite/InsertIntoStatementTest.php | 24 ++-- .../QueryGenerator/Sqlite/JoinClausesTest.php | 81 +++++++---- .../QueryGenerator/Sqlite/PaginateTest.php | 12 +- .../Sqlite/SelectColumnsTest.php | 129 +++++++++++------- .../Sqlite/UpdateStatementTest.php | 32 ++--- .../Sqlite/WhereClausesTest.php | 72 +++++----- .../Sqlite/WhereDateClausesTest.php | 12 +- 13 files changed, 356 insertions(+), 261 deletions(-) diff --git a/tests/Unit/Database/QueryGenerator/Postgres/UpdateStatementTest.php b/tests/Unit/Database/QueryGenerator/Postgres/UpdateStatementTest.php index c1a9ca0b..cb5d02fe 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/UpdateStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/UpdateStatementTest.php @@ -18,7 +18,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = $1 WHERE id = $2"; + $expected = "UPDATE \"users\" SET \"name\" = $1 WHERE \"id\" = $2"; expect($dml)->toBe($expected); expect($params)->toBe([$name, 1]); @@ -36,7 +36,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = $1, active = $2 WHERE verified_at IS NOT NULL AND role_id = $3"; + $expected = "UPDATE \"users\" SET \"name\" = $1, \"active\" = $2 WHERE \"verified_at\" IS NOT NULL AND \"role_id\" = $3"; expect($dml)->toBe($expected); expect($params)->toBe([$name, true, 2]); @@ -51,7 +51,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET status = $1 WHERE id = $2"; + $expected = "UPDATE \"users\" SET \"status\" = $1 WHERE \"id\" = $2"; expect($dml)->toBe($expected); expect($params)->toBe(['inactive', 5]); @@ -66,7 +66,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET status = $1 WHERE id IN ($2, $3, $4)"; + $expected = "UPDATE \"users\" SET \"status\" = $1 WHERE \"id\" IN ($2, $3, $4)"; expect($dml)->toBe($expected); expect($params)->toBe(['active', 1, 2, 3]); @@ -84,7 +84,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET email = $1, verified = $2 WHERE status = $3 AND created_at > $4"; + $expected = "UPDATE \"users\" SET \"email\" = $1, \"verified\" = $2 WHERE \"status\" = $3 AND \"created_at\" > $4"; expect($dml)->toBe($expected); expect($params)->toBe([$email, true, 'pending', '2024-01-01']); @@ -99,7 +99,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET access_level = $1 WHERE role != $2"; + $expected = "UPDATE \"users\" SET \"access_level\" = $1 WHERE \"role\" != $2"; expect($dml)->toBe($expected); expect($params)->toBe([1, 'admin']); @@ -114,7 +114,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET last_login = $1 WHERE deleted_at IS NULL"; + $expected = "UPDATE \"users\" SET \"last_login\" = $1 WHERE \"deleted_at\" IS NULL"; expect($dml)->toBe($expected); expect($params)->toBe(['2024-12-30']); @@ -138,8 +138,8 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = $1, email = $2, updated_at = $3 " - . "WHERE status = $4 AND email_verified_at IS NOT NULL AND login_count < $5"; + $expected = "UPDATE \"users\" SET \"name\" = $1, \"email\" = $2, \"updated_at\" = $3 " + . "WHERE \"status\" = $4 AND \"email_verified_at\" IS NOT NULL AND \"login_count\" < $5"; expect($dml)->toBe($expected); expect($params)->toBe([$name, $email, '2024-12-30', 'active', 5]); @@ -155,7 +155,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING id, name, email, updated_at"; + $expected = "UPDATE \"users\" SET \"name\" = $1, \"email\" = $2 WHERE \"id\" = $3 RETURNING \"id\", \"name\", \"email\", \"updated_at\""; expect($dml)->toBe($expected); expect($params)->toBe(['John Updated', 'john@new.com', 1]); @@ -171,7 +171,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET status = $1, activated_at = $2 WHERE status IN ($3, $4) RETURNING *"; + $expected = "UPDATE \"users\" SET \"status\" = $1, \"activated_at\" = $2 WHERE \"status\" IN ($3, $4) RETURNING *"; expect($dml)->toBe($expected); expect($params)->toBe(['active', '2024-12-31', 'pending', 'inactive']); @@ -186,7 +186,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE settings SET updated_at = $1 RETURNING id, key, value"; + $expected = "UPDATE \"settings\" SET \"updated_at\" = $1 RETURNING \"id\", \"key\", \"value\""; expect($dml)->toBe($expected); expect($params)->toBe(['2024-12-31']); @@ -206,9 +206,9 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = $1, status = $2 " - . "WHERE status = $3 AND created_at > $4 AND email IS NOT NULL " - . "RETURNING id, name, status, created_at"; + $expected = "UPDATE \"users\" SET \"name\" = $1, \"status\" = $2 " + . "WHERE \"status\" = $3 AND \"created_at\" > $4 AND \"email\" IS NOT NULL " + . "RETURNING \"id\", \"name\", \"status\", \"created_at\""; expect($dml)->toBe($expected); expect($params)->toBe([$name, 'active', 'pending', '2024-01-01']); @@ -224,7 +224,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE posts SET published_at = $1 WHERE id = $2 RETURNING id, title, published_at"; + $expected = "UPDATE \"posts\" SET \"published_at\" = $1 WHERE \"id\" = $2 RETURNING \"id\", \"title\", \"published_at\""; expect($dml)->toBe($expected); expect($params)->toBe(['2024-12-31 10:00:00', 42]); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php index 4087629f..396a605f 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php @@ -20,7 +20,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users WHERE id = $1'); + expect($dml)->toBe('SELECT * FROM "users" WHERE "id" = $1'); expect($params)->toBe([1]); }); @@ -37,7 +37,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users WHERE username = $1 AND email = $2 AND document = $3'); + expect($dml)->toBe('SELECT * FROM "users" WHERE "username" = $1 AND "email" = $2 AND "document" = $3'); expect($params)->toBe(['john', 'john@mail.com', 123456]); }); @@ -55,7 +55,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE {$column} {$operator} $1"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"{$column}\" {$operator} $1"); expect($params)->toBe([$value]); })->with([ ['whereNotEqual', 'id', Operator::NOT_EQUAL->value, 1], @@ -75,7 +75,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT id, name, email FROM users WHERE id = $1'); + expect($dml)->toBe('SELECT "id", "name", "email" FROM "users" WHERE "id" = $1'); expect($params)->toBe([1]); }); @@ -89,7 +89,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE id {$operator} ($1, $2, $3)"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"id\" {$operator} ($1, $2, $3)"); expect($params)->toBe([1, 2, 3]); })->with([ ['whereIn', Operator::IN->value], @@ -111,8 +111,8 @@ $date = date('Y-m-d'); - $expected = "SELECT * FROM users WHERE id {$operator} " - . "(SELECT id FROM users WHERE created_at >= $1)"; + $expected = "SELECT * FROM \"users\" WHERE \"id\" {$operator} " + . "(SELECT \"id\" FROM \"users\" WHERE \"created_at\" >= $1)"; expect($dml)->toBe($expected); expect($params)->toBe([$date]); @@ -130,7 +130,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE verified_at {$operator}"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"verified_at\" {$operator}"); expect($params)->toBe([]); })->with([ ['whereNull', Operator::IS_NULL->value], @@ -149,7 +149,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > $1 OR verified_at {$operator}"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"created_at\" > $1 OR \"verified_at\" {$operator}"); expect($params)->toBe([$date]); })->with([ ['orWhereNull', Operator::IS_NULL->value], @@ -165,7 +165,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE enabled {$operator}"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"enabled\" {$operator}"); expect($params)->toBe([]); })->with([ ['whereTrue', Operator::IS_TRUE->value], @@ -184,7 +184,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > $1 OR enabled {$operator}"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"created_at\" > $1 OR \"enabled\" {$operator}"); expect($params)->toBe([$date]); })->with([ ['orWhereTrue', Operator::IS_TRUE->value], @@ -206,7 +206,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE verified_at IS NOT NULL AND created_at > $1 OR updated_at < $2"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"verified_at\" IS NOT NULL AND \"created_at\" > $1 OR \"updated_at\" < $2"); expect($params)->toBe([$date, $date]); }); @@ -225,7 +225,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > $1 OR updated_at < $2 AND verified_at IS NOT NULL"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"created_at\" > $1 OR \"updated_at\" < $2 AND \"verified_at\" IS NOT NULL"); expect($params)->toBe([$date, $date]); }); @@ -257,7 +257,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE verified_at IS NOT NULL OR {$column} {$operator} {$placeholders}"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"verified_at\" IS NOT NULL OR \"{$column}\" {$operator} {$placeholders}"); expect($params)->toBe([...(array)$value]); })->with([ ['orWhereLessThan', 'updated_at', date('Y-m-d'), Operator::LESS_THAN->value], @@ -280,7 +280,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE age {$operator} $1 AND $2"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"age\" {$operator} $1 AND $2"); expect($params)->toBe([20, 30]); })->with([ ['whereBetween', Operator::BETWEEN->value], @@ -301,7 +301,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > $1 OR updated_at {$operator} $2 AND $3"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"created_at\" > $1 OR \"updated_at\" {$operator} $2 AND $3"); expect($params)->toBe([$date, $startDate, $endDate]); })->with([ ['orWhereBetween', Operator::BETWEEN->value], @@ -319,9 +319,12 @@ $operator = Operator::ORDER_BY->value; - $column = implode(', ', (array) $column); + $column = implode(', ', array_map( + fn (string $column): string => '"' . str_replace('.', '"."', $column) . '"', + (array) $column + )); - expect($dml)->toBe("SELECT * FROM users {$operator} {$column} {$order}"); + expect($dml)->toBe("SELECT * FROM \"users\" {$operator} {$column} {$order}"); expect($params)->toBe($params); })->with([ ['id', Order::ASC->value], @@ -343,7 +346,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users ORDER BY (CASE WHEN city IS NULL THEN country ELSE city END) ASC"); + expect($dml)->toBe("SELECT * FROM \"users\" ORDER BY (CASE WHEN \"city\" IS NULL THEN 'country' ELSE 'city' END) ASC"); expect($params)->toBe($params); }); @@ -360,9 +363,12 @@ $operator = Operator::ORDER_BY->value; - $column = implode(', ', (array) $column); + $column = implode(', ', array_map( + fn (string $column): string => '"' . str_replace('.', '"."', $column) . '"', + (array) $column + )); - expect($dml)->toBe("SELECT * FROM users WHERE id = $1 {$operator} {$column} {$order} LIMIT 1"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"id\" = $1 {$operator} {$column} {$order} LIMIT 1"); expect($params)->toBe([1]); })->with([ ['id', Order::ASC->value], @@ -385,8 +391,8 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM users WHERE {$operator} " - . "(SELECT * FROM user_role WHERE user_id = $1 AND role_id = $2 LIMIT 1)"; + $expected = "SELECT * FROM \"users\" WHERE {$operator} " + . "(SELECT * FROM \"user_role\" WHERE \"user_id\" = $1 AND \"role_id\" = $2 LIMIT 1)"; expect($dml)->toBe($expected); expect($params)->toBe([1, 9]); @@ -412,8 +418,8 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM users WHERE is_admin IS TRUE OR {$operator} " - . "(SELECT * FROM user_role WHERE user_id = $1 LIMIT 1)"; + $expected = "SELECT * FROM \"users\" WHERE \"is_admin\" IS TRUE OR {$operator} " + . "(SELECT * FROM \"user_role\" WHERE \"user_id\" = $1 LIMIT 1)"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -437,8 +443,8 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM products WHERE {$column} {$operator} " - . '(SELECT ' . Functions::max('price') . ' FROM products)'; + $expected = "SELECT * FROM \"products\" WHERE \"{$column}\" {$operator} " + . '(SELECT MAX("price") FROM "products")'; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -469,8 +475,8 @@ [$dml, $params] = $sql; - $expected = "SELECT description FROM products WHERE id {$comparisonOperator} {$operator}" - . "(SELECT product_id FROM orders WHERE quantity > $1)"; + $expected = "SELECT \"description\" FROM \"products\" WHERE \"id\" {$comparisonOperator} {$operator}" + . "(SELECT \"product_id\" FROM \"orders\" WHERE \"quantity\" > $1)"; expect($dml)->toBe($expected); expect($params)->toBe([10]); @@ -502,7 +508,7 @@ $sql = $query->table('employees') ->{$method}(['manager_id', 'department_id'], function (Subquery $subquery) { - $subquery->select(['id, department_id']) + $subquery->select(['id', 'department_id']) ->from('managers') ->whereEqual('location_id', 1); }) @@ -511,10 +517,10 @@ [$dml, $params] = $sql; - $subquery = 'SELECT id, department_id FROM managers WHERE location_id = $1'; + $subquery = 'SELECT "id", "department_id" FROM "managers" WHERE "location_id" = $1'; - $expected = "SELECT name FROM employees " - . "WHERE ROW(manager_id, department_id) {$operator} ({$subquery})"; + $expected = "SELECT \"name\" FROM \"employees\" " + . "WHERE ROW(\"manager_id\", \"department_id\") {$operator} ({$subquery})"; expect($dml)->toBe($expected); expect($params)->toBe([1]); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/WhereDateClausesTest.php b/tests/Unit/Database/QueryGenerator/Postgres/WhereDateClausesTest.php index 077fd006..42028af7 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/WhereDateClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/WhereDateClausesTest.php @@ -24,7 +24,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE DATE(created_at) {$operator} $1"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE DATE(\"created_at\") {$operator} $1"); expect($params)->toBe([$value]); })->with([ ['whereDateEqual', Carbon::now(), Carbon::now()->format('Y-m-d'), Operator::EQUAL->value], @@ -52,7 +52,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE active IS FALSE OR DATE(created_at) {$operator} $1"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"active\" IS FALSE OR DATE(\"created_at\") {$operator} $1"); expect($params)->toBe([$value]); })->with([ ['orWhereDateEqual', date('Y-m-d'), date('Y-m-d'), Operator::EQUAL->value], @@ -78,7 +78,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE MONTH(created_at) {$operator} $1"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE MONTH(\"created_at\") {$operator} $1"); expect($params)->toBe([$value]); })->with([ ['whereMonthEqual', Carbon::now(), Carbon::now()->format('m'), Operator::EQUAL->value], @@ -106,7 +106,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE active IS FALSE OR MONTH(created_at) {$operator} $1"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"active\" IS FALSE OR MONTH(\"created_at\") {$operator} $1"); expect($params)->toBe([$value]); })->with([ ['orWhereMonthEqual', Carbon::now(), Carbon::now()->format('m'), Operator::EQUAL->value], @@ -133,7 +133,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE YEAR(created_at) {$operator} $1"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE YEAR(\"created_at\") {$operator} $1"); expect($params)->toBe([$value]); })->with([ ['whereYearEqual', Carbon::now(), Carbon::now()->format('Y'), Operator::EQUAL->value], @@ -161,7 +161,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE active IS FALSE OR YEAR(created_at) {$operator} $1"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"active\" IS FALSE OR YEAR(\"created_at\") {$operator} $1"); expect($params)->toBe([$value]); })->with([ ['orWhereYearEqual', Carbon::now(), Carbon::now()->format('Y'), Operator::EQUAL->value], diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/DeleteStatementTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/DeleteStatementTest.php index 7eedbb1c..79e7b113 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/DeleteStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/DeleteStatementTest.php @@ -14,7 +14,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE id = ?"; + $expected = "DELETE FROM \"users\" WHERE \"id\" = ?"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -28,7 +28,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users"; + $expected = "DELETE FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -44,7 +44,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE status = ? AND role = ?"; + $expected = "DELETE FROM \"users\" WHERE \"status\" = ? AND \"role\" = ?"; expect($dml)->toBe($expected); expect($params)->toBe(['inactive', 'user']); @@ -59,7 +59,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE id IN (?, ?, ?)"; + $expected = "DELETE FROM \"users\" WHERE \"id\" IN (?, ?, ?)"; expect($dml)->toBe($expected); expect($params)->toBe([1, 2, 3]); @@ -74,7 +74,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE status != ?"; + $expected = "DELETE FROM \"users\" WHERE \"status\" != ?"; expect($dml)->toBe($expected); expect($params)->toBe(['active']); @@ -89,7 +89,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE age > ?"; + $expected = "DELETE FROM \"users\" WHERE \"age\" > ?"; expect($dml)->toBe($expected); expect($params)->toBe([18]); @@ -104,7 +104,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE age < ?"; + $expected = "DELETE FROM \"users\" WHERE \"age\" < ?"; expect($dml)->toBe($expected); expect($params)->toBe([65]); @@ -119,7 +119,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE deleted_at IS NULL"; + $expected = "DELETE FROM \"users\" WHERE \"deleted_at\" IS NULL"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -134,7 +134,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE email IS NOT NULL"; + $expected = "DELETE FROM \"users\" WHERE \"email\" IS NOT NULL"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -150,7 +150,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE id = ? RETURNING id, name, email"; + $expected = "DELETE FROM \"users\" WHERE \"id\" = ? RETURNING \"id\", \"name\", \"email\""; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -166,7 +166,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE status IN (?, ?) RETURNING *"; + $expected = "DELETE FROM \"users\" WHERE \"status\" IN (?, ?) RETURNING *"; expect($dml)->toBe($expected); expect($params)->toBe(['inactive', 'deleted']); @@ -181,7 +181,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users RETURNING id, email"; + $expected = "DELETE FROM \"users\" RETURNING \"id\", \"email\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -198,7 +198,7 @@ [$dml, $params] = $sql; - $expected = "DELETE FROM users WHERE status = ? AND age > ? RETURNING id, name, status, age"; + $expected = "DELETE FROM \"users\" WHERE \"status\" = ? AND \"age\" > ? RETURNING \"id\", \"name\", \"status\", \"age\""; expect($dml)->toBe($expected); expect($params)->toBe(['inactive', 65]); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php index 784a5bb5..3f22b6c9 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php @@ -8,7 +8,7 @@ use Phenix\Database\Join; use Phenix\Database\QueryGenerator; -it('generates a grouped query', function (Functions|string $column, Functions|array|string $groupBy, string $rawGroup): void { +it('generates a grouped query', function (Functions|string $column, Functions|array|string $groupBy, string $rawGroup, string $rawColumn): void { $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ @@ -25,23 +25,24 @@ [$dml, $params] = $sql; - $expected = "SELECT {$column}, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " + $expected = "SELECT {$rawColumn}, \"products\".\"category_id\", \"categories\".\"description\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " . "GROUP BY {$rawGroup}"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', 'category_id'], - ['location_id', ['category_id', 'location_id'], 'category_id, location_id'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT(products.id)'], + [Functions::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], + [Functions::count('products.id'), Functions::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped and ordered query', function ( Functions|string $column, Functions|array|string $groupBy, - string $rawGroup + string $rawGroup, + string $rawColumn ): void { $query = new QueryGenerator(Driver::SQLITE); @@ -60,18 +61,18 @@ [$dml, $params] = $sql; - $expected = "SELECT {$column}, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " + $expected = "SELECT {$rawColumn}, \"products\".\"category_id\", \"categories\".\"description\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " . "GROUP BY {$rawGroup} " - . "ORDER BY products.id DESC"; + . "ORDER BY \"products\".\"id\" DESC"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', 'category_id'], - ['location_id', ['category_id', 'location_id'], 'category_id, location_id'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT(products.id)'], + [Functions::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], + [Functions::count('products.id'), Functions::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped query with where clause', function (): void { @@ -88,10 +89,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id), products.category_id " - . "FROM products " - . "WHERE products.status = ? " - . "GROUP BY category_id"; + $expected = "SELECT COUNT(\"products\".\"id\"), \"products\".\"category_id\" " + . "FROM \"products\" " + . "WHERE \"products\".\"status\" = ? " + . "GROUP BY \"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe(['active']); @@ -113,10 +114,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS product_count, products.category_id " - . "FROM products " - . "HAVING product_count > ? " - . "GROUP BY category_id"; + $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " + . "FROM \"products\" " + . "HAVING \"product_count\" > ? " + . "GROUP BY \"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe([5]); @@ -137,9 +138,9 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id), SUM(products.price), AVG(products.price), products.category_id " - . "FROM products " - . "GROUP BY category_id"; + $expected = "SELECT COUNT(\"products\".\"id\"), SUM(\"products\".\"price\"), AVG(\"products\".\"price\"), \"products\".\"category_id\" " + . "FROM \"products\" " + . "GROUP BY \"category_id\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php index d026ea28..be4fdf09 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php @@ -28,10 +28,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS identifiers, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " - . "HAVING identifiers > ? GROUP BY products.category_id"; + $expected = "SELECT COUNT(\"products\".\"id\") AS \"identifiers\", \"products\".\"category_id\", \"categories\".\"description\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " + . "HAVING \"identifiers\" > ? GROUP BY \"products\".\"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe([5]); @@ -58,10 +58,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS identifiers, products.category_id, categories.description " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " - . "HAVING identifiers > ? AND products.category_id > ? GROUP BY products.category_id"; + $expected = "SELECT COUNT(\"products\".\"id\") AS \"identifiers\", \"products\".\"category_id\", \"categories\".\"description\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " + . "HAVING \"identifiers\" > ? AND \"products\".\"category_id\" > ? GROUP BY \"products\".\"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe([5, 10]); @@ -84,10 +84,10 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS product_count, products.category_id " - . "FROM products " - . "WHERE products.status = ? " - . "HAVING product_count > ? GROUP BY products.category_id"; + $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " + . "FROM \"products\" " + . "WHERE \"products\".\"status\" = ? " + . "HAVING \"product_count\" > ? GROUP BY \"products\".\"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe(['active', 3]); @@ -109,9 +109,9 @@ [$dml, $params] = $sql; - $expected = "SELECT SUM(orders.total) AS total_sales, orders.customer_id " - . "FROM orders " - . "HAVING total_sales < ? GROUP BY orders.customer_id"; + $expected = "SELECT SUM(\"orders\".\"total\") AS \"total_sales\", \"orders\".\"customer_id\" " + . "FROM \"orders\" " + . "HAVING \"total_sales\" < ? GROUP BY \"orders\".\"customer_id\""; expect($dml)->toBe($expected); expect($params)->toBe([1000]); @@ -133,10 +133,34 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(products.id) AS product_count, products.category_id " - . "FROM products " - . "HAVING product_count = ? GROUP BY products.category_id"; + $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " + . "FROM \"products\" " + . "HAVING \"product_count\" = ? GROUP BY \"products\".\"category_id\""; expect($dml)->toBe($expected); expect($params)->toBe([10]); }); + +it('generates a query using having with date clause', function () { + $query = new QueryGenerator(Driver::SQLITE); + + $sql = $query->select([ + Functions::count('products.id')->as('product_count'), + 'products.created_at', + ]) + ->from('products') + ->groupBy('products.created_at') + ->having(function (Having $having): void { + $having->whereDateEqual('products.created_at', '2026-01-15'); + }) + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"created_at\" " + . "FROM \"products\" " + . "HAVING DATE(\"products\".\"created_at\") = ? GROUP BY \"products\".\"created_at\""; + + expect($dml)->toBe($expected); + expect($params)->toBe(['2026-01-15']); +}); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/InsertIntoStatementTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/InsertIntoStatementTest.php index e319b049..4d2ef0a4 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/InsertIntoStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/InsertIntoStatementTest.php @@ -22,7 +22,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES (?, ?)"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\") VALUES (?, ?)"; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -48,7 +48,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES (?, ?), (?, ?)"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\") VALUES (?, ?), (?, ?)"; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name, $email, $name]); @@ -68,7 +68,7 @@ [$dml, $params] = $sql; - $expected = "INSERT OR IGNORE INTO users (email, name) VALUES (?, ?)"; + $expected = "INSERT OR IGNORE INTO \"users\" (\"email\", \"name\") VALUES (?, ?)"; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -89,7 +89,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES (?, ?) RETURNING id"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\") VALUES (?, ?) RETURNING \"id\""; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -110,7 +110,7 @@ [$dml, $params] = $sql; - $expected = "INSERT OR IGNORE INTO users (email, name) VALUES (?, ?) RETURNING id, email"; + $expected = "INSERT OR IGNORE INTO \"users\" (\"email\", \"name\") VALUES (?, ?) RETURNING \"id\", \"email\""; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -130,8 +130,8 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name) VALUES (?, ?) " - . "ON CONFLICT (name) DO UPDATE SET name = excluded.name"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\") VALUES (?, ?) " + . "ON CONFLICT (\"name\") DO UPDATE SET \"name\" = excluded.\"name\""; expect($dml)->toBe($expected); expect($params)->toBe([$email, $name]); @@ -151,8 +151,8 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (email, name, username) VALUES (?, ?, ?) " - . "ON CONFLICT (name, username) DO UPDATE SET name = excluded.name, username = excluded.username"; + $expected = "INSERT INTO \"users\" (\"email\", \"name\", \"username\") VALUES (?, ?, ?) " + . "ON CONFLICT (\"name\", \"username\") DO UPDATE SET \"name\" = excluded.\"name\", \"username\" = excluded.\"username\""; \ksort($data); @@ -172,7 +172,7 @@ [$dml, $params] = $sql; - $expected = "INSERT INTO users (name, email) SELECT name, email FROM customers WHERE verified_at IS NOT NULL"; + $expected = "INSERT INTO \"users\" (\"name\", \"email\") SELECT \"name\", \"email\" FROM \"customers\" WHERE \"verified_at\" IS NOT NULL"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -190,8 +190,8 @@ [$dml, $params] = $sql; - $expected = "INSERT OR IGNORE INTO users (name, email) " - . "SELECT name, email FROM customers WHERE verified_at IS NOT NULL"; + $expected = "INSERT OR IGNORE INTO \"users\" (\"name\", \"email\") " + . "SELECT \"name\", \"email\" FROM \"customers\" WHERE \"verified_at\" IS NOT NULL"; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/JoinClausesTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/JoinClausesTest.php index eab97eb1..85e5d27e 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/JoinClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/JoinClausesTest.php @@ -23,10 +23,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "{$joinType} categories " - . "ON products.category_id = categories.id"; + $expected = "SELECT \"products\".\"id\", \"products\".\"description\", \"categories\".\"description\" " + . "FROM \"products\" " + . "{$joinType} \"categories\" " + . "ON \"products\".\"category_id\" = \"categories\".\"id\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -55,10 +55,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "INNER JOIN categories " - . "ON products.category_id != categories.id"; + $expected = "SELECT \"products\".\"id\", \"products\".\"description\", \"categories\".\"description\" " + . "FROM \"products\" " + . "INNER JOIN \"categories\" " + . "ON \"products\".\"category_id\" != \"categories\".\"id\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -86,10 +86,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "INNER JOIN categories " - . "ON products.category_id = categories.id {$clause}"; + $expected = "SELECT \"products\".\"id\", \"products\".\"description\", \"categories\".\"description\" " + . "FROM \"products\" " + . "INNER JOIN \"categories\" " + . "ON \"products\".\"category_id\" = \"categories\".\"id\" {$clause}"; expect($dml)->toBe($expected); expect($params)->toBe($joinParams); @@ -97,25 +97,25 @@ [ 'orOnEqual', ['products.location_id', 'categories.location_id'], - 'OR products.location_id = categories.location_id', + 'OR "products"."location_id" = "categories"."location_id"', [], ], [ 'whereEqual', ['categories.name', 'php'], - 'AND categories.name = ?', + 'AND "categories"."name" = ?', ['php'], ], [ 'orOnNotEqual', ['products.location_id', 'categories.location_id'], - 'OR products.location_id != categories.location_id', + 'OR "products"."location_id" != "categories"."location_id"', [], ], [ 'orWhereEqual', ['categories.name', 'php'], - 'OR categories.name = ?', + 'OR "categories"."name" = ?', ['php'], ], ]); @@ -134,10 +134,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, products.description, categories.description " - . "FROM products " - . "{$joinType} categories " - . "ON products.category_id = categories.id"; + $expected = "SELECT \"products\".\"id\", \"products\".\"description\", \"categories\".\"description\" " + . "FROM \"products\" " + . "{$joinType} \"categories\" " + . "ON \"products\".\"category_id\" = \"categories\".\"id\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -147,6 +147,31 @@ ['rightJoinOnEqual', JoinType::RIGHT->value], ]); +it('generates query with join date clause', function () { + $query = new QueryGenerator(Driver::SQLITE); + + $sql = $query->select([ + 'products.id', + 'categories.description', + ]) + ->from('products') + ->innerJoin('categories', function (Join $join) { + $join->onEqual('products.category_id', 'categories.id') + ->whereDateEqual('categories.created_at', '2026-01-15'); + }) + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT \"products\".\"id\", \"categories\".\"description\" " + . "FROM \"products\" " + . "INNER JOIN \"categories\" " + . "ON \"products\".\"category_id\" = \"categories\".\"id\" AND DATE(\"categories\".\"created_at\") = ?"; + + expect($dml)->toBe($expected); + expect($params)->toBe(['2026-01-15']); +}); + it('generates query with multiple joins', function () { $query = new QueryGenerator(Driver::SQLITE); @@ -166,10 +191,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, categories.name, suppliers.name " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " - . "LEFT JOIN suppliers ON products.supplier_id = suppliers.id"; + $expected = "SELECT \"products\".\"id\", \"categories\".\"name\", \"suppliers\".\"name\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " + . "LEFT JOIN \"suppliers\" ON \"products\".\"supplier_id\" = \"suppliers\".\"id\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -191,10 +216,10 @@ [$dml, $params] = $sql; - $expected = "SELECT products.id, categories.name " - . "FROM products " - . "LEFT JOIN categories ON products.category_id = categories.id " - . "WHERE products.status = ?"; + $expected = "SELECT \"products\".\"id\", \"categories\".\"name\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " + . "WHERE \"products\".\"status\" = ?"; expect($dml)->toBe($expected); expect($params)->toBe(['active']); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/PaginateTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/PaginateTest.php index 0b67a705..d7c2b1c6 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/PaginateTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/PaginateTest.php @@ -15,7 +15,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 15 OFFSET 0'); + expect($dml)->toBe('SELECT * FROM "users" LIMIT 15 OFFSET 0'); expect($params)->toBeEmpty(); }); @@ -28,7 +28,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 15 OFFSET 30'); + expect($dml)->toBe('SELECT * FROM "users" LIMIT 15 OFFSET 30'); expect($params)->toBeEmpty(); }); @@ -42,7 +42,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 15 OFFSET 15'); + expect($dml)->toBe('SELECT * FROM "users" LIMIT 15 OFFSET 15'); expect($params)->toBeEmpty(); }); @@ -56,7 +56,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users WHERE status = ? LIMIT 15 OFFSET 15'); + expect($dml)->toBe('SELECT * FROM "users" WHERE "status" = ? LIMIT 15 OFFSET 15'); expect($params)->toBe(['active']); }); @@ -70,7 +70,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users ORDER BY created_at ASC LIMIT 15 OFFSET 0'); + expect($dml)->toBe('SELECT * FROM "users" ORDER BY "created_at" ASC LIMIT 15 OFFSET 0'); expect($params)->toBeEmpty(); }); @@ -83,6 +83,6 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users LIMIT 25 OFFSET 25'); + expect($dml)->toBe('SELECT * FROM "users" LIMIT 25 OFFSET 25'); expect($params)->toBeEmpty(); }); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php index 9f1cd124..0d83232a 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php @@ -10,7 +10,6 @@ use Phenix\Database\Functions; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; -use Phenix\Database\Value; it('generates query to select all columns of table', function () { $query = new QueryGenerator(Driver::SQLITE); @@ -22,7 +21,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users'); + expect($dml)->toBe('SELECT * FROM "users"'); expect($params)->toBeEmpty(); }); @@ -36,7 +35,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users'); + expect($dml)->toBe('SELECT * FROM "users"'); expect($params)->toBeEmpty(); }); @@ -49,14 +48,14 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT {$rawFunction} FROM products"); + expect($dml)->toBe("SELECT {$rawFunction} FROM \"products\""); expect($params)->toBeEmpty(); })->with([ - ['avg', 'price', 'AVG(price)'], - ['sum', 'price', 'SUM(price)'], - ['min', 'price', 'MIN(price)'], - ['max', 'price', 'MAX(price)'], - ['count', 'id', 'COUNT(id)'], + ['avg', 'price', 'AVG("price")'], + ['sum', 'price', 'SUM("price")'], + ['min', 'price', 'MIN("price")'], + ['max', 'price', 'MAX("price")'], + ['count', 'id', 'COUNT("id")'], ]); it('generates a query using sql functions with alias', function ( @@ -73,14 +72,14 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT {$rawFunction} FROM products"); + expect($dml)->toBe("SELECT {$rawFunction} FROM \"products\""); expect($params)->toBeEmpty(); })->with([ - ['avg', 'price', 'value', 'AVG(price) AS value'], - ['sum', 'price', 'value', 'SUM(price) AS value'], - ['min', 'price', 'value', 'MIN(price) AS value'], - ['max', 'price', 'value', 'MAX(price) AS value'], - ['count', 'id', 'value', 'COUNT(id) AS value'], + ['avg', 'price', 'value', 'AVG("price") AS "value"'], + ['sum', 'price', 'value', 'SUM("price") AS "value"'], + ['min', 'price', 'value', 'MIN("price") AS "value"'], + ['max', 'price', 'value', 'MAX("price") AS "value"'], + ['count', 'id', 'value', 'COUNT("id") AS "value"'], ]); it('selects field from subquery', function () { @@ -96,7 +95,7 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, email FROM (SELECT * FROM users WHERE verified_at = ?)"; + $expected = "SELECT \"id\", \"name\", \"email\" FROM (SELECT * FROM \"users\" WHERE \"verified_at\" = ?)"; expect($dml)->toBe($expected); expect($params)->toBe([$date]); @@ -120,8 +119,8 @@ [$dml, $params] = $sql; - $subquery = "SELECT name FROM countries WHERE users.country_id = countries.id LIMIT 1"; - $expected = "SELECT id, name, ({$subquery}) AS country_name FROM users"; + $subquery = "SELECT \"name\" FROM \"countries\" WHERE \"users\".\"country_id\" = \"countries\".\"id\" LIMIT 1"; + $expected = "SELECT \"id\", \"name\", ({$subquery}) AS \"country_name\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -156,7 +155,7 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name AS full_name FROM users"; + $expected = "SELECT \"id\", \"name\" AS \"full_name\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -174,7 +173,7 @@ [$dml, $params] = $sql; - $expected = "SELECT id AS model_id, name AS full_name FROM users"; + $expected = "SELECT \"id\" AS \"model_id\", \"name\" AS \"full_name\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -188,8 +187,6 @@ ) { [$column, $value, $result] = $data; - $value = Value::from($value); - $query = new QueryGenerator(Driver::SQLITE); $case = Functions::case() @@ -207,8 +204,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, description, (CASE WHEN {$column} {$operator} {$value} " - . "THEN {$result} ELSE $defaultResult END) AS type FROM products"; + $expected = "SELECT \"id\", \"description\", (CASE WHEN \"{$column}\" {$operator} {$value} " + . "THEN '{$result}' ELSE '{$defaultResult}' END) AS \"type\" FROM \"products\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -246,8 +243,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, (CASE WHEN {$column} {$operator} " - . "THEN {$result} ELSE $defaultResult END) AS status FROM users"; + $expected = "SELECT \"id\", \"name\", (CASE WHEN \"{$column}\" {$operator} " + . "THEN '{$result}' ELSE '{$defaultResult}' END) AS \"status\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -264,9 +261,9 @@ $query = new QueryGenerator(Driver::SQLITE); $case = Functions::case() - ->whenNull('created_at', Value::from('inactive')) - ->whenGreaterThan('created_at', Value::from($date), Value::from('new user')) - ->defaultResult(Value::from('old user')) + ->whenNull('created_at', 'inactive') + ->whenGreaterThan('created_at', $date, 'new user') + ->defaultResult('old user') ->as('status'); $sql = $query->select([ @@ -279,8 +276,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, (CASE WHEN created_at IS NULL THEN 'inactive' " - . "WHEN created_at > '{$date}' THEN 'new user' ELSE 'old user' END) AS status FROM users"; + $expected = "SELECT \"id\", \"name\", (CASE WHEN \"created_at\" IS NULL THEN 'inactive' " + . "WHEN \"created_at\" > '{$date}' THEN 'new user' ELSE 'old user' END) AS \"status\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -292,8 +289,8 @@ $query = new QueryGenerator(Driver::SQLITE); $case = Functions::case() - ->whenNull('created_at', Value::from('inactive')) - ->whenGreaterThan('created_at', Value::from($date), Value::from('new user')) + ->whenNull('created_at', 'inactive') + ->whenGreaterThan('created_at', $date, 'new user') ->as('status'); $sql = $query->select([ @@ -306,8 +303,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, name, (CASE WHEN created_at IS NULL THEN 'inactive' " - . "WHEN created_at > '{$date}' THEN 'new user' END) AS status FROM users"; + $expected = "SELECT \"id\", \"name\", (CASE WHEN \"created_at\" IS NULL THEN 'inactive' " + . "WHEN \"created_at\" > '{$date}' THEN 'new user' END) AS \"status\" FROM \"users\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -317,8 +314,8 @@ $query = new QueryGenerator(Driver::SQLITE); $case = Functions::case() - ->whenGreaterThanOrEqual(Functions::avg('price'), 4, Value::from('expensive')) - ->defaultResult(Value::from('cheap')) + ->whenGreaterThanOrEqual(Functions::avg('price'), 4, 'expensive') + ->defaultResult('cheap') ->as('message'); $sql = $query->select([ @@ -332,8 +329,8 @@ [$dml, $params] = $sql; - $expected = "SELECT id, description, price, (CASE WHEN AVG(price) >= 4 THEN 'expensive' ELSE 'cheap' END) " - . "AS message FROM products"; + $expected = "SELECT \"id\", \"description\", \"price\", (CASE WHEN AVG(\"price\") >= 4 THEN 'expensive' ELSE 'cheap' END) " + . "AS \"message\" FROM \"products\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -346,7 +343,7 @@ [$dml, $params] = $sql; - $expected = "SELECT COUNT(*) FROM products"; + $expected = "SELECT COUNT(*) FROM \"products\""; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -362,7 +359,7 @@ [$dml, $params] = $sql; $expected = "SELECT EXISTS" - . " (SELECT 1 FROM products WHERE id = ?) AS 'exists'"; + . " (SELECT 1 FROM \"products\" WHERE \"id\" = ?) AS \"exists\""; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -378,7 +375,7 @@ [$dml, $params] = $sql; $expected = "SELECT NOT EXISTS" - . " (SELECT 1 FROM products WHERE id = ?) AS 'exists'"; + . " (SELECT 1 FROM \"products\" WHERE \"id\" = ?) AS \"exists\""; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -393,7 +390,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM products WHERE id = ? LIMIT 1"; + $expected = "SELECT * FROM \"products\" WHERE \"id\" = ? LIMIT 1"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -408,7 +405,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users'); + expect($dml)->toBe('SELECT * FROM "users"'); expect($params)->toBeEmpty(); }); @@ -424,7 +421,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -440,7 +437,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -458,7 +455,7 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL"; expect($dml)->toBe($expected); expect($params)->toBe([]); @@ -476,7 +473,43 @@ [$dml, $params] = $builder->get(); - $expected = "SELECT * FROM tasks WHERE reserved_at IS NULL"; + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL"; + + expect($dml)->toBe($expected); + expect($params)->toBe([]); +}); + +it('tries to generate lock using sqlite', function () { + $query = new QueryGenerator(Driver::SQLITE); + + expect($query->getDriver())->toBe(Driver::SQLITE); + + $sql = $query->from('tasks') + ->whereNull('reserved_at') + ->lockForNoKeyUpdateNoWait() + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL"; + + expect($dml)->toBe($expected); + expect($params)->toBe([]); +}); + +it('tries to generate lock using sqlite with constants', function () { + $query = new QueryGenerator(Driver::SQLITE); + + expect($query->getDriver())->toBe(Driver::SQLITE); + + $sql = $query->from('tasks') + ->whereNull('reserved_at') + ->lock(Lock::FOR_NO_KEY_UPDATE) + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT * FROM \"tasks\" WHERE \"reserved_at\" IS NULL"; expect($dml)->toBe($expected); expect($params)->toBe([]); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/UpdateStatementTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/UpdateStatementTest.php index c8f8f85c..0f345d82 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/UpdateStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/UpdateStatementTest.php @@ -18,7 +18,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = ? WHERE id = ?"; + $expected = "UPDATE \"users\" SET \"name\" = ? WHERE \"id\" = ?"; expect($dml)->toBe($expected); expect($params)->toBe([$name, 1]); @@ -36,7 +36,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = ?, active = ? WHERE verified_at IS NOT NULL AND role_id = ?"; + $expected = "UPDATE \"users\" SET \"name\" = ?, \"active\" = ? WHERE \"verified_at\" IS NOT NULL AND \"role_id\" = ?"; expect($dml)->toBe($expected); expect($params)->toBe([$name, true, 2]); @@ -51,7 +51,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET status = ? WHERE id = ?"; + $expected = "UPDATE \"users\" SET \"status\" = ? WHERE \"id\" = ?"; expect($dml)->toBe($expected); expect($params)->toBe(['inactive', 5]); @@ -66,7 +66,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET status = ? WHERE id IN (?, ?, ?)"; + $expected = "UPDATE \"users\" SET \"status\" = ? WHERE \"id\" IN (?, ?, ?)"; expect($dml)->toBe($expected); expect($params)->toBe(['active', 1, 2, 3]); @@ -84,7 +84,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET email = ?, verified = ? WHERE status = ? AND created_at > ?"; + $expected = "UPDATE \"users\" SET \"email\" = ?, \"verified\" = ? WHERE \"status\" = ? AND \"created_at\" > ?"; expect($dml)->toBe($expected); expect($params)->toBe([$email, true, 'pending', '2024-01-01']); @@ -99,7 +99,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET access_level = ? WHERE role != ?"; + $expected = "UPDATE \"users\" SET \"access_level\" = ? WHERE \"role\" != ?"; expect($dml)->toBe($expected); expect($params)->toBe([1, 'admin']); @@ -114,7 +114,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET last_login = ? WHERE deleted_at IS NULL"; + $expected = "UPDATE \"users\" SET \"last_login\" = ? WHERE \"deleted_at\" IS NULL"; expect($dml)->toBe($expected); expect($params)->toBe(['2024-12-30']); @@ -138,8 +138,8 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = ?, email = ?, updated_at = ? " - . "WHERE status = ? AND email_verified_at IS NOT NULL AND login_count < ?"; + $expected = "UPDATE \"users\" SET \"name\" = ?, \"email\" = ?, \"updated_at\" = ? " + . "WHERE \"status\" = ? AND \"email_verified_at\" IS NOT NULL AND \"login_count\" < ?"; expect($dml)->toBe($expected); expect($params)->toBe([$name, $email, '2024-12-30', 'active', 5]); @@ -155,7 +155,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = ?, email = ? WHERE id = ? RETURNING id, name, email, updated_at"; + $expected = "UPDATE \"users\" SET \"name\" = ?, \"email\" = ? WHERE \"id\" = ? RETURNING \"id\", \"name\", \"email\", \"updated_at\""; expect($dml)->toBe($expected); expect($params)->toBe(['John Updated', 'john@new.com', 1]); @@ -171,7 +171,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET status = ?, activated_at = ? WHERE status IN (?, ?) RETURNING *"; + $expected = "UPDATE \"users\" SET \"status\" = ?, \"activated_at\" = ? WHERE \"status\" IN (?, ?) RETURNING *"; expect($dml)->toBe($expected); expect($params)->toBe(['active', '2024-12-31', 'pending', 'inactive']); @@ -186,7 +186,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE settings SET updated_at = ? RETURNING id, key, value"; + $expected = "UPDATE \"settings\" SET \"updated_at\" = ? RETURNING \"id\", \"key\", \"value\""; expect($dml)->toBe($expected); expect($params)->toBe(['2024-12-31']); @@ -206,9 +206,9 @@ [$dml, $params] = $sql; - $expected = "UPDATE users SET name = ?, status = ? " - . "WHERE status = ? AND created_at > ? AND email IS NOT NULL " - . "RETURNING id, name, status, created_at"; + $expected = "UPDATE \"users\" SET \"name\" = ?, \"status\" = ? " + . "WHERE \"status\" = ? AND \"created_at\" > ? AND \"email\" IS NOT NULL " + . "RETURNING \"id\", \"name\", \"status\", \"created_at\""; expect($dml)->toBe($expected); expect($params)->toBe([$name, 'active', 'pending', '2024-01-01']); @@ -224,7 +224,7 @@ [$dml, $params] = $sql; - $expected = "UPDATE posts SET published_at = ? WHERE id = ? RETURNING id, title, published_at"; + $expected = "UPDATE \"posts\" SET \"published_at\" = ? WHERE \"id\" = ? RETURNING \"id\", \"title\", \"published_at\""; expect($dml)->toBe($expected); expect($params)->toBe(['2024-12-31 10:00:00', 42]); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php index b75fff0c..b8c1e24e 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php @@ -20,7 +20,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users WHERE id = ?'); + expect($dml)->toBe('SELECT * FROM "users" WHERE "id" = ?'); expect($params)->toBe([1]); }); @@ -37,7 +37,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT * FROM users WHERE username = ? AND email = ? AND document = ?'); + expect($dml)->toBe('SELECT * FROM "users" WHERE "username" = ? AND "email" = ? AND "document" = ?'); expect($params)->toBe(['john', 'john@mail.com', 123456]); }); @@ -55,7 +55,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE {$column} {$operator} ?"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"{$column}\" {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['whereNotEqual', 'id', Operator::NOT_EQUAL->value, 1], @@ -75,7 +75,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe('SELECT id, name, email FROM users WHERE id = ?'); + expect($dml)->toBe('SELECT "id", "name", "email" FROM "users" WHERE "id" = ?'); expect($params)->toBe([1]); }); @@ -89,7 +89,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE id {$operator} (?, ?, ?)"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"id\" {$operator} (?, ?, ?)"); expect($params)->toBe([1, 2, 3]); })->with([ ['whereIn', Operator::IN->value], @@ -111,8 +111,8 @@ $date = date('Y-m-d'); - $expected = "SELECT * FROM users WHERE id {$operator} " - . "(SELECT id FROM users WHERE created_at >= ?)"; + $expected = "SELECT * FROM \"users\" WHERE \"id\" {$operator} " + . "(SELECT \"id\" FROM \"users\" WHERE \"created_at\" >= ?)"; expect($dml)->toBe($expected); expect($params)->toBe([$date]); @@ -130,7 +130,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE verified_at {$operator}"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"verified_at\" {$operator}"); expect($params)->toBe([]); })->with([ ['whereNull', Operator::IS_NULL->value], @@ -149,7 +149,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > ? OR verified_at {$operator}"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"created_at\" > ? OR \"verified_at\" {$operator}"); expect($params)->toBe([$date]); })->with([ ['orWhereNull', Operator::IS_NULL->value], @@ -165,7 +165,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE enabled {$operator}"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"enabled\" {$operator}"); expect($params)->toBe([]); })->with([ ['whereTrue', Operator::IS_TRUE->value], @@ -184,7 +184,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > ? OR enabled {$operator}"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"created_at\" > ? OR \"enabled\" {$operator}"); expect($params)->toBe([$date]); })->with([ ['orWhereTrue', Operator::IS_TRUE->value], @@ -206,7 +206,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE verified_at IS NOT NULL AND created_at > ? OR updated_at < ?"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"verified_at\" IS NOT NULL AND \"created_at\" > ? OR \"updated_at\" < ?"); expect($params)->toBe([$date, $date]); }); @@ -225,7 +225,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > ? OR updated_at < ? AND verified_at IS NOT NULL"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"created_at\" > ? OR \"updated_at\" < ? AND \"verified_at\" IS NOT NULL"); expect($params)->toBe([$date, $date]); }); @@ -254,7 +254,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE verified_at IS NOT NULL OR {$column} {$operator} {$placeholders}"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"verified_at\" IS NOT NULL OR \"{$column}\" {$operator} {$placeholders}"); expect($params)->toBe([...(array)$value]); })->with([ ['orWhereLessThan', 'updated_at', date('Y-m-d'), Operator::LESS_THAN->value], @@ -277,7 +277,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE age {$operator} ? AND ?"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"age\" {$operator} ? AND ?"); expect($params)->toBe([20, 30]); })->with([ ['whereBetween', Operator::BETWEEN->value], @@ -298,7 +298,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE created_at > ? OR updated_at {$operator} ? AND ?"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"created_at\" > ? OR \"updated_at\" {$operator} ? AND ?"); expect($params)->toBe([$date, $startDate, $endDate]); })->with([ ['orWhereBetween', Operator::BETWEEN->value], @@ -316,9 +316,12 @@ $operator = Operator::ORDER_BY->value; - $column = implode(', ', (array) $column); + $column = implode(', ', array_map( + fn (string $column): string => '"' . str_replace('.', '"."', $column) . '"', + (array) $column + )); - expect($dml)->toBe("SELECT * FROM users {$operator} {$column} {$order}"); + expect($dml)->toBe("SELECT * FROM \"users\" {$operator} {$column} {$order}"); expect($params)->toBe($params); })->with([ ['id', Order::ASC->value], @@ -340,7 +343,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users ORDER BY (CASE WHEN city IS NULL THEN country ELSE city END) ASC"); + expect($dml)->toBe("SELECT * FROM \"users\" ORDER BY (CASE WHEN \"city\" IS NULL THEN 'country' ELSE 'city' END) ASC"); expect($params)->toBe($params); }); @@ -357,9 +360,12 @@ $operator = Operator::ORDER_BY->value; - $column = implode(', ', (array) $column); + $column = implode(', ', array_map( + fn (string $column): string => '"' . str_replace('.', '"."', $column) . '"', + (array) $column + )); - expect($dml)->toBe("SELECT * FROM users WHERE id = ? {$operator} {$column} {$order} LIMIT 1"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"id\" = ? {$operator} {$column} {$order} LIMIT 1"); expect($params)->toBe([1]); })->with([ ['id', Order::ASC->value], @@ -382,8 +388,8 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM users WHERE {$operator} " - . "(SELECT * FROM user_role WHERE user_id = ? AND role_id = ? LIMIT 1)"; + $expected = "SELECT * FROM \"users\" WHERE {$operator} " + . "(SELECT * FROM \"user_role\" WHERE \"user_id\" = ? AND \"role_id\" = ? LIMIT 1)"; expect($dml)->toBe($expected); expect($params)->toBe([1, 9]); @@ -409,8 +415,8 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM users WHERE is_admin IS TRUE OR {$operator} " - . "(SELECT * FROM user_role WHERE user_id = ? LIMIT 1)"; + $expected = "SELECT * FROM \"users\" WHERE \"is_admin\" IS TRUE OR {$operator} " + . "(SELECT * FROM \"user_role\" WHERE \"user_id\" = ? LIMIT 1)"; expect($dml)->toBe($expected); expect($params)->toBe([1]); @@ -434,8 +440,8 @@ [$dml, $params] = $sql; - $expected = "SELECT * FROM products WHERE {$column} {$operator} " - . '(SELECT ' . Functions::max('price') . ' FROM products)'; + $expected = "SELECT * FROM \"products\" WHERE \"{$column}\" {$operator} " + . '(SELECT MAX("price") FROM "products")'; expect($dml)->toBe($expected); expect($params)->toBeEmpty(); @@ -466,8 +472,8 @@ [$dml, $params] = $sql; - $expected = "SELECT description FROM products WHERE id {$comparisonOperator} {$operator}" - . "(SELECT product_id FROM orders WHERE quantity > ?)"; + $expected = "SELECT \"description\" FROM \"products\" WHERE \"id\" {$comparisonOperator} {$operator}" + . "(SELECT \"product_id\" FROM \"orders\" WHERE \"quantity\" > ?)"; expect($dml)->toBe($expected); expect($params)->toBe([10]); @@ -499,7 +505,7 @@ $sql = $query->table('employees') ->{$method}(['manager_id', 'department_id'], function (Subquery $subquery) { - $subquery->select(['id, department_id']) + $subquery->select(['id', 'department_id']) ->from('managers') ->whereEqual('location_id', 1); }) @@ -508,10 +514,10 @@ [$dml, $params] = $sql; - $subquery = 'SELECT id, department_id FROM managers WHERE location_id = ?'; + $subquery = 'SELECT "id", "department_id" FROM "managers" WHERE "location_id" = ?'; - $expected = "SELECT name FROM employees " - . "WHERE ROW(manager_id, department_id) {$operator} ({$subquery})"; + $expected = "SELECT \"name\" FROM \"employees\" " + . "WHERE ROW(\"manager_id\", \"department_id\") {$operator} ({$subquery})"; expect($dml)->toBe($expected); expect($params)->toBe([1]); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/WhereDateClausesTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/WhereDateClausesTest.php index 6c0abc9b..3dc44450 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/WhereDateClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/WhereDateClausesTest.php @@ -24,7 +24,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE DATE(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE DATE(\"created_at\") {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['whereDateEqual', Carbon::now(), Carbon::now()->format('Y-m-d'), Operator::EQUAL->value], @@ -52,7 +52,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE active IS FALSE OR DATE(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"active\" IS FALSE OR DATE(\"created_at\") {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['orWhereDateEqual', date('Y-m-d'), date('Y-m-d'), Operator::EQUAL->value], @@ -78,7 +78,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE MONTH(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE MONTH(\"created_at\") {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['whereMonthEqual', Carbon::now(), Carbon::now()->format('m'), Operator::EQUAL->value], @@ -106,7 +106,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE active IS FALSE OR MONTH(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"active\" IS FALSE OR MONTH(\"created_at\") {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['orWhereMonthEqual', Carbon::now(), Carbon::now()->format('m'), Operator::EQUAL->value], @@ -133,7 +133,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE YEAR(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE YEAR(\"created_at\") {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['whereYearEqual', Carbon::now(), Carbon::now()->format('Y'), Operator::EQUAL->value], @@ -161,7 +161,7 @@ [$dml, $params] = $sql; - expect($dml)->toBe("SELECT * FROM users WHERE active IS FALSE OR YEAR(created_at) {$operator} ?"); + expect($dml)->toBe("SELECT * FROM \"users\" WHERE \"active\" IS FALSE OR YEAR(\"created_at\") {$operator} ?"); expect($params)->toBe([$value]); })->with([ ['orWhereYearEqual', Carbon::now(), Carbon::now()->format('Y'), Operator::EQUAL->value], From 0dcd17050f5e7767de3b00a3087656bb87df0c48 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Apr 2026 13:32:11 +0000 Subject: [PATCH 03/32] refactor: change strategy to compile join into dialects Co-authored-by: Copilot --- src/Database/Concerns/Query/HasJoinClause.php | 6 +- src/Database/Dialects/CompiledClause.php | 9 +++ .../Dialects/Compilers/ClauseCompiler.php | 2 + .../Dialects/Compilers/JoinCompiler.php | 72 +++++++++++++++++++ .../Dialects/Compilers/SelectCompiler.php | 22 +++++- .../Dialects/Mysql/Compilers/Select.php | 3 + .../Dialects/Postgres/Compilers/Select.php | 3 + .../Dialects/Sqlite/Compilers/Select.php | 3 + src/Database/Join.php | 68 ++++++++---------- src/Database/QueryAst.php | 2 +- .../QueryGenerator/JoinClausesTest.php | 31 ++++++++ .../Postgres/JoinClausesTest.php | 26 +++++++ .../QueryGenerator/WhereClausesTest.php | 2 - 13 files changed, 200 insertions(+), 49 deletions(-) create mode 100644 src/Database/Dialects/Compilers/JoinCompiler.php diff --git a/src/Database/Concerns/Query/HasJoinClause.php b/src/Database/Concerns/Query/HasJoinClause.php index 0fe5f1a4..aeff36c6 100644 --- a/src/Database/Concerns/Query/HasJoinClause.php +++ b/src/Database/Concerns/Query/HasJoinClause.php @@ -95,10 +95,6 @@ protected function pushJoin(Join $join): void { $join->setDriver($this->driver); - [$dml, $arguments] = $join->toSql(); - - $this->joins[] = $dml; - - $this->arguments = array_merge($this->arguments, $arguments); + $this->joins[] = $join; } } diff --git a/src/Database/Dialects/CompiledClause.php b/src/Database/Dialects/CompiledClause.php index 89b0b556..41b20726 100644 --- a/src/Database/Dialects/CompiledClause.php +++ b/src/Database/Dialects/CompiledClause.php @@ -15,4 +15,13 @@ public function __construct( public array $params = [] ) { } + + /** + * @return array{0: string, 1: array} + * TODO: Remove + */ + public function sqlWithParams(): array + { + return [$this->sql, $this->params]; + } } diff --git a/src/Database/Dialects/Compilers/ClauseCompiler.php b/src/Database/Dialects/Compilers/ClauseCompiler.php index 748073ad..9ace6688 100644 --- a/src/Database/Dialects/Compilers/ClauseCompiler.php +++ b/src/Database/Dialects/Compilers/ClauseCompiler.php @@ -9,4 +9,6 @@ abstract class ClauseCompiler implements ClauseCompilerContract { protected WhereCompiler $whereCompiler; + + protected JoinCompiler $joinCompiler; } diff --git a/src/Database/Dialects/Compilers/JoinCompiler.php b/src/Database/Dialects/Compilers/JoinCompiler.php new file mode 100644 index 00000000..169704b3 --- /dev/null +++ b/src/Database/Dialects/Compilers/JoinCompiler.php @@ -0,0 +1,72 @@ + $this->compileClause($clause), + $join->getClauses() + ); + + return new CompiledClause( + "{$join->getType()->value} {$this->compileRelationship($join)} ON " . implode(' ', $clauses), + $join->getArguments() + ); + } + + protected function compileClause(WhereClause $clause): string + { + $column = $clause->getColumn(); + $column = $column ? Wrapper::column($this->driver, $column) : null; + + if ($clause instanceof DateWhereClause) { + $function = $clause->getFunction()->name; + $column = "{$function}({$column})"; + $value = $clause->renderValue(); + } else { + $value = $clause->renderValue(); + + if (! $clause instanceof BasicWhereClause || ! $clause->usesPlaceholder()) { + $value = Wrapper::column($this->driver, $value); + } + } + + $sql = "{$column} {$clause->getOperator()->value} {$value}"; + + if ($connector = $clause->getConnector()) { + $sql = "{$connector->value} {$sql}"; + } + + return $sql; + } + + protected function compileRelationship(Join $join): string + { + $relationship = $join->getRelationship(); + + if ($relationship instanceof Alias) { + return (string) $relationship->setDriver($this->driver); + } + + return (string) Wrapper::of($this->driver, $relationship); + } +} diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index 32bf5940..cf6c512f 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -38,7 +38,12 @@ public function compile(QueryAst $ast): CompiledClause ]; if (! empty($ast->joins)) { - $sql[] = $ast->joins; + $joins = $this->compileJoins($ast); + + if ($joins->sql !== '') { + $sql[] = $joins->sql; + $this->params = [...$joins->params, ...$this->params]; + } } if (! empty($ast->wheres)) { @@ -137,6 +142,21 @@ protected function compileOrders(array $orders, Driver $driver): string return Arr::implodeDeeply([Arr::implodeDeeply($compiled, ', '), $order]); } + private function compileJoins(QueryAst $ast): CompiledClause + { + $sql = []; + $params = []; + + foreach ($ast->joins as $join) { + $compiled = $this->joinCompiler->compile($join); + + $sql[] = $compiled->sql; + $params = [...$params, ...$compiled->params]; + } + + return new CompiledClause(Arr::implodeDeeply($sql), $params); + } + /** * @param Subquery $subquery * @return string diff --git a/src/Database/Dialects/Mysql/Compilers/Select.php b/src/Database/Dialects/Mysql/Compilers/Select.php index 01ea4095..c3e9b074 100644 --- a/src/Database/Dialects/Mysql/Compilers/Select.php +++ b/src/Database/Dialects/Mysql/Compilers/Select.php @@ -4,7 +4,9 @@ namespace Phenix\Database\Dialects\Mysql\Compilers; +use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Lock; +use Phenix\Database\Dialects\Compilers\JoinCompiler; use Phenix\Database\Dialects\Compilers\SelectCompiler; use Phenix\Database\QueryAst; @@ -13,6 +15,7 @@ class Select extends SelectCompiler public function __construct() { $this->whereCompiler = new Where(); + $this->joinCompiler = new JoinCompiler(Driver::MYSQL); } protected function compileLock(QueryAst $ast): string diff --git a/src/Database/Dialects/Postgres/Compilers/Select.php b/src/Database/Dialects/Postgres/Compilers/Select.php index 59eb6a0f..c44c18a3 100644 --- a/src/Database/Dialects/Postgres/Compilers/Select.php +++ b/src/Database/Dialects/Postgres/Compilers/Select.php @@ -4,8 +4,10 @@ namespace Phenix\Database\Dialects\Postgres\Compilers; +use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Lock; use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Dialects\Compilers\JoinCompiler; use Phenix\Database\Dialects\Compilers\SelectCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; use Phenix\Database\QueryAst; @@ -17,6 +19,7 @@ class Select extends SelectCompiler public function __construct() { $this->whereCompiler = new Where(); + $this->joinCompiler = new JoinCompiler(Driver::POSTGRESQL); } public function compile(QueryAst $ast): CompiledClause diff --git a/src/Database/Dialects/Sqlite/Compilers/Select.php b/src/Database/Dialects/Sqlite/Compilers/Select.php index 66cd505b..a59789e8 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Select.php +++ b/src/Database/Dialects/Sqlite/Compilers/Select.php @@ -4,6 +4,8 @@ namespace Phenix\Database\Dialects\Sqlite\Compilers; +use Phenix\Database\Constants\Driver; +use Phenix\Database\Dialects\Compilers\JoinCompiler; use Phenix\Database\Dialects\Compilers\SelectCompiler; use Phenix\Database\QueryAst; @@ -12,6 +14,7 @@ class Select extends SelectCompiler public function __construct() { $this->whereCompiler = new Where(); + $this->joinCompiler = new JoinCompiler(Driver::SQLITE); } protected function compileLock(QueryAst $ast): string diff --git a/src/Database/Join.php b/src/Database/Join.php index 534f39dc..753beb9d 100644 --- a/src/Database/Join.php +++ b/src/Database/Join.php @@ -5,11 +5,12 @@ namespace Phenix\Database; use Phenix\Database\Clauses\BasicWhereClause; -use Phenix\Database\Clauses\DateWhereClause; +use Phenix\Database\Clauses\WhereClause; use Phenix\Database\Constants\JoinType; use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; use Phenix\Database\Contracts\Builder; +use Phenix\Database\Dialects\Compilers\JoinCompiler; class Join extends Clause implements Builder { @@ -49,51 +50,38 @@ public function orOnNotEqual(string $column, string $value): self return $this; } - public function toSql(): array + public function getRelationship(): Alias|string { - $sql = []; - - foreach ($this->clauses as $clause) { - $connector = $clause->getConnector(); - - $column = $clause->getColumn(); - $column = $column ? Wrapper::column($this->driver, $clause->getColumn()) : null; - - $operator = $clause->getOperator(); - - if ($clause instanceof DateWhereClause) { - $function = $clause->getFunction()->name; - $column = "{$function}({$column})"; - $value = $clause->renderValue(); - } else { - $value = $clause->renderValue(); - - if (! $clause instanceof BasicWhereClause || ! $clause->usesPlaceholder()) { - $value = Wrapper::column($this->driver, $value); - } - } - - $clauseSql = "{$column} {$operator->value} {$value}"; - - if ($connector !== null) { - $clauseSql = "{$connector->value} {$clauseSql}"; - } + return $this->relationship; + } - $sql[] = $clauseSql; - } + public function getType(): JoinType + { + return $this->type; + } - return [ - "{$this->type->value} {$this->prepareRelationship()} ON " . implode(' ', $sql), - $this->arguments, - ]; + /** + * @return array + */ + public function getClauses(): array + { + return $this->clauses; } - protected function prepareRelationship(): string + /** + * @return array + */ + public function getArguments(): array { - if ($this->relationship instanceof Alias) { - return (string) $this->relationship->setDriver($this->driver); - } + return $this->arguments; + } - return (string) Wrapper::of($this->driver, $this->relationship); + /** + * @deprecated Join is now a semantic AST node. Let the active SQL dialect compile it instead. + * TODO: Remove this method in a future major release. + */ + public function toSql(): array + { + return (new JoinCompiler($this->driver))->compile($this)->sqlWithParams(); } } diff --git a/src/Database/QueryAst.php b/src/Database/QueryAst.php index 8ffef80d..05392305 100644 --- a/src/Database/QueryAst.php +++ b/src/Database/QueryAst.php @@ -30,7 +30,7 @@ class QueryAst public array $values = []; /** - * @var array + * @var array */ public array $joins = []; diff --git a/tests/Unit/Database/QueryGenerator/JoinClausesTest.php b/tests/Unit/Database/QueryGenerator/JoinClausesTest.php index 5e55437d..19938a25 100644 --- a/tests/Unit/Database/QueryGenerator/JoinClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/JoinClausesTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\JoinType; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; @@ -170,3 +171,33 @@ expect($dml)->toBe($expected); expect($params)->toBe(['2026-01-15']); }); + +it('generates query with multiple joins using params', function () { + $query = new QueryGenerator(); + + $sql = $query->select([ + 'products.id', + 'categories.name', + 'suppliers.name', + ]) + ->from('products') + ->leftJoin('categories', function (Join $join) { + $join->onEqual('products.category_id', 'categories.id') + ->whereEqual('categories.status', 'active'); + }) + ->leftJoin('suppliers', function (Join $join) { + $join->onEqual('products.supplier_id', 'suppliers.id') + ->whereEqual('suppliers.region', 'latam'); + }) + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT `products`.`id`, `categories`.`name`, `suppliers`.`name` " + . "FROM `products` " + . "LEFT JOIN `categories` ON `products`.`category_id` = `categories`.`id` AND `categories`.`status` = ? " + . "LEFT JOIN `suppliers` ON `products`.`supplier_id` = `suppliers`.`id` AND `suppliers`.`region` = ?"; + + expect($dml)->toBe($expected); + expect($params)->toBe(['active', 'latam']); +}); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php b/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php index 0e40b803..f0915d77 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php @@ -224,3 +224,29 @@ expect($dml)->toBe($expected); expect($params)->toBe(['active']); }); + +it('orders join params before where params for postgresql placeholders', function () { + $query = new QueryGenerator(Driver::POSTGRESQL); + + $sql = $query->select([ + 'products.id', + 'categories.name', + ]) + ->from('products') + ->leftJoin('categories', function (Join $join) { + $join->onEqual('products.category_id', 'categories.id') + ->whereEqual('categories.status', 'active'); + }) + ->whereEqual('products.status', 'published') + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT \"products\".\"id\", \"categories\".\"name\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" AND \"categories\".\"status\" = $1 " + . "WHERE \"products\".\"status\" = $2"; + + expect($dml)->toBe($expected); + expect($params)->toBe(['active', 'published']); +}); diff --git a/tests/Unit/Database/QueryGenerator/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/WhereClausesTest.php index 32c1b5bf..26c81054 100644 --- a/tests/Unit/Database/QueryGenerator/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/WhereClausesTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -use Phenix\Database\Clauses\RowWhereClause; -use Phenix\Database\Clauses\SubqueryWhereClause; use Phenix\Database\Constants\Operator; use Phenix\Database\Constants\Order; use Phenix\Database\Functions; From 5f34105d772ed4adb6705a5e7706046f5c0b228c Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Apr 2026 14:28:58 +0000 Subject: [PATCH 04/32] refactor: restructure Clause and Join classes to enhance code organization --- src/Database/Clause.php | 120 +---------------- src/Database/ClauseBuilder.php | 127 ++++++++++++++++++ src/Database/Dialects/CompiledClause.php | 9 -- src/Database/Join.php | 12 +- .../QueryGenerator/JoinClausesTest.php | 1 - 5 files changed, 130 insertions(+), 139 deletions(-) create mode 100644 src/Database/ClauseBuilder.php diff --git a/src/Database/Clause.php b/src/Database/Clause.php index 4ea6f58f..656201cf 100644 --- a/src/Database/Clause.php +++ b/src/Database/Clause.php @@ -4,125 +4,9 @@ namespace Phenix\Database; -use Closure; -use Phenix\Database\Clauses\BasicWhereClause; -use Phenix\Database\Clauses\RowWhereClause; -use Phenix\Database\Clauses\SubqueryWhereClause; -use Phenix\Database\Clauses\WhereClause; -use Phenix\Database\Concerns\Query\HasWhereClause; -use Phenix\Database\Concerns\Query\PrepareColumns; -use Phenix\Database\Constants\LogicalConnector; -use Phenix\Database\Constants\Operator; use Phenix\Database\Contracts\Builder; -use function count; - -abstract class Clause extends Grammar implements Builder +abstract class Clause extends ClauseBuilder implements Builder { - use HasWhereClause; - use PrepareColumns; - - /** - * @var array - */ - protected array $clauses; - - protected array $arguments; - - protected function resolveWhereMethod( - string $column, - Operator $operator, - Closure|array|string|int $value, - LogicalConnector $logicalConnector = LogicalConnector::AND - ): void { - if ($value instanceof Closure) { - $this->whereSubquery( - subquery: $value, - comparisonOperator: $operator, - column: $column, - logicalConnector: $logicalConnector - ); - } else { - $this->pushWhereWithArgs($column, $operator, $value, $logicalConnector); - } - } - - protected function whereSubquery( - Closure $subquery, - Operator $comparisonOperator, - string|null $column = null, - Operator|null $operator = null, - LogicalConnector $logicalConnector = LogicalConnector::AND - ): void { - $builder = new Subquery($this->driver); - $builder->setDriver($this->driver); - $builder->selectAllColumns(); - - $subquery($builder); - - [$dml, $arguments] = $builder->toSql(); - - $connector = count($this->clauses) === 0 ? null : $logicalConnector; - - $this->clauses[] = new SubqueryWhereClause( - comparisonOperator: $comparisonOperator, - sql: trim($dml, '()'), - params: $arguments, - column: $column, - operator: $operator, - connector: $connector - ); - - $this->arguments = array_merge($this->arguments, $arguments); - } - - /** - * @param array $columns - */ - protected function whereRowSubquery( - Closure $subquery, - Operator $comparisonOperator, - array $columns, - LogicalConnector $logicalConnector = LogicalConnector::AND - ): void { - $builder = new Subquery($this->driver); - $builder->setDriver($this->driver); - $builder->selectAllColumns(); - - $subquery($builder); - - [$dml, $arguments] = $builder->toSql(); - - $connector = count($this->clauses) === 0 ? null : $logicalConnector; - - $this->clauses[] = new RowWhereClause( - columns: $columns, - comparisonOperator: $comparisonOperator, - sql: trim($dml, '()'), - params: $arguments, - connector: $connector - ); - - $this->arguments = array_merge($this->arguments, $arguments); - } - - protected function pushWhereWithArgs( - string $column, - Operator $operator, - array|string|int $value, - LogicalConnector $logicalConnector = LogicalConnector::AND - ): void { - $this->pushClause(new BasicWhereClause($column, $operator, $value, null, true), $logicalConnector); - - $this->arguments = array_merge($this->arguments, (array) $value); - } - - protected function pushClause(WhereClause $where, LogicalConnector $logicalConnector = LogicalConnector::AND): void - { - if (count($this->clauses) > 0) { - $where->setConnector($logicalConnector); - } - - $this->clauses[] = $where; - } + // } diff --git a/src/Database/ClauseBuilder.php b/src/Database/ClauseBuilder.php new file mode 100644 index 00000000..de85b952 --- /dev/null +++ b/src/Database/ClauseBuilder.php @@ -0,0 +1,127 @@ + + */ + protected array $clauses; + + protected array $arguments; + + protected function resolveWhereMethod( + string $column, + Operator $operator, + Closure|array|string|int $value, + LogicalConnector $logicalConnector = LogicalConnector::AND + ): void { + if ($value instanceof Closure) { + $this->whereSubquery( + subquery: $value, + comparisonOperator: $operator, + column: $column, + logicalConnector: $logicalConnector + ); + } else { + $this->pushWhereWithArgs($column, $operator, $value, $logicalConnector); + } + } + + protected function whereSubquery( + Closure $subquery, + Operator $comparisonOperator, + string|null $column = null, + Operator|null $operator = null, + LogicalConnector $logicalConnector = LogicalConnector::AND + ): void { + $builder = new Subquery($this->driver); + $builder->setDriver($this->driver); + $builder->selectAllColumns(); + + $subquery($builder); + + [$dml, $arguments] = $builder->toSql(); + + $connector = count($this->clauses) === 0 ? null : $logicalConnector; + + $this->clauses[] = new SubqueryWhereClause( + comparisonOperator: $comparisonOperator, + sql: trim($dml, '()'), + params: $arguments, + column: $column, + operator: $operator, + connector: $connector + ); + + $this->arguments = array_merge($this->arguments, $arguments); + } + + /** + * @param array $columns + */ + protected function whereRowSubquery( + Closure $subquery, + Operator $comparisonOperator, + array $columns, + LogicalConnector $logicalConnector = LogicalConnector::AND + ): void { + $builder = new Subquery($this->driver); + $builder->setDriver($this->driver); + $builder->selectAllColumns(); + + $subquery($builder); + + [$dml, $arguments] = $builder->toSql(); + + $connector = count($this->clauses) === 0 ? null : $logicalConnector; + + $this->clauses[] = new RowWhereClause( + columns: $columns, + comparisonOperator: $comparisonOperator, + sql: trim($dml, '()'), + params: $arguments, + connector: $connector + ); + + $this->arguments = array_merge($this->arguments, $arguments); + } + + protected function pushWhereWithArgs( + string $column, + Operator $operator, + array|string|int $value, + LogicalConnector $logicalConnector = LogicalConnector::AND + ): void { + $this->pushClause(new BasicWhereClause($column, $operator, $value, null, true), $logicalConnector); + + $this->arguments = array_merge($this->arguments, (array) $value); + } + + protected function pushClause(WhereClause $where, LogicalConnector $logicalConnector = LogicalConnector::AND): void + { + if (count($this->clauses) > 0) { + $where->setConnector($logicalConnector); + } + + $this->clauses[] = $where; + } +} diff --git a/src/Database/Dialects/CompiledClause.php b/src/Database/Dialects/CompiledClause.php index 41b20726..89b0b556 100644 --- a/src/Database/Dialects/CompiledClause.php +++ b/src/Database/Dialects/CompiledClause.php @@ -15,13 +15,4 @@ public function __construct( public array $params = [] ) { } - - /** - * @return array{0: string, 1: array} - * TODO: Remove - */ - public function sqlWithParams(): array - { - return [$this->sql, $this->params]; - } } diff --git a/src/Database/Join.php b/src/Database/Join.php index 753beb9d..4b82be46 100644 --- a/src/Database/Join.php +++ b/src/Database/Join.php @@ -9,10 +9,9 @@ use Phenix\Database\Constants\JoinType; use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; -use Phenix\Database\Contracts\Builder; use Phenix\Database\Dialects\Compilers\JoinCompiler; -class Join extends Clause implements Builder +class Join extends ClauseBuilder { public function __construct( protected Alias|string $relationship, @@ -75,13 +74,4 @@ public function getArguments(): array { return $this->arguments; } - - /** - * @deprecated Join is now a semantic AST node. Let the active SQL dialect compile it instead. - * TODO: Remove this method in a future major release. - */ - public function toSql(): array - { - return (new JoinCompiler($this->driver))->compile($this)->sqlWithParams(); - } } diff --git a/tests/Unit/Database/QueryGenerator/JoinClausesTest.php b/tests/Unit/Database/QueryGenerator/JoinClausesTest.php index 19938a25..4a3a024e 100644 --- a/tests/Unit/Database/QueryGenerator/JoinClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/JoinClausesTest.php @@ -2,7 +2,6 @@ declare(strict_types=1); -use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\JoinType; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; From 3bfc84c6399bd13604bb86cf427c00e3d9e5d75f Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Apr 2026 16:30:39 +0000 Subject: [PATCH 05/32] feat: add having compiler --- src/Database/Concerns/Query/BuildsQuery.php | 6 +- .../Dialects/Compilers/ClauseCompiler.php | 2 + .../Dialects/Compilers/HavingCompiler.php | 30 ++++++++ .../Dialects/Compilers/SelectCompiler.php | 13 ++-- .../Dialects/Mysql/Compilers/Select.php | 2 + .../Dialects/Postgres/Compilers/Select.php | 2 + .../Dialects/Sqlite/Compilers/Select.php | 2 + src/Database/Having.php | 37 ++++------ src/Database/Join.php | 1 - src/Database/QueryAst.php | 5 +- src/Database/QueryBase.php | 3 +- .../QueryGenerator/HavingClauseTest.php | 6 +- .../Postgres/GroupByStatementTest.php | 4 +- .../Postgres/HavingClauseTest.php | 69 +++++++++++++++++-- .../Sqlite/GroupByStatementTest.php | 4 +- .../Sqlite/HavingClauseTest.php | 12 ++-- 16 files changed, 141 insertions(+), 57 deletions(-) create mode 100644 src/Database/Dialects/Compilers/HavingCompiler.php diff --git a/src/Database/Concerns/Query/BuildsQuery.php b/src/Database/Concerns/Query/BuildsQuery.php index 3525a3a4..29f6df8e 100644 --- a/src/Database/Concerns/Query/BuildsQuery.php +++ b/src/Database/Concerns/Query/BuildsQuery.php @@ -82,11 +82,7 @@ public function having(Closure $clause): static $clause($having); - [$dml, $arguments] = $having->toSql(); - - $this->having = $dml; - - $this->arguments = array_merge($this->arguments, $arguments); + $this->having = $having; return $this; } diff --git a/src/Database/Dialects/Compilers/ClauseCompiler.php b/src/Database/Dialects/Compilers/ClauseCompiler.php index 9ace6688..ccf06512 100644 --- a/src/Database/Dialects/Compilers/ClauseCompiler.php +++ b/src/Database/Dialects/Compilers/ClauseCompiler.php @@ -11,4 +11,6 @@ abstract class ClauseCompiler implements ClauseCompilerContract protected WhereCompiler $whereCompiler; protected JoinCompiler $joinCompiler; + + protected HavingCompiler $havingCompiler; } diff --git a/src/Database/Dialects/Compilers/HavingCompiler.php b/src/Database/Dialects/Compilers/HavingCompiler.php new file mode 100644 index 00000000..7c6829fb --- /dev/null +++ b/src/Database/Dialects/Compilers/HavingCompiler.php @@ -0,0 +1,30 @@ +whereCompiler->compile($having->getClauses()); + + if ($compiled->sql === '') { + return new CompiledClause('', []); + } + + return new CompiledClause( + "HAVING {$compiled->sql}", + $having->getArguments() + ); + } +} diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index cf6c512f..3468dd84 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -55,15 +55,20 @@ public function compile(QueryAst $ast): CompiledClause } } - if ($ast->having !== null) { - $sql[] = $ast->having; - } - if (! empty($ast->groups)) { $sql[] = Operator::GROUP_BY->value; $sql[] = $this->compileGroups($ast->groups, $ast->driver); } + if ($ast->having !== null) { + $having = $this->havingCompiler->compile($ast->having); + + if ($having->sql !== '') { + $sql[] = $having->sql; + $this->params = [...$this->params, ...$having->params]; + } + } + if (! empty($ast->orders)) { $sql[] = Operator::ORDER_BY->value; $sql[] = $this->compileOrders($ast->orders, $ast->driver); diff --git a/src/Database/Dialects/Mysql/Compilers/Select.php b/src/Database/Dialects/Mysql/Compilers/Select.php index c3e9b074..a3115f57 100644 --- a/src/Database/Dialects/Mysql/Compilers/Select.php +++ b/src/Database/Dialects/Mysql/Compilers/Select.php @@ -6,6 +6,7 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Lock; +use Phenix\Database\Dialects\Compilers\HavingCompiler; use Phenix\Database\Dialects\Compilers\JoinCompiler; use Phenix\Database\Dialects\Compilers\SelectCompiler; use Phenix\Database\QueryAst; @@ -16,6 +17,7 @@ public function __construct() { $this->whereCompiler = new Where(); $this->joinCompiler = new JoinCompiler(Driver::MYSQL); + $this->havingCompiler = new HavingCompiler($this->whereCompiler); } protected function compileLock(QueryAst $ast): string diff --git a/src/Database/Dialects/Postgres/Compilers/Select.php b/src/Database/Dialects/Postgres/Compilers/Select.php index c44c18a3..296fc6e6 100644 --- a/src/Database/Dialects/Postgres/Compilers/Select.php +++ b/src/Database/Dialects/Postgres/Compilers/Select.php @@ -7,6 +7,7 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Lock; use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Dialects\Compilers\HavingCompiler; use Phenix\Database\Dialects\Compilers\JoinCompiler; use Phenix\Database\Dialects\Compilers\SelectCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; @@ -20,6 +21,7 @@ public function __construct() { $this->whereCompiler = new Where(); $this->joinCompiler = new JoinCompiler(Driver::POSTGRESQL); + $this->havingCompiler = new HavingCompiler($this->whereCompiler); } public function compile(QueryAst $ast): CompiledClause diff --git a/src/Database/Dialects/Sqlite/Compilers/Select.php b/src/Database/Dialects/Sqlite/Compilers/Select.php index a59789e8..dd7ce169 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Select.php +++ b/src/Database/Dialects/Sqlite/Compilers/Select.php @@ -5,6 +5,7 @@ namespace Phenix\Database\Dialects\Sqlite\Compilers; use Phenix\Database\Constants\Driver; +use Phenix\Database\Dialects\Compilers\HavingCompiler; use Phenix\Database\Dialects\Compilers\JoinCompiler; use Phenix\Database\Dialects\Compilers\SelectCompiler; use Phenix\Database\QueryAst; @@ -15,6 +16,7 @@ public function __construct() { $this->whereCompiler = new Where(); $this->joinCompiler = new JoinCompiler(Driver::SQLITE); + $this->havingCompiler = new HavingCompiler($this->whereCompiler); } protected function compileLock(QueryAst $ast): string diff --git a/src/Database/Having.php b/src/Database/Having.php index 0136cbeb..293fa8e2 100644 --- a/src/Database/Having.php +++ b/src/Database/Having.php @@ -4,10 +4,9 @@ namespace Phenix\Database; -use Phenix\Database\Clauses\DateWhereClause; -use Phenix\Database\Constants\SQL; +use Phenix\Database\Clauses\WhereClause; -class Having extends Clause +class Having extends ClauseBuilder { public function __construct() { @@ -15,27 +14,19 @@ public function __construct() $this->arguments = []; } - public function toSql(): array + /** + * @return array + */ + public function getClauses(): array { - $sql = []; - - foreach ($this->clauses as $clause) { - $column = Wrapper::column($this->driver, $clause->getColumn()); - - if ($clause instanceof DateWhereClause) { - $function = $clause->getFunction()->name; - $column = "{$function}({$column})"; - } - - $clauseSql = "{$column} {$clause->getOperator()->value} " . SQL::PLACEHOLDER->value; - - if ($connector = $clause->getConnector()) { - $clauseSql = "{$connector->value} {$clauseSql}"; - } - - $sql[] = $clauseSql; - } + return $this->clauses; + } - return ['HAVING ' . implode(' ', $sql), $this->arguments]; + /** + * @return array + */ + public function getArguments(): array + { + return $this->arguments; } } diff --git a/src/Database/Join.php b/src/Database/Join.php index 4b82be46..6018dd90 100644 --- a/src/Database/Join.php +++ b/src/Database/Join.php @@ -9,7 +9,6 @@ use Phenix\Database\Constants\JoinType; use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; -use Phenix\Database\Dialects\Compilers\JoinCompiler; class Join extends ClauseBuilder { diff --git a/src/Database/QueryAst.php b/src/Database/QueryAst.php index 05392305..c9a3e5ef 100644 --- a/src/Database/QueryAst.php +++ b/src/Database/QueryAst.php @@ -39,10 +39,7 @@ class QueryAst */ public array $wheres = []; - /** - * @var string|null - */ - public string|null $having = null; + public Having|null $having = null; /** * @var array diff --git a/src/Database/QueryBase.php b/src/Database/QueryBase.php index 253ca55b..b5ea426a 100644 --- a/src/Database/QueryBase.php +++ b/src/Database/QueryBase.php @@ -30,7 +30,7 @@ abstract class QueryBase extends Clause implements QueryBuilder, Builder protected array $joins; - protected string $having; + protected Having|null $having; protected array $groupBy; @@ -65,6 +65,7 @@ protected function resetBaseProperties(): void $this->joins = []; $this->columns = []; $this->values = []; + $this->having = null; $this->clauses = []; $this->arguments = []; $this->uniqueColumns = []; diff --git a/tests/Unit/Database/QueryGenerator/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/HavingClauseTest.php index 5e0ff38c..67dc8235 100644 --- a/tests/Unit/Database/QueryGenerator/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/HavingClauseTest.php @@ -30,7 +30,7 @@ $expected = "SELECT COUNT(`products`.`id`) AS `identifiers`, `products`.`category_id`, `categories`.`description` " . "FROM `products` " . "LEFT JOIN `categories` ON `products`.`category_id` = `categories`.`id` " - . "HAVING `identifiers` > ? GROUP BY `products`.`category_id`"; + . "GROUP BY `products`.`category_id` HAVING `identifiers` > ?"; expect($dml)->toBe($expected); expect($params)->toBe([5]); @@ -60,7 +60,7 @@ $expected = "SELECT COUNT(`products`.`id`) AS `identifiers`, `products`.`category_id`, `categories`.`description` " . "FROM `products` " . "LEFT JOIN `categories` ON `products`.`category_id` = `categories`.`id` " - . "HAVING `identifiers` > ? AND `products`.`category_id` > ? GROUP BY `products`.`category_id`"; + . "GROUP BY `products`.`category_id` HAVING `identifiers` > ? AND `products`.`category_id` > ?"; expect($dml)->toBe($expected); expect($params)->toBe([5, 10]); @@ -84,7 +84,7 @@ $expected = "SELECT COUNT(`products`.`id`) AS `product_count`, `products`.`created_at` " . "FROM `products` " - . "HAVING DATE(`products`.`created_at`) = ? GROUP BY `products`.`created_at`"; + . "GROUP BY `products`.`created_at` HAVING DATE(`products`.`created_at`) = ?"; expect($dml)->toBe($expected); expect($params)->toBe(['2026-01-15']); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php index e3ddb443..295ed349 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php @@ -116,8 +116,8 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " . "FROM \"products\" " - . "HAVING \"product_count\" > $1 " - . "GROUP BY \"category_id\""; + . "GROUP BY \"category_id\" " + . "HAVING \"product_count\" > $1"; expect($dml)->toBe($expected); expect($params)->toBe([5]); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php index b50340cd..e9b998af 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php @@ -31,7 +31,7 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"identifiers\", \"products\".\"category_id\", \"categories\".\"description\" " . "FROM \"products\" " . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " - . "HAVING \"identifiers\" > $1 GROUP BY \"products\".\"category_id\""; + . "GROUP BY \"products\".\"category_id\" HAVING \"identifiers\" > $1"; expect($dml)->toBe($expected); expect($params)->toBe([5]); @@ -61,7 +61,7 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"identifiers\", \"products\".\"category_id\", \"categories\".\"description\" " . "FROM \"products\" " . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " - . "HAVING \"identifiers\" > $1 AND \"products\".\"category_id\" > $2 GROUP BY \"products\".\"category_id\""; + . "GROUP BY \"products\".\"category_id\" HAVING \"identifiers\" > $1 AND \"products\".\"category_id\" > $2"; expect($dml)->toBe($expected); expect($params)->toBe([5, 10]); @@ -87,7 +87,7 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " . "FROM \"products\" " . "WHERE \"products\".\"status\" = $1 " - . "HAVING \"product_count\" > $2 GROUP BY \"products\".\"category_id\""; + . "GROUP BY \"products\".\"category_id\" HAVING \"product_count\" > $2"; expect($dml)->toBe($expected); expect($params)->toBe(['active', 3]); @@ -111,7 +111,7 @@ $expected = "SELECT SUM(\"orders\".\"total\") AS \"total_sales\", \"orders\".\"customer_id\" " . "FROM \"orders\" " - . "HAVING \"total_sales\" < $1 GROUP BY \"orders\".\"customer_id\""; + . "GROUP BY \"orders\".\"customer_id\" HAVING \"total_sales\" < $1"; expect($dml)->toBe($expected); expect($params)->toBe([1000]); @@ -135,7 +135,7 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " . "FROM \"products\" " - . "HAVING \"product_count\" = $1 GROUP BY \"products\".\"category_id\""; + . "GROUP BY \"products\".\"category_id\" HAVING \"product_count\" = $1"; expect($dml)->toBe($expected); expect($params)->toBe([10]); @@ -159,8 +159,65 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"created_at\" " . "FROM \"products\" " - . "HAVING DATE(\"products\".\"created_at\") = $1 GROUP BY \"products\".\"created_at\""; + . "GROUP BY \"products\".\"created_at\" HAVING DATE(\"products\".\"created_at\") = $1"; expect($dml)->toBe($expected); expect($params)->toBe(['2026-01-15']); }); + +it('orders join where and having params by final sql position', function () { + $query = new QueryGenerator(Driver::POSTGRESQL); + + $sql = $query->select([ + Functions::count('products.id')->as('product_count'), + 'products.category_id', + ]) + ->from('products') + ->leftJoin('categories', function (Join $join) { + $join->onEqual('products.category_id', 'categories.id') + ->whereEqual('categories.status', 'enabled'); + }) + ->whereEqual('products.status', 'active') + ->groupBy('products.category_id') + ->having(function (Having $having): void { + $having->whereGreaterThan('product_count', 3); + }) + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " + . "FROM \"products\" " + . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" AND \"categories\".\"status\" = $1 " + . "WHERE \"products\".\"status\" = $2 " + . "GROUP BY \"products\".\"category_id\" HAVING \"product_count\" > $3"; + + expect($dml)->toBe($expected); + expect($params)->toBe(['enabled', 'active', 3]); +}); + +it('orders where params before having params regardless of call order', function () { + $query = new QueryGenerator(Driver::POSTGRESQL); + + $sql = $query->select([ + Functions::count('products.id')->as('product_count'), + 'products.category_id', + ]) + ->from('products') + ->groupBy('products.category_id') + ->having(function (Having $having): void { + $having->whereGreaterThan('product_count', 3); + }) + ->whereEqual('products.status', 'active') + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " + . "FROM \"products\" " + . "WHERE \"products\".\"status\" = $1 " + . "GROUP BY \"products\".\"category_id\" HAVING \"product_count\" > $2"; + + expect($dml)->toBe($expected); + expect($params)->toBe(['active', 3]); +}); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php index 3f22b6c9..c3fc33a9 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php @@ -116,8 +116,8 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " . "FROM \"products\" " - . "HAVING \"product_count\" > ? " - . "GROUP BY \"category_id\""; + . "GROUP BY \"category_id\" " + . "HAVING \"product_count\" > ?"; expect($dml)->toBe($expected); expect($params)->toBe([5]); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php index be4fdf09..f52b1a88 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php @@ -31,7 +31,7 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"identifiers\", \"products\".\"category_id\", \"categories\".\"description\" " . "FROM \"products\" " . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " - . "HAVING \"identifiers\" > ? GROUP BY \"products\".\"category_id\""; + . "GROUP BY \"products\".\"category_id\" HAVING \"identifiers\" > ?"; expect($dml)->toBe($expected); expect($params)->toBe([5]); @@ -61,7 +61,7 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"identifiers\", \"products\".\"category_id\", \"categories\".\"description\" " . "FROM \"products\" " . "LEFT JOIN \"categories\" ON \"products\".\"category_id\" = \"categories\".\"id\" " - . "HAVING \"identifiers\" > ? AND \"products\".\"category_id\" > ? GROUP BY \"products\".\"category_id\""; + . "GROUP BY \"products\".\"category_id\" HAVING \"identifiers\" > ? AND \"products\".\"category_id\" > ?"; expect($dml)->toBe($expected); expect($params)->toBe([5, 10]); @@ -87,7 +87,7 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " . "FROM \"products\" " . "WHERE \"products\".\"status\" = ? " - . "HAVING \"product_count\" > ? GROUP BY \"products\".\"category_id\""; + . "GROUP BY \"products\".\"category_id\" HAVING \"product_count\" > ?"; expect($dml)->toBe($expected); expect($params)->toBe(['active', 3]); @@ -111,7 +111,7 @@ $expected = "SELECT SUM(\"orders\".\"total\") AS \"total_sales\", \"orders\".\"customer_id\" " . "FROM \"orders\" " - . "HAVING \"total_sales\" < ? GROUP BY \"orders\".\"customer_id\""; + . "GROUP BY \"orders\".\"customer_id\" HAVING \"total_sales\" < ?"; expect($dml)->toBe($expected); expect($params)->toBe([1000]); @@ -135,7 +135,7 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"category_id\" " . "FROM \"products\" " - . "HAVING \"product_count\" = ? GROUP BY \"products\".\"category_id\""; + . "GROUP BY \"products\".\"category_id\" HAVING \"product_count\" = ?"; expect($dml)->toBe($expected); expect($params)->toBe([10]); @@ -159,7 +159,7 @@ $expected = "SELECT COUNT(\"products\".\"id\") AS \"product_count\", \"products\".\"created_at\" " . "FROM \"products\" " - . "HAVING DATE(\"products\".\"created_at\") = ? GROUP BY \"products\".\"created_at\""; + . "GROUP BY \"products\".\"created_at\" HAVING DATE(\"products\".\"created_at\") = ?"; expect($dml)->toBe($expected); expect($params)->toBe(['2026-01-15']); From 182e7cfe8420935b6a11670cedab6fc3314134ac Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Apr 2026 17:41:41 +0000 Subject: [PATCH 06/32] fix: remove unused PrepareColumns trait to enhance code clarity --- src/Database/ClauseBuilder.php | 2 - .../Concerns/Query/PrepareColumns.php | 45 ------------------- 2 files changed, 47 deletions(-) delete mode 100644 src/Database/Concerns/Query/PrepareColumns.php diff --git a/src/Database/ClauseBuilder.php b/src/Database/ClauseBuilder.php index de85b952..4930d2f1 100644 --- a/src/Database/ClauseBuilder.php +++ b/src/Database/ClauseBuilder.php @@ -10,7 +10,6 @@ use Phenix\Database\Clauses\SubqueryWhereClause; use Phenix\Database\Clauses\WhereClause; use Phenix\Database\Concerns\Query\HasWhereClause; -use Phenix\Database\Concerns\Query\PrepareColumns; use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; @@ -19,7 +18,6 @@ abstract class ClauseBuilder extends Grammar { use HasWhereClause; - use PrepareColumns; /** * @var array diff --git a/src/Database/Concerns/Query/PrepareColumns.php b/src/Database/Concerns/Query/PrepareColumns.php deleted file mode 100644 index c3f6f7e7..00000000 --- a/src/Database/Concerns/Query/PrepareColumns.php +++ /dev/null @@ -1,45 +0,0 @@ - (string) Alias::of($key)->as($value), - $value instanceof Functions => (string) $value, - $value instanceof SelectCase => (string) $value, - $value instanceof Subquery => $this->resolveSubquery($value), - default => $value, - }; - }); - - return Arr::implodeDeeply($columns, ', '); - } - - private function resolveSubquery(Subquery $subquery): string - { - [$dml, $arguments] = $subquery->toSql(); - - if (! str_contains($dml, 'LIMIT 1')) { - throw new QueryErrorException('The subquery must be limited to one record'); - } - - $this->arguments = array_merge($this->arguments, $arguments); - - return $dml; - } -} From ef1b3e7c8a6e5170eadb1c0a2d4c27cd419ca633 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Apr 2026 19:31:50 +0000 Subject: [PATCH 07/32] Enhance Query AST Management and Security Improvements - Refactored ClauseBuilder to improve handling of where clauses and arguments. - Updated BuildsQuery trait to utilize AST for managing query properties. - Modified HasJoinClause and HasLock traits to store join and lock information in the AST. - Improved HasWhereClause and HasWhereDateClause to push where clauses directly to the AST. - Adjusted QueryBase to reset properties and manage AST effectively. - Updated QueryGenerator to ensure driver and action are set in the AST. - Enhanced tests to verify correct parameter handling and AST synchronization. - Added normalization for placeholders in Postgres dialect. - Implemented new tests to validate the integrity of query parameters across various scenarios. Co-authored-by: Copilot --- src/Database/ClauseBuilder.php | 64 ++++++-- src/Database/Concerns/Query/BuildsQuery.php | 41 ++---- src/Database/Concerns/Query/HasJoinClause.php | 2 +- src/Database/Concerns/Query/HasLock.php | 12 +- .../Concerns/Query/HasWhereClause.php | 63 +++----- .../Concerns/Query/HasWhereDateClause.php | 2 +- .../Dialects/Postgres/Compilers/Select.php | 2 +- .../Postgres/Concerns/HasPlaceholders.php | 14 ++ .../QueryBuilders/DatabaseQueryBuilder.php | 13 +- src/Database/QueryBase.php | 139 +++++++++++------- src/Database/QueryBuilder.php | 31 ++-- src/Database/QueryGenerator.php | 9 +- .../InsertIntoStatementTest.php | 25 ++++ .../Postgres/WhereClausesTest.php | 21 +++ .../QueryGenerator/SelectColumnsTest.php | 55 +++++++ .../QueryGenerator/WhereDateClausesTest.php | 15 -- 16 files changed, 310 insertions(+), 198 deletions(-) diff --git a/src/Database/ClauseBuilder.php b/src/Database/ClauseBuilder.php index 4930d2f1..5781f867 100644 --- a/src/Database/ClauseBuilder.php +++ b/src/Database/ClauseBuilder.php @@ -24,8 +24,48 @@ abstract class ClauseBuilder extends Grammar */ protected array $clauses; + /** + * @var array + */ protected array $arguments; + /** + * @return array + */ + protected function getClauses(): array + { + return $this->clauses; + } + + /** + * @return array + */ + protected function getArguments(): array + { + return $this->arguments; + } + + protected function hasWhereClauses(): bool + { + return count($this->getClauses()) > 0; + } + + protected function addArguments(array $arguments): void + { + $this->arguments = [...$this->arguments, ...$arguments]; + } + + protected function pushWhereClause( + WhereClause $where, + LogicalConnector $logicalConnector = LogicalConnector::AND + ): void { + if ($this->hasWhereClauses()) { + $where->setConnector($logicalConnector); + } + + $this->clauses[] = $where; + } + protected function resolveWhereMethod( string $column, Operator $operator, @@ -59,18 +99,18 @@ protected function whereSubquery( [$dml, $arguments] = $builder->toSql(); - $connector = count($this->clauses) === 0 ? null : $logicalConnector; + $connector = $this->hasWhereClauses() ? $logicalConnector : null; - $this->clauses[] = new SubqueryWhereClause( + $this->pushWhereClause(new SubqueryWhereClause( comparisonOperator: $comparisonOperator, sql: trim($dml, '()'), params: $arguments, column: $column, operator: $operator, connector: $connector - ); + ), $logicalConnector); - $this->arguments = array_merge($this->arguments, $arguments); + $this->addArguments($arguments); } /** @@ -90,17 +130,17 @@ protected function whereRowSubquery( [$dml, $arguments] = $builder->toSql(); - $connector = count($this->clauses) === 0 ? null : $logicalConnector; + $connector = $this->hasWhereClauses() ? $logicalConnector : null; - $this->clauses[] = new RowWhereClause( + $this->pushWhereClause(new RowWhereClause( columns: $columns, comparisonOperator: $comparisonOperator, sql: trim($dml, '()'), params: $arguments, connector: $connector - ); + ), $logicalConnector); - $this->arguments = array_merge($this->arguments, $arguments); + $this->addArguments($arguments); } protected function pushWhereWithArgs( @@ -111,15 +151,11 @@ protected function pushWhereWithArgs( ): void { $this->pushClause(new BasicWhereClause($column, $operator, $value, null, true), $logicalConnector); - $this->arguments = array_merge($this->arguments, (array) $value); + $this->addArguments((array) $value); } protected function pushClause(WhereClause $where, LogicalConnector $logicalConnector = LogicalConnector::AND): void { - if (count($this->clauses) > 0) { - $where->setConnector($logicalConnector); - } - - $this->clauses[] = $where; + $this->pushWhereClause($where, $logicalConnector); } } diff --git a/src/Database/Concerns/Query/BuildsQuery.php b/src/Database/Concerns/Query/BuildsQuery.php index 29f6df8e..64806b60 100644 --- a/src/Database/Concerns/Query/BuildsQuery.php +++ b/src/Database/Concerns/Query/BuildsQuery.php @@ -6,7 +6,6 @@ use Closure; use Phenix\Database\Constants\Action; -use Phenix\Database\Constants\Operator; use Phenix\Database\Constants\Order; use Phenix\Database\Dialects\DialectFactory; use Phenix\Database\Functions; @@ -21,7 +20,7 @@ trait BuildsQuery { public function table(string $table): static { - $this->table = $table; + $this->ast->table = $table; return $this; } @@ -39,7 +38,7 @@ public function from(Closure|string $table): static $this->table($dml); - $this->arguments = array_merge($this->arguments, $arguments); + $this->addArguments($arguments); } else { $this->table($table); @@ -50,9 +49,9 @@ public function from(Closure|string $table): static public function select(array $columns): static { - $this->action = Action::SELECT; + $this->ast->action = Action::SELECT; - $this->columns = $columns; + $this->ast->columns = $columns; return $this; } @@ -70,7 +69,7 @@ public function groupBy(Functions|array|string $column): static $column = [$column]; } - $this->groupBy = $column; + $this->ast->groups = $column; return $this; } @@ -82,7 +81,7 @@ public function having(Closure $clause): static $clause($having); - $this->having = $having; + $this->ast->having = $having; return $this; } @@ -93,14 +92,14 @@ public function orderBy(SelectCase|array|string $column, Order $order = Order::D $column = [$column]; } - $this->orderBy = [$column, $order->value]; + $this->ast->orders = [$column, $order->value]; return $this; } public function limit(int $number): static { - $this->limit = [Operator::LIMIT->value, abs($number)]; + $this->ast->limit = abs($number); return $this; } @@ -113,7 +112,7 @@ public function page(int $page = 1, int $perPage = 15): static $offset = $page === 1 ? 0 : (($page - 1) * abs($perPage)); - $this->offset = [Operator::OFFSET->value, $offset]; + $this->ast->offset = $offset; return $this; } @@ -131,26 +130,6 @@ public function toSql(): array protected function buildAst(): QueryAst { - $ast = new QueryAst(); - $ast->driver = $this->driver; - $ast->action = $this->action; - $ast->table = $this->table; - $ast->columns = $this->columns; - $ast->values = $this->values ?? []; - $ast->wheres = $this->clauses ?? []; - $ast->joins = $this->joins ?? []; - $ast->groups = $this->groupBy ?? []; - $ast->orders = $this->orderBy ?? []; - $ast->limit = isset($this->limit) ? $this->limit[1] : null; - $ast->offset = isset($this->offset) ? $this->offset[1] : null; - $ast->lock = $this->lockType ?? null; - $ast->having = $this->having ?? null; - $ast->rawStatement = $this->rawStatement ?? null; - $ast->ignore = $this->ignore ?? false; - $ast->uniqueColumns = $this->uniqueColumns ?? []; - $ast->returning = $this->returning ?? []; - $ast->params = $this->arguments; - - return $ast; + return $this->ast; } } diff --git a/src/Database/Concerns/Query/HasJoinClause.php b/src/Database/Concerns/Query/HasJoinClause.php index aeff36c6..1edc9182 100644 --- a/src/Database/Concerns/Query/HasJoinClause.php +++ b/src/Database/Concerns/Query/HasJoinClause.php @@ -95,6 +95,6 @@ protected function pushJoin(Join $join): void { $join->setDriver($this->driver); - $this->joins[] = $join; + $this->ast->joins[] = $join; } } diff --git a/src/Database/Concerns/Query/HasLock.php b/src/Database/Concerns/Query/HasLock.php index 011731ab..df7e63b4 100644 --- a/src/Database/Concerns/Query/HasLock.php +++ b/src/Database/Concerns/Query/HasLock.php @@ -13,19 +13,17 @@ trait HasLock { protected bool $isLocked = false; - protected Lock|null $lockType = null; - public function lock(Lock $lockType): static { if (! $this->supportsLock($lockType)) { $this->isLocked = false; - $this->lockType = null; + $this->ast->lock = null; return $this; } $this->isLocked = true; - $this->lockType = $lockType; + $this->ast->lock = $lockType; return $this; } @@ -83,7 +81,7 @@ public function lockForNoKeyUpdateSkipLocked(): static public function unlock(): static { $this->isLocked = false; - $this->lockType = null; + $this->ast->lock = null; return $this; } @@ -96,7 +94,7 @@ public function isLocked(): bool protected function buildLock(): string { if ($this->driver === Driver::POSTGRESQL) { - return match ($this->lockType) { + return match ($this->ast->lock) { Lock::FOR_UPDATE => 'FOR UPDATE', Lock::FOR_SHARE => 'FOR SHARE', Lock::FOR_NO_KEY_UPDATE => 'FOR NO KEY UPDATE', @@ -111,7 +109,7 @@ protected function buildLock(): string }; } - return match ($this->lockType) { + return match ($this->ast->lock) { Lock::FOR_UPDATE => 'FOR UPDATE', Lock::FOR_SHARE => 'FOR SHARE', Lock::FOR_UPDATE_SKIP_LOCKED => 'FOR UPDATE SKIP LOCKED', diff --git a/src/Database/Concerns/Query/HasWhereClause.php b/src/Database/Concerns/Query/HasWhereClause.php index 78d6db39..ca7a6fc2 100644 --- a/src/Database/Concerns/Query/HasWhereClause.php +++ b/src/Database/Concerns/Query/HasWhereClause.php @@ -12,8 +12,6 @@ use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; -use function count; - trait HasWhereClause { use HasWhereAllClause; @@ -136,15 +134,12 @@ public function orWhereNotIn(string $column, Closure|array $value): static public function whereNull(string $column): static { - $connector = count($this->clauses) === 0 ? null : LogicalConnector::AND; - $clause = new NullWhereClause( column: $column, operator: Operator::IS_NULL, - connector: $connector ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause); return $this; } @@ -154,25 +149,21 @@ public function orWhereNull(string $column): static $clause = new NullWhereClause( column: $column, operator: Operator::IS_NULL, - connector: LogicalConnector::OR ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause, LogicalConnector::OR); return $this; } public function whereNotNull(string $column): static { - $connector = count($this->clauses) === 0 ? null : LogicalConnector::AND; - $clause = new NullWhereClause( column: $column, operator: Operator::IS_NOT_NULL, - connector: $connector ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause); return $this; } @@ -182,25 +173,21 @@ public function orWhereNotNull(string $column): static $clause = new NullWhereClause( column: $column, operator: Operator::IS_NOT_NULL, - connector: LogicalConnector::OR ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause, LogicalConnector::OR); return $this; } public function whereTrue(string $column): static { - $connector = count($this->clauses) === 0 ? null : LogicalConnector::AND; - $clause = new BooleanWhereClause( column: $column, operator: Operator::IS_TRUE, - connector: $connector ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause); return $this; } @@ -210,25 +197,21 @@ public function orWhereTrue(string $column): static $clause = new BooleanWhereClause( column: $column, operator: Operator::IS_TRUE, - connector: LogicalConnector::OR ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause, LogicalConnector::OR); return $this; } public function whereFalse(string $column): static { - $connector = count($this->clauses) === 0 ? null : LogicalConnector::AND; - $clause = new BooleanWhereClause( column: $column, operator: Operator::IS_FALSE, - connector: $connector ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause); return $this; } @@ -238,28 +221,24 @@ public function orWhereFalse(string $column): static $clause = new BooleanWhereClause( column: $column, operator: Operator::IS_FALSE, - connector: LogicalConnector::OR ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause, LogicalConnector::OR); return $this; } public function whereBetween(string $column, array $values): static { - $connector = count($this->clauses) === 0 ? null : LogicalConnector::AND; - $clause = new BetweenWhereClause( column: $column, operator: Operator::BETWEEN, values: $values, - connector: $connector ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause); - $this->arguments = array_merge($this->arguments, (array) $values); + $this->addArguments((array) $values); return $this; } @@ -270,30 +249,26 @@ public function orWhereBetween(string $column, array $values): static column: $column, operator: Operator::BETWEEN, values: $values, - connector: LogicalConnector::OR ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause, LogicalConnector::OR); - $this->arguments = array_merge($this->arguments, (array) $values); + $this->addArguments((array) $values); return $this; } public function whereNotBetween(string $column, array $values): static { - $connector = count($this->clauses) === 0 ? null : LogicalConnector::AND; - $clause = new BetweenWhereClause( column: $column, operator: Operator::NOT_BETWEEN, values: $values, - connector: $connector ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause); - $this->arguments = array_merge($this->arguments, (array) $values); + $this->addArguments((array) $values); return $this; } @@ -304,12 +279,11 @@ public function orWhereNotBetween(string $column, array $values): static column: $column, operator: Operator::NOT_BETWEEN, values: $values, - connector: LogicalConnector::OR ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause, LogicalConnector::OR); - $this->arguments = array_merge($this->arguments, (array) $values); + $this->addArguments((array) $values); return $this; } @@ -352,16 +326,13 @@ public function orWhereNotExists(Closure $subquery): static public function whereColumn(string $localColumn, string $foreignColumn): static { - $connector = count($this->clauses) === 0 ? null : LogicalConnector::AND; - $clause = new ColumnWhereClause( column: $localColumn, operator: Operator::EQUAL, compareColumn: $foreignColumn, - connector: $connector ); - $this->clauses[] = $clause; + $this->pushWhereClause($clause); return $this; } diff --git a/src/Database/Concerns/Query/HasWhereDateClause.php b/src/Database/Concerns/Query/HasWhereDateClause.php index d3417cb0..315ad810 100644 --- a/src/Database/Concerns/Query/HasWhereDateClause.php +++ b/src/Database/Concerns/Query/HasWhereDateClause.php @@ -269,6 +269,6 @@ protected function pushTimeClause( LogicalConnector $logicalConnector = LogicalConnector::AND ): void { $this->pushClause(new DateWhereClause($column, $operator, $function, $value), $logicalConnector); - $this->arguments = array_merge($this->arguments, [$value]); + $this->addArguments([$value]); } } diff --git a/src/Database/Dialects/Postgres/Compilers/Select.php b/src/Database/Dialects/Postgres/Compilers/Select.php index 296fc6e6..8e2bc5ab 100644 --- a/src/Database/Dialects/Postgres/Compilers/Select.php +++ b/src/Database/Dialects/Postgres/Compilers/Select.php @@ -29,7 +29,7 @@ public function compile(QueryAst $ast): CompiledClause $result = parent::compile($ast); return new CompiledClause( - $this->convertPlaceholders($result->sql), + $this->normalizePlaceholders($result->sql), $result->params ); } diff --git a/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php b/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php index ee3e7665..25d6727a 100644 --- a/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php +++ b/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php @@ -6,6 +6,7 @@ trait HasPlaceholders { + // TODO: Refactor this to be more efficient and handle edge cases protected function convertPlaceholders(string $sql, int $startIndex = 0): string { $index = $startIndex + 1; @@ -18,4 +19,17 @@ function () use (&$index): string { $sql ); } + + protected function normalizePlaceholders(string $sql, int $startIndex = 0): string + { + $index = $startIndex + 1; + + return preg_replace_callback( + '/\?|\$\d+/', + function () use (&$index): string { + return '$' . ($index++); + }, + $sql + ); + } } diff --git a/src/Database/Models/QueryBuilders/DatabaseQueryBuilder.php b/src/Database/Models/QueryBuilders/DatabaseQueryBuilder.php index e3f25e50..88ffb555 100644 --- a/src/Database/Models/QueryBuilders/DatabaseQueryBuilder.php +++ b/src/Database/Models/QueryBuilders/DatabaseQueryBuilder.php @@ -54,14 +54,13 @@ public function __clone(): void parent::__clone(); $this->relationships = []; $this->isLocked = false; - $this->lockType = null; } public function addSelect(array $columns): static { - $this->action = Action::SELECT; + $this->ast->action = Action::SELECT; - $this->columns = array_merge($this->columns, $columns); + $this->ast->columns = array_merge($this->ast->columns, $columns); return $this; } @@ -76,7 +75,7 @@ public function setModel(DatabaseModel $model): self $this->model = $model; } - $this->table = $this->model->getTable(); + $this->ast->table = $this->model->getTable(); return $this; } @@ -132,8 +131,8 @@ public function with(array|string $relationships): self */ public function get(): Collection { - $this->action = Action::SELECT; - $this->columns = empty($this->columns) ? ['*'] : $this->columns; + $this->ast->action = Action::SELECT; + $this->ast->columns = empty($this->ast->columns) ? ['*'] : $this->ast->columns; [$dml, $params] = $this->toSql(); @@ -157,7 +156,7 @@ public function get(): Collection */ public function first(): DatabaseModel|null { - $this->action = Action::SELECT; + $this->ast->action = Action::SELECT; $this->limit(1); diff --git a/src/Database/QueryBase.php b/src/Database/QueryBase.php index b5ea426a..3919ca33 100644 --- a/src/Database/QueryBase.php +++ b/src/Database/QueryBase.php @@ -5,103 +5,134 @@ namespace Phenix\Database; use Closure; +use Phenix\Database\Clauses\WhereClause; use Phenix\Database\Concerns\Query\BuildsQuery; use Phenix\Database\Concerns\Query\HasJoinClause; use Phenix\Database\Concerns\Query\HasLock; use Phenix\Database\Constants\Action; +use Phenix\Database\Constants\Driver; +use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; use Phenix\Database\Constants\SQL; use Phenix\Database\Contracts\Builder; use Phenix\Database\Contracts\QueryBuilder; +use function count; + abstract class QueryBase extends Clause implements QueryBuilder, Builder { use BuildsQuery; use HasLock; use HasJoinClause; - protected string $table; - - protected Action $action; - - protected array $columns; - - protected array $values; - - protected array $joins; + protected QueryAst $ast; - protected Having|null $having; + public function __construct() + { + $this->resetBaseProperties(); + } - protected array $groupBy; + public function __clone(): void + { + $this->ast = clone $this->ast; + $this->ast->lock = null; + } - protected array $orderBy; + protected function resetBaseProperties(): void + { + $this->ast = $this->makeFreshAst(); + } - protected array $limit; + public function setDriver(Driver $driver): static + { + $this->driver = $driver; - protected array $offset; + if (isset($this->ast)) { + $this->ast->driver = $driver; + } - protected string $rawStatement; + return $this; + } - protected bool $ignore = false; + protected function makeFreshAst(): QueryAst + { + $ast = new QueryAst(); + $ast->columns = []; - protected array $uniqueColumns; + if (isset($this->driver)) { + $ast->driver = $this->driver; + } - protected array $returning = []; + return $ast; + } - public function __construct() + /** + * @return array + */ + protected function getClauses(): array { - $this->ignore = false; + return $this->ast->wheres; + } - $this->resetBaseProperties(); + /** + * @return array + */ + protected function getArguments(): array + { + return $this->ast->params; } - public function __clone(): void + protected function hasWhereClauses(): bool { - $this->resetBaseProperties(); + return count($this->ast->wheres) > 0; } - protected function resetBaseProperties(): void + protected function addArguments(array $arguments): void { - $this->joins = []; - $this->columns = []; - $this->values = []; - $this->having = null; - $this->clauses = []; - $this->arguments = []; - $this->uniqueColumns = []; - $this->returning = []; + $this->ast->params = [...$this->ast->params, ...$arguments]; + } + + protected function pushWhereClause( + WhereClause $where, + LogicalConnector $logicalConnector = LogicalConnector::AND + ): void { + if ($this->hasWhereClauses()) { + $where->setConnector($logicalConnector); + } + + $this->ast->wheres[] = $where; } public function count(string $column = '*'): array|int { - $this->action = Action::SELECT; + $this->ast->action = Action::SELECT; - $this->columns = [Functions::count($column)]; + $this->ast->columns = [Functions::count($column)]; return $this->toSql(); } public function exists(): array|bool { - $this->action = Action::EXISTS; + $this->ast->action = Action::EXISTS; - $this->columns = [Operator::EXISTS->value]; + $this->ast->columns = [Operator::EXISTS->value]; return $this->toSql(); } public function doesntExist(): array|bool { - $this->action = Action::EXISTS; + $this->ast->action = Action::EXISTS; - $this->columns = [Operator::NOT_EXISTS->value]; + $this->ast->columns = [Operator::NOT_EXISTS->value]; return $this->toSql(); } public function insert(array $data): array|bool { - $this->action = Action::INSERT; + $this->ast->action = Action::INSERT; $this->prepareDataToInsert($data); @@ -110,7 +141,7 @@ public function insert(array $data): array|bool public function insertOrIgnore(array $values): array|bool { - $this->ignore = true; + $this->ast->ignore = true; $this->insert($values); @@ -126,33 +157,33 @@ public function insertFrom(Closure $subquery, array $columns, bool $ignore = fal [$dml, $arguments] = $builder->toSql(); - $this->rawStatement = trim($dml, '()'); + $this->ast->rawStatement = trim($dml, '()'); - $this->arguments = array_merge($this->arguments, $arguments); + $this->addArguments($arguments); - $this->action = Action::INSERT; + $this->ast->action = Action::INSERT; - $this->ignore = $ignore; + $this->ast->ignore = $ignore; - $this->columns = $columns; + $this->ast->columns = $columns; return $this->toSql(); } public function update(array $values): array|bool { - $this->action = Action::UPDATE; + $this->ast->action = Action::UPDATE; - $this->values = $values; + $this->ast->values = $values; return $this->toSql(); } public function upsert(array $values, array $columns): array|bool { - $this->action = Action::INSERT; + $this->ast->action = Action::INSERT; - $this->uniqueColumns = $columns; + $this->ast->uniqueColumns = $columns; $this->prepareDataToInsert($values); @@ -161,7 +192,7 @@ public function upsert(array $values, array $columns): array|bool public function delete(): array|bool { - $this->action = Action::DELETE; + $this->ast->action = Action::DELETE; return $this->toSql(); } @@ -173,7 +204,7 @@ public function delete(): array|bool */ public function returning(array $columns = ['*']): static { - $this->returning = array_unique($columns); + $this->ast->returning = array_unique($columns); return $this; } @@ -190,10 +221,10 @@ protected function prepareDataToInsert(array $data): void ksort($data); - $this->columns = array_unique([...$this->columns, ...array_keys($data)]); + $this->ast->columns = array_unique([...$this->ast->columns, ...array_keys($data)]); - $this->arguments = \array_merge($this->arguments, array_values($data)); + $this->addArguments(array_values($data)); - $this->values[] = array_fill(0, count($data), SQL::PLACEHOLDER->value); + $this->ast->values[] = array_fill(0, count($data), SQL::PLACEHOLDER->value); } } diff --git a/src/Database/QueryBuilder.php b/src/Database/QueryBuilder.php index 283c69fb..463fa284 100644 --- a/src/Database/QueryBuilder.php +++ b/src/Database/QueryBuilder.php @@ -45,7 +45,6 @@ public function __clone(): void $this->connection = $connection; $this->transaction = $transaction; $this->isLocked = false; - $this->lockType = null; } public function connection(SqlConnection|string $connection): self @@ -68,7 +67,7 @@ public function getConnection(): SqlConnection public function count(string $column = '*'): int { - $this->action = Action::SELECT; + $this->ast->action = Action::SELECT; [$dml, $params] = parent::count($column); @@ -80,7 +79,7 @@ public function count(string $column = '*'): int public function exists(): bool { - $this->action = Action::EXISTS; + $this->ast->action = Action::EXISTS; [$dml, $params] = parent::exists(); @@ -96,7 +95,7 @@ public function doesntExist(): bool public function paginate(Http $uri, int $defaultPage = 1, int $defaultPerPage = 15): Paginator { - $this->action = Action::SELECT; + $this->ast->action = Action::SELECT; $query = Query::fromUri($uri); @@ -132,7 +131,7 @@ public function insert(array $data): bool public function insertOrIgnore(array $values): bool { - $this->ignore = true; + $this->ast->ignore = true; return $this->insert($values); } @@ -146,15 +145,15 @@ public function insertFrom(Closure $subquery, array $columns, bool $ignore = fal [$dml, $arguments] = $builder->toSql(); - $this->rawStatement = trim($dml, '()'); + $this->ast->rawStatement = trim($dml, '()'); - $this->arguments = array_merge($this->arguments, $arguments); + $this->addArguments($arguments); - $this->action = Action::INSERT; + $this->ast->action = Action::INSERT; - $this->ignore = $ignore; + $this->ast->ignore = $ignore; - $this->columns = $columns; + $this->ast->columns = $columns; try { [$dml, $params] = $this->toSql(); @@ -216,7 +215,7 @@ public function update(array $values): bool */ public function updateReturning(array $values, array $columns = ['*']): Collection { - $this->returning = array_unique($columns); + $this->ast->returning = array_unique($columns); [$dml, $params] = parent::update($values); @@ -239,9 +238,9 @@ public function updateReturning(array $values, array $columns = ['*']): Collecti public function upsert(array $values, array $columns): bool { - $this->action = Action::INSERT; + $this->ast->action = Action::INSERT; - $this->uniqueColumns = $columns; + $this->ast->uniqueColumns = $columns; return $this->insert($values); } @@ -269,7 +268,7 @@ public function delete(): bool */ public function deleteReturning(array $columns = ['*']): Collection { - $this->returning = array_unique($columns); + $this->ast->returning = array_unique($columns); [$dml, $params] = parent::delete(); @@ -295,7 +294,7 @@ public function deleteReturning(array $columns = ['*']): Collection */ public function get(): Collection { - $this->action = Action::SELECT; + $this->ast->action = Action::SELECT; [$dml, $params] = $this->toSql(); @@ -315,7 +314,7 @@ public function get(): Collection */ public function first(): object|array|null { - $this->action = Action::SELECT; + $this->ast->action = Action::SELECT; $this->limit(1); diff --git a/src/Database/QueryGenerator.php b/src/Database/QueryGenerator.php index e2514f92..ac406faf 100644 --- a/src/Database/QueryGenerator.php +++ b/src/Database/QueryGenerator.php @@ -14,14 +14,13 @@ public function __construct(Driver $driver = Driver::MYSQL) { parent::__construct(); - $this->driver = $driver; + $this->setDriver($driver); } public function __clone(): void { parent::__clone(); $this->isLocked = false; - $this->lockType = null; } public function insert(array $data): array @@ -31,7 +30,7 @@ public function insert(array $data): array public function insertOrIgnore(array $values): array { - $this->ignore = true; + $this->ast->ignore = true; $this->insert($values); @@ -75,14 +74,14 @@ public function doesntExist(): array public function get(): array { - $this->action = Action::SELECT; + $this->ast->action = Action::SELECT; return $this->toSql(); } public function first(): array { - $this->action = Action::SELECT; + $this->ast->action = Action::SELECT; return $this->limit(1)->toSql(); } diff --git a/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php b/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php index 94a660dd..297a4cd7 100644 --- a/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php @@ -136,6 +136,31 @@ expect($params)->toBeEmpty(); }); +it('stores insert from subquery params directly in query ast', function () { + $query = new class () extends QueryGenerator { + public function params(): array + { + return $this->buildAst()->params; + } + }; + + $sql = $query->table('users') + ->insertFrom(function (Subquery $subquery) { + $subquery->table('customers') + ->select(['name', 'email']) + ->whereEqual('status', 'active'); + }, ['name', 'email']); + + [$dml, $params] = $sql; + + $expected = "INSERT INTO `users` (`name`, `email`) " + . "SELECT `name`, `email` FROM `customers` WHERE `status` = ?"; + + expect($query->params())->toBe(['active']); + expect($dml)->toBe($expected); + expect($params)->toBe(['active']); +}); + it('generates insert ignore statement from subquery', function () { $query = new QueryGenerator(); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php index 396a605f..d56efaab 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php @@ -121,6 +121,27 @@ ['whereNotIn', Operator::NOT_IN->value], ]); +it('keeps postgres placeholder order across where and subquery params', function () { + $query = new QueryGenerator(Driver::POSTGRESQL); + + $sql = $query->table('users') + ->whereEqual('status', 'active') + ->whereIn('id', function (Subquery $query) { + $query->select(['user_id']) + ->from('orders') + ->whereGreaterThan('total', 100); + }) + ->get(); + + [$dml, $params] = $sql; + + $expected = 'SELECT * FROM "users" WHERE "status" = $1 AND "id" IN ' + . '(SELECT "user_id" FROM "orders" WHERE "total" > $2)'; + + expect($dml)->toBe($expected); + expect($params)->toBe(['active', 100]); +}); + it('generates query to select null or not null columns', function (string $method, string $operator) { $query = new QueryGenerator(Driver::POSTGRESQL); diff --git a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php index e9341b00..929ac8a3 100644 --- a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php @@ -3,9 +3,11 @@ declare(strict_types=1); use Phenix\Database\Alias; +use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Operator; use Phenix\Database\Exceptions\QueryErrorException; use Phenix\Database\Functions; +use Phenix\Database\QueryAst; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; @@ -37,6 +39,59 @@ expect($params)->toBeEmpty(); }); +it('keeps query ast synchronized as primary query state', function (): void { + $query = new class () extends QueryGenerator { + public function ast(): QueryAst + { + return $this->buildAst(); + } + }; + + $query->setDriver(Driver::POSTGRESQL) + ->table('users') + ->select(['id']) + ->whereEqual('id', 1); + + $ast = $query->ast(); + [$dml, $params] = $query->get(); + + expect($ast->driver)->toBe(Driver::POSTGRESQL); + expect($ast->table)->toBe('users'); + expect($ast->columns)->toBe(['id']); + expect($ast->params)->toBe([1]); + expect($dml)->toBe('SELECT "id" FROM "users" WHERE "id" = $1'); + expect($params)->toBe([1]); +}); + +it('stores where and subquery params directly in query ast', function (): void { + $query = new class () extends QueryGenerator { + public function ast(): QueryAst + { + return $this->buildAst(); + } + }; + + $query->select(['id']) + ->from(function (Subquery $subquery): void { + $subquery->from('users') + ->whereEqual('verified_at', '2026-01-15'); + }) + ->whereEqual('status', 'active') + ->whereBetween('age', [18, 65]) + ->whereDateEqual('created_at', '2026-01-30'); + + $ast = $query->ast(); + [$dml, $params] = $query->get(); + + $expected = "SELECT `id` FROM (SELECT * FROM `users` WHERE `verified_at` = ?) " + . "WHERE `status` = ? AND `age` BETWEEN ? AND ? AND DATE(`created_at`) = ?"; + + expect($ast->wheres)->toHaveCount(3); + expect($ast->params)->toBe(['2026-01-15', 'active', 18, 65, '2026-01-30']); + expect($dml)->toBe($expected); + expect($params)->toBe($ast->params); +}); + it('generates a query using sql functions', function (string $function, string $column, string $rawFunction) { $query = new QueryGenerator(); diff --git a/tests/Unit/Database/QueryGenerator/WhereDateClausesTest.php b/tests/Unit/Database/QueryGenerator/WhereDateClausesTest.php index f8a5dde8..549b3745 100644 --- a/tests/Unit/Database/QueryGenerator/WhereDateClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/WhereDateClausesTest.php @@ -4,8 +4,6 @@ use Carbon\Carbon; use Carbon\CarbonInterface; -use Phenix\Database\Clauses\BasicWhereClause; -use Phenix\Database\Clauses\DateWhereClause; use Phenix\Database\Constants\Operator; use Phenix\Database\QueryGenerator; @@ -172,16 +170,3 @@ ['orWhereYearLessThan', date('Y'), date('Y'), Operator::LESS_THAN->value], ['orWhereYearLessThanOrEqual', date('Y'), date('Y'), Operator::LESS_THAN_OR_EQUAL->value], ]); - -it('stores date where clauses as DateWhereClause instances', function () { - $query = new QueryGenerator(); - $query->table('users')->whereDateEqual('created_at', '2026-01-15'); - - $reflection = new ReflectionClass($query); - $property = $reflection->getProperty('clauses'); - $clauses = $property->getValue($query); - - expect($clauses)->toHaveCount(1); - expect($clauses[0])->toBeInstanceOf(DateWhereClause::class); - expect($clauses[0])->not->toBeInstanceOf(BasicWhereClause::class); -}); From 563e2fc886ca2a55246812841314f46cbeefa14d Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Apr 2026 19:35:21 +0000 Subject: [PATCH 08/32] refactor: simplify placeholder conversion using arrow functions Co-authored-by: Copilot --- .../Dialects/Postgres/Concerns/HasPlaceholders.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php b/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php index 25d6727a..1a398ca1 100644 --- a/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php +++ b/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php @@ -13,9 +13,7 @@ protected function convertPlaceholders(string $sql, int $startIndex = 0): string return preg_replace_callback( '/\?/', - function () use (&$index): string { - return '$' . ($index++); - }, + fn (): string => '$' . ($index++), $sql ); } @@ -26,9 +24,7 @@ protected function normalizePlaceholders(string $sql, int $startIndex = 0): stri return preg_replace_callback( '/\?|\$\d+/', - function () use (&$index): string { - return '$' . ($index++); - }, + fn (): string => '$' . ($index++), $sql ); } From 602145423b67bef43bcff543647e5047e57dbbe1 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Thu, 30 Apr 2026 19:39:24 +0000 Subject: [PATCH 09/32] refactor: streamline SQL compilation by directly invoking buildAst in toSql method --- src/Database/Concerns/Query/BuildsQuery.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Database/Concerns/Query/BuildsQuery.php b/src/Database/Concerns/Query/BuildsQuery.php index 64806b60..3f6f7055 100644 --- a/src/Database/Concerns/Query/BuildsQuery.php +++ b/src/Database/Concerns/Query/BuildsQuery.php @@ -122,10 +122,9 @@ public function page(int $page = 1, int $perPage = 15): static */ public function toSql(): array { - $ast = $this->buildAst(); $dialect = DialectFactory::fromDriver($this->driver); - return $dialect->compile($ast); + return $dialect->compile($this->buildAst()); } protected function buildAst(): QueryAst From 4dd1cf0ddf7334e6d1911c9f6306d4fb3b4e2b3b Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 1 May 2026 09:31:53 -0500 Subject: [PATCH 10/32] Refactor ClauseCompiler and related compilers to use QueryAst directly - Updated ClauseCompiler interface to separate AST setting and compilation methods. - Modified all compiler classes (Insert, Update, Delete, Select, Exists, etc.) to utilize the new AST handling. - Removed redundant QueryAst parameters from compile methods. - Ensured that all compilers now access the QueryAst through a protected method. - Added tests to verify that compiler state does not leak between compilations. --- src/Database/Contracts/ClauseCompiler.php | 4 +- .../Dialects/Compilers/ClauseCompiler.php | 36 ++++++++ .../Dialects/Compilers/DeleteCompiler.php | 16 ++-- .../Dialects/Compilers/ExistsCompiler.php | 21 ++--- .../Dialects/Compilers/InsertCompiler.php | 31 +++---- .../Dialects/Compilers/SelectCompiler.php | 92 +++++++++---------- .../Dialects/Compilers/UpdateCompiler.php | 26 +++--- src/Database/Dialects/Dialect.php | 10 +- .../Dialects/Mysql/Compilers/Insert.php | 10 +- .../Dialects/Mysql/Compilers/Select.php | 5 +- .../Dialects/Mysql/Compilers/Update.php | 6 +- .../Dialects/Postgres/Compilers/Delete.php | 5 +- .../Dialects/Postgres/Compilers/Exists.php | 5 +- .../Dialects/Postgres/Compilers/Insert.php | 37 ++++---- .../Dialects/Postgres/Compilers/Select.php | 9 +- .../Dialects/Postgres/Compilers/Update.php | 11 +-- .../Postgres/Concerns/HasPlaceholders.php | 8 +- .../Dialects/Sqlite/Compilers/Delete.php | 20 ++-- .../Dialects/Sqlite/Compilers/Insert.php | 21 ++--- .../Dialects/Sqlite/Compilers/Select.php | 3 +- .../Dialects/Sqlite/Compilers/Update.php | 4 +- .../QueryGenerator/SelectColumnsTest.php | 18 ++++ 22 files changed, 211 insertions(+), 187 deletions(-) diff --git a/src/Database/Contracts/ClauseCompiler.php b/src/Database/Contracts/ClauseCompiler.php index 00493450..3989fce4 100644 --- a/src/Database/Contracts/ClauseCompiler.php +++ b/src/Database/Contracts/ClauseCompiler.php @@ -9,5 +9,7 @@ interface ClauseCompiler { - public function compile(QueryAst $ast): CompiledClause; + public function setAst(QueryAst $ast): static; + + public function compile(): CompiledClause; } diff --git a/src/Database/Dialects/Compilers/ClauseCompiler.php b/src/Database/Dialects/Compilers/ClauseCompiler.php index ccf06512..f0fa0b28 100644 --- a/src/Database/Dialects/Compilers/ClauseCompiler.php +++ b/src/Database/Dialects/Compilers/ClauseCompiler.php @@ -4,13 +4,49 @@ namespace Phenix\Database\Dialects\Compilers; +use LogicException; use Phenix\Database\Contracts\ClauseCompiler as ClauseCompilerContract; +use Phenix\Database\QueryAst; +use Phenix\Database\Wrapper; abstract class ClauseCompiler implements ClauseCompilerContract { + protected QueryAst $ast; + protected WhereCompiler $whereCompiler; protected JoinCompiler $joinCompiler; protected HavingCompiler $havingCompiler; + + public function setAst(QueryAst $ast): static + { + $this->ast = $ast; + + return $this; + } + + protected function ast(): QueryAst + { + if (! isset($this->ast)) { + throw new LogicException('Query AST must be set before compiling.'); + } + + return $this->ast; + } + + protected function wrap(string $value): string + { + return Wrapper::column($this->ast->driver, $value); + } + + protected function wrapOf(string $value): string + { + return (string) Wrapper::of($this->ast->driver, $value); + } + + protected function wrapList(array $values): array + { + return Wrapper::columnList($this->ast->driver, $values); + } } diff --git a/src/Database/Dialects/Compilers/DeleteCompiler.php b/src/Database/Dialects/Compilers/DeleteCompiler.php index de1ccc19..ddf85429 100644 --- a/src/Database/Dialects/Compilers/DeleteCompiler.php +++ b/src/Database/Dialects/Compilers/DeleteCompiler.php @@ -4,25 +4,21 @@ namespace Phenix\Database\Dialects\Compilers; -use Phenix\Database\Contracts\ClauseCompiler; use Phenix\Database\Dialects\CompiledClause; -use Phenix\Database\QueryAst; use Phenix\Database\Wrapper; use Phenix\Util\Arr; -abstract class DeleteCompiler implements ClauseCompiler +abstract class DeleteCompiler extends ClauseCompiler { - protected WhereCompiler $whereCompiler; - - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { $parts = []; $parts[] = 'DELETE FROM'; - $parts[] = Wrapper::of($ast->driver, $ast->table); + $parts[] = $this->wrap($this->ast->table); - if (! empty($ast->wheres)) { - $whereCompiled = $this->whereCompiler->compile($ast->wheres); + if (! empty($this->ast->wheres)) { + $whereCompiled = $this->whereCompiler->compile($this->ast->wheres); $parts[] = 'WHERE'; $parts[] = $whereCompiled->sql; @@ -30,6 +26,6 @@ public function compile(QueryAst $ast): CompiledClause $sql = Arr::implodeDeeply($parts); - return new CompiledClause($sql, $ast->params); + return new CompiledClause($sql, $this->ast->params); } } diff --git a/src/Database/Dialects/Compilers/ExistsCompiler.php b/src/Database/Dialects/Compilers/ExistsCompiler.php index 22c291e0..af4e199b 100644 --- a/src/Database/Dialects/Compilers/ExistsCompiler.php +++ b/src/Database/Dialects/Compilers/ExistsCompiler.php @@ -4,30 +4,25 @@ namespace Phenix\Database\Dialects\Compilers; -use Phenix\Database\Contracts\ClauseCompiler; use Phenix\Database\Dialects\CompiledClause; -use Phenix\Database\QueryAst; -use Phenix\Database\Wrapper; use Phenix\Util\Arr; -abstract class ExistsCompiler implements ClauseCompiler +abstract class ExistsCompiler extends ClauseCompiler { - protected $whereCompiler; - - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { $parts = []; $parts[] = 'SELECT'; - $column = ! empty($ast->columns) ? $ast->columns[0] : 'EXISTS'; + $column = ! empty($this->ast->columns) ? $this->ast->columns[0] : 'EXISTS'; $parts[] = $column; $subquery = []; $subquery[] = 'SELECT 1 FROM'; - $subquery[] = Wrapper::of($ast->driver, $ast->table); + $subquery[] = $this->wrapOf($this->ast->table); - if (! empty($ast->wheres)) { - $whereCompiled = $this->whereCompiler->compile($ast->wheres); + if (! empty($this->ast->wheres)) { + $whereCompiled = $this->whereCompiler->compile($this->ast->wheres); $subquery[] = 'WHERE'; $subquery[] = $whereCompiled->sql; @@ -35,10 +30,10 @@ public function compile(QueryAst $ast): CompiledClause $parts[] = '(' . Arr::implodeDeeply($subquery) . ')'; $parts[] = 'AS'; - $parts[] = Wrapper::column($ast->driver, 'exists'); + $parts[] = $this->wrap('exists'); $sql = Arr::implodeDeeply($parts); - return new CompiledClause($sql, $ast->params); + return new CompiledClause($sql, $this->ast->params); } } diff --git a/src/Database/Dialects/Compilers/InsertCompiler.php b/src/Database/Dialects/Compilers/InsertCompiler.php index 73affacc..2082e75b 100644 --- a/src/Database/Dialects/Compilers/InsertCompiler.php +++ b/src/Database/Dialects/Compilers/InsertCompiler.php @@ -4,43 +4,41 @@ namespace Phenix\Database\Dialects\Compilers; -use Phenix\Database\Contracts\ClauseCompiler; use Phenix\Database\Dialects\CompiledClause; -use Phenix\Database\QueryAst; use Phenix\Database\Wrapper; use Phenix\Util\Arr; -abstract class InsertCompiler implements ClauseCompiler +abstract class InsertCompiler extends ClauseCompiler { - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { $parts = []; - $params = $ast->params; + $params = $this->ast->params; // INSERT [IGNORE] INTO - $parts[] = $this->compileInsertClause($ast); + $parts[] = $this->compileInsertClause(); - $parts[] = Wrapper::of($ast->driver, $ast->table); + $parts[] = $this->wrapOf($this->ast->table); // (column1, column2, ...) - $parts[] = '(' . Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->columns), ', ') . ')'; + $parts[] = '(' . Arr::implodeDeeply($this->wrapList($this->ast->columns), ', ') . ')'; // VALUES (...), (...) or raw statement - if ($ast->rawStatement !== null) { - $parts[] = $ast->rawStatement; + if ($this->ast->rawStatement !== null) { + $parts[] = $this->ast->rawStatement; } else { $parts[] = 'VALUES'; $placeholders = array_map(function (array $value): string { return '(' . Arr::implodeDeeply($value, ', ') . ')'; - }, $ast->values); + }, $this->ast->values); $parts[] = Arr::implodeDeeply(array_values($placeholders), ', '); } // Dialect-specific UPSERT/ON CONFLICT handling - if (! empty($ast->uniqueColumns)) { - $parts[] = $this->compileUpsert($ast); + if (! empty($this->ast->uniqueColumns)) { + $parts[] = $this->compileUpsert(); } $sql = Arr::implodeDeeply($parts); @@ -48,9 +46,9 @@ public function compile(QueryAst $ast): CompiledClause return new CompiledClause($sql, $params); } - protected function compileInsertClause(QueryAst $ast): string + protected function compileInsertClause(): string { - if ($ast->ignore) { + if ($this->ast->ignore) { return $this->compileInsertIgnore(); } @@ -71,8 +69,7 @@ abstract protected function compileInsertIgnore(): string; * PostgreSQL: ON CONFLICT (...) DO UPDATE SET * SQLite: ON CONFLICT (...) DO UPDATE SET * - * @param QueryAst $ast Query AST with uniqueColumns * @return string UPSERT clause */ - abstract protected function compileUpsert(QueryAst $ast): string; + abstract protected function compileUpsert(): string; } diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index 3468dd84..818670f0 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -5,44 +5,38 @@ namespace Phenix\Database\Dialects\Compilers; use Phenix\Database\Alias; -use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Operator; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Exceptions\QueryErrorException; use Phenix\Database\Functions; -use Phenix\Database\QueryAst; use Phenix\Database\SelectCase; use Phenix\Database\Subquery; -use Phenix\Database\Wrapper; use Phenix\Util\Arr; use function is_string; abstract class SelectCompiler extends ClauseCompiler { - protected array $params = []; + abstract protected function compileLock(): string; - abstract protected function compileLock(QueryAst $ast): string; - - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { - $this->params = $ast->params; - + $ast = $this->ast(); $columns = empty($ast->columns) ? ['*'] : $ast->columns; $sql = [ 'SELECT', - $this->compileColumns($columns, $ast->driver), + $this->compileColumns($columns), 'FROM', - $this->compileTable($ast->table, $ast->driver), + $this->compileTable(), ]; if (! empty($ast->joins)) { - $joins = $this->compileJoins($ast); + $joins = $this->compileJoins(); if ($joins->sql !== '') { $sql[] = $joins->sql; - $this->params = [...$joins->params, ...$this->params]; + $ast->params = [...$joins->params, ...$ast->params]; } } @@ -57,7 +51,7 @@ public function compile(QueryAst $ast): CompiledClause if (! empty($ast->groups)) { $sql[] = Operator::GROUP_BY->value; - $sql[] = $this->compileGroups($ast->groups, $ast->driver); + $sql[] = $this->compileGroups($ast->groups); } if ($ast->having !== null) { @@ -65,13 +59,13 @@ public function compile(QueryAst $ast): CompiledClause if ($having->sql !== '') { $sql[] = $having->sql; - $this->params = [...$this->params, ...$having->params]; + $ast->params = [...$ast->params, ...$having->params]; } } if (! empty($ast->orders)) { $sql[] = Operator::ORDER_BY->value; - $sql[] = $this->compileOrders($ast->orders, $ast->driver); + $sql[] = $this->compileOrders($ast->orders); } if ($ast->limit !== null) { @@ -83,7 +77,7 @@ public function compile(QueryAst $ast): CompiledClause } if ($ast->lock !== null) { - $lockSql = $this->compileLock($ast); + $lockSql = $this->compileLock(); if ($lockSql !== '') { $sql[] = $lockSql; @@ -92,7 +86,7 @@ public function compile(QueryAst $ast): CompiledClause return new CompiledClause( Arr::implodeDeeply($sql), - $this->params + $ast->params ); } @@ -100,16 +94,16 @@ public function compile(QueryAst $ast): CompiledClause * @param array $columns * @return string */ - protected function compileColumns(array $columns, Driver $driver): string + protected function compileColumns(array $columns): string { - $compiled = Arr::map($columns, function (string|Alias|Functions|SelectCase|Subquery $value, int|string $key) use ($driver): string { + $compiled = Arr::map($columns, function (string|Alias|Functions|SelectCase|Subquery $value, int|string $key): string { return match (true) { - is_string($key) => (string) Alias::of($key)->as($value)->setDriver($driver), - $value instanceof Alias => (string) $value->setDriver($driver), - $value instanceof Functions => (string) $value->setDriver($driver), - $value instanceof SelectCase => (string) $value->setDriver($driver), - $value instanceof Subquery => $this->compileSubquery($value, $driver), - default => (string) Wrapper::column($driver, (string) $value), + is_string($key) => (string) Alias::of($key)->as($value)->setDriver($this->ast()->driver), + $value instanceof Alias => (string) $value->setDriver($this->ast()->driver), + $value instanceof Functions => (string) $value->setDriver($this->ast()->driver), + $value instanceof SelectCase => (string) $value->setDriver($this->ast()->driver), + $value instanceof Subquery => $this->compileSubquery($value), + default => $this->wrap((string) $value), }; }); @@ -120,39 +114,39 @@ protected function compileColumns(array $columns, Driver $driver): string * @param array $groups * @return string */ - protected function compileGroups(array $groups, Driver $driver): string + protected function compileGroups(array $groups): string { - $compiled = Arr::map($groups, function (string|Functions $value) use ($driver): string { + $compiled = Arr::map($groups, function (string|Functions $value): string { return match (true) { - $value instanceof Functions => (string) $value->setDriver($driver), - default => (string) Wrapper::column($driver, $value), + $value instanceof Functions => (string) $value->setDriver($this->ast()->driver), + default => $this->wrap((string) $value), }; }); return Arr::implodeDeeply($compiled, ', '); } - protected function compileOrders(array $orders, Driver $driver): string + protected function compileOrders(array $orders): string { [$columns, $order] = $orders; - $compiled = Arr::map($columns, function (string|Functions|SelectCase $value) use ($driver): string { + $compiled = Arr::map($columns, function (string|Functions|SelectCase $value): string { return match (true) { - $value instanceof Functions => (string) $value->setDriver($driver), - $value instanceof SelectCase => '(' . (string) $value->setDriver($driver) . ')', - default => (string) Wrapper::column($driver, $value), + $value instanceof Functions => (string) $value->setDriver($this->ast()->driver), + $value instanceof SelectCase => '(' . (string) $value->setDriver($this->ast()->driver) . ')', + default => $this->wrap((string) $value), }; }); return Arr::implodeDeeply([Arr::implodeDeeply($compiled, ', '), $order]); } - private function compileJoins(QueryAst $ast): CompiledClause + private function compileJoins(): CompiledClause { $sql = []; $params = []; - foreach ($ast->joins as $join) { + foreach ($this->ast()->joins as $join) { $compiled = $this->joinCompiler->compile($join); $sql[] = $compiled->sql; @@ -166,29 +160,35 @@ private function compileJoins(QueryAst $ast): CompiledClause * @param Subquery $subquery * @return string */ - private function compileSubquery(Subquery $subquery, Driver $driver): string + private function compileSubquery(Subquery $subquery): string { - $subquery->setDriver($driver); + $parentAst = $this->ast(); + $subquery->setDriver($parentAst->driver); - [$dml, $arguments] = $subquery->toSql(); + try { + [$dml, $arguments] = $subquery->toSql(); + } finally { + $this->setAst($parentAst); + } if (! str_contains($dml, 'LIMIT 1')) { throw new QueryErrorException('The subquery must be limited to one record'); } - $this->params = [...$this->params, ...$arguments]; + $parentAst->params = [...$parentAst->params, ...$arguments]; return $dml; } - private function compileTable(string $table, Driver $driver): string + private function compileTable(): string { - $trimmed = trim($table); + $ast = $this->ast(); + $table = trim($ast->table); - if ($trimmed !== '' && str_starts_with($trimmed, '(')) { - return $table; + if ($table !== '' && str_starts_with($table, '(')) { + return $ast->table; } - return (string) Wrapper::of($driver, $table); + return $this->wrapOf($ast->table); } } diff --git a/src/Database/Dialects/Compilers/UpdateCompiler.php b/src/Database/Dialects/Compilers/UpdateCompiler.php index 84992398..f9d159e6 100644 --- a/src/Database/Dialects/Compilers/UpdateCompiler.php +++ b/src/Database/Dialects/Compilers/UpdateCompiler.php @@ -5,50 +5,46 @@ namespace Phenix\Database\Dialects\Compilers; use Phenix\Database\Constants\Driver; -use Phenix\Database\Contracts\ClauseCompiler; use Phenix\Database\Dialects\CompiledClause; -use Phenix\Database\QueryAst; use Phenix\Database\Wrapper; use Phenix\Util\Arr; use function count; -abstract class UpdateCompiler implements ClauseCompiler +abstract class UpdateCompiler extends ClauseCompiler { - protected $whereCompiler; - - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { $parts = []; $params = []; $parts[] = 'UPDATE'; - $parts[] = Wrapper::of($ast->driver, $ast->table); + $parts[] = $this->wrapOf($this->ast->table); // SET col1 = ?, col2 = ? // Extract params from values (these are actual values, not placeholders) $columns = []; - foreach ($ast->values as $column => $value) { + foreach ($this->ast->values as $column => $value) { $params[] = $value; - $columns[] = $this->compileSetClause($ast->driver, $column, count($params)); + $columns[] = $this->compileSetClause($column, count($params)); } $parts[] = 'SET'; $parts[] = Arr::implodeDeeply($columns, ', '); - if (! empty($ast->wheres)) { - $whereCompiled = $this->whereCompiler->compile($ast->wheres); + if (! empty($this->ast->wheres)) { + $whereCompiled = $this->whereCompiler->compile($this->ast->wheres); $parts[] = 'WHERE'; $parts[] = $whereCompiled->sql; - $params = array_merge($params, $ast->params); + $params = array_merge($params, $this->ast->params); } - if (! empty($ast->returning)) { + if (! empty($this->ast->returning)) { $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->returning), ', '); + $parts[] = Arr::implodeDeeply($this->wrapList($this->ast->returning), ', '); } $sql = Arr::implodeDeeply($parts); @@ -60,5 +56,5 @@ public function compile(QueryAst $ast): CompiledClause * Compile the SET clause for a column assignment * This is dialect-specific for placeholder syntax */ - abstract protected function compileSetClause(Driver $driver, string $column, int $paramIndex): string; + abstract protected function compileSetClause(string $column, int $paramIndex): string; } diff --git a/src/Database/Dialects/Dialect.php b/src/Database/Dialects/Dialect.php index d1aae500..90fed3d2 100644 --- a/src/Database/Dialects/Dialect.php +++ b/src/Database/Dialects/Dialect.php @@ -43,7 +43,7 @@ public function compile(QueryAst $ast): array */ private function compileSelect(QueryAst $ast): array { - $compiled = $this->selectCompiler->compile($ast); + $compiled = $this->selectCompiler->setAst($ast)->compile(); return [$compiled->sql, $compiled->params]; } @@ -53,7 +53,7 @@ private function compileSelect(QueryAst $ast): array */ private function compileInsert(QueryAst $ast): array { - $compiled = $this->insertCompiler->compile($ast); + $compiled = $this->insertCompiler->setAst($ast)->compile(); return [$compiled->sql, $compiled->params]; } @@ -63,7 +63,7 @@ private function compileInsert(QueryAst $ast): array */ private function compileUpdate(QueryAst $ast): array { - $compiled = $this->updateCompiler->compile($ast); + $compiled = $this->updateCompiler->setAst($ast)->compile(); return [$compiled->sql, $compiled->params]; } @@ -73,7 +73,7 @@ private function compileUpdate(QueryAst $ast): array */ private function compileDelete(QueryAst $ast): array { - $compiled = $this->deleteCompiler->compile($ast); + $compiled = $this->deleteCompiler->setAst($ast)->compile(); return [$compiled->sql, $compiled->params]; } @@ -83,7 +83,7 @@ private function compileDelete(QueryAst $ast): array */ private function compileExists(QueryAst $ast): array { - $compiled = $this->existsCompiler->compile($ast); + $compiled = $this->existsCompiler->setAst($ast)->compile(); return [$compiled->sql, $compiled->params]; } diff --git a/src/Database/Dialects/Mysql/Compilers/Insert.php b/src/Database/Dialects/Mysql/Compilers/Insert.php index 0254b5ac..cc81ae8f 100644 --- a/src/Database/Dialects/Mysql/Compilers/Insert.php +++ b/src/Database/Dialects/Mysql/Compilers/Insert.php @@ -5,8 +5,6 @@ namespace Phenix\Database\Dialects\Mysql\Compilers; use Phenix\Database\Dialects\Compilers\InsertCompiler; -use Phenix\Database\QueryAst; -use Phenix\Database\Wrapper; use Phenix\Util\Arr; class Insert extends InsertCompiler @@ -16,15 +14,15 @@ protected function compileInsertIgnore(): string return 'INSERT IGNORE INTO'; } - protected function compileUpsert(QueryAst $ast): string + protected function compileUpsert(): string { $columns = array_map( - function (string $column) use ($ast): string { - $column = Wrapper::column($ast->driver, $column); + function (string $column): string { + $column = $this->wrap($column); return "{$column} = VALUES({$column})"; }, - $ast->uniqueColumns + $this->ast->uniqueColumns ); return 'ON DUPLICATE KEY UPDATE ' . Arr::implodeDeeply($columns, ', '); diff --git a/src/Database/Dialects/Mysql/Compilers/Select.php b/src/Database/Dialects/Mysql/Compilers/Select.php index a3115f57..a4991aa6 100644 --- a/src/Database/Dialects/Mysql/Compilers/Select.php +++ b/src/Database/Dialects/Mysql/Compilers/Select.php @@ -9,7 +9,6 @@ use Phenix\Database\Dialects\Compilers\HavingCompiler; use Phenix\Database\Dialects\Compilers\JoinCompiler; use Phenix\Database\Dialects\Compilers\SelectCompiler; -use Phenix\Database\QueryAst; class Select extends SelectCompiler { @@ -20,9 +19,9 @@ public function __construct() $this->havingCompiler = new HavingCompiler($this->whereCompiler); } - protected function compileLock(QueryAst $ast): string + protected function compileLock(): string { - return match ($ast->lock) { + return match ($this->ast->lock) { Lock::FOR_UPDATE => 'FOR UPDATE', Lock::FOR_SHARE => 'FOR SHARE', Lock::FOR_UPDATE_SKIP_LOCKED => 'FOR UPDATE SKIP LOCKED', diff --git a/src/Database/Dialects/Mysql/Compilers/Update.php b/src/Database/Dialects/Mysql/Compilers/Update.php index 3242fd48..54c6c2e3 100644 --- a/src/Database/Dialects/Mysql/Compilers/Update.php +++ b/src/Database/Dialects/Mysql/Compilers/Update.php @@ -4,9 +4,7 @@ namespace Phenix\Database\Dialects\Mysql\Compilers; -use Phenix\Database\Constants\Driver; use Phenix\Database\Dialects\Compilers\UpdateCompiler; -use Phenix\Database\Wrapper; class Update extends UpdateCompiler { @@ -15,9 +13,9 @@ public function __construct() $this->whereCompiler = new Where(); } - protected function compileSetClause(Driver $driver, string $column, int $paramIndex): string + protected function compileSetClause(string $column, int $paramIndex): string { - $column = Wrapper::column($driver, $column); + $column = $this->wrap($column); return "{$column} = ?"; } diff --git a/src/Database/Dialects/Postgres/Compilers/Delete.php b/src/Database/Dialects/Postgres/Compilers/Delete.php index d10ab846..3ae43e55 100644 --- a/src/Database/Dialects/Postgres/Compilers/Delete.php +++ b/src/Database/Dialects/Postgres/Compilers/Delete.php @@ -7,7 +7,6 @@ use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; use Phenix\Database\Dialects\Sqlite\Compilers\Delete as SQLiteDelete; -use Phenix\Database\QueryAst; class Delete extends SQLiteDelete { @@ -18,9 +17,9 @@ public function __construct() $this->whereCompiler = new Where(); } - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { - $clause = parent::compile($ast); + $clause = parent::compile(); $sql = $this->convertPlaceholders($clause->sql); return new CompiledClause($sql, $clause->params); diff --git a/src/Database/Dialects/Postgres/Compilers/Exists.php b/src/Database/Dialects/Postgres/Compilers/Exists.php index 5d422adf..36f79413 100644 --- a/src/Database/Dialects/Postgres/Compilers/Exists.php +++ b/src/Database/Dialects/Postgres/Compilers/Exists.php @@ -7,7 +7,6 @@ use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\ExistsCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; -use Phenix\Database\QueryAst; class Exists extends ExistsCompiler { @@ -18,9 +17,9 @@ public function __construct() $this->whereCompiler = new Where(); } - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { - $result = parent::compile($ast); + $result = parent::compile(); return new CompiledClause( $this->convertPlaceholders($result->sql), diff --git a/src/Database/Dialects/Postgres/Compilers/Insert.php b/src/Database/Dialects/Postgres/Compilers/Insert.php index 07543c20..b011ec8d 100644 --- a/src/Database/Dialects/Postgres/Compilers/Insert.php +++ b/src/Database/Dialects/Postgres/Compilers/Insert.php @@ -7,7 +7,6 @@ use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\InsertCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; -use Phenix\Database\QueryAst; use Phenix\Database\Wrapper; use Phenix\Util\Arr; @@ -27,15 +26,15 @@ protected function compileInsertIgnore(): string return 'INSERT INTO'; } - protected function compileUpsert(QueryAst $ast): string + protected function compileUpsert(): string { - $conflictColumns = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->uniqueColumns), ', '); + $conflictColumns = Arr::implodeDeeply($this->wrapList($this->ast->uniqueColumns), ', '); - $updateColumns = array_map(function (string $column) use ($ast): string { - $column = Wrapper::column($ast->driver, $column); + $updateColumns = array_map(function (string $column): string { + $column = $this->wrap($column); return "{$column} = EXCLUDED.{$column}"; - }, $ast->uniqueColumns); + }, $this->ast->uniqueColumns); return sprintf( 'ON CONFLICT (%s) DO UPDATE SET %s', @@ -44,45 +43,45 @@ protected function compileUpsert(QueryAst $ast): string ); } - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { - if ($ast->ignore && empty($ast->uniqueColumns)) { + if ($this->ast->ignore && empty($this->ast->uniqueColumns)) { $parts = []; $parts[] = 'INSERT INTO'; - $parts[] = Wrapper::of($ast->driver, $ast->table); - $parts[] = '(' . Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->columns), ', ') . ')'; + $parts[] = $this->wrapOf($this->ast->table); + $parts[] = '(' . Arr::implodeDeeply($this->wrapList($this->ast->columns), ', ') . ')'; - if ($ast->rawStatement !== null) { - $parts[] = $ast->rawStatement; + if ($this->ast->rawStatement !== null) { + $parts[] = $this->ast->rawStatement; } else { $parts[] = 'VALUES'; $placeholders = array_map(function (array $value): string { return '(' . Arr::implodeDeeply($value, ', ') . ')'; - }, $ast->values); + }, $this->ast->values); $parts[] = Arr::implodeDeeply(array_values($placeholders), ', '); } $parts[] = 'ON CONFLICT DO NOTHING'; - if (! empty($ast->returning)) { + if (! empty($this->ast->returning)) { $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->returning), ', '); + $parts[] = Arr::implodeDeeply($this->wrapList($this->ast->returning), ', '); } $sql = Arr::implodeDeeply($parts); $sql = $this->convertPlaceholders($sql); - return new CompiledClause($sql, $ast->params); + return new CompiledClause($sql, $this->ast->params); } - $result = parent::compile($ast); + $result = parent::compile(); $parts = [$result->sql]; - if (! empty($ast->returning)) { + if (! empty($this->ast->returning)) { $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->returning), ', '); + $parts[] = Arr::implodeDeeply($this->wrapList($this->ast->returning), ', '); } return new CompiledClause( diff --git a/src/Database/Dialects/Postgres/Compilers/Select.php b/src/Database/Dialects/Postgres/Compilers/Select.php index 8e2bc5ab..c7882e22 100644 --- a/src/Database/Dialects/Postgres/Compilers/Select.php +++ b/src/Database/Dialects/Postgres/Compilers/Select.php @@ -11,7 +11,6 @@ use Phenix\Database\Dialects\Compilers\JoinCompiler; use Phenix\Database\Dialects\Compilers\SelectCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; -use Phenix\Database\QueryAst; class Select extends SelectCompiler { @@ -24,9 +23,9 @@ public function __construct() $this->havingCompiler = new HavingCompiler($this->whereCompiler); } - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { - $result = parent::compile($ast); + $result = parent::compile(); return new CompiledClause( $this->normalizePlaceholders($result->sql), @@ -34,9 +33,9 @@ public function compile(QueryAst $ast): CompiledClause ); } - protected function compileLock(QueryAst $ast): string + protected function compileLock(): string { - return match ($ast->lock) { + return match ($this->ast->lock) { Lock::FOR_UPDATE => 'FOR UPDATE', Lock::FOR_SHARE => 'FOR SHARE', Lock::FOR_NO_KEY_UPDATE => 'FOR NO KEY UPDATE', diff --git a/src/Database/Dialects/Postgres/Compilers/Update.php b/src/Database/Dialects/Postgres/Compilers/Update.php index dd69fcdf..3e2ea208 100644 --- a/src/Database/Dialects/Postgres/Compilers/Update.php +++ b/src/Database/Dialects/Postgres/Compilers/Update.php @@ -8,7 +8,6 @@ use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\UpdateCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; -use Phenix\Database\QueryAst; use Phenix\Database\Wrapper; use function count; @@ -22,18 +21,18 @@ public function __construct() $this->whereCompiler = new Where(); } - protected function compileSetClause(Driver $driver, string $column, int $paramIndex): string + protected function compileSetClause(string $column, int $paramIndex): string { - $column = Wrapper::column($driver, $column); + $column = $this->wrap($column); return "{$column} = $" . $paramIndex; } - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { - $result = parent::compile($ast); + $result = parent::compile(); - $paramsCount = count($ast->values); + $paramsCount = count($this->ast->values); return new CompiledClause( $this->convertPlaceholders($result->sql, $paramsCount), diff --git a/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php b/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php index 1a398ca1..25d6727a 100644 --- a/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php +++ b/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php @@ -13,7 +13,9 @@ protected function convertPlaceholders(string $sql, int $startIndex = 0): string return preg_replace_callback( '/\?/', - fn (): string => '$' . ($index++), + function () use (&$index): string { + return '$' . ($index++); + }, $sql ); } @@ -24,7 +26,9 @@ protected function normalizePlaceholders(string $sql, int $startIndex = 0): stri return preg_replace_callback( '/\?|\$\d+/', - fn (): string => '$' . ($index++), + function () use (&$index): string { + return '$' . ($index++); + }, $sql ); } diff --git a/src/Database/Dialects/Sqlite/Compilers/Delete.php b/src/Database/Dialects/Sqlite/Compilers/Delete.php index 836245f8..d4974033 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Delete.php +++ b/src/Database/Dialects/Sqlite/Compilers/Delete.php @@ -7,7 +7,6 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\DeleteCompiler; -use Phenix\Database\QueryAst; use Phenix\Database\Wrapper; use Phenix\Util\Arr; @@ -18,32 +17,27 @@ public function __construct() $this->whereCompiler = new Where(); } - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { $parts = []; $parts[] = 'DELETE FROM'; - $parts[] = Wrapper::of($ast->driver, $ast->table); + $parts[] = $this->wrapOf($this->ast->table); - if (! empty($ast->wheres)) { - $whereCompiled = $this->whereCompiler->compile($ast->wheres); + if (! empty($this->ast->wheres)) { + $whereCompiled = $this->whereCompiler->compile($this->ast->wheres); $parts[] = 'WHERE'; $parts[] = $whereCompiled->sql; } - if (! empty($ast->returning)) { + if (! empty($this->ast->returning)) { $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply($this->wrapColumns($ast->returning), ', '); + $parts[] = Arr::implodeDeeply($this->wrapList($this->ast->returning), ', '); } $sql = Arr::implodeDeeply($parts); - return new CompiledClause($sql, $ast->params); - } - - protected function wrapColumns(array $columns): array - { - return array_map(fn (string $col): string => Wrapper::column(Driver::SQLITE, $col), $columns); + return new CompiledClause($sql, $this->ast->params); } } diff --git a/src/Database/Dialects/Sqlite/Compilers/Insert.php b/src/Database/Dialects/Sqlite/Compilers/Insert.php index 6158bccd..22052e2c 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Insert.php +++ b/src/Database/Dialects/Sqlite/Compilers/Insert.php @@ -6,8 +6,6 @@ use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\InsertCompiler; -use Phenix\Database\QueryAst; -use Phenix\Database\Wrapper; use Phenix\Util\Arr; /** @@ -25,18 +23,17 @@ protected function compileInsertIgnore(): string /** * Syntax: ON CONFLICT (col1, col2) DO UPDATE SET col1 = excluded.col1 * - * @param QueryAst $ast Query AST with uniqueColumns * @return string ON CONFLICT clause */ - protected function compileUpsert(QueryAst $ast): string + protected function compileUpsert(): string { - $conflictColumns = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->uniqueColumns), ', '); + $conflictColumns = Arr::implodeDeeply($this->wrapList($this->ast->uniqueColumns), ', '); - $updateColumns = array_map(function (string $column) use ($ast): string { - $column = Wrapper::column($ast->driver, $column); + $updateColumns = array_map(function (string $column) { + $column = $this->wrap($column); return "{$column} = excluded.{$column}"; - }, $ast->uniqueColumns); + }, $this->ast->uniqueColumns); return sprintf( 'ON CONFLICT (%s) DO UPDATE SET %s', @@ -45,14 +42,14 @@ protected function compileUpsert(QueryAst $ast): string ); } - public function compile(QueryAst $ast): CompiledClause + public function compile(): CompiledClause { - $result = parent::compile($ast); + $result = parent::compile(); $parts = [$result->sql]; - if (! empty($ast->returning)) { + if (! empty($this->ast->returning)) { $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply(Wrapper::columnList($ast->driver, $ast->returning), ', '); + $parts[] = Arr::implodeDeeply($this->wrapList($this->ast->returning), ', '); } return new CompiledClause( diff --git a/src/Database/Dialects/Sqlite/Compilers/Select.php b/src/Database/Dialects/Sqlite/Compilers/Select.php index dd7ce169..a8584d49 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Select.php +++ b/src/Database/Dialects/Sqlite/Compilers/Select.php @@ -8,7 +8,6 @@ use Phenix\Database\Dialects\Compilers\HavingCompiler; use Phenix\Database\Dialects\Compilers\JoinCompiler; use Phenix\Database\Dialects\Compilers\SelectCompiler; -use Phenix\Database\QueryAst; class Select extends SelectCompiler { @@ -19,7 +18,7 @@ public function __construct() $this->havingCompiler = new HavingCompiler($this->whereCompiler); } - protected function compileLock(QueryAst $ast): string + protected function compileLock(): string { // SQLite doesn't support row-level locks return ''; diff --git a/src/Database/Dialects/Sqlite/Compilers/Update.php b/src/Database/Dialects/Sqlite/Compilers/Update.php index 327fd19f..3dd85cf6 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Update.php +++ b/src/Database/Dialects/Sqlite/Compilers/Update.php @@ -15,9 +15,9 @@ public function __construct() $this->whereCompiler = new Where(); } - protected function compileSetClause(Driver $driver, string $column, int $paramIndex): string + protected function compileSetClause(string $column, int $paramIndex): string { - $column = Wrapper::column($driver, $column); + $column = $this->wrap($column); return "{$column} = ?"; } diff --git a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php index 929ac8a3..9a10848b 100644 --- a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php @@ -5,6 +5,7 @@ use Phenix\Database\Alias; use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Operator; +use Phenix\Database\Dialects\DialectFactory; use Phenix\Database\Exceptions\QueryErrorException; use Phenix\Database\Functions; use Phenix\Database\QueryAst; @@ -63,6 +64,23 @@ public function ast(): QueryAst expect($params)->toBe([1]); }); +it('does not leak cached dialect compiler state across compilations', function (): void { + DialectFactory::clearCache(); + + $first = (new QueryGenerator()) + ->table('users') + ->whereEqual('id', 1) + ->get(); + + $second = (new QueryGenerator()) + ->table('posts') + ->whereEqual('slug', 'hello') + ->get(); + + expect($first)->toBe(['SELECT * FROM `users` WHERE `id` = ?', [1]]); + expect($second)->toBe(['SELECT * FROM `posts` WHERE `slug` = ?', ['hello']]); +}); + it('stores where and subquery params directly in query ast', function (): void { $query = new class () extends QueryGenerator { public function ast(): QueryAst From fc806687416427218f002bae0834d6c9824ebbc1 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 1 May 2026 09:43:50 -0500 Subject: [PATCH 11/32] refactor: streamline access to AST properties in SelectCompiler --- .../Dialects/Compilers/SelectCompiler.php | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index 818670f0..ad1bd48e 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -21,8 +21,7 @@ abstract protected function compileLock(): string; public function compile(): CompiledClause { - $ast = $this->ast(); - $columns = empty($ast->columns) ? ['*'] : $ast->columns; + $columns = empty($this->ast->columns) ? ['*'] : $this->ast->columns; $sql = [ 'SELECT', @@ -31,17 +30,17 @@ public function compile(): CompiledClause $this->compileTable(), ]; - if (! empty($ast->joins)) { + if (! empty($this->ast->joins)) { $joins = $this->compileJoins(); if ($joins->sql !== '') { $sql[] = $joins->sql; - $ast->params = [...$joins->params, ...$ast->params]; + $this->ast->params = [...$joins->params, ...$this->ast->params]; } } - if (! empty($ast->wheres)) { - $whereCompiled = $this->whereCompiler->compile($ast->wheres); + if (! empty($this->ast->wheres)) { + $whereCompiled = $this->whereCompiler->compile($this->ast->wheres); if ($whereCompiled->sql !== '') { $sql[] = 'WHERE'; @@ -49,34 +48,34 @@ public function compile(): CompiledClause } } - if (! empty($ast->groups)) { + if (! empty($this->ast->groups)) { $sql[] = Operator::GROUP_BY->value; - $sql[] = $this->compileGroups($ast->groups); + $sql[] = $this->compileGroups($this->ast->groups); } - if ($ast->having !== null) { - $having = $this->havingCompiler->compile($ast->having); + if ($this->ast->having !== null) { + $having = $this->havingCompiler->compile($this->ast->having); if ($having->sql !== '') { $sql[] = $having->sql; - $ast->params = [...$ast->params, ...$having->params]; + $this->ast->params = [...$this->ast->params, ...$having->params]; } } - if (! empty($ast->orders)) { + if (! empty($this->ast->orders)) { $sql[] = Operator::ORDER_BY->value; - $sql[] = $this->compileOrders($ast->orders); + $sql[] = $this->compileOrders($this->ast->orders); } - if ($ast->limit !== null) { - $sql[] = "LIMIT {$ast->limit}"; + if ($this->ast->limit !== null) { + $sql[] = "LIMIT {$this->ast->limit}"; } - if ($ast->offset !== null) { - $sql[] = "OFFSET {$ast->offset}"; + if ($this->ast->offset !== null) { + $sql[] = "OFFSET {$this->ast->offset}"; } - if ($ast->lock !== null) { + if ($this->ast->lock !== null) { $lockSql = $this->compileLock(); if ($lockSql !== '') { @@ -86,7 +85,7 @@ public function compile(): CompiledClause return new CompiledClause( Arr::implodeDeeply($sql), - $ast->params + $this->ast->params ); } @@ -182,13 +181,12 @@ private function compileSubquery(Subquery $subquery): string private function compileTable(): string { - $ast = $this->ast(); - $table = trim($ast->table); + $table = trim($this->ast->table); if ($table !== '' && str_starts_with($table, '(')) { - return $ast->table; + return $this->ast->table; } - return $this->wrapOf($ast->table); + return $this->wrapOf($this->ast->table); } } From ce1dc01eda52a20825dc646ac32beb464bf990ec Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 1 May 2026 09:56:03 -0500 Subject: [PATCH 12/32] refactor: update AST driver access in compile methods for consistency --- .../Dialects/Compilers/SelectCompiler.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index ad1bd48e..85aa68e3 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -97,10 +97,10 @@ protected function compileColumns(array $columns): string { $compiled = Arr::map($columns, function (string|Alias|Functions|SelectCase|Subquery $value, int|string $key): string { return match (true) { - is_string($key) => (string) Alias::of($key)->as($value)->setDriver($this->ast()->driver), - $value instanceof Alias => (string) $value->setDriver($this->ast()->driver), - $value instanceof Functions => (string) $value->setDriver($this->ast()->driver), - $value instanceof SelectCase => (string) $value->setDriver($this->ast()->driver), + is_string($key) => (string) Alias::of($key)->as($value)->setDriver($this->ast->driver), + $value instanceof Alias => (string) $value->setDriver($this->ast->driver), + $value instanceof Functions => (string) $value->setDriver($this->ast->driver), + $value instanceof SelectCase => (string) $value->setDriver($this->ast->driver), $value instanceof Subquery => $this->compileSubquery($value), default => $this->wrap((string) $value), }; @@ -117,7 +117,7 @@ protected function compileGroups(array $groups): string { $compiled = Arr::map($groups, function (string|Functions $value): string { return match (true) { - $value instanceof Functions => (string) $value->setDriver($this->ast()->driver), + $value instanceof Functions => (string) $value->setDriver($this->ast->driver), default => $this->wrap((string) $value), }; }); @@ -131,8 +131,8 @@ protected function compileOrders(array $orders): string $compiled = Arr::map($columns, function (string|Functions|SelectCase $value): string { return match (true) { - $value instanceof Functions => (string) $value->setDriver($this->ast()->driver), - $value instanceof SelectCase => '(' . (string) $value->setDriver($this->ast()->driver) . ')', + $value instanceof Functions => (string) $value->setDriver($this->ast->driver), + $value instanceof SelectCase => '(' . (string) $value->setDriver($this->ast->driver) . ')', default => $this->wrap((string) $value), }; }); @@ -145,7 +145,7 @@ private function compileJoins(): CompiledClause $sql = []; $params = []; - foreach ($this->ast()->joins as $join) { + foreach ($this->ast->joins as $join) { $compiled = $this->joinCompiler->compile($join); $sql[] = $compiled->sql; From 9f52addd1acced021c974db07a71a2f17a40e4d2 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 1 May 2026 10:32:53 -0500 Subject: [PATCH 13/32] refactor: remove unused ast() method and streamline AST access in SelectCompiler --- src/Database/Dialects/Compilers/ClauseCompiler.php | 10 ---------- src/Database/Dialects/Compilers/SelectCompiler.php | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Database/Dialects/Compilers/ClauseCompiler.php b/src/Database/Dialects/Compilers/ClauseCompiler.php index f0fa0b28..10a81dde 100644 --- a/src/Database/Dialects/Compilers/ClauseCompiler.php +++ b/src/Database/Dialects/Compilers/ClauseCompiler.php @@ -4,7 +4,6 @@ namespace Phenix\Database\Dialects\Compilers; -use LogicException; use Phenix\Database\Contracts\ClauseCompiler as ClauseCompilerContract; use Phenix\Database\QueryAst; use Phenix\Database\Wrapper; @@ -26,15 +25,6 @@ public function setAst(QueryAst $ast): static return $this; } - protected function ast(): QueryAst - { - if (! isset($this->ast)) { - throw new LogicException('Query AST must be set before compiling.'); - } - - return $this->ast; - } - protected function wrap(string $value): string { return Wrapper::column($this->ast->driver, $value); diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index 85aa68e3..7e70fbe7 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -161,7 +161,7 @@ private function compileJoins(): CompiledClause */ private function compileSubquery(Subquery $subquery): string { - $parentAst = $this->ast(); + $parentAst = $this->ast; $subquery->setDriver($parentAst->driver); try { From 87c54ef5eed407912b3dad6a96c56cb843cb38bd Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 1 May 2026 12:05:42 -0500 Subject: [PATCH 14/32] refactor: improve subquery handling and remove dialect factory caching Co-authored-by: Copilot --- .../Dialects/Compilers/SelectCompiler.php | 11 +++-------- src/Database/Dialects/DialectFactory.php | 12 +----------- .../Database/Dialects/DialectFactoryTest.php | 18 +++--------------- .../QueryGenerator/SelectColumnsTest.php | 3 --- 4 files changed, 7 insertions(+), 37 deletions(-) diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index 7e70fbe7..38b29dce 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -161,20 +161,15 @@ private function compileJoins(): CompiledClause */ private function compileSubquery(Subquery $subquery): string { - $parentAst = $this->ast; - $subquery->setDriver($parentAst->driver); + $subquery->setDriver($this->ast->driver); - try { - [$dml, $arguments] = $subquery->toSql(); - } finally { - $this->setAst($parentAst); - } + [$dml, $arguments] = $subquery->toSql(); if (! str_contains($dml, 'LIMIT 1')) { throw new QueryErrorException('The subquery must be limited to one record'); } - $parentAst->params = [...$parentAst->params, ...$arguments]; + $this->ast->params = [...$this->ast->params, ...$arguments]; return $dml; } diff --git a/src/Database/Dialects/DialectFactory.php b/src/Database/Dialects/DialectFactory.php index 8e023278..3c4731a6 100644 --- a/src/Database/Dialects/DialectFactory.php +++ b/src/Database/Dialects/DialectFactory.php @@ -12,11 +12,6 @@ class DialectFactory { - /** - * @var array - */ - private static array $instances = []; - private function __construct() { // Prevent instantiation @@ -24,16 +19,11 @@ private function __construct() public static function fromDriver(Driver $driver): Dialect { - return self::$instances[$driver->value] ??= match ($driver) { + return match ($driver) { Driver::MYSQL => new MysqlDialect(), Driver::POSTGRESQL => new PostgresDialect(), Driver::SQLITE => new SqliteDialect(), default => new MysqlDialect(), }; } - - public static function clearCache(): void - { - self::$instances = []; - } } diff --git a/tests/Unit/Database/Dialects/DialectFactoryTest.php b/tests/Unit/Database/Dialects/DialectFactoryTest.php index 4d0db28b..c007a51e 100644 --- a/tests/Unit/Database/Dialects/DialectFactoryTest.php +++ b/tests/Unit/Database/Dialects/DialectFactoryTest.php @@ -8,10 +8,6 @@ use Phenix\Database\Dialects\Postgres\PostgresDialect; use Phenix\Database\Dialects\Sqlite\SqliteDialect; -afterEach(function (): void { - DialectFactory::clearCache(); -}); - test('DialectFactory creates MySQL dialect for MySQL driver', function () { $dialect = DialectFactory::fromDriver(Driver::MYSQL); @@ -30,19 +26,11 @@ expect($dialect)->toBeInstanceOf(SqliteDialect::class); }); -test('DialectFactory returns same instance for repeated calls (singleton)', function () { +test('DialectFactory returns new instance for each call', function () { $dialect1 = DialectFactory::fromDriver(Driver::MYSQL); $dialect2 = DialectFactory::fromDriver(Driver::MYSQL); - expect($dialect1)->toBe($dialect2); -}); - -test('DialectFactory clearCache clears cached instances', function () { - $dialect1 = DialectFactory::fromDriver(Driver::MYSQL); - - DialectFactory::clearCache(); - - $dialect2 = DialectFactory::fromDriver(Driver::MYSQL); - expect($dialect1)->not->toBe($dialect2); + expect($dialect1)->toBeInstanceOf(MysqlDialect::class); + expect($dialect2)->toBeInstanceOf(MysqlDialect::class); }); diff --git a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php index 9a10848b..9117b28f 100644 --- a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php @@ -5,7 +5,6 @@ use Phenix\Database\Alias; use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Operator; -use Phenix\Database\Dialects\DialectFactory; use Phenix\Database\Exceptions\QueryErrorException; use Phenix\Database\Functions; use Phenix\Database\QueryAst; @@ -65,8 +64,6 @@ public function ast(): QueryAst }); it('does not leak cached dialect compiler state across compilations', function (): void { - DialectFactory::clearCache(); - $first = (new QueryGenerator()) ->table('users') ->whereEqual('id', 1) From c39a9ff1b63b1bc455a9e3118c8c9a223c06772f Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 1 May 2026 12:07:31 -0500 Subject: [PATCH 15/32] refactor: replace Functions with Funct for improved function handling and consistency across query components --- src/Database/Concerns/Query/BuildsQuery.php | 6 ++--- .../Dialects/Compilers/SelectCompiler.php | 14 +++++----- src/Database/{Functions.php => Funct.php} | 2 +- src/Database/QueryBase.php | 2 +- src/Database/SelectCase.php | 18 ++++++------- .../QueryGenerator/GroupByStatementTest.php | 16 ++++++------ .../QueryGenerator/HavingClauseTest.php | 8 +++--- .../Postgres/GroupByStatementTest.php | 26 +++++++++---------- .../Postgres/HavingClauseTest.php | 18 ++++++------- .../Postgres/SelectColumnsTest.php | 18 ++++++------- .../Postgres/WhereClausesTest.php | 6 ++--- .../QueryGenerator/SelectColumnsTest.php | 18 ++++++------- .../Sqlite/GroupByStatementTest.php | 26 +++++++++---------- .../Sqlite/HavingClauseTest.php | 14 +++++----- .../Sqlite/SelectColumnsTest.php | 18 ++++++------- .../Sqlite/WhereClausesTest.php | 6 ++--- .../QueryGenerator/WhereClausesTest.php | 6 ++--- 17 files changed, 111 insertions(+), 111 deletions(-) rename src/Database/{Functions.php => Funct.php} (98%) diff --git a/src/Database/Concerns/Query/BuildsQuery.php b/src/Database/Concerns/Query/BuildsQuery.php index 3f6f7055..2ec111bd 100644 --- a/src/Database/Concerns/Query/BuildsQuery.php +++ b/src/Database/Concerns/Query/BuildsQuery.php @@ -8,7 +8,7 @@ use Phenix\Database\Constants\Action; use Phenix\Database\Constants\Order; use Phenix\Database\Dialects\DialectFactory; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\Having; use Phenix\Database\QueryAst; use Phenix\Database\SelectCase; @@ -63,9 +63,9 @@ public function selectAllColumns(): static return $this; } - public function groupBy(Functions|array|string $column): static + public function groupBy(Funct|array|string $column): static { - if ($column instanceof Functions || is_string($column)) { + if ($column instanceof Funct || is_string($column)) { $column = [$column]; } diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index 38b29dce..2aef68e7 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -8,7 +8,7 @@ use Phenix\Database\Constants\Operator; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Exceptions\QueryErrorException; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\SelectCase; use Phenix\Database\Subquery; use Phenix\Util\Arr; @@ -95,11 +95,11 @@ public function compile(): CompiledClause */ protected function compileColumns(array $columns): string { - $compiled = Arr::map($columns, function (string|Alias|Functions|SelectCase|Subquery $value, int|string $key): string { + $compiled = Arr::map($columns, function (string|Alias|Funct|SelectCase|Subquery $value, int|string $key): string { return match (true) { is_string($key) => (string) Alias::of($key)->as($value)->setDriver($this->ast->driver), $value instanceof Alias => (string) $value->setDriver($this->ast->driver), - $value instanceof Functions => (string) $value->setDriver($this->ast->driver), + $value instanceof Funct => (string) $value->setDriver($this->ast->driver), $value instanceof SelectCase => (string) $value->setDriver($this->ast->driver), $value instanceof Subquery => $this->compileSubquery($value), default => $this->wrap((string) $value), @@ -115,9 +115,9 @@ protected function compileColumns(array $columns): string */ protected function compileGroups(array $groups): string { - $compiled = Arr::map($groups, function (string|Functions $value): string { + $compiled = Arr::map($groups, function (string|Funct $value): string { return match (true) { - $value instanceof Functions => (string) $value->setDriver($this->ast->driver), + $value instanceof Funct => (string) $value->setDriver($this->ast->driver), default => $this->wrap((string) $value), }; }); @@ -129,9 +129,9 @@ protected function compileOrders(array $orders): string { [$columns, $order] = $orders; - $compiled = Arr::map($columns, function (string|Functions|SelectCase $value): string { + $compiled = Arr::map($columns, function (string|Funct|SelectCase $value): string { return match (true) { - $value instanceof Functions => (string) $value->setDriver($this->ast->driver), + $value instanceof Funct => (string) $value->setDriver($this->ast->driver), $value instanceof SelectCase => '(' . (string) $value->setDriver($this->ast->driver) . ')', default => $this->wrap((string) $value), }; diff --git a/src/Database/Functions.php b/src/Database/Funct.php similarity index 98% rename from src/Database/Functions.php rename to src/Database/Funct.php index 905ae1f7..69eccb96 100644 --- a/src/Database/Functions.php +++ b/src/Database/Funct.php @@ -8,7 +8,7 @@ use Phenix\Database\Constants\DatabaseFunction; use Stringable; -class Functions implements Stringable +class Funct implements Stringable { use HasDriver; diff --git a/src/Database/QueryBase.php b/src/Database/QueryBase.php index 3919ca33..e9129fd5 100644 --- a/src/Database/QueryBase.php +++ b/src/Database/QueryBase.php @@ -107,7 +107,7 @@ public function count(string $column = '*'): array|int { $this->ast->action = Action::SELECT; - $this->ast->columns = [Functions::count($column)]; + $this->ast->columns = [Funct::count($column)]; return $this->toSql(); } diff --git a/src/Database/SelectCase.php b/src/Database/SelectCase.php index 90df3d20..dd837676 100644 --- a/src/Database/SelectCase.php +++ b/src/Database/SelectCase.php @@ -27,7 +27,7 @@ public function __construct() $this->cases = []; } - public function whenEqual(Functions|string $column, RawValue|string|int $value, RawValue|string|int $result): self + public function whenEqual(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): self { $this->pushCase( $column, @@ -39,7 +39,7 @@ public function whenEqual(Functions|string $column, RawValue|string|int $value, return $this; } - public function whenNotEqual(Functions|string $column, RawValue|string|int $value, RawValue|string|int $result): self + public function whenNotEqual(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): self { $this->pushCase( $column, @@ -51,7 +51,7 @@ public function whenNotEqual(Functions|string $column, RawValue|string|int $valu return $this; } - public function whenGreaterThan(Functions|string $column, RawValue|string|int $value, RawValue|string|int $result): self + public function whenGreaterThan(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): self { $this->pushCase( $column, @@ -64,7 +64,7 @@ public function whenGreaterThan(Functions|string $column, RawValue|string|int $v } public function whenGreaterThanOrEqual( - Functions|string $column, + Funct|string $column, RawValue|string|int $value, RawValue|string|int $result ): self { @@ -78,7 +78,7 @@ public function whenGreaterThanOrEqual( return $this; } - public function whenLessThan(Functions|string $column, RawValue|string|int $value, RawValue|string|int $result): self + public function whenLessThan(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): self { $this->pushCase( $column, @@ -90,7 +90,7 @@ public function whenLessThan(Functions|string $column, RawValue|string|int $valu return $this; } - public function whenLessThanOrEqual(Functions|string $column, RawValue|string|int $value, RawValue|string|int $result): self + public function whenLessThanOrEqual(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): self { $this->pushCase( $column, @@ -182,7 +182,7 @@ public function __toString(): string } protected function pushCase( - Functions|string $column, + Funct|string $column, Operator $operators, RawValue|string|int $result, RawValue|string|int|null $value = null @@ -204,9 +204,9 @@ protected function compileCase(array $case): string return "WHEN {$column} {$operator} " . $this->renderOperand($case[3]) . " THEN " . $this->renderOperand($case[5]); } - protected function compileColumn(Functions|string $column): string + protected function compileColumn(Funct|string $column): string { - if ($column instanceof Functions) { + if ($column instanceof Funct) { return (string) $column->setDriver($this->getDriver()); } diff --git a/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php index 8be1ee7a..c10b3dfe 100644 --- a/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; -it('generates a grouped query', function (Functions|string $column, Functions|array|string $groupBy, string $rawGroup, string $rawColumn) { +it('generates a grouped query', function (Funct|string $column, Funct|array|string $groupBy, string $rawGroup, string $rawColumn) { $query = new QueryGenerator(); $sql = $query->select([ @@ -31,14 +31,14 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', '`category_id`', 'COUNT(`products`.`id`)'], + [Funct::count('products.id'), 'category_id', '`category_id`', 'COUNT(`products`.`id`)'], ['location_id', ['category_id', 'location_id'], '`category_id`, `location_id`', '`location_id`'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT(`products`.`id`)', 'COUNT(`products`.`id`)'], + [Funct::count('products.id'), Funct::count('products.id'), 'COUNT(`products`.`id`)', 'COUNT(`products`.`id`)'], ]); it('generates a grouped and ordered query', function ( - Functions|string $column, - Functions|array|string $groupBy, + Funct|string $column, + Funct|array|string $groupBy, string $rawGroup, string $rawColumn ) { @@ -68,7 +68,7 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', '`category_id`', 'COUNT(`products`.`id`)'], + [Funct::count('products.id'), 'category_id', '`category_id`', 'COUNT(`products`.`id`)'], ['location_id', ['category_id', 'location_id'], '`category_id`, `location_id`', '`location_id`'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT(`products`.`id`)', 'COUNT(`products`.`id`)'], + [Funct::count('products.id'), Funct::count('products.id'), 'COUNT(`products`.`id`)', 'COUNT(`products`.`id`)'], ]); diff --git a/tests/Unit/Database/QueryGenerator/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/HavingClauseTest.php index 67dc8235..cd1e9aaa 100644 --- a/tests/Unit/Database/QueryGenerator/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/HavingClauseTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\Having; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; @@ -11,7 +11,7 @@ $query = new QueryGenerator(); $sql = $query->select([ - Functions::count('products.id')->as('identifiers'), + Funct::count('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -40,7 +40,7 @@ $query = new QueryGenerator(); $sql = $query->select([ - Functions::count('products.id')->as('identifiers'), + Funct::count('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -70,7 +70,7 @@ $query = new QueryGenerator(); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.created_at', ]) ->from('products') diff --git a/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php index 295ed349..df9974e1 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php @@ -3,12 +3,12 @@ declare(strict_types=1); use Phenix\Database\Constants\Driver; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\Having; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; -it('generates a grouped query', function (Functions|string $column, Functions|array|string $groupBy, string $rawGroup, string $rawColumn): void { +it('generates a grouped query', function (Funct|string $column, Funct|array|string $groupBy, string $rawGroup, string $rawColumn): void { $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ @@ -33,14 +33,14 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + [Funct::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], + [Funct::count('products.id'), Funct::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped and ordered query', function ( - Functions|string $column, - Functions|array|string $groupBy, + Funct|string $column, + Funct|array|string $groupBy, string $rawGroup, string $rawColumn ) { @@ -70,16 +70,16 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + [Funct::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], + [Funct::count('products.id'), Funct::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped query with where clause', function (): void { $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::count('products.id'), + Funct::count('products.id'), 'products.category_id', ]) ->from('products') @@ -102,7 +102,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -127,9 +127,9 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::count('products.id'), - Functions::sum('products.price'), - Functions::avg('products.price'), + Funct::count('products.id'), + Funct::sum('products.price'), + Funct::avg('products.price'), 'products.category_id', ]) ->from('products') diff --git a/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php index e9b998af..e9885c19 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php @@ -3,7 +3,7 @@ declare(strict_types=1); use Phenix\Database\Constants\Driver; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\Having; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; @@ -12,7 +12,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::count('products.id')->as('identifiers'), + Funct::count('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -41,7 +41,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::count('products.id')->as('identifiers'), + Funct::count('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -71,7 +71,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -97,7 +97,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::sum('orders.total')->as('total_sales'), + Funct::sum('orders.total')->as('total_sales'), 'orders.customer_id', ]) ->from('orders') @@ -121,7 +121,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -145,7 +145,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.created_at', ]) ->from('products') @@ -169,7 +169,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -200,7 +200,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') diff --git a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php index 1f10b284..23672ace 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php @@ -7,7 +7,7 @@ use Phenix\Database\Constants\Lock; use Phenix\Database\Constants\Operator; use Phenix\Database\Exceptions\QueryErrorException; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; @@ -43,7 +43,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->table('products') - ->select([Functions::{$function}($column)]) + ->select([Funct::{$function}($column)]) ->get(); [$dml, $params] = $sql; @@ -67,7 +67,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->table('products') - ->select([Functions::{$function}($column)->as($alias)]) + ->select([Funct::{$function}($column)->as($alias)]) ->get(); [$dml, $params] = $sql; @@ -189,7 +189,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); - $case = Functions::case() + $case = Funct::case() ->{$method}($column, $value, $result) ->defaultResult($defaultResult) ->as('type'); @@ -228,7 +228,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); - $case = Functions::case() + $case = Funct::case() ->{$method}(...$data) ->defaultResult($defaultResult) ->as('status'); @@ -260,7 +260,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); - $case = Functions::case() + $case = Funct::case() ->whenNull('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->defaultResult('old user') @@ -288,7 +288,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); - $case = Functions::case() + $case = Funct::case() ->whenNull('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->as('status'); @@ -313,8 +313,8 @@ it('generates query with select-case using functions', function () { $query = new QueryGenerator(Driver::POSTGRESQL); - $case = Functions::case() - ->whenGreaterThanOrEqual(Functions::avg('price'), 4, 'expensive') + $case = Funct::case() + ->whenGreaterThanOrEqual(Funct::avg('price'), 4, 'expensive') ->defaultResult('cheap') ->as('message'); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php index d56efaab..103a1ec7 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php @@ -5,7 +5,7 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Operator; use Phenix\Database\Constants\Order; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; @@ -355,7 +355,7 @@ ]); it('generates a column-ordered query using select-case', function () { - $case = Functions::case() + $case = Funct::case() ->whenNull('city', 'country') ->defaultResult('city'); @@ -458,7 +458,7 @@ $sql = $query->table('products') ->{$method}($column, function (Subquery $subquery) { - $subquery->select([Functions::max('price')])->from('products'); + $subquery->select([Funct::max('price')])->from('products'); }) ->get(); diff --git a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php index 9117b28f..1141310c 100644 --- a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php @@ -6,7 +6,7 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Operator; use Phenix\Database\Exceptions\QueryErrorException; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\QueryAst; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; @@ -111,7 +111,7 @@ public function ast(): QueryAst $query = new QueryGenerator(); $sql = $query->table('products') - ->select([Functions::{$function}($column)]) + ->select([Funct::{$function}($column)]) ->get(); [$dml, $params] = $sql; @@ -135,7 +135,7 @@ public function ast(): QueryAst $query = new QueryGenerator(); $sql = $query->table('products') - ->select([Functions::{$function}($column)->as($alias)]) + ->select([Funct::{$function}($column)->as($alias)]) ->get(); [$dml, $params] = $sql; @@ -257,7 +257,7 @@ public function ast(): QueryAst $query = new QueryGenerator(); - $case = Functions::case() + $case = Funct::case() ->{$method}($column, $value, $result) ->defaultResult($defaultResult) ->as('type'); @@ -296,7 +296,7 @@ public function ast(): QueryAst $query = new QueryGenerator(); - $case = Functions::case() + $case = Funct::case() ->{$method}(...$data) ->defaultResult($defaultResult) ->as('status'); @@ -328,7 +328,7 @@ public function ast(): QueryAst $query = new QueryGenerator(); - $case = Functions::case() + $case = Funct::case() ->whenNull('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->defaultResult('old user') @@ -356,7 +356,7 @@ public function ast(): QueryAst $query = new QueryGenerator(); - $case = Functions::case() + $case = Funct::case() ->whenNull('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->as('status'); @@ -381,8 +381,8 @@ public function ast(): QueryAst it('generates query with select-case using functions', function () { $query = new QueryGenerator(); - $case = Functions::case() - ->whenGreaterThanOrEqual(Functions::avg('price'), 4, 'expensive') + $case = Funct::case() + ->whenGreaterThanOrEqual(Funct::avg('price'), 4, 'expensive') ->defaultResult('cheap') ->as('message'); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php index c3fc33a9..70104f3c 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php @@ -3,12 +3,12 @@ declare(strict_types=1); use Phenix\Database\Constants\Driver; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\Having; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; -it('generates a grouped query', function (Functions|string $column, Functions|array|string $groupBy, string $rawGroup, string $rawColumn): void { +it('generates a grouped query', function (Funct|string $column, Funct|array|string $groupBy, string $rawGroup, string $rawColumn): void { $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ @@ -33,14 +33,14 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + [Funct::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], + [Funct::count('products.id'), Funct::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped and ordered query', function ( - Functions|string $column, - Functions|array|string $groupBy, + Funct|string $column, + Funct|array|string $groupBy, string $rawGroup, string $rawColumn ): void { @@ -70,16 +70,16 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Functions::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + [Funct::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], - [Functions::count('products.id'), Functions::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], + [Funct::count('products.id'), Funct::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped query with where clause', function (): void { $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Functions::count('products.id'), + Funct::count('products.id'), 'products.category_id', ]) ->from('products') @@ -102,7 +102,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -127,9 +127,9 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Functions::count('products.id'), - Functions::sum('products.price'), - Functions::avg('products.price'), + Funct::count('products.id'), + Funct::sum('products.price'), + Funct::avg('products.price'), 'products.category_id', ]) ->from('products') diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php index f52b1a88..b617a95f 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php @@ -3,7 +3,7 @@ declare(strict_types=1); use Phenix\Database\Constants\Driver; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\Having; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; @@ -12,7 +12,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Functions::count('products.id')->as('identifiers'), + Funct::count('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -41,7 +41,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Functions::count('products.id')->as('identifiers'), + Funct::count('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -71,7 +71,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -97,7 +97,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Functions::sum('orders.total')->as('total_sales'), + Funct::sum('orders.total')->as('total_sales'), 'orders.customer_id', ]) ->from('orders') @@ -121,7 +121,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -145,7 +145,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Functions::count('products.id')->as('product_count'), + Funct::count('products.id')->as('product_count'), 'products.created_at', ]) ->from('products') diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php index 0d83232a..23432cc7 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php @@ -7,7 +7,7 @@ use Phenix\Database\Constants\Lock; use Phenix\Database\Constants\Operator; use Phenix\Database\Exceptions\QueryErrorException; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; @@ -43,7 +43,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->table('products') - ->select([Functions::{$function}($column)]) + ->select([Funct::{$function}($column)]) ->get(); [$dml, $params] = $sql; @@ -67,7 +67,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->table('products') - ->select([Functions::{$function}($column)->as($alias)]) + ->select([Funct::{$function}($column)->as($alias)]) ->get(); [$dml, $params] = $sql; @@ -189,7 +189,7 @@ $query = new QueryGenerator(Driver::SQLITE); - $case = Functions::case() + $case = Funct::case() ->{$method}($column, $value, $result) ->defaultResult($defaultResult) ->as('type'); @@ -228,7 +228,7 @@ $query = new QueryGenerator(Driver::SQLITE); - $case = Functions::case() + $case = Funct::case() ->{$method}(...$data) ->defaultResult($defaultResult) ->as('status'); @@ -260,7 +260,7 @@ $query = new QueryGenerator(Driver::SQLITE); - $case = Functions::case() + $case = Funct::case() ->whenNull('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->defaultResult('old user') @@ -288,7 +288,7 @@ $query = new QueryGenerator(Driver::SQLITE); - $case = Functions::case() + $case = Funct::case() ->whenNull('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->as('status'); @@ -313,8 +313,8 @@ it('generates query with select-case using functions', function () { $query = new QueryGenerator(Driver::SQLITE); - $case = Functions::case() - ->whenGreaterThanOrEqual(Functions::avg('price'), 4, 'expensive') + $case = Funct::case() + ->whenGreaterThanOrEqual(Funct::avg('price'), 4, 'expensive') ->defaultResult('cheap') ->as('message'); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php index b8c1e24e..dbf46b29 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php @@ -5,7 +5,7 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Operator; use Phenix\Database\Constants\Order; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; @@ -331,7 +331,7 @@ ]); it('generates a column-ordered query using select-case', function () { - $case = Functions::case() + $case = Funct::case() ->whenNull('city', 'country') ->defaultResult('city'); @@ -434,7 +434,7 @@ $sql = $query->table('products') ->{$method}($column, function (Subquery $subquery) { - $subquery->select([Functions::max('price')])->from('products'); + $subquery->select([Funct::max('price')])->from('products'); }) ->get(); diff --git a/tests/Unit/Database/QueryGenerator/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/WhereClausesTest.php index 26c81054..9b687613 100644 --- a/tests/Unit/Database/QueryGenerator/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/WhereClausesTest.php @@ -4,7 +4,7 @@ use Phenix\Database\Constants\Operator; use Phenix\Database\Constants\Order; -use Phenix\Database\Functions; +use Phenix\Database\Funct; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; @@ -330,7 +330,7 @@ ]); it('generates a column-ordered query using select-case', function () { - $case = Functions::case() + $case = Funct::case() ->whenNull('city', 'country') ->defaultResult('city'); @@ -433,7 +433,7 @@ $sql = $query->table('products') ->{$method}($column, function (Subquery $subquery) { - $subquery->select([Functions::max('price')])->from('products'); + $subquery->select([Funct::max('price')])->from('products'); }) ->get(); From 287bcc15d71870245dadae54ceaa4842b963fc8e Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Sat, 2 May 2026 07:00:25 -0500 Subject: [PATCH 16/32] Refactor query generator tests to use new function syntax - Replaced instances of Funct::count, Funct::sum, Funct::avg, and other aggregate functions with their corresponding global functions (count_of, sum, avg) across various test files. - Updated test cases in GroupByStatementTest, HavingClauseTest, SelectColumnsTest, and WhereClausesTest for PostgreSQL, SQLite, and other drivers to reflect the new function usage. - Ensured consistency in the use of function names and improved readability of the test cases. --- composer.json | 3 +- src/Database/functions.php | 107 ++++++++++++++++++ .../QueryGenerator/GroupByStatementTest.php | 10 +- .../QueryGenerator/HavingClauseTest.php | 9 +- .../Postgres/GroupByStatementTest.php | 22 ++-- .../Postgres/HavingClauseTest.php | 20 ++-- .../Postgres/SelectColumnsTest.php | 71 ++++++------ .../Postgres/WhereClausesTest.php | 9 +- .../QueryGenerator/SelectColumnsTest.php | 71 ++++++------ .../Sqlite/GroupByStatementTest.php | 22 ++-- .../Sqlite/HavingClauseTest.php | 16 +-- .../Sqlite/SelectColumnsTest.php | 71 ++++++------ .../Sqlite/WhereClausesTest.php | 9 +- .../QueryGenerator/WhereClausesTest.php | 9 +- 14 files changed, 295 insertions(+), 154 deletions(-) create mode 100644 src/Database/functions.php diff --git a/composer.json b/composer.json index 8d355f61..f96b3b36 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,8 @@ "Phenix\\": "src/" }, "files": [ - "src/functions.php" + "src/functions.php", + "src/Database/functions.php" ] }, "autoload-dev": { diff --git a/src/Database/functions.php b/src/Database/functions.php new file mode 100644 index 00000000..a9384569 --- /dev/null +++ b/src/Database/functions.php @@ -0,0 +1,107 @@ +whenEqual($column, $value, $result); +} + +function when_not_equal(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): SelectCase +{ + return Funct::case()->whenNotEqual($column, $value, $result); +} + +function when_gt(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): SelectCase +{ + return Funct::case()->whenGreaterThan($column, $value, $result); +} + +function when_gte(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): SelectCase +{ + return Funct::case()->whenGreaterThanOrEqual($column, $value, $result); +} + +function when_lt(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): SelectCase +{ + return Funct::case()->whenLessThan($column, $value, $result); +} + +function when_lte(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): SelectCase +{ + return Funct::case()->whenLessThanOrEqual($column, $value, $result); +} + +function when_null(string $column, RawValue|string|int $result): SelectCase +{ + return Funct::case()->whenNull($column, $result); +} + +function when_not_null(string $column, RawValue|string|int $result): SelectCase +{ + return Funct::case()->whenNotNull($column, $result); +} + +function when_true(string $column, RawValue|string|int $result): SelectCase +{ + return Funct::case()->whenTrue($column, $result); +} + +function when_false(string $column, RawValue|string|int $result): SelectCase +{ + return Funct::case()->whenFalse($column, $result); +} diff --git a/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php index c10b3dfe..0fd4f2bf 100644 --- a/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/GroupByStatementTest.php @@ -6,6 +6,8 @@ use Phenix\Database\Join; use Phenix\Database\QueryGenerator; +use function Phenix\Database\count_of; + it('generates a grouped query', function (Funct|string $column, Funct|array|string $groupBy, string $rawGroup, string $rawColumn) { $query = new QueryGenerator(); @@ -31,9 +33,9 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Funct::count('products.id'), 'category_id', '`category_id`', 'COUNT(`products`.`id`)'], + [count_of('products.id'), 'category_id', '`category_id`', 'COUNT(`products`.`id`)'], ['location_id', ['category_id', 'location_id'], '`category_id`, `location_id`', '`location_id`'], - [Funct::count('products.id'), Funct::count('products.id'), 'COUNT(`products`.`id`)', 'COUNT(`products`.`id`)'], + [count_of('products.id'), count_of('products.id'), 'COUNT(`products`.`id`)', 'COUNT(`products`.`id`)'], ]); it('generates a grouped and ordered query', function ( @@ -68,7 +70,7 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Funct::count('products.id'), 'category_id', '`category_id`', 'COUNT(`products`.`id`)'], + [count_of('products.id'), 'category_id', '`category_id`', 'COUNT(`products`.`id`)'], ['location_id', ['category_id', 'location_id'], '`category_id`, `location_id`', '`location_id`'], - [Funct::count('products.id'), Funct::count('products.id'), 'COUNT(`products`.`id`)', 'COUNT(`products`.`id`)'], + [count_of('products.id'), count_of('products.id'), 'COUNT(`products`.`id`)', 'COUNT(`products`.`id`)'], ]); diff --git a/tests/Unit/Database/QueryGenerator/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/HavingClauseTest.php index cd1e9aaa..4ed6198b 100644 --- a/tests/Unit/Database/QueryGenerator/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/HavingClauseTest.php @@ -2,16 +2,17 @@ declare(strict_types=1); -use Phenix\Database\Funct; use Phenix\Database\Having; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; +use function Phenix\Database\count_of; + it('generates a query using having clause', function () { $query = new QueryGenerator(); $sql = $query->select([ - Funct::count('products.id')->as('identifiers'), + count_of('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -40,7 +41,7 @@ $query = new QueryGenerator(); $sql = $query->select([ - Funct::count('products.id')->as('identifiers'), + count_of('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -70,7 +71,7 @@ $query = new QueryGenerator(); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.created_at', ]) ->from('products') diff --git a/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php index df9974e1..c9e6ebfd 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/GroupByStatementTest.php @@ -8,6 +8,10 @@ use Phenix\Database\Join; use Phenix\Database\QueryGenerator; +use function Phenix\Database\avg; +use function Phenix\Database\count_of; +use function Phenix\Database\sum; + it('generates a grouped query', function (Funct|string $column, Funct|array|string $groupBy, string $rawGroup, string $rawColumn): void { $query = new QueryGenerator(Driver::POSTGRESQL); @@ -33,9 +37,9 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Funct::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + [count_of('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], - [Funct::count('products.id'), Funct::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], + [count_of('products.id'), count_of('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped and ordered query', function ( @@ -70,16 +74,16 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Funct::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + [count_of('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], - [Funct::count('products.id'), Funct::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], + [count_of('products.id'), count_of('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped query with where clause', function (): void { $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::count('products.id'), + count_of('products.id'), 'products.category_id', ]) ->from('products') @@ -102,7 +106,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -127,9 +131,9 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::count('products.id'), - Funct::sum('products.price'), - Funct::avg('products.price'), + count_of('products.id'), + sum('products.price'), + avg('products.price'), 'products.category_id', ]) ->from('products') diff --git a/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php index e9885c19..887772a3 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/HavingClauseTest.php @@ -3,16 +3,18 @@ declare(strict_types=1); use Phenix\Database\Constants\Driver; -use Phenix\Database\Funct; use Phenix\Database\Having; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; +use function Phenix\Database\count_of; +use function Phenix\Database\sum; + it('generates a query using having clause', function () { $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::count('products.id')->as('identifiers'), + count_of('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -41,7 +43,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::count('products.id')->as('identifiers'), + count_of('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -71,7 +73,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -97,7 +99,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::sum('orders.total')->as('total_sales'), + sum('orders.total')->as('total_sales'), 'orders.customer_id', ]) ->from('orders') @@ -121,7 +123,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -145,7 +147,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.created_at', ]) ->from('products') @@ -169,7 +171,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -200,7 +202,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') diff --git a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php index 23672ace..2ebd7822 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php @@ -7,10 +7,14 @@ use Phenix\Database\Constants\Lock; use Phenix\Database\Constants\Operator; use Phenix\Database\Exceptions\QueryErrorException; -use Phenix\Database\Funct; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; +use function Phenix\Database\avg; +use function Phenix\Database\subquery; +use function Phenix\Database\when_gte; +use function Phenix\Database\when_null; + it('generates query to select all columns of table', function () { $query = new QueryGenerator(Driver::POSTGRESQL); @@ -41,9 +45,10 @@ it('generates a query using sql functions', function (string $function, string $column, string $rawFunction) { $query = new QueryGenerator(Driver::POSTGRESQL); + $factory = "Phenix\\Database\\{$function}"; $sql = $query->table('products') - ->select([Funct::{$function}($column)]) + ->select([$factory($column)]) ->get(); [$dml, $params] = $sql; @@ -53,9 +58,9 @@ })->with([ ['avg', 'price', 'AVG("price")'], ['sum', 'price', 'SUM("price")'], - ['min', 'price', 'MIN("price")'], - ['max', 'price', 'MAX("price")'], - ['count', 'id', 'COUNT("id")'], + ['min_of', 'price', 'MIN("price")'], + ['max_of', 'price', 'MAX("price")'], + ['count_of', 'id', 'COUNT("id")'], ]); it('generates a query using sql functions with alias', function ( @@ -65,9 +70,10 @@ string $rawFunction ) { $query = new QueryGenerator(Driver::POSTGRESQL); + $factory = "Phenix\\Database\\{$function}"; $sql = $query->table('products') - ->select([Funct::{$function}($column)->as($alias)]) + ->select([$factory($column)->as($alias)]) ->get(); [$dml, $params] = $sql; @@ -77,9 +83,9 @@ })->with([ ['avg', 'price', 'value', 'AVG("price") AS "value"'], ['sum', 'price', 'value', 'SUM("price") AS "value"'], - ['min', 'price', 'value', 'MIN("price") AS "value"'], - ['max', 'price', 'value', 'MAX("price") AS "value"'], - ['count', 'id', 'value', 'COUNT("id") AS "value"'], + ['min_of', 'price', 'value', 'MIN("price") AS "value"'], + ['max_of', 'price', 'value', 'MAX("price") AS "value"'], + ['count_of', 'id', 'value', 'COUNT("id") AS "value"'], ]); it('selects field from subquery', function () { @@ -108,7 +114,7 @@ $sql = $query->select([ 'id', 'name', - Subquery::make(Driver::POSTGRESQL)->select(['name']) + subquery()->select(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name') @@ -133,7 +139,7 @@ $query->select([ 'id', 'name', - Subquery::make(Driver::POSTGRESQL)->select(['name']) + subquery()->select(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name'), @@ -180,7 +186,7 @@ }); it('generates query with select-cases using comparisons', function ( - string $method, + string $function, array $data, string $defaultResult, string $operator @@ -189,8 +195,9 @@ $query = new QueryGenerator(Driver::POSTGRESQL); - $case = Funct::case() - ->{$method}($column, $value, $result) + $factory = "Phenix\\Database\\{$function}"; + + $case = $factory($column, $value, $result) ->defaultResult($defaultResult) ->as('type'); @@ -210,16 +217,16 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - ['whenEqual', ['price', 100, 'expensive'], 'cheap', Operator::EQUAL->value], - ['whenNotEqual', ['price', 100, 'expensive'], 'cheap', Operator::NOT_EQUAL->value], - ['whenGreaterThan', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN->value], - ['whenGreaterThanOrEqual', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN_OR_EQUAL->value], - ['whenLessThan', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN->value], - ['whenLessThanOrEqual', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN_OR_EQUAL->value], + ['when_equal', ['price', 100, 'expensive'], 'cheap', Operator::EQUAL->value], + ['when_not_equal', ['price', 100, 'expensive'], 'cheap', Operator::NOT_EQUAL->value], + ['when_gt', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN->value], + ['when_gte', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN_OR_EQUAL->value], + ['when_lt', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN->value], + ['when_lte', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN_OR_EQUAL->value], ]); it('generates query with select-cases using logical comparisons', function ( - string $method, + string $function, array $data, string $defaultResult, string $operator @@ -228,8 +235,9 @@ $query = new QueryGenerator(Driver::POSTGRESQL); - $case = Funct::case() - ->{$method}(...$data) + $factory = "Phenix\\Database\\{$function}"; + + $case = $factory(...$data) ->defaultResult($defaultResult) ->as('status'); @@ -249,10 +257,10 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - ['whenNull', ['created_at', 'inactive'], 'active', Operator::IS_NULL->value], - ['whenNotNull', ['created_at', 'active'], 'inactive', Operator::IS_NOT_NULL->value], - ['whenTrue', ['is_verified', 'active'], 'inactive', Operator::IS_TRUE->value], - ['whenFalse', ['is_verified', 'inactive'], 'active', Operator::IS_FALSE->value], + ['when_null', ['created_at', 'inactive'], 'active', Operator::IS_NULL->value], + ['when_not_null', ['created_at', 'active'], 'inactive', Operator::IS_NOT_NULL->value], + ['when_true', ['is_verified', 'active'], 'inactive', Operator::IS_TRUE->value], + ['when_false', ['is_verified', 'inactive'], 'active', Operator::IS_FALSE->value], ]); it('generates query with select-cases with multiple conditions and string values', function () { @@ -260,8 +268,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); - $case = Funct::case() - ->whenNull('created_at', 'inactive') + $case = when_null('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->defaultResult('old user') ->as('status'); @@ -288,8 +295,7 @@ $query = new QueryGenerator(Driver::POSTGRESQL); - $case = Funct::case() - ->whenNull('created_at', 'inactive') + $case = when_null('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->as('status'); @@ -313,8 +319,7 @@ it('generates query with select-case using functions', function () { $query = new QueryGenerator(Driver::POSTGRESQL); - $case = Funct::case() - ->whenGreaterThanOrEqual(Funct::avg('price'), 4, 'expensive') + $case = when_gte(avg('price'), 4, 'expensive') ->defaultResult('cheap') ->as('message'); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php index 103a1ec7..b692fc1f 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/WhereClausesTest.php @@ -5,10 +5,12 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Operator; use Phenix\Database\Constants\Order; -use Phenix\Database\Funct; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; +use function Phenix\Database\max_of; +use function Phenix\Database\when_null; + it('generates query to select a record by column', function () { $query = new QueryGenerator(Driver::POSTGRESQL); @@ -355,8 +357,7 @@ ]); it('generates a column-ordered query using select-case', function () { - $case = Funct::case() - ->whenNull('city', 'country') + $case = when_null('city', 'country') ->defaultResult('city'); $query = new QueryGenerator(Driver::POSTGRESQL); @@ -458,7 +459,7 @@ $sql = $query->table('products') ->{$method}($column, function (Subquery $subquery) { - $subquery->select([Funct::max('price')])->from('products'); + $subquery->select([max_of('price')])->from('products'); }) ->get(); diff --git a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php index 1141310c..fb892c61 100644 --- a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php @@ -6,11 +6,15 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Operator; use Phenix\Database\Exceptions\QueryErrorException; -use Phenix\Database\Funct; use Phenix\Database\QueryAst; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; +use function Phenix\Database\avg; +use function Phenix\Database\subquery; +use function Phenix\Database\when_gte; +use function Phenix\Database\when_null; + it('generates query to select all columns of table', function (): void { $query = new QueryGenerator(); @@ -109,9 +113,10 @@ public function ast(): QueryAst it('generates a query using sql functions', function (string $function, string $column, string $rawFunction) { $query = new QueryGenerator(); + $factory = "Phenix\\Database\\{$function}"; $sql = $query->table('products') - ->select([Funct::{$function}($column)]) + ->select([$factory($column)]) ->get(); [$dml, $params] = $sql; @@ -121,9 +126,9 @@ public function ast(): QueryAst })->with([ ['avg', 'price', 'AVG(`price`)'], ['sum', 'price', 'SUM(`price`)'], - ['min', 'price', 'MIN(`price`)'], - ['max', 'price', 'MAX(`price`)'], - ['count', 'id', 'COUNT(`id`)'], + ['min_of', 'price', 'MIN(`price`)'], + ['max_of', 'price', 'MAX(`price`)'], + ['count_of', 'id', 'COUNT(`id`)'], ]); it('generates a query using sql functions with alias', function ( @@ -133,9 +138,10 @@ public function ast(): QueryAst string $rawFunction ) { $query = new QueryGenerator(); + $factory = "Phenix\\Database\\{$function}"; $sql = $query->table('products') - ->select([Funct::{$function}($column)->as($alias)]) + ->select([$factory($column)->as($alias)]) ->get(); [$dml, $params] = $sql; @@ -145,9 +151,9 @@ public function ast(): QueryAst })->with([ ['avg', 'price', 'value', 'AVG(`price`) AS `value`'], ['sum', 'price', 'value', 'SUM(`price`) AS `value`'], - ['min', 'price', 'value', 'MIN(`price`) AS `value`'], - ['max', 'price', 'value', 'MAX(`price`) AS `value`'], - ['count', 'id', 'value', 'COUNT(`id`) AS `value`'], + ['min_of', 'price', 'value', 'MIN(`price`) AS `value`'], + ['max_of', 'price', 'value', 'MAX(`price`) AS `value`'], + ['count_of', 'id', 'value', 'COUNT(`id`) AS `value`'], ]); it('selects field from subquery', function () { @@ -176,7 +182,7 @@ public function ast(): QueryAst $sql = $query->select([ 'id', 'name', - Subquery::make()->select(['name']) + subquery()->select(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name') @@ -201,7 +207,7 @@ public function ast(): QueryAst $query->select([ 'id', 'name', - Subquery::make()->select(['name']) + subquery()->select(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name'), @@ -248,7 +254,7 @@ public function ast(): QueryAst }); it('generates query with select-cases using comparisons', function ( - string $method, + string $function, array $data, string $defaultResult, string $operator @@ -257,8 +263,9 @@ public function ast(): QueryAst $query = new QueryGenerator(); - $case = Funct::case() - ->{$method}($column, $value, $result) + $factory = "Phenix\\Database\\{$function}"; + + $case = $factory($column, $value, $result) ->defaultResult($defaultResult) ->as('type'); @@ -278,16 +285,16 @@ public function ast(): QueryAst expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - ['whenEqual', ['price', 100, 'expensive'], 'cheap', Operator::EQUAL->value], - ['whenNotEqual', ['price', 100, 'expensive'], 'cheap', Operator::NOT_EQUAL->value], - ['whenGreaterThan', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN->value], - ['whenGreaterThanOrEqual', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN_OR_EQUAL->value], - ['whenLessThan', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN->value], - ['whenLessThanOrEqual', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN_OR_EQUAL->value], + ['when_equal', ['price', 100, 'expensive'], 'cheap', Operator::EQUAL->value], + ['when_not_equal', ['price', 100, 'expensive'], 'cheap', Operator::NOT_EQUAL->value], + ['when_gt', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN->value], + ['when_gte', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN_OR_EQUAL->value], + ['when_lt', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN->value], + ['when_lte', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN_OR_EQUAL->value], ]); it('generates query with select-cases using logical comparisons', function ( - string $method, + string $function, array $data, string $defaultResult, string $operator @@ -296,8 +303,9 @@ public function ast(): QueryAst $query = new QueryGenerator(); - $case = Funct::case() - ->{$method}(...$data) + $factory = "Phenix\\Database\\{$function}"; + + $case = $factory(...$data) ->defaultResult($defaultResult) ->as('status'); @@ -317,10 +325,10 @@ public function ast(): QueryAst expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - ['whenNull', ['created_at', 'inactive'], 'active', Operator::IS_NULL->value], - ['whenNotNull', ['created_at', 'active'], 'inactive', Operator::IS_NOT_NULL->value], - ['whenTrue', ['is_verified', 'active'], 'inactive', Operator::IS_TRUE->value], - ['whenFalse', ['is_verified', 'inactive'], 'active', Operator::IS_FALSE->value], + ['when_null', ['created_at', 'inactive'], 'active', Operator::IS_NULL->value], + ['when_not_null', ['created_at', 'active'], 'inactive', Operator::IS_NOT_NULL->value], + ['when_true', ['is_verified', 'active'], 'inactive', Operator::IS_TRUE->value], + ['when_false', ['is_verified', 'inactive'], 'active', Operator::IS_FALSE->value], ]); it('generates query with select-cases with multiple conditions and string values', function () { @@ -328,8 +336,7 @@ public function ast(): QueryAst $query = new QueryGenerator(); - $case = Funct::case() - ->whenNull('created_at', 'inactive') + $case = when_null('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->defaultResult('old user') ->as('status'); @@ -356,8 +363,7 @@ public function ast(): QueryAst $query = new QueryGenerator(); - $case = Funct::case() - ->whenNull('created_at', 'inactive') + $case = when_null('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->as('status'); @@ -381,8 +387,7 @@ public function ast(): QueryAst it('generates query with select-case using functions', function () { $query = new QueryGenerator(); - $case = Funct::case() - ->whenGreaterThanOrEqual(Funct::avg('price'), 4, 'expensive') + $case = when_gte(avg('price'), 4, 'expensive') ->defaultResult('cheap') ->as('message'); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php index 70104f3c..e13ee4d0 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/GroupByStatementTest.php @@ -8,6 +8,10 @@ use Phenix\Database\Join; use Phenix\Database\QueryGenerator; +use function Phenix\Database\avg; +use function Phenix\Database\count_of; +use function Phenix\Database\sum; + it('generates a grouped query', function (Funct|string $column, Funct|array|string $groupBy, string $rawGroup, string $rawColumn): void { $query = new QueryGenerator(Driver::SQLITE); @@ -33,9 +37,9 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Funct::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + [count_of('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], - [Funct::count('products.id'), Funct::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], + [count_of('products.id'), count_of('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped and ordered query', function ( @@ -70,16 +74,16 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - [Funct::count('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], + [count_of('products.id'), 'category_id', '"category_id"', 'COUNT("products"."id")'], ['location_id', ['category_id', 'location_id'], '"category_id", "location_id"', '"location_id"'], - [Funct::count('products.id'), Funct::count('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], + [count_of('products.id'), count_of('products.id'), 'COUNT("products"."id")', 'COUNT("products"."id")'], ]); it('generates a grouped query with where clause', function (): void { $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Funct::count('products.id'), + count_of('products.id'), 'products.category_id', ]) ->from('products') @@ -102,7 +106,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -127,9 +131,9 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Funct::count('products.id'), - Funct::sum('products.price'), - Funct::avg('products.price'), + count_of('products.id'), + sum('products.price'), + avg('products.price'), 'products.category_id', ]) ->from('products') diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php index b617a95f..12e2aad4 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/HavingClauseTest.php @@ -3,16 +3,18 @@ declare(strict_types=1); use Phenix\Database\Constants\Driver; -use Phenix\Database\Funct; use Phenix\Database\Having; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; +use function Phenix\Database\count_of; +use function Phenix\Database\sum; + it('generates a query using having clause', function () { $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Funct::count('products.id')->as('identifiers'), + count_of('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -41,7 +43,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Funct::count('products.id')->as('identifiers'), + count_of('products.id')->as('identifiers'), 'products.category_id', 'categories.description', ]) @@ -71,7 +73,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -97,7 +99,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Funct::sum('orders.total')->as('total_sales'), + sum('orders.total')->as('total_sales'), 'orders.customer_id', ]) ->from('orders') @@ -121,7 +123,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.category_id', ]) ->from('products') @@ -145,7 +147,7 @@ $query = new QueryGenerator(Driver::SQLITE); $sql = $query->select([ - Funct::count('products.id')->as('product_count'), + count_of('products.id')->as('product_count'), 'products.created_at', ]) ->from('products') diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php index 23432cc7..1fc63024 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php @@ -7,10 +7,14 @@ use Phenix\Database\Constants\Lock; use Phenix\Database\Constants\Operator; use Phenix\Database\Exceptions\QueryErrorException; -use Phenix\Database\Funct; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; +use function Phenix\Database\avg; +use function Phenix\Database\subquery; +use function Phenix\Database\when_gte; +use function Phenix\Database\when_null; + it('generates query to select all columns of table', function () { $query = new QueryGenerator(Driver::SQLITE); @@ -41,9 +45,10 @@ it('generates a query using sql functions', function (string $function, string $column, string $rawFunction) { $query = new QueryGenerator(Driver::SQLITE); + $factory = "Phenix\\Database\\{$function}"; $sql = $query->table('products') - ->select([Funct::{$function}($column)]) + ->select([$factory($column)]) ->get(); [$dml, $params] = $sql; @@ -53,9 +58,9 @@ })->with([ ['avg', 'price', 'AVG("price")'], ['sum', 'price', 'SUM("price")'], - ['min', 'price', 'MIN("price")'], - ['max', 'price', 'MAX("price")'], - ['count', 'id', 'COUNT("id")'], + ['min_of', 'price', 'MIN("price")'], + ['max_of', 'price', 'MAX("price")'], + ['count_of', 'id', 'COUNT("id")'], ]); it('generates a query using sql functions with alias', function ( @@ -65,9 +70,10 @@ string $rawFunction ) { $query = new QueryGenerator(Driver::SQLITE); + $factory = "Phenix\\Database\\{$function}"; $sql = $query->table('products') - ->select([Funct::{$function}($column)->as($alias)]) + ->select([$factory($column)->as($alias)]) ->get(); [$dml, $params] = $sql; @@ -77,9 +83,9 @@ })->with([ ['avg', 'price', 'value', 'AVG("price") AS "value"'], ['sum', 'price', 'value', 'SUM("price") AS "value"'], - ['min', 'price', 'value', 'MIN("price") AS "value"'], - ['max', 'price', 'value', 'MAX("price") AS "value"'], - ['count', 'id', 'value', 'COUNT("id") AS "value"'], + ['min_of', 'price', 'value', 'MIN("price") AS "value"'], + ['max_of', 'price', 'value', 'MAX("price") AS "value"'], + ['count_of', 'id', 'value', 'COUNT("id") AS "value"'], ]); it('selects field from subquery', function () { @@ -108,7 +114,7 @@ $sql = $query->select([ 'id', 'name', - Subquery::make(Driver::SQLITE)->select(['name']) + subquery()->select(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name') @@ -133,7 +139,7 @@ $query->select([ 'id', 'name', - Subquery::make(Driver::SQLITE)->select(['name']) + subquery()->select(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name'), @@ -180,7 +186,7 @@ }); it('generates query with select-cases using comparisons', function ( - string $method, + string $function, array $data, string $defaultResult, string $operator @@ -189,8 +195,9 @@ $query = new QueryGenerator(Driver::SQLITE); - $case = Funct::case() - ->{$method}($column, $value, $result) + $factory = "Phenix\\Database\\{$function}"; + + $case = $factory($column, $value, $result) ->defaultResult($defaultResult) ->as('type'); @@ -210,16 +217,16 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - ['whenEqual', ['price', 100, 'expensive'], 'cheap', Operator::EQUAL->value], - ['whenNotEqual', ['price', 100, 'expensive'], 'cheap', Operator::NOT_EQUAL->value], - ['whenGreaterThan', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN->value], - ['whenGreaterThanOrEqual', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN_OR_EQUAL->value], - ['whenLessThan', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN->value], - ['whenLessThanOrEqual', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN_OR_EQUAL->value], + ['when_equal', ['price', 100, 'expensive'], 'cheap', Operator::EQUAL->value], + ['when_not_equal', ['price', 100, 'expensive'], 'cheap', Operator::NOT_EQUAL->value], + ['when_gt', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN->value], + ['when_gte', ['price', 100, 'expensive'], 'cheap', Operator::GREATER_THAN_OR_EQUAL->value], + ['when_lt', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN->value], + ['when_lte', ['price', 100, 'cheap'], 'expensive', Operator::LESS_THAN_OR_EQUAL->value], ]); it('generates query with select-cases using logical comparisons', function ( - string $method, + string $function, array $data, string $defaultResult, string $operator @@ -228,8 +235,9 @@ $query = new QueryGenerator(Driver::SQLITE); - $case = Funct::case() - ->{$method}(...$data) + $factory = "Phenix\\Database\\{$function}"; + + $case = $factory(...$data) ->defaultResult($defaultResult) ->as('status'); @@ -249,10 +257,10 @@ expect($dml)->toBe($expected); expect($params)->toBeEmpty(); })->with([ - ['whenNull', ['created_at', 'inactive'], 'active', Operator::IS_NULL->value], - ['whenNotNull', ['created_at', 'active'], 'inactive', Operator::IS_NOT_NULL->value], - ['whenTrue', ['is_verified', 'active'], 'inactive', Operator::IS_TRUE->value], - ['whenFalse', ['is_verified', 'inactive'], 'active', Operator::IS_FALSE->value], + ['when_null', ['created_at', 'inactive'], 'active', Operator::IS_NULL->value], + ['when_not_null', ['created_at', 'active'], 'inactive', Operator::IS_NOT_NULL->value], + ['when_true', ['is_verified', 'active'], 'inactive', Operator::IS_TRUE->value], + ['when_false', ['is_verified', 'inactive'], 'active', Operator::IS_FALSE->value], ]); it('generates query with select-cases with multiple conditions and string values', function () { @@ -260,8 +268,7 @@ $query = new QueryGenerator(Driver::SQLITE); - $case = Funct::case() - ->whenNull('created_at', 'inactive') + $case = when_null('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->defaultResult('old user') ->as('status'); @@ -288,8 +295,7 @@ $query = new QueryGenerator(Driver::SQLITE); - $case = Funct::case() - ->whenNull('created_at', 'inactive') + $case = when_null('created_at', 'inactive') ->whenGreaterThan('created_at', $date, 'new user') ->as('status'); @@ -313,8 +319,7 @@ it('generates query with select-case using functions', function () { $query = new QueryGenerator(Driver::SQLITE); - $case = Funct::case() - ->whenGreaterThanOrEqual(Funct::avg('price'), 4, 'expensive') + $case = when_gte(avg('price'), 4, 'expensive') ->defaultResult('cheap') ->as('message'); diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php index dbf46b29..838a52f1 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/WhereClausesTest.php @@ -5,10 +5,12 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Operator; use Phenix\Database\Constants\Order; -use Phenix\Database\Funct; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; +use function Phenix\Database\max_of; +use function Phenix\Database\when_null; + it('generates query to select a record by column', function () { $query = new QueryGenerator(Driver::SQLITE); @@ -331,8 +333,7 @@ ]); it('generates a column-ordered query using select-case', function () { - $case = Funct::case() - ->whenNull('city', 'country') + $case = when_null('city', 'country') ->defaultResult('city'); $query = new QueryGenerator(Driver::SQLITE); @@ -434,7 +435,7 @@ $sql = $query->table('products') ->{$method}($column, function (Subquery $subquery) { - $subquery->select([Funct::max('price')])->from('products'); + $subquery->select([max_of('price')])->from('products'); }) ->get(); diff --git a/tests/Unit/Database/QueryGenerator/WhereClausesTest.php b/tests/Unit/Database/QueryGenerator/WhereClausesTest.php index 9b687613..4b906686 100644 --- a/tests/Unit/Database/QueryGenerator/WhereClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/WhereClausesTest.php @@ -4,10 +4,12 @@ use Phenix\Database\Constants\Operator; use Phenix\Database\Constants\Order; -use Phenix\Database\Funct; use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; +use function Phenix\Database\max_of; +use function Phenix\Database\when_null; + it('generates query to select a record by column', function () { $query = new QueryGenerator(); @@ -330,8 +332,7 @@ ]); it('generates a column-ordered query using select-case', function () { - $case = Funct::case() - ->whenNull('city', 'country') + $case = when_null('city', 'country') ->defaultResult('city'); $query = new QueryGenerator(); @@ -433,7 +434,7 @@ $sql = $query->table('products') ->{$method}($column, function (Subquery $subquery) { - $subquery->select([Funct::max('price')])->from('products'); + $subquery->select([max_of('price')])->from('products'); }) ->get(); From 91745f9d79602e1bdaee44ea17b9b3df9da255c0 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Sat, 2 May 2026 07:22:10 -0500 Subject: [PATCH 17/32] style: php cs --- src/Database/Dialects/Compilers/DeleteCompiler.php | 1 - src/Database/Dialects/Compilers/InsertCompiler.php | 1 - src/Database/Dialects/Compilers/UpdateCompiler.php | 2 -- src/Database/Dialects/Postgres/Compilers/Insert.php | 1 - src/Database/Dialects/Postgres/Compilers/Update.php | 2 -- src/Database/Dialects/Sqlite/Compilers/Delete.php | 2 -- src/Database/Dialects/Sqlite/Compilers/Update.php | 2 -- 7 files changed, 11 deletions(-) diff --git a/src/Database/Dialects/Compilers/DeleteCompiler.php b/src/Database/Dialects/Compilers/DeleteCompiler.php index ddf85429..24354377 100644 --- a/src/Database/Dialects/Compilers/DeleteCompiler.php +++ b/src/Database/Dialects/Compilers/DeleteCompiler.php @@ -5,7 +5,6 @@ namespace Phenix\Database\Dialects\Compilers; use Phenix\Database\Dialects\CompiledClause; -use Phenix\Database\Wrapper; use Phenix\Util\Arr; abstract class DeleteCompiler extends ClauseCompiler diff --git a/src/Database/Dialects/Compilers/InsertCompiler.php b/src/Database/Dialects/Compilers/InsertCompiler.php index 2082e75b..4d81f8f6 100644 --- a/src/Database/Dialects/Compilers/InsertCompiler.php +++ b/src/Database/Dialects/Compilers/InsertCompiler.php @@ -5,7 +5,6 @@ namespace Phenix\Database\Dialects\Compilers; use Phenix\Database\Dialects\CompiledClause; -use Phenix\Database\Wrapper; use Phenix\Util\Arr; abstract class InsertCompiler extends ClauseCompiler diff --git a/src/Database/Dialects/Compilers/UpdateCompiler.php b/src/Database/Dialects/Compilers/UpdateCompiler.php index f9d159e6..99b14a67 100644 --- a/src/Database/Dialects/Compilers/UpdateCompiler.php +++ b/src/Database/Dialects/Compilers/UpdateCompiler.php @@ -4,9 +4,7 @@ namespace Phenix\Database\Dialects\Compilers; -use Phenix\Database\Constants\Driver; use Phenix\Database\Dialects\CompiledClause; -use Phenix\Database\Wrapper; use Phenix\Util\Arr; use function count; diff --git a/src/Database/Dialects/Postgres/Compilers/Insert.php b/src/Database/Dialects/Postgres/Compilers/Insert.php index b011ec8d..22a09871 100644 --- a/src/Database/Dialects/Postgres/Compilers/Insert.php +++ b/src/Database/Dialects/Postgres/Compilers/Insert.php @@ -7,7 +7,6 @@ use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\InsertCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; -use Phenix\Database\Wrapper; use Phenix\Util\Arr; use function sprintf; diff --git a/src/Database/Dialects/Postgres/Compilers/Update.php b/src/Database/Dialects/Postgres/Compilers/Update.php index 3e2ea208..a1e0e820 100644 --- a/src/Database/Dialects/Postgres/Compilers/Update.php +++ b/src/Database/Dialects/Postgres/Compilers/Update.php @@ -4,11 +4,9 @@ namespace Phenix\Database\Dialects\Postgres\Compilers; -use Phenix\Database\Constants\Driver; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\UpdateCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; -use Phenix\Database\Wrapper; use function count; diff --git a/src/Database/Dialects/Sqlite/Compilers/Delete.php b/src/Database/Dialects/Sqlite/Compilers/Delete.php index d4974033..21591857 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Delete.php +++ b/src/Database/Dialects/Sqlite/Compilers/Delete.php @@ -4,10 +4,8 @@ namespace Phenix\Database\Dialects\Sqlite\Compilers; -use Phenix\Database\Constants\Driver; use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\DeleteCompiler; -use Phenix\Database\Wrapper; use Phenix\Util\Arr; class Delete extends DeleteCompiler diff --git a/src/Database/Dialects/Sqlite/Compilers/Update.php b/src/Database/Dialects/Sqlite/Compilers/Update.php index 3dd85cf6..e47d82f1 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Update.php +++ b/src/Database/Dialects/Sqlite/Compilers/Update.php @@ -4,9 +4,7 @@ namespace Phenix\Database\Dialects\Sqlite\Compilers; -use Phenix\Database\Constants\Driver; use Phenix\Database\Dialects\Compilers\UpdateCompiler; -use Phenix\Database\Wrapper; class Update extends UpdateCompiler { From e897a7623b21214d238fbda824eb2e489ab1787b Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Sat, 2 May 2026 07:36:48 -0500 Subject: [PATCH 18/32] refactor: update subquery function to accept columns parameter and enhance test cases --- src/Database/functions.php | 7 +++++-- .../QueryGenerator/Postgres/SelectColumnsTest.php | 4 ++-- .../Database/QueryGenerator/SelectColumnsTest.php | 14 ++++++++++++-- .../QueryGenerator/Sqlite/SelectColumnsTest.php | 4 ++-- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Database/functions.php b/src/Database/functions.php index a9384569..7960677d 100644 --- a/src/Database/functions.php +++ b/src/Database/functions.php @@ -46,9 +46,12 @@ function year(string $column): Funct return Funct::year($column); } -function subquery(): Subquery +/** + * @param array $columns + */ +function subquery(array $columns = ['*']): Subquery { - return Subquery::make(); + return Subquery::make()->select($columns); } function case_of(): SelectCase diff --git a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php index 2ebd7822..5f27d972 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php @@ -114,7 +114,7 @@ $sql = $query->select([ 'id', 'name', - subquery()->select(['name']) + subquery(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name') @@ -139,7 +139,7 @@ $query->select([ 'id', 'name', - subquery()->select(['name']) + subquery(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name'), diff --git a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php index fb892c61..b78a54ac 100644 --- a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php @@ -175,6 +175,16 @@ public function ast(): QueryAst expect($params)->toBe([$date]); }); +it('initializes subquery helper selecting all columns by default', function () { + $sql = subquery() + ->from('countries') + ->toSql(); + + [$dml, $params] = $sql; + + expect($dml)->toBe('(SELECT * FROM `countries`)'); + expect($params)->toBeEmpty(); +}); it('generates query using subqueries in column selection', function () { $query = new QueryGenerator(); @@ -182,7 +192,7 @@ public function ast(): QueryAst $sql = $query->select([ 'id', 'name', - subquery()->select(['name']) + subquery(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name') @@ -207,7 +217,7 @@ public function ast(): QueryAst $query->select([ 'id', 'name', - subquery()->select(['name']) + subquery(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name'), diff --git a/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php index 1fc63024..9351d7e3 100644 --- a/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Sqlite/SelectColumnsTest.php @@ -114,7 +114,7 @@ $sql = $query->select([ 'id', 'name', - subquery()->select(['name']) + subquery(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name') @@ -139,7 +139,7 @@ $query->select([ 'id', 'name', - subquery()->select(['name']) + subquery(['name']) ->from('countries') ->whereColumn('users.country_id', 'countries.id') ->as('country_name'), From 30e9dbe686b19a3df0ffc59204dcc510753f8f70 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Sat, 2 May 2026 11:14:14 -0500 Subject: [PATCH 19/32] refactor: remove unused case_of function to streamline code --- src/Database/functions.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Database/functions.php b/src/Database/functions.php index 7960677d..a97ff13c 100644 --- a/src/Database/functions.php +++ b/src/Database/functions.php @@ -54,11 +54,6 @@ function subquery(array $columns = ['*']): Subquery return Subquery::make()->select($columns); } -function case_of(): SelectCase -{ - return Funct::case(); -} - function when_equal(Funct|string $column, RawValue|string|int $value, RawValue|string|int $result): SelectCase { return Funct::case()->whenEqual($column, $value, $result); From 2529af2399b823d3a94b5a9c9f816d9b3bbe78eb Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Mon, 4 May 2026 08:01:52 -0500 Subject: [PATCH 20/32] feat: add alias function and update tests to use it for improved query handling --- src/Database/functions.php | 5 +++++ .../Database/QueryGenerator/Postgres/SelectColumnsTest.php | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Database/functions.php b/src/Database/functions.php index a97ff13c..6f25c68a 100644 --- a/src/Database/functions.php +++ b/src/Database/functions.php @@ -103,3 +103,8 @@ function when_false(string $column, RawValue|string|int $result): SelectCase { return Funct::case()->whenFalse($column, $result); } + +function alias(string $name, string $alias): Alias +{ + return Alias::of($name)->as($alias); +} diff --git a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php index 5f27d972..88adfa10 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php @@ -2,7 +2,6 @@ declare(strict_types=1); -use Phenix\Database\Alias; use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Lock; use Phenix\Database\Constants\Operator; @@ -11,6 +10,7 @@ use Phenix\Database\Subquery; use function Phenix\Database\avg; +use function Phenix\Database\alias; use function Phenix\Database\subquery; use function Phenix\Database\when_gte; use function Phenix\Database\when_null; @@ -154,7 +154,7 @@ $sql = $query->select([ 'id', - Alias::of('name')->as('full_name'), + alias('name', 'full_name'), ]) ->from('users') ->get(); From 852abb45f7ac76d2c7a14d4d9d8b21e83e95a36a Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Mon, 4 May 2026 08:46:01 -0500 Subject: [PATCH 21/32] style: php cs --- .../Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php index 88adfa10..e9c09836 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php @@ -9,8 +9,8 @@ use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; -use function Phenix\Database\avg; use function Phenix\Database\alias; +use function Phenix\Database\avg; use function Phenix\Database\subquery; use function Phenix\Database\when_gte; use function Phenix\Database\when_null; From 4a6d75633572e1e2459c8c08ccd7b9b62fcb2deb Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Mon, 4 May 2026 08:46:21 -0500 Subject: [PATCH 22/32] refactor: streamline having and lock compilation in SelectCompiler --- .../Dialects/Compilers/SelectCompiler.php | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index 2aef68e7..83e22a4f 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -53,13 +53,8 @@ public function compile(): CompiledClause $sql[] = $this->compileGroups($this->ast->groups); } - if ($this->ast->having !== null) { - $having = $this->havingCompiler->compile($this->ast->having); - - if ($having->sql !== '') { - $sql[] = $having->sql; - $this->ast->params = [...$this->ast->params, ...$having->params]; - } + if ($this->ast->having !== null && $havingSql = $this->compileHaving()) { + $sql[] = $havingSql; } if (! empty($this->ast->orders)) { @@ -75,12 +70,8 @@ public function compile(): CompiledClause $sql[] = "OFFSET {$this->ast->offset}"; } - if ($this->ast->lock !== null) { - $lockSql = $this->compileLock(); - - if ($lockSql !== '') { - $sql[] = $lockSql; - } + if ($this->ast->lock !== null && $lockSql = $this->compileLock()) { + $sql[] = $lockSql; } return new CompiledClause( @@ -109,6 +100,19 @@ protected function compileColumns(array $columns): string return Arr::implodeDeeply($compiled, ', '); } + protected function compileHaving(): string|null + { + $having = $this->havingCompiler->compile($this->ast->having); + + if ($having->sql === '') { + return null; + } + + $this->ast->params = [...$this->ast->params, ...$having->params]; + + return $having->sql; + } + /** * @param array $groups * @return string From a0f6cc164723af00f59a76e46ba8734e8839718e Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Mon, 4 May 2026 08:52:19 -0500 Subject: [PATCH 23/32] refactor: reorganize imports and simplify null/boolean clause compilation --- .../Dialects/Mysql/Compilers/Where.php | 24 +++++++++++-------- .../Dialects/Postgres/Compilers/Where.php | 24 +++++++++++-------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Database/Dialects/Mysql/Compilers/Where.php b/src/Database/Dialects/Mysql/Compilers/Where.php index ad44832e..5d511d6e 100644 --- a/src/Database/Dialects/Mysql/Compilers/Where.php +++ b/src/Database/Dialects/Mysql/Compilers/Where.php @@ -4,18 +4,19 @@ namespace Phenix\Database\Dialects\Mysql\Compilers; +use Phenix\Database\Wrapper; +use Phenix\Database\Constants\SQL; +use Phenix\Database\Constants\Driver; +use Phenix\Database\Clauses\WhereClause; +use Phenix\Database\Clauses\RowWhereClause; +use Phenix\Database\Clauses\DateWhereClause; +use Phenix\Database\Clauses\NullWhereClause; use Phenix\Database\Clauses\BasicWhereClause; +use Phenix\Database\Clauses\ColumnWhereClause; use Phenix\Database\Clauses\BetweenWhereClause; use Phenix\Database\Clauses\BooleanWhereClause; -use Phenix\Database\Clauses\ColumnWhereClause; -use Phenix\Database\Clauses\DateWhereClause; -use Phenix\Database\Clauses\NullWhereClause; -use Phenix\Database\Clauses\RowWhereClause; use Phenix\Database\Clauses\SubqueryWhereClause; -use Phenix\Database\Constants\Driver; -use Phenix\Database\Constants\SQL; use Phenix\Database\Dialects\Compilers\WhereCompiler; -use Phenix\Database\Wrapper; class Where extends WhereCompiler { @@ -80,12 +81,15 @@ protected function compileColumnClause(ColumnWhereClause $clause): string protected function compileNullClause(NullWhereClause $clause): string { - $column = Wrapper::column(Driver::MYSQL, $clause->getColumn()); - - return "{$column} {$clause->getOperator()->value}"; + return $this->compileCommonClause($clause); } protected function compileBooleanClause(BooleanWhereClause $clause): string + { + return $this->compileCommonClause($clause); + } + + private function compileCommonClause(WhereClause $clause): string { $column = Wrapper::column(Driver::MYSQL, $clause->getColumn()); diff --git a/src/Database/Dialects/Postgres/Compilers/Where.php b/src/Database/Dialects/Postgres/Compilers/Where.php index ce3995c7..883fe725 100644 --- a/src/Database/Dialects/Postgres/Compilers/Where.php +++ b/src/Database/Dialects/Postgres/Compilers/Where.php @@ -4,18 +4,19 @@ namespace Phenix\Database\Dialects\Postgres\Compilers; +use Phenix\Database\Wrapper; +use Phenix\Database\Constants\SQL; +use Phenix\Database\Constants\Driver; +use Phenix\Database\Clauses\WhereClause; +use Phenix\Database\Clauses\RowWhereClause; +use Phenix\Database\Clauses\DateWhereClause; +use Phenix\Database\Clauses\NullWhereClause; use Phenix\Database\Clauses\BasicWhereClause; +use Phenix\Database\Clauses\ColumnWhereClause; use Phenix\Database\Clauses\BetweenWhereClause; use Phenix\Database\Clauses\BooleanWhereClause; -use Phenix\Database\Clauses\ColumnWhereClause; -use Phenix\Database\Clauses\DateWhereClause; -use Phenix\Database\Clauses\NullWhereClause; -use Phenix\Database\Clauses\RowWhereClause; use Phenix\Database\Clauses\SubqueryWhereClause; -use Phenix\Database\Constants\Driver; -use Phenix\Database\Constants\SQL; use Phenix\Database\Dialects\Compilers\WhereCompiler; -use Phenix\Database\Wrapper; class Where extends WhereCompiler { @@ -86,12 +87,15 @@ protected function compileColumnClause(ColumnWhereClause $clause): string protected function compileNullClause(NullWhereClause $clause): string { - $column = Wrapper::column(Driver::POSTGRESQL, $clause->getColumn()); - - return "{$column} {$clause->getOperator()->value}"; + return $this->compileCommonClause($clause); } protected function compileBooleanClause(BooleanWhereClause $clause): string + { + return $this->compileCommonClause($clause); + } + + private function compileCommonClause(WhereClause $clause): string { $column = Wrapper::column(Driver::POSTGRESQL, $clause->getColumn()); From f0b4ca3be0a5519be8cb026598a816d4f504f46a Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Mon, 4 May 2026 08:52:26 -0500 Subject: [PATCH 24/32] refactor: remove TODO comment for efficiency in HasPlaceholders trait --- src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php b/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php index 25d6727a..cc847d62 100644 --- a/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php +++ b/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php @@ -6,7 +6,6 @@ trait HasPlaceholders { - // TODO: Refactor this to be more efficient and handle edge cases protected function convertPlaceholders(string $sql, int $startIndex = 0): string { $index = $startIndex + 1; From dfb31c6813f3cc21efc87ef38d0133264a8285ed Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Mon, 4 May 2026 08:59:01 -0500 Subject: [PATCH 25/32] fix: refactor resetBaseProperties to directly initialize QueryAst --- src/Database/QueryBase.php | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Database/QueryBase.php b/src/Database/QueryBase.php index e9129fd5..52963fef 100644 --- a/src/Database/QueryBase.php +++ b/src/Database/QueryBase.php @@ -40,7 +40,14 @@ public function __clone(): void protected function resetBaseProperties(): void { - $this->ast = $this->makeFreshAst(); + $ast = new QueryAst(); + $ast->columns = []; + + if (isset($this->driver)) { + $ast->driver = $this->driver; + } + + $this->ast = $ast; } public function setDriver(Driver $driver): static @@ -54,18 +61,6 @@ public function setDriver(Driver $driver): static return $this; } - protected function makeFreshAst(): QueryAst - { - $ast = new QueryAst(); - $ast->columns = []; - - if (isset($this->driver)) { - $ast->driver = $this->driver; - } - - return $ast; - } - /** * @return array */ From d064fc8a4c574ff0f17b95fa59b55aa6ef900187 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Mon, 4 May 2026 09:40:21 -0500 Subject: [PATCH 26/32] style: php cs --- src/Database/Dialects/Mysql/Compilers/Where.php | 16 ++++++++-------- .../Dialects/Postgres/Compilers/Where.php | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Database/Dialects/Mysql/Compilers/Where.php b/src/Database/Dialects/Mysql/Compilers/Where.php index 5d511d6e..96947dab 100644 --- a/src/Database/Dialects/Mysql/Compilers/Where.php +++ b/src/Database/Dialects/Mysql/Compilers/Where.php @@ -4,19 +4,19 @@ namespace Phenix\Database\Dialects\Mysql\Compilers; -use Phenix\Database\Wrapper; -use Phenix\Database\Constants\SQL; -use Phenix\Database\Constants\Driver; -use Phenix\Database\Clauses\WhereClause; -use Phenix\Database\Clauses\RowWhereClause; -use Phenix\Database\Clauses\DateWhereClause; -use Phenix\Database\Clauses\NullWhereClause; use Phenix\Database\Clauses\BasicWhereClause; -use Phenix\Database\Clauses\ColumnWhereClause; use Phenix\Database\Clauses\BetweenWhereClause; use Phenix\Database\Clauses\BooleanWhereClause; +use Phenix\Database\Clauses\ColumnWhereClause; +use Phenix\Database\Clauses\DateWhereClause; +use Phenix\Database\Clauses\NullWhereClause; +use Phenix\Database\Clauses\RowWhereClause; use Phenix\Database\Clauses\SubqueryWhereClause; +use Phenix\Database\Clauses\WhereClause; +use Phenix\Database\Constants\Driver; +use Phenix\Database\Constants\SQL; use Phenix\Database\Dialects\Compilers\WhereCompiler; +use Phenix\Database\Wrapper; class Where extends WhereCompiler { diff --git a/src/Database/Dialects/Postgres/Compilers/Where.php b/src/Database/Dialects/Postgres/Compilers/Where.php index 883fe725..8a4e75a7 100644 --- a/src/Database/Dialects/Postgres/Compilers/Where.php +++ b/src/Database/Dialects/Postgres/Compilers/Where.php @@ -4,19 +4,19 @@ namespace Phenix\Database\Dialects\Postgres\Compilers; -use Phenix\Database\Wrapper; -use Phenix\Database\Constants\SQL; -use Phenix\Database\Constants\Driver; -use Phenix\Database\Clauses\WhereClause; -use Phenix\Database\Clauses\RowWhereClause; -use Phenix\Database\Clauses\DateWhereClause; -use Phenix\Database\Clauses\NullWhereClause; use Phenix\Database\Clauses\BasicWhereClause; -use Phenix\Database\Clauses\ColumnWhereClause; use Phenix\Database\Clauses\BetweenWhereClause; use Phenix\Database\Clauses\BooleanWhereClause; +use Phenix\Database\Clauses\ColumnWhereClause; +use Phenix\Database\Clauses\DateWhereClause; +use Phenix\Database\Clauses\NullWhereClause; +use Phenix\Database\Clauses\RowWhereClause; use Phenix\Database\Clauses\SubqueryWhereClause; +use Phenix\Database\Clauses\WhereClause; +use Phenix\Database\Constants\Driver; +use Phenix\Database\Constants\SQL; use Phenix\Database\Dialects\Compilers\WhereCompiler; +use Phenix\Database\Wrapper; class Where extends WhereCompiler { From 76b7c97e2790136192d36686f2eaa8c816cfcd0b Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 5 May 2026 07:35:21 -0500 Subject: [PATCH 27/32] refactor: rename alias function to alias_of for clarity --- src/Database/functions.php | 2 +- .../Database/QueryGenerator/Postgres/SelectColumnsTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/functions.php b/src/Database/functions.php index 6f25c68a..066811d0 100644 --- a/src/Database/functions.php +++ b/src/Database/functions.php @@ -104,7 +104,7 @@ function when_false(string $column, RawValue|string|int $result): SelectCase return Funct::case()->whenFalse($column, $result); } -function alias(string $name, string $alias): Alias +function alias_of(string $name, string $alias): Alias { return Alias::of($name)->as($alias); } diff --git a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php index e9c09836..276c9587 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php @@ -9,7 +9,7 @@ use Phenix\Database\QueryGenerator; use Phenix\Database\Subquery; -use function Phenix\Database\alias; +use function Phenix\Database\alias_of; use function Phenix\Database\avg; use function Phenix\Database\subquery; use function Phenix\Database\when_gte; @@ -154,7 +154,7 @@ $sql = $query->select([ 'id', - alias('name', 'full_name'), + alias_of('name', 'full_name'), ]) ->from('users') ->get(); From b840d46b8ecc8158153591f270dd4b8a12ab8b64 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 12 May 2026 21:31:30 +0000 Subject: [PATCH 28/32] Refactor SQL Compilers to Use SqlData Class - Introduced a new SqlData class to encapsulate SQL strings and parameters. - Updated InsertCompiler, JoinCompiler, SelectCompiler, UpdateCompiler, and WhereCompiler to return SqlData instead of CompiledClause. - Refactored compile methods to handle parameter collection more efficiently. - Adjusted the handling of placeholders in SQL strings across various compilers. - Ensured that the new structure maintains compatibility with existing query generation logic. - Updated tests to verify correct parameter ordering and SQL generation for subqueries and joins. --- src/Database/ClauseBuilder.php | 22 +++-- src/Database/Clauses/BasicWhereClause.php | 18 +++- src/Database/Clauses/BetweenWhereClause.php | 12 ++- src/Database/Clauses/DateWhereClause.php | 12 ++- src/Database/Clauses/SubqueryWhereClause.php | 8 ++ src/Database/Clauses/WhereClause.php | 8 ++ src/Database/Concerns/Query/BuildsQuery.php | 15 ++- .../Concerns/Query/HasWhereClause.php | 8 -- .../Concerns/Query/HasWhereDateClause.php | 1 - .../Constants/{SQL.php => SqlMark.php} | 4 +- .../{ClauseCompiler.php => SqlCompiler.php} | 6 +- .../Dialects/Compilers/DeleteCompiler.php | 20 +++- .../Dialects/Compilers/ExistsCompiler.php | 15 ++- .../Dialects/Compilers/HavingCompiler.php | 10 +- .../Dialects/Compilers/InsertCompiler.php | 13 +-- .../Dialects/Compilers/JoinCompiler.php | 19 ++-- .../Dialects/Compilers/SelectCompiler.php | 96 +++++++++---------- .../{ClauseCompiler.php => SqlCompiler.php} | 25 ++++- .../Dialects/Compilers/UpdateCompiler.php | 26 ++--- .../Dialects/Compilers/WhereCompiler.php | 10 +- .../Dialects/Mysql/Compilers/Update.php | 7 -- .../Dialects/Mysql/Compilers/Where.php | 6 +- .../Dialects/Postgres/Compilers/Delete.php | 9 -- .../Dialects/Postgres/Compilers/Exists.php | 11 --- .../Dialects/Postgres/Compilers/Insert.php | 16 ++-- .../Dialects/Postgres/Compilers/Select.php | 10 +- .../Dialects/Postgres/Compilers/Update.php | 22 ----- .../Dialects/Postgres/Compilers/Where.php | 19 ++-- .../Postgres/Concerns/HasPlaceholders.php | 24 +++-- .../{CompiledClause.php => SqlData.php} | 2 +- .../Dialects/Sqlite/Compilers/Delete.php | 12 ++- .../Dialects/Sqlite/Compilers/Insert.php | 16 ++-- .../Dialects/Sqlite/Compilers/Update.php | 7 -- src/Database/Having.php | 2 +- src/Database/Join.php | 2 +- src/Database/QueryAst.php | 2 +- src/Database/QueryBase.php | 12 ++- src/Database/Wrapper.php | 4 +- .../InsertIntoStatementTest.php | 2 +- .../QueryGenerator/JoinClausesTest.php | 28 ++++++ .../Postgres/JoinClausesTest.php | 28 ++++++ .../Postgres/SelectColumnsTest.php | 21 ++++ .../QueryGenerator/SelectColumnsTest.php | 13 +-- 43 files changed, 372 insertions(+), 251 deletions(-) rename src/Database/Constants/{SQL.php => SqlMark.php} (59%) rename src/Database/Contracts/{ClauseCompiler.php => SqlCompiler.php} (57%) rename src/Database/Dialects/Compilers/{ClauseCompiler.php => SqlCompiler.php} (52%) rename src/Database/Dialects/{CompiledClause.php => SqlData.php} (91%) diff --git a/src/Database/ClauseBuilder.php b/src/Database/ClauseBuilder.php index 5781f867..f2469614 100644 --- a/src/Database/ClauseBuilder.php +++ b/src/Database/ClauseBuilder.php @@ -42,7 +42,7 @@ protected function getClauses(): array */ protected function getArguments(): array { - return $this->arguments; + return $this->getClauseArguments(); } protected function hasWhereClauses(): bool @@ -55,6 +55,20 @@ protected function addArguments(array $arguments): void $this->arguments = [...$this->arguments, ...$arguments]; } + /** + * @return array + */ + protected function getClauseArguments(): array + { + $arguments = []; + + foreach ($this->getClauses() as $clause) { + $arguments = [...$arguments, ...$clause->getParams()]; + } + + return $arguments; + } + protected function pushWhereClause( WhereClause $where, LogicalConnector $logicalConnector = LogicalConnector::AND @@ -109,8 +123,6 @@ protected function whereSubquery( operator: $operator, connector: $connector ), $logicalConnector); - - $this->addArguments($arguments); } /** @@ -139,8 +151,6 @@ protected function whereRowSubquery( params: $arguments, connector: $connector ), $logicalConnector); - - $this->addArguments($arguments); } protected function pushWhereWithArgs( @@ -150,8 +160,6 @@ protected function pushWhereWithArgs( LogicalConnector $logicalConnector = LogicalConnector::AND ): void { $this->pushClause(new BasicWhereClause($column, $operator, $value, null, true), $logicalConnector); - - $this->addArguments((array) $value); } protected function pushClause(WhereClause $where, LogicalConnector $logicalConnector = LogicalConnector::AND): void diff --git a/src/Database/Clauses/BasicWhereClause.php b/src/Database/Clauses/BasicWhereClause.php index 34dbbe70..4febcbe6 100644 --- a/src/Database/Clauses/BasicWhereClause.php +++ b/src/Database/Clauses/BasicWhereClause.php @@ -6,7 +6,7 @@ use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; -use Phenix\Database\Constants\SQL; +use Phenix\Database\Constants\SqlMark; use function count; use function is_array; @@ -55,10 +55,10 @@ public function renderValue(): string if ($this->usePlaceholder) { // In WHERE context with parameterized queries, use placeholder if (is_array($this->value)) { - return '(' . implode(', ', array_fill(0, count($this->value), SQL::PLACEHOLDER->value)) . ')'; + return '(' . implode(', ', array_fill(0, count($this->value), SqlMark::PLACEHOLDER->value)) . ')'; } - return SQL::PLACEHOLDER->value; + return SqlMark::Placeholder->value; } // In JOIN ON context, render the value directly (typically a column name) @@ -83,4 +83,16 @@ public function usesPlaceholder(): bool { return $this->usePlaceholder; } + + /** + * @return array + */ + public function getParams(): array + { + if (! $this->usePlaceholder) { + return []; + } + + return (array) $this->value; + } } diff --git a/src/Database/Clauses/BetweenWhereClause.php b/src/Database/Clauses/BetweenWhereClause.php index 6dbf0852..4d926693 100644 --- a/src/Database/Clauses/BetweenWhereClause.php +++ b/src/Database/Clauses/BetweenWhereClause.php @@ -6,7 +6,7 @@ use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; -use Phenix\Database\Constants\SQL; +use Phenix\Database\Constants\SqlMark; class BetweenWhereClause extends WhereClause { @@ -40,6 +40,14 @@ public function getOperator(): Operator public function renderValue(): string { - return SQL::PLACEHOLDER->value . ' AND ' . SQL::PLACEHOLDER->value; + return SqlMark::Placeholder->value . ' AND ' . SqlMark::Placeholder->value; + } + + /** + * @return array + */ + public function getParams(): array + { + return $this->values; } } diff --git a/src/Database/Clauses/DateWhereClause.php b/src/Database/Clauses/DateWhereClause.php index aaa8bb77..b4bed394 100644 --- a/src/Database/Clauses/DateWhereClause.php +++ b/src/Database/Clauses/DateWhereClause.php @@ -7,7 +7,7 @@ use Phenix\Database\Constants\DatabaseFunction; use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; -use Phenix\Database\Constants\SQL; +use Phenix\Database\Constants\SqlMark; class DateWhereClause extends WhereClause { @@ -55,6 +55,14 @@ public function getValue(): string|int public function renderValue(): string { - return SQL::PLACEHOLDER->value; + return SqlMark::Placeholder->value; + } + + /** + * @return array + */ + public function getParams(): array + { + return [$this->value]; } } diff --git a/src/Database/Clauses/SubqueryWhereClause.php b/src/Database/Clauses/SubqueryWhereClause.php index 9bac5bf2..5bb0ab93 100644 --- a/src/Database/Clauses/SubqueryWhereClause.php +++ b/src/Database/Clauses/SubqueryWhereClause.php @@ -65,6 +65,14 @@ public function getSql(): string return $this->sql; } + /** + * @return array + */ + public function getParams(): array + { + return $this->params; + } + public function renderValue(): string { // Render subquery with optional operator (ANY, ALL, SOME) diff --git a/src/Database/Clauses/WhereClause.php b/src/Database/Clauses/WhereClause.php index 1187d22b..0125d387 100644 --- a/src/Database/Clauses/WhereClause.php +++ b/src/Database/Clauses/WhereClause.php @@ -21,6 +21,14 @@ abstract public function getOperator(): Operator|null; */ abstract public function renderValue(): string; + /** + * @return array + */ + public function getParams(): array + { + return []; + } + public function setConnector(LogicalConnector $connector): void { $this->connector = $connector; diff --git a/src/Database/Concerns/Query/BuildsQuery.php b/src/Database/Concerns/Query/BuildsQuery.php index 2ec111bd..52491036 100644 --- a/src/Database/Concerns/Query/BuildsQuery.php +++ b/src/Database/Concerns/Query/BuildsQuery.php @@ -25,7 +25,7 @@ public function table(string $table): static return $this; } - public function from(Closure|string $table): static + public function from(Closure|Subquery|string $table): static { if ($table instanceof Closure) { $builder = new Subquery($this->driver); @@ -34,12 +34,11 @@ public function from(Closure|string $table): static $table($builder); - [$dml, $arguments] = $builder->toSql(); - - $this->table($dml); - - $this->addArguments($arguments); + $this->ast->table = $builder; + } elseif ($table instanceof Subquery) { + $table->setDriver($this->driver); + $this->ast->table = $table; } else { $this->table($table); } @@ -124,10 +123,10 @@ public function toSql(): array { $dialect = DialectFactory::fromDriver($this->driver); - return $dialect->compile($this->buildAst()); + return $dialect->compile($this->getAst()); } - protected function buildAst(): QueryAst + protected function getAst(): QueryAst { return $this->ast; } diff --git a/src/Database/Concerns/Query/HasWhereClause.php b/src/Database/Concerns/Query/HasWhereClause.php index ca7a6fc2..23f74509 100644 --- a/src/Database/Concerns/Query/HasWhereClause.php +++ b/src/Database/Concerns/Query/HasWhereClause.php @@ -238,8 +238,6 @@ public function whereBetween(string $column, array $values): static $this->pushWhereClause($clause); - $this->addArguments((array) $values); - return $this; } @@ -253,8 +251,6 @@ public function orWhereBetween(string $column, array $values): static $this->pushWhereClause($clause, LogicalConnector::OR); - $this->addArguments((array) $values); - return $this; } @@ -268,8 +264,6 @@ public function whereNotBetween(string $column, array $values): static $this->pushWhereClause($clause); - $this->addArguments((array) $values); - return $this; } @@ -283,8 +277,6 @@ public function orWhereNotBetween(string $column, array $values): static $this->pushWhereClause($clause, LogicalConnector::OR); - $this->addArguments((array) $values); - return $this; } diff --git a/src/Database/Concerns/Query/HasWhereDateClause.php b/src/Database/Concerns/Query/HasWhereDateClause.php index 315ad810..23332a05 100644 --- a/src/Database/Concerns/Query/HasWhereDateClause.php +++ b/src/Database/Concerns/Query/HasWhereDateClause.php @@ -269,6 +269,5 @@ protected function pushTimeClause( LogicalConnector $logicalConnector = LogicalConnector::AND ): void { $this->pushClause(new DateWhereClause($column, $operator, $function, $value), $logicalConnector); - $this->addArguments([$value]); } } diff --git a/src/Database/Constants/SQL.php b/src/Database/Constants/SqlMark.php similarity index 59% rename from src/Database/Constants/SQL.php rename to src/Database/Constants/SqlMark.php index fca2fffd..0ea09949 100644 --- a/src/Database/Constants/SQL.php +++ b/src/Database/Constants/SqlMark.php @@ -4,7 +4,7 @@ namespace Phenix\Database\Constants; -enum SQL: string +enum SqlMark: string { - case PLACEHOLDER = '?'; + case Placeholder = '{?}'; } diff --git a/src/Database/Contracts/ClauseCompiler.php b/src/Database/Contracts/SqlCompiler.php similarity index 57% rename from src/Database/Contracts/ClauseCompiler.php rename to src/Database/Contracts/SqlCompiler.php index 3989fce4..c036303c 100644 --- a/src/Database/Contracts/ClauseCompiler.php +++ b/src/Database/Contracts/SqlCompiler.php @@ -4,12 +4,12 @@ namespace Phenix\Database\Contracts; -use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Dialects\SqlData; use Phenix\Database\QueryAst; -interface ClauseCompiler +interface SqlCompiler { public function setAst(QueryAst $ast): static; - public function compile(): CompiledClause; + public function compile(): SqlData; } diff --git a/src/Database/Dialects/Compilers/DeleteCompiler.php b/src/Database/Dialects/Compilers/DeleteCompiler.php index 24354377..a5f766f3 100644 --- a/src/Database/Dialects/Compilers/DeleteCompiler.php +++ b/src/Database/Dialects/Compilers/DeleteCompiler.php @@ -4,27 +4,37 @@ namespace Phenix\Database\Dialects\Compilers; -use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Dialects\SqlData; +use Phenix\Database\Subquery; use Phenix\Util\Arr; -abstract class DeleteCompiler extends ClauseCompiler +abstract class DeleteCompiler extends SqlCompiler { - public function compile(): CompiledClause + public function compile(): SqlData { $parts = []; + $params = []; $parts[] = 'DELETE FROM'; - $parts[] = $this->wrap($this->ast->table); + + if ($this->ast->table instanceof Subquery) { + $table = $this->compileTable(); + $parts[] = $table->sql; + $params = [...$params, ...$table->params]; + } else { + $parts[] = $this->wrap($this->ast->table); + } if (! empty($this->ast->wheres)) { $whereCompiled = $this->whereCompiler->compile($this->ast->wheres); $parts[] = 'WHERE'; $parts[] = $whereCompiled->sql; + $params = [...$params, ...$whereCompiled->params]; } $sql = Arr::implodeDeeply($parts); - return new CompiledClause($sql, $this->ast->params); + return new SqlData($this->replacePlaceholders($sql), $params); } } diff --git a/src/Database/Dialects/Compilers/ExistsCompiler.php b/src/Database/Dialects/Compilers/ExistsCompiler.php index af4e199b..fefd6de8 100644 --- a/src/Database/Dialects/Compilers/ExistsCompiler.php +++ b/src/Database/Dialects/Compilers/ExistsCompiler.php @@ -4,14 +4,17 @@ namespace Phenix\Database\Dialects\Compilers; -use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Dialects\SqlData; use Phenix\Util\Arr; -abstract class ExistsCompiler extends ClauseCompiler +abstract class ExistsCompiler extends SqlCompiler { - public function compile(): CompiledClause + public function compile(): SqlData { $parts = []; + $params = []; + $table = $this->compileTable(); + $parts[] = 'SELECT'; $column = ! empty($this->ast->columns) ? $this->ast->columns[0] : 'EXISTS'; @@ -19,13 +22,15 @@ public function compile(): CompiledClause $subquery = []; $subquery[] = 'SELECT 1 FROM'; - $subquery[] = $this->wrapOf($this->ast->table); + $subquery[] = $table->sql; + $params = [...$params, ...$table->params]; if (! empty($this->ast->wheres)) { $whereCompiled = $this->whereCompiler->compile($this->ast->wheres); $subquery[] = 'WHERE'; $subquery[] = $whereCompiled->sql; + $params = [...$params, ...$whereCompiled->params]; } $parts[] = '(' . Arr::implodeDeeply($subquery) . ')'; @@ -34,6 +39,6 @@ public function compile(): CompiledClause $sql = Arr::implodeDeeply($parts); - return new CompiledClause($sql, $this->ast->params); + return new SqlData($this->replacePlaceholders($sql), $params); } } diff --git a/src/Database/Dialects/Compilers/HavingCompiler.php b/src/Database/Dialects/Compilers/HavingCompiler.php index 7c6829fb..9d0b69ae 100644 --- a/src/Database/Dialects/Compilers/HavingCompiler.php +++ b/src/Database/Dialects/Compilers/HavingCompiler.php @@ -4,7 +4,7 @@ namespace Phenix\Database\Dialects\Compilers; -use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Dialects\SqlData; use Phenix\Database\Having; class HavingCompiler @@ -14,17 +14,17 @@ public function __construct( ) { } - public function compile(Having $having): CompiledClause + public function compile(Having $having): SqlData { $compiled = $this->whereCompiler->compile($having->getClauses()); if ($compiled->sql === '') { - return new CompiledClause('', []); + return new SqlData(''); } - return new CompiledClause( + return new SqlData( "HAVING {$compiled->sql}", - $having->getArguments() + $compiled->params ); } } diff --git a/src/Database/Dialects/Compilers/InsertCompiler.php b/src/Database/Dialects/Compilers/InsertCompiler.php index 4d81f8f6..023b045a 100644 --- a/src/Database/Dialects/Compilers/InsertCompiler.php +++ b/src/Database/Dialects/Compilers/InsertCompiler.php @@ -4,20 +4,21 @@ namespace Phenix\Database\Dialects\Compilers; -use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Dialects\SqlData; use Phenix\Util\Arr; -abstract class InsertCompiler extends ClauseCompiler +abstract class InsertCompiler extends SqlCompiler { - public function compile(): CompiledClause + public function compile(): SqlData { $parts = []; - $params = $this->ast->params; + $table = $this->compileTable(); + $params = [...$table->params, ...$this->ast->params]; // INSERT [IGNORE] INTO $parts[] = $this->compileInsertClause(); - $parts[] = $this->wrapOf($this->ast->table); + $parts[] = $table->sql; // (column1, column2, ...) $parts[] = '(' . Arr::implodeDeeply($this->wrapList($this->ast->columns), ', ') . ')'; @@ -42,7 +43,7 @@ public function compile(): CompiledClause $sql = Arr::implodeDeeply($parts); - return new CompiledClause($sql, $params); + return new SqlData($this->replacePlaceholders($sql), $params); } protected function compileInsertClause(): string diff --git a/src/Database/Dialects/Compilers/JoinCompiler.php b/src/Database/Dialects/Compilers/JoinCompiler.php index 169704b3..ae300f65 100644 --- a/src/Database/Dialects/Compilers/JoinCompiler.php +++ b/src/Database/Dialects/Compilers/JoinCompiler.php @@ -9,7 +9,7 @@ use Phenix\Database\Clauses\DateWhereClause; use Phenix\Database\Clauses\WhereClause; use Phenix\Database\Constants\Driver; -use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Dialects\SqlData; use Phenix\Database\Join; use Phenix\Database\Wrapper; @@ -20,16 +20,19 @@ public function __construct( ) { } - public function compile(Join $join): CompiledClause + public function compile(Join $join): SqlData { - $clauses = array_map( - fn (WhereClause $clause): string => $this->compileClause($clause), - $join->getClauses() - ); + $clauses = []; + $params = []; + + foreach ($join->getClauses() as $clause) { + $clauses[] = $this->compileClause($clause); + $params = [...$params, ...$clause->getParams()]; + } - return new CompiledClause( + return new SqlData( "{$join->getType()->value} {$this->compileRelationship($join)} ON " . implode(' ', $clauses), - $join->getArguments() + $params ); } diff --git a/src/Database/Dialects/Compilers/SelectCompiler.php b/src/Database/Dialects/Compilers/SelectCompiler.php index 83e22a4f..47b14840 100644 --- a/src/Database/Dialects/Compilers/SelectCompiler.php +++ b/src/Database/Dialects/Compilers/SelectCompiler.php @@ -6,7 +6,7 @@ use Phenix\Database\Alias; use Phenix\Database\Constants\Operator; -use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Dialects\SqlData; use Phenix\Database\Exceptions\QueryErrorException; use Phenix\Database\Funct; use Phenix\Database\SelectCase; @@ -15,19 +15,22 @@ use function is_string; -abstract class SelectCompiler extends ClauseCompiler +abstract class SelectCompiler extends SqlCompiler { abstract protected function compileLock(): string; - public function compile(): CompiledClause + public function compile(): SqlData { $columns = empty($this->ast->columns) ? ['*'] : $this->ast->columns; + $columnsCompiled = $this->compileColumns($columns); + $tableCompiled = $this->compileTable(); + $params = [...$columnsCompiled->params, ...$tableCompiled->params]; $sql = [ 'SELECT', - $this->compileColumns($columns), + $columnsCompiled->sql, 'FROM', - $this->compileTable(), + $tableCompiled->sql, ]; if (! empty($this->ast->joins)) { @@ -35,7 +38,7 @@ public function compile(): CompiledClause if ($joins->sql !== '') { $sql[] = $joins->sql; - $this->ast->params = [...$joins->params, ...$this->ast->params]; + $params = [...$params, ...$joins->params]; } } @@ -45,6 +48,7 @@ public function compile(): CompiledClause if ($whereCompiled->sql !== '') { $sql[] = 'WHERE'; $sql[] = $whereCompiled->sql; + $params = [...$params, ...$whereCompiled->params]; } } @@ -53,8 +57,9 @@ public function compile(): CompiledClause $sql[] = $this->compileGroups($this->ast->groups); } - if ($this->ast->having !== null && $havingSql = $this->compileHaving()) { - $sql[] = $havingSql; + if ($this->ast->having !== null && $havingCompiled = $this->compileHaving()) { + $sql[] = $havingCompiled->sql; + $params = [...$params, ...$havingCompiled->params]; } if (! empty($this->ast->orders)) { @@ -74,33 +79,45 @@ public function compile(): CompiledClause $sql[] = $lockSql; } - return new CompiledClause( - Arr::implodeDeeply($sql), - $this->ast->params + $sql = Arr::implodeDeeply($sql); + + return new SqlData( + $this->replacePlaceholders($sql), + $params ); } /** * @param array $columns - * @return string */ - protected function compileColumns(array $columns): string + protected function compileColumns(array $columns): SqlData { - $compiled = Arr::map($columns, function (string|Alias|Funct|SelectCase|Subquery $value, int|string $key): string { - return match (true) { + $compiled = []; + $params = []; + + foreach ($columns as $key => $value) { + if ($value instanceof Subquery) { + $subquery = $this->compileSubquery($value, true); + + $compiled[] = $subquery->sql; + $params = [...$params, ...$subquery->params]; + + continue; + } + + $compiled[] = match (true) { is_string($key) => (string) Alias::of($key)->as($value)->setDriver($this->ast->driver), $value instanceof Alias => (string) $value->setDriver($this->ast->driver), $value instanceof Funct => (string) $value->setDriver($this->ast->driver), $value instanceof SelectCase => (string) $value->setDriver($this->ast->driver), - $value instanceof Subquery => $this->compileSubquery($value), default => $this->wrap((string) $value), }; - }); + } - return Arr::implodeDeeply($compiled, ', '); + return new SqlData(Arr::implodeDeeply($compiled, ', '), $params); } - protected function compileHaving(): string|null + protected function compileHaving(): SqlData|null { $having = $this->havingCompiler->compile($this->ast->having); @@ -108,9 +125,7 @@ protected function compileHaving(): string|null return null; } - $this->ast->params = [...$this->ast->params, ...$having->params]; - - return $having->sql; + return $having; } /** @@ -119,12 +134,13 @@ protected function compileHaving(): string|null */ protected function compileGroups(array $groups): string { - $compiled = Arr::map($groups, function (string|Funct $value): string { - return match (true) { + $compiled = Arr::map( + $groups, + fn (string|Funct $value): string => match (true) { $value instanceof Funct => (string) $value->setDriver($this->ast->driver), default => $this->wrap((string) $value), - }; - }); + } + ); return Arr::implodeDeeply($compiled, ', '); } @@ -144,48 +160,30 @@ protected function compileOrders(array $orders): string return Arr::implodeDeeply([Arr::implodeDeeply($compiled, ', '), $order]); } - private function compileJoins(): CompiledClause + protected function compileJoins(): SqlData { $sql = []; $params = []; foreach ($this->ast->joins as $join) { $compiled = $this->joinCompiler->compile($join); - $sql[] = $compiled->sql; $params = [...$params, ...$compiled->params]; } - return new CompiledClause(Arr::implodeDeeply($sql), $params); + return new SqlData(Arr::implodeDeeply($sql), $params); } - /** - * @param Subquery $subquery - * @return string - */ - private function compileSubquery(Subquery $subquery): string + protected function compileSubquery(Subquery $subquery, bool $requiresLimit = false): SqlData { $subquery->setDriver($this->ast->driver); [$dml, $arguments] = $subquery->toSql(); - if (! str_contains($dml, 'LIMIT 1')) { + if ($requiresLimit && ! str_contains($dml, 'LIMIT 1')) { throw new QueryErrorException('The subquery must be limited to one record'); } - $this->ast->params = [...$this->ast->params, ...$arguments]; - - return $dml; - } - - private function compileTable(): string - { - $table = trim($this->ast->table); - - if ($table !== '' && str_starts_with($table, '(')) { - return $this->ast->table; - } - - return $this->wrapOf($this->ast->table); + return new SqlData($dml, $arguments); } } diff --git a/src/Database/Dialects/Compilers/ClauseCompiler.php b/src/Database/Dialects/Compilers/SqlCompiler.php similarity index 52% rename from src/Database/Dialects/Compilers/ClauseCompiler.php rename to src/Database/Dialects/Compilers/SqlCompiler.php index 10a81dde..25dc6cc5 100644 --- a/src/Database/Dialects/Compilers/ClauseCompiler.php +++ b/src/Database/Dialects/Compilers/SqlCompiler.php @@ -4,11 +4,14 @@ namespace Phenix\Database\Dialects\Compilers; -use Phenix\Database\Contracts\ClauseCompiler as ClauseCompilerContract; +use Phenix\Database\Constants\SqlMark; +use Phenix\Database\Contracts\SqlCompiler as SqlCompilerContract; +use Phenix\Database\Dialects\SqlData; use Phenix\Database\QueryAst; +use Phenix\Database\Subquery; use Phenix\Database\Wrapper; -abstract class ClauseCompiler implements ClauseCompilerContract +abstract class SqlCompiler implements SqlCompilerContract { protected QueryAst $ast; @@ -25,6 +28,11 @@ public function setAst(QueryAst $ast): static return $this; } + protected function replacePlaceholders(string $sql): string + { + return str_replace(SqlMark::Placeholder->value, '?', $sql); + } + protected function wrap(string $value): string { return Wrapper::column($this->ast->driver, $value); @@ -35,6 +43,19 @@ protected function wrapOf(string $value): string return (string) Wrapper::of($this->ast->driver, $value); } + protected function compileTable(): SqlData + { + if ($this->ast->table instanceof Subquery) { + $this->ast->table->setDriver($this->ast->driver); + + [$sql, $params] = $this->ast->table->toSql(); + + return new SqlData($sql, $params); + } + + return new SqlData($this->wrapOf($this->ast->table)); + } + protected function wrapList(array $values): array { return Wrapper::columnList($this->ast->driver, $values); diff --git a/src/Database/Dialects/Compilers/UpdateCompiler.php b/src/Database/Dialects/Compilers/UpdateCompiler.php index 99b14a67..178496c7 100644 --- a/src/Database/Dialects/Compilers/UpdateCompiler.php +++ b/src/Database/Dialects/Compilers/UpdateCompiler.php @@ -4,20 +4,23 @@ namespace Phenix\Database\Dialects\Compilers; -use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Constants\SqlMark; +use Phenix\Database\Dialects\SqlData; use Phenix\Util\Arr; use function count; -abstract class UpdateCompiler extends ClauseCompiler +abstract class UpdateCompiler extends SqlCompiler { - public function compile(): CompiledClause + public function compile(): SqlData { $parts = []; $params = []; + $table = $this->compileTable(); $parts[] = 'UPDATE'; - $parts[] = $this->wrapOf($this->ast->table); + $parts[] = $table->sql; + $params = [...$params, ...$table->params]; // SET col1 = ?, col2 = ? // Extract params from values (these are actual values, not placeholders) @@ -37,7 +40,7 @@ public function compile(): CompiledClause $parts[] = 'WHERE'; $parts[] = $whereCompiled->sql; - $params = array_merge($params, $this->ast->params); + $params = [...$params, ...$whereCompiled->params]; } if (! empty($this->ast->returning)) { @@ -47,12 +50,13 @@ public function compile(): CompiledClause $sql = Arr::implodeDeeply($parts); - return new CompiledClause($sql, $params); + return new SqlData($this->replacePlaceholders($sql), $params); } - /** - * Compile the SET clause for a column assignment - * This is dialect-specific for placeholder syntax - */ - abstract protected function compileSetClause(string $column, int $paramIndex): string; + protected function compileSetClause(string $column): string + { + $column = $this->wrap($column); + + return "{$column} = " . SqlMark::Placeholder->value; + } } diff --git a/src/Database/Dialects/Compilers/WhereCompiler.php b/src/Database/Dialects/Compilers/WhereCompiler.php index 026b0cb6..6efe4797 100644 --- a/src/Database/Dialects/Compilers/WhereCompiler.php +++ b/src/Database/Dialects/Compilers/WhereCompiler.php @@ -13,17 +13,18 @@ use Phenix\Database\Clauses\RowWhereClause; use Phenix\Database\Clauses\SubqueryWhereClause; use Phenix\Database\Clauses\WhereClause; -use Phenix\Database\Dialects\CompiledClause; +use Phenix\Database\Dialects\SqlData; abstract class WhereCompiler { /** * @param array $wheres - * @return CompiledClause + * @return SqlData */ - public function compile(array $wheres): CompiledClause + public function compile(array $wheres): SqlData { $sql = []; + $params = []; foreach ($wheres as $index => $where) { // Add logical connector if not the first clause @@ -32,9 +33,10 @@ public function compile(array $wheres): CompiledClause } $sql[] = $this->compileClause($where); + $params = [...$params, ...$where->getParams()]; } - return new CompiledClause(implode(' ', $sql), []); + return new SqlData(implode(' ', $sql), $params); } protected function compileClause(WhereClause $clause): string diff --git a/src/Database/Dialects/Mysql/Compilers/Update.php b/src/Database/Dialects/Mysql/Compilers/Update.php index 54c6c2e3..cc5cf54b 100644 --- a/src/Database/Dialects/Mysql/Compilers/Update.php +++ b/src/Database/Dialects/Mysql/Compilers/Update.php @@ -12,11 +12,4 @@ public function __construct() { $this->whereCompiler = new Where(); } - - protected function compileSetClause(string $column, int $paramIndex): string - { - $column = $this->wrap($column); - - return "{$column} = ?"; - } } diff --git a/src/Database/Dialects/Mysql/Compilers/Where.php b/src/Database/Dialects/Mysql/Compilers/Where.php index 96947dab..9bebe73c 100644 --- a/src/Database/Dialects/Mysql/Compilers/Where.php +++ b/src/Database/Dialects/Mysql/Compilers/Where.php @@ -14,7 +14,7 @@ use Phenix\Database\Clauses\SubqueryWhereClause; use Phenix\Database\Clauses\WhereClause; use Phenix\Database\Constants\Driver; -use Phenix\Database\Constants\SQL; +use Phenix\Database\Constants\SqlMark; use Phenix\Database\Dialects\Compilers\WhereCompiler; use Phenix\Database\Wrapper; @@ -25,12 +25,12 @@ protected function compileBasicClause(BasicWhereClause $clause): string $column = Wrapper::column(Driver::MYSQL, $clause->getColumn()); if ($clause->isInOperator()) { - $placeholders = str_repeat(SQL::PLACEHOLDER->value . ', ', $clause->getValueCount() - 1) . SQL::PLACEHOLDER->value; + $placeholders = str_repeat(SqlMark::Placeholder->value . ', ', $clause->getValueCount() - 1) . SqlMark::Placeholder->value; return "{$column} {$clause->getOperator()->value} ({$placeholders})"; } - return "{$column} {$clause->getOperator()->value} " . SQL::PLACEHOLDER->value; + return "{$column} {$clause->getOperator()->value} " . SqlMark::Placeholder->value; } protected function compileDateClause(DateWhereClause $clause): string diff --git a/src/Database/Dialects/Postgres/Compilers/Delete.php b/src/Database/Dialects/Postgres/Compilers/Delete.php index 3ae43e55..61af21a1 100644 --- a/src/Database/Dialects/Postgres/Compilers/Delete.php +++ b/src/Database/Dialects/Postgres/Compilers/Delete.php @@ -4,7 +4,6 @@ namespace Phenix\Database\Dialects\Postgres\Compilers; -use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; use Phenix\Database\Dialects\Sqlite\Compilers\Delete as SQLiteDelete; @@ -16,12 +15,4 @@ public function __construct() { $this->whereCompiler = new Where(); } - - public function compile(): CompiledClause - { - $clause = parent::compile(); - $sql = $this->convertPlaceholders($clause->sql); - - return new CompiledClause($sql, $clause->params); - } } diff --git a/src/Database/Dialects/Postgres/Compilers/Exists.php b/src/Database/Dialects/Postgres/Compilers/Exists.php index 36f79413..4da6ba82 100644 --- a/src/Database/Dialects/Postgres/Compilers/Exists.php +++ b/src/Database/Dialects/Postgres/Compilers/Exists.php @@ -4,7 +4,6 @@ namespace Phenix\Database\Dialects\Postgres\Compilers; -use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\ExistsCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; @@ -16,14 +15,4 @@ public function __construct() { $this->whereCompiler = new Where(); } - - public function compile(): CompiledClause - { - $result = parent::compile(); - - return new CompiledClause( - $this->convertPlaceholders($result->sql), - $result->params - ); - } } diff --git a/src/Database/Dialects/Postgres/Compilers/Insert.php b/src/Database/Dialects/Postgres/Compilers/Insert.php index 22a09871..674d116c 100644 --- a/src/Database/Dialects/Postgres/Compilers/Insert.php +++ b/src/Database/Dialects/Postgres/Compilers/Insert.php @@ -4,9 +4,9 @@ namespace Phenix\Database\Dialects\Postgres\Compilers; -use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\InsertCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; +use Phenix\Database\Dialects\SqlData; use Phenix\Util\Arr; use function sprintf; @@ -42,12 +42,14 @@ protected function compileUpsert(): string ); } - public function compile(): CompiledClause + public function compile(): SqlData { if ($this->ast->ignore && empty($this->ast->uniqueColumns)) { $parts = []; + $table = $this->compileTable(); + $parts[] = 'INSERT INTO'; - $parts[] = $this->wrapOf($this->ast->table); + $parts[] = $table->sql; $parts[] = '(' . Arr::implodeDeeply($this->wrapList($this->ast->columns), ', ') . ')'; if ($this->ast->rawStatement !== null) { @@ -70,9 +72,9 @@ public function compile(): CompiledClause } $sql = Arr::implodeDeeply($parts); - $sql = $this->convertPlaceholders($sql); + $sql = $this->replacePlaceholders($sql); - return new CompiledClause($sql, $this->ast->params); + return new SqlData($sql, [...$table->params, ...$this->ast->params]); } $result = parent::compile(); @@ -83,8 +85,8 @@ public function compile(): CompiledClause $parts[] = Arr::implodeDeeply($this->wrapList($this->ast->returning), ', '); } - return new CompiledClause( - $this->convertPlaceholders(Arr::implodeDeeply($parts)), + return new SqlData( + $this->replacePlaceholders(Arr::implodeDeeply($parts)), $result->params ); } diff --git a/src/Database/Dialects/Postgres/Compilers/Select.php b/src/Database/Dialects/Postgres/Compilers/Select.php index c7882e22..0caaa0c9 100644 --- a/src/Database/Dialects/Postgres/Compilers/Select.php +++ b/src/Database/Dialects/Postgres/Compilers/Select.php @@ -6,11 +6,11 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\Lock; -use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\HavingCompiler; use Phenix\Database\Dialects\Compilers\JoinCompiler; use Phenix\Database\Dialects\Compilers\SelectCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; +use Phenix\Database\Dialects\SqlData; class Select extends SelectCompiler { @@ -23,12 +23,12 @@ public function __construct() $this->havingCompiler = new HavingCompiler($this->whereCompiler); } - public function compile(): CompiledClause + protected function compileTable(): SqlData { - $result = parent::compile(); + $result = parent::compileTable(); - return new CompiledClause( - $this->normalizePlaceholders($result->sql), + return new SqlData( + $this->resetPlaceholders($result->sql), $result->params ); } diff --git a/src/Database/Dialects/Postgres/Compilers/Update.php b/src/Database/Dialects/Postgres/Compilers/Update.php index a1e0e820..bf194bac 100644 --- a/src/Database/Dialects/Postgres/Compilers/Update.php +++ b/src/Database/Dialects/Postgres/Compilers/Update.php @@ -4,12 +4,9 @@ namespace Phenix\Database\Dialects\Postgres\Compilers; -use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\UpdateCompiler; use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; -use function count; - class Update extends UpdateCompiler { use HasPlaceholders; @@ -18,23 +15,4 @@ public function __construct() { $this->whereCompiler = new Where(); } - - protected function compileSetClause(string $column, int $paramIndex): string - { - $column = $this->wrap($column); - - return "{$column} = $" . $paramIndex; - } - - public function compile(): CompiledClause - { - $result = parent::compile(); - - $paramsCount = count($this->ast->values); - - return new CompiledClause( - $this->convertPlaceholders($result->sql, $paramsCount), - $result->params - ); - } } diff --git a/src/Database/Dialects/Postgres/Compilers/Where.php b/src/Database/Dialects/Postgres/Compilers/Where.php index 8a4e75a7..378ef3f7 100644 --- a/src/Database/Dialects/Postgres/Compilers/Where.php +++ b/src/Database/Dialects/Postgres/Compilers/Where.php @@ -14,24 +14,27 @@ use Phenix\Database\Clauses\SubqueryWhereClause; use Phenix\Database\Clauses\WhereClause; use Phenix\Database\Constants\Driver; -use Phenix\Database\Constants\SQL; +use Phenix\Database\Constants\SqlMark; use Phenix\Database\Dialects\Compilers\WhereCompiler; +use Phenix\Database\Dialects\Postgres\Concerns\HasPlaceholders; use Phenix\Database\Wrapper; class Where extends WhereCompiler { + use HasPlaceholders; + protected function compileBasicClause(BasicWhereClause $clause): string { $column = Wrapper::column(Driver::POSTGRESQL, $clause->getColumn()); $operator = $clause->getOperator(); if ($clause->isInOperator()) { - $placeholders = str_repeat(SQL::PLACEHOLDER->value . ', ', $clause->getValueCount() - 1) . SQL::PLACEHOLDER->value; + $placeholders = str_repeat(SqlMark::Placeholder->value . ', ', $clause->getValueCount() - 1) . SqlMark::Placeholder->value; return "{$column} {$operator->value} ({$placeholders})"; } - return "{$column} {$operator->value} " . SQL::PLACEHOLDER->value; + return "{$column} {$operator->value} " . SqlMark::Placeholder->value; } protected function compileDateClause(DateWhereClause $clause): string @@ -66,13 +69,11 @@ protected function compileSubqueryClause(SubqueryWhereClause $clause): string } $parts[] = $clause->getOperator()->value; + $sql = $this->resetPlaceholders($clause->getSql()); - if ($clause->getSubqueryOperator() !== null) { - // For ANY/ALL/SOME, no space between operator and subquery - $parts[] = $clause->getSubqueryOperator()->value . '(' . $clause->getSql() . ')'; - } else { - $parts[] = '(' . $clause->getSql() . ')'; - } + $parts[] = $clause->getSubqueryOperator() !== null + ? "{$clause->getSubqueryOperator()->value}({$sql})" // For ANY/ALL/SOME, no space between operator and subquery + : "({$sql})"; return implode(' ', $parts); } diff --git a/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php b/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php index cc847d62..a2b8c7ed 100644 --- a/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php +++ b/src/Database/Dialects/Postgres/Concerns/HasPlaceholders.php @@ -4,31 +4,29 @@ namespace Phenix\Database\Dialects\Postgres\Concerns; +use Phenix\Database\Constants\SqlMark; + trait HasPlaceholders { - protected function convertPlaceholders(string $sql, int $startIndex = 0): string + protected function replacePlaceholders(string $sql, int $startIndex = 0): string { $index = $startIndex + 1; return preg_replace_callback( - '/\?/', + '/\{\?\}/', function () use (&$index): string { - return '$' . ($index++); + $placeholder = "\${$index}"; + + $index++; + + return $placeholder; }, $sql ); } - protected function normalizePlaceholders(string $sql, int $startIndex = 0): string + protected function resetPlaceholders(string $sql): string { - $index = $startIndex + 1; - - return preg_replace_callback( - '/\?|\$\d+/', - function () use (&$index): string { - return '$' . ($index++); - }, - $sql - ); + return preg_replace('/\$\d+/', SqlMark::Placeholder->value, $sql); } } diff --git a/src/Database/Dialects/CompiledClause.php b/src/Database/Dialects/SqlData.php similarity index 91% rename from src/Database/Dialects/CompiledClause.php rename to src/Database/Dialects/SqlData.php index 89b0b556..5459f4bc 100644 --- a/src/Database/Dialects/CompiledClause.php +++ b/src/Database/Dialects/SqlData.php @@ -4,7 +4,7 @@ namespace Phenix\Database\Dialects; -readonly class CompiledClause +readonly class SqlData { /** * @param string $sql The compiled SQL string diff --git a/src/Database/Dialects/Sqlite/Compilers/Delete.php b/src/Database/Dialects/Sqlite/Compilers/Delete.php index 21591857..a7484370 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Delete.php +++ b/src/Database/Dialects/Sqlite/Compilers/Delete.php @@ -4,8 +4,8 @@ namespace Phenix\Database\Dialects\Sqlite\Compilers; -use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\DeleteCompiler; +use Phenix\Database\Dialects\SqlData; use Phenix\Util\Arr; class Delete extends DeleteCompiler @@ -15,18 +15,22 @@ public function __construct() $this->whereCompiler = new Where(); } - public function compile(): CompiledClause + public function compile(): SqlData { $parts = []; + $params = []; + $table = $this->compileTable(); $parts[] = 'DELETE FROM'; - $parts[] = $this->wrapOf($this->ast->table); + $parts[] = $table->sql; + $params = [...$params, ...$table->params]; if (! empty($this->ast->wheres)) { $whereCompiled = $this->whereCompiler->compile($this->ast->wheres); $parts[] = 'WHERE'; $parts[] = $whereCompiled->sql; + $params = [...$params, ...$whereCompiled->params]; } if (! empty($this->ast->returning)) { @@ -36,6 +40,6 @@ public function compile(): CompiledClause $sql = Arr::implodeDeeply($parts); - return new CompiledClause($sql, $this->ast->params); + return new SqlData($this->replacePlaceholders($sql), $params); } } diff --git a/src/Database/Dialects/Sqlite/Compilers/Insert.php b/src/Database/Dialects/Sqlite/Compilers/Insert.php index 22052e2c..ed8a9b29 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Insert.php +++ b/src/Database/Dialects/Sqlite/Compilers/Insert.php @@ -4,8 +4,8 @@ namespace Phenix\Database\Dialects\Sqlite\Compilers; -use Phenix\Database\Dialects\CompiledClause; use Phenix\Database\Dialects\Compilers\InsertCompiler; +use Phenix\Database\Dialects\SqlData; use Phenix\Util\Arr; /** @@ -42,18 +42,20 @@ protected function compileUpsert(): string ); } - public function compile(): CompiledClause + public function compile(): SqlData { $result = parent::compile(); - $parts = [$result->sql]; + $sql = [$result->sql]; if (! empty($this->ast->returning)) { - $parts[] = 'RETURNING'; - $parts[] = Arr::implodeDeeply($this->wrapList($this->ast->returning), ', '); + $sql[] = 'RETURNING'; + $sql[] = Arr::implodeDeeply($this->wrapList($this->ast->returning), ', '); } - return new CompiledClause( - Arr::implodeDeeply($parts), + $sql = Arr::implodeDeeply($sql); + + return new SqlData( + $this->replacePlaceholders($sql), $result->params ); } diff --git a/src/Database/Dialects/Sqlite/Compilers/Update.php b/src/Database/Dialects/Sqlite/Compilers/Update.php index e47d82f1..abc4ae07 100644 --- a/src/Database/Dialects/Sqlite/Compilers/Update.php +++ b/src/Database/Dialects/Sqlite/Compilers/Update.php @@ -12,11 +12,4 @@ public function __construct() { $this->whereCompiler = new Where(); } - - protected function compileSetClause(string $column, int $paramIndex): string - { - $column = $this->wrap($column); - - return "{$column} = ?"; - } } diff --git a/src/Database/Having.php b/src/Database/Having.php index 293fa8e2..843a36ea 100644 --- a/src/Database/Having.php +++ b/src/Database/Having.php @@ -27,6 +27,6 @@ public function getClauses(): array */ public function getArguments(): array { - return $this->arguments; + return $this->getClauseArguments(); } } diff --git a/src/Database/Join.php b/src/Database/Join.php index 6018dd90..4b98d7b2 100644 --- a/src/Database/Join.php +++ b/src/Database/Join.php @@ -71,6 +71,6 @@ public function getClauses(): array */ public function getArguments(): array { - return $this->arguments; + return $this->getClauseArguments(); } } diff --git a/src/Database/QueryAst.php b/src/Database/QueryAst.php index c9a3e5ef..cf1fb5de 100644 --- a/src/Database/QueryAst.php +++ b/src/Database/QueryAst.php @@ -15,7 +15,7 @@ class QueryAst public Action $action; - public string $table; + public string|Subquery $table; /** * @var array diff --git a/src/Database/QueryBase.php b/src/Database/QueryBase.php index 52963fef..e58f9a6b 100644 --- a/src/Database/QueryBase.php +++ b/src/Database/QueryBase.php @@ -13,7 +13,7 @@ use Phenix\Database\Constants\Driver; use Phenix\Database\Constants\LogicalConnector; use Phenix\Database\Constants\Operator; -use Phenix\Database\Constants\SQL; +use Phenix\Database\Constants\SqlMark; use Phenix\Database\Contracts\Builder; use Phenix\Database\Contracts\QueryBuilder; @@ -74,7 +74,13 @@ protected function getClauses(): array */ protected function getArguments(): array { - return $this->ast->params; + $arguments = $this->ast->params; + + foreach ($this->ast->wheres as $where) { + $arguments = [...$arguments, ...$where->getParams()]; + } + + return $arguments; } protected function hasWhereClauses(): bool @@ -220,6 +226,6 @@ protected function prepareDataToInsert(array $data): void $this->addArguments(array_values($data)); - $this->ast->values[] = array_fill(0, count($data), SQL::PLACEHOLDER->value); + $this->ast->values[] = array_fill(0, count($data), SqlMark::Placeholder->value); } } diff --git a/src/Database/Wrapper.php b/src/Database/Wrapper.php index 923e5f5e..755f4387 100644 --- a/src/Database/Wrapper.php +++ b/src/Database/Wrapper.php @@ -5,7 +5,7 @@ namespace Phenix\Database; use Phenix\Database\Constants\Driver; -use Phenix\Database\Constants\SQL; +use Phenix\Database\Constants\SqlMark; use Phenix\Database\Contracts\RawValue; class Wrapper implements RawValue @@ -19,7 +19,7 @@ private function __construct( public function __toString(): string { - if (empty($this->value) || $this->value === '*' || $this->value === SQL::PLACEHOLDER->value) { + if (empty($this->value) || $this->value === '*' || $this->value === SqlMark::Placeholder->value) { return $this->value; } diff --git a/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php b/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php index 297a4cd7..b1629c80 100644 --- a/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php +++ b/tests/Unit/Database/QueryGenerator/InsertIntoStatementTest.php @@ -140,7 +140,7 @@ $query = new class () extends QueryGenerator { public function params(): array { - return $this->buildAst()->params; + return $this->getAst()->params; } }; diff --git a/tests/Unit/Database/QueryGenerator/JoinClausesTest.php b/tests/Unit/Database/QueryGenerator/JoinClausesTest.php index 4a3a024e..d4e14c87 100644 --- a/tests/Unit/Database/QueryGenerator/JoinClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/JoinClausesTest.php @@ -5,6 +5,7 @@ use Phenix\Database\Constants\JoinType; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; +use Phenix\Database\Subquery; it('generates query for all join types', function (string $method, string $joinType) { $query = new QueryGenerator(); @@ -200,3 +201,30 @@ expect($dml)->toBe($expected); expect($params)->toBe(['active', 'latam']); }); + +it('keeps params ordered when from subquery and join both use placeholders', function (): void { + $query = new QueryGenerator(); + + $sql = $query->select(['products.id']) + ->from(function (Subquery $subquery): void { + $subquery->from('products') + ->whereEqual('tenant_id', 7); + }) + ->leftJoin('categories', function (Join $join): void { + $join->onEqual('products.category_id', 'categories.id') + ->whereEqual('categories.status', 'active'); + }) + ->whereEqual('products.status', 'published') + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT `products`.`id` " + . "FROM (SELECT * FROM `products` WHERE `tenant_id` = ?) " + . "LEFT JOIN `categories` " + . "ON `products`.`category_id` = `categories`.`id` AND `categories`.`status` = ? " + . "WHERE `products`.`status` = ?"; + + expect($dml)->toBe($expected); + expect($params)->toBe([7, 'active', 'published']); +}); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php b/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php index f0915d77..26520cf5 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/JoinClausesTest.php @@ -6,6 +6,7 @@ use Phenix\Database\Constants\JoinType; use Phenix\Database\Join; use Phenix\Database\QueryGenerator; +use Phenix\Database\Subquery; it('generates query for all join types', function (string $method, string $joinType) { $query = new QueryGenerator(Driver::POSTGRESQL); @@ -250,3 +251,30 @@ expect($dml)->toBe($expected); expect($params)->toBe(['active', 'published']); }); + +it('keeps params ordered when from subquery and join both use postgresql placeholders', function (): void { + $query = new QueryGenerator(Driver::POSTGRESQL); + + $sql = $query->select(['products.id']) + ->from(function (Subquery $subquery): void { + $subquery->from('products') + ->whereEqual('tenant_id', 7); + }) + ->leftJoin('categories', function (Join $join): void { + $join->onEqual('products.category_id', 'categories.id') + ->whereEqual('categories.status', 'active'); + }) + ->whereEqual('products.status', 'published') + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT \"products\".\"id\" " + . "FROM (SELECT * FROM \"products\" WHERE \"tenant_id\" = $1) " + . "LEFT JOIN \"categories\" " + . "ON \"products\".\"category_id\" = \"categories\".\"id\" AND \"categories\".\"status\" = $2 " + . "WHERE \"products\".\"status\" = $3"; + + expect($dml)->toBe($expected); + expect($params)->toBe([7, 'active', 'published']); +}); diff --git a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php index 276c9587..d2cb52f2 100644 --- a/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/Postgres/SelectColumnsTest.php @@ -12,6 +12,7 @@ use function Phenix\Database\alias_of; use function Phenix\Database\avg; use function Phenix\Database\subquery; +use function Phenix\Database\when_equal; use function Phenix\Database\when_gte; use function Phenix\Database\when_null; @@ -263,6 +264,26 @@ ['when_false', ['is_verified', 'inactive'], 'active', Operator::IS_FALSE->value], ]); +it('does not rewrite placeholders inside select-case string literals', function (): void { + $query = new QueryGenerator(Driver::POSTGRESQL); + + $case = when_equal('status', 'draft', 'needs?') + ->defaultResult("it's ok?") + ->as('label'); + + $sql = $query->select([$case]) + ->from('tasks') + ->get(); + + [$dml, $params] = $sql; + + $expected = "SELECT (CASE WHEN \"status\" = 'draft' " + . "THEN 'needs?' ELSE 'it''s ok?' END) AS \"label\" FROM \"tasks\""; + + expect($dml)->toBe($expected); + expect($params)->toBeEmpty(); +}); + it('generates query with select-cases with multiple conditions and string values', function () { $date = date('Y-m-d H:i:s'); diff --git a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php index b78a54ac..a42c9ea4 100644 --- a/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php +++ b/tests/Unit/Database/QueryGenerator/SelectColumnsTest.php @@ -47,7 +47,7 @@ $query = new class () extends QueryGenerator { public function ast(): QueryAst { - return $this->buildAst(); + return $this->getAst(); } }; @@ -62,7 +62,7 @@ public function ast(): QueryAst expect($ast->driver)->toBe(Driver::POSTGRESQL); expect($ast->table)->toBe('users'); expect($ast->columns)->toBe(['id']); - expect($ast->params)->toBe([1]); + expect($ast->params)->toBeEmpty(); expect($dml)->toBe('SELECT "id" FROM "users" WHERE "id" = $1'); expect($params)->toBe([1]); }); @@ -82,11 +82,11 @@ public function ast(): QueryAst expect($second)->toBe(['SELECT * FROM `posts` WHERE `slug` = ?', ['hello']]); }); -it('stores where and subquery params directly in query ast', function (): void { +it('keeps where and from subquery params on clauses until compile', function (): void { $query = new class () extends QueryGenerator { public function ast(): QueryAst { - return $this->buildAst(); + return $this->getAst(); } }; @@ -106,9 +106,10 @@ public function ast(): QueryAst . "WHERE `status` = ? AND `age` BETWEEN ? AND ? AND DATE(`created_at`) = ?"; expect($ast->wheres)->toHaveCount(3); - expect($ast->params)->toBe(['2026-01-15', 'active', 18, 65, '2026-01-30']); + expect($ast->table)->toBeInstanceOf(Subquery::class); + expect($ast->params)->toBeEmpty(); expect($dml)->toBe($expected); - expect($params)->toBe($ast->params); + expect($params)->toBe(['2026-01-15', 'active', 18, 65, '2026-01-30']); }); it('generates a query using sql functions', function (string $function, string $column, string $rawFunction) { From 7aef5d8bfc3c47cf7a2c016fcc30d8e99602754d Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 12 May 2026 22:25:19 +0000 Subject: [PATCH 29/32] fix: update SqlMark constant usage to match casing in renderValue method --- src/Database/Clauses/BasicWhereClause.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Clauses/BasicWhereClause.php b/src/Database/Clauses/BasicWhereClause.php index 4febcbe6..2df1e008 100644 --- a/src/Database/Clauses/BasicWhereClause.php +++ b/src/Database/Clauses/BasicWhereClause.php @@ -55,7 +55,7 @@ public function renderValue(): string if ($this->usePlaceholder) { // In WHERE context with parameterized queries, use placeholder if (is_array($this->value)) { - return '(' . implode(', ', array_fill(0, count($this->value), SqlMark::PLACEHOLDER->value)) . ')'; + return '(' . implode(', ', array_fill(0, count($this->value), SqlMark::Placeholder->value)) . ')'; } return SqlMark::Placeholder->value; From e06c2ea7d416cd50c7c79ecb9c36fde88f6e6df9 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 12 May 2026 22:40:02 +0000 Subject: [PATCH 30/32] fix: remove unnecessary parameter from compileSetClause method call --- src/Database/Dialects/Compilers/UpdateCompiler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Dialects/Compilers/UpdateCompiler.php b/src/Database/Dialects/Compilers/UpdateCompiler.php index 178496c7..62175b66 100644 --- a/src/Database/Dialects/Compilers/UpdateCompiler.php +++ b/src/Database/Dialects/Compilers/UpdateCompiler.php @@ -28,7 +28,7 @@ public function compile(): SqlData foreach ($this->ast->values as $column => $value) { $params[] = $value; - $columns[] = $this->compileSetClause($column, count($params)); + $columns[] = $this->compileSetClause($column); } $parts[] = 'SET'; From 04c07d60ba470267336ad9bbb9a013eddb2d2a8b Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 12 May 2026 22:42:09 +0000 Subject: [PATCH 31/32] style: php cs --- src/Database/Dialects/Compilers/UpdateCompiler.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Database/Dialects/Compilers/UpdateCompiler.php b/src/Database/Dialects/Compilers/UpdateCompiler.php index 62175b66..9e357329 100644 --- a/src/Database/Dialects/Compilers/UpdateCompiler.php +++ b/src/Database/Dialects/Compilers/UpdateCompiler.php @@ -8,8 +8,6 @@ use Phenix\Database\Dialects\SqlData; use Phenix\Util\Arr; -use function count; - abstract class UpdateCompiler extends SqlCompiler { public function compile(): SqlData From 0d70e44ec457a33dba66ea9de66b7fc691726e2c Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 12 May 2026 23:20:17 +0000 Subject: [PATCH 32/32] chore: ignore files [skip ci] --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 837c7528..3c2bc1f6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ Thumbs.db build .env knowledge +.agents +AGENTS.md +.devcontainer