From ad4aac815b915430d76ab83e7b226560c08bb453 Mon Sep 17 00:00:00 2001 From: Headgent Date: Mon, 10 Aug 2026 14:28:39 +0200 Subject: [PATCH] feat(dbquery): dialect-aware auto-quoting of simple identifiers in condition and field positions (#8) Simple identifiers (ident / alias.ident) in WHERE/HAVING/ORDER BY/ GROUP BY/SELECT lists and UPDATE/DELETE conditions are now quoted per dialect (backtick for MySQL/MariaDB/SQLite, double quotes for Postgres), matching the existing INSERT/SET quoting. Raw expressions, *, pre-quoted strings, Expression::raw() and niladic SQL literals (NULL, TRUE, FALSE, DEFAULT, CURRENT_* et al.) stay untouched. Fixes generated Postgres queries against camelCase columns (SQLSTATE 42703; found by the Jardis Builder concurrency probe). 2268 tests green incl. camelCase matrix on three drivers and Postgres INSERT->WHERE roundtrip with quoted DDL. --- CHANGELOG.md | 58 +++ README.md | 45 ++- src/Command/Delete/DeleteSqlBuilder.php | 38 +- src/Command/Update/UpdateSqlBuilder.php | 38 +- src/DbQuery.php | 4 +- src/Query/Builder/Clause/ConditionBuilder.php | 6 + src/Query/Builder/Clause/GroupByBuilder.php | 19 +- src/Query/Builder/Clause/OrderByBuilder.php | 26 +- src/Query/Builder/Clause/SelectBuilder.php | 5 +- src/Query/Builder/Method/ResolveField.php | 17 +- .../Formatter/FieldListIdentifierQuoter.php | 138 +++++++ .../Formatter/IdentifierMarkerReplacer.php | 55 +++ .../Formatter/SimpleIdentifierQuoter.php | 78 ++++ src/Query/SqlBuilder.php | 56 ++- .../Mysql/CamelCaseIdentifierMySqlTest.php | 102 +++++ .../CamelCaseIdentifierPostgresTest.php | 137 +++++++ .../Sqlite/CamelCaseIdentifierSqliteTest.php | 103 +++++ .../Unit/Command/Delete/DbDeleteMySqlTest.php | 50 ++- .../Command/Delete/DbDeletePostgresTest.php | 42 +- .../Command/Delete/DbDeleteSqliteTest.php | 40 +- .../Unit/Command/Insert/DbInsertMySqlTest.php | 12 +- .../Command/Insert/DbInsertPostgresTest.php | 12 +- .../Command/Insert/DbInsertSqliteTest.php | 12 +- .../Unit/Command/Update/DbUpdateMySqlTest.php | 82 ++-- .../Command/Update/DbUpdatePostgresTest.php | 64 +-- .../Command/Update/DbUpdateSqliteTest.php | 64 +-- .../Unit/Query/IdentifierAutoQuotingTest.php | 380 ++++++++++++++++++ .../Query/Mysql/DbQueryMySqlBasicTest.php | 14 +- .../Unit/Query/Mysql/DbQueryMySqlCteTest.php | 72 ++-- .../Query/Mysql/DbQueryMySqlExistsTest.php | 58 +-- .../Mysql/DbQueryMySqlGroupOrderLimitTest.php | 42 +- .../Unit/Query/Mysql/DbQueryMySqlJoinTest.php | 16 +- .../Query/Mysql/DbQueryMySqlSubqueryTest.php | 74 ++-- .../Query/Mysql/DbQueryMySqlUnionTest.php | 90 +++-- .../Query/Mysql/DbQueryMySqlWhereTest.php | 86 ++-- .../Query/Mysql/DbQueryMySqlWindowTest.php | 44 +- .../Postgres/DbQueryPostgresBasicTest.php | 14 +- .../Query/Postgres/DbQueryPostgresCteTest.php | 32 +- .../Postgres/DbQueryPostgresExistsTest.php | 36 +- .../DbQueryPostgresGroupOrderLimitTest.php | 42 +- .../Postgres/DbQueryPostgresJoinTest.php | 16 +- .../Postgres/DbQueryPostgresSubqueryTest.php | 68 ++-- .../Postgres/DbQueryPostgresUnionTest.php | 38 +- .../Postgres/DbQueryPostgresWhereTest.php | 86 ++-- .../Postgres/DbQueryPostgresWindowTest.php | 42 +- .../Query/Sqlite/DbQuerySqliteBasicTest.php | 14 +- .../Query/Sqlite/DbQuerySqliteCteTest.php | 62 +-- .../Query/Sqlite/DbQuerySqliteExistsTest.php | 42 +- .../DbQuerySqliteGroupOrderLimitTest.php | 42 +- .../Query/Sqlite/DbQuerySqliteJoinTest.php | 16 +- .../Sqlite/DbQuerySqliteSubqueryTest.php | 54 +-- .../Query/Sqlite/DbQuerySqliteUnionTest.php | 64 +-- .../Query/Sqlite/DbQuerySqliteWhereTest.php | 120 +++--- .../Query/Sqlite/DbQuerySqliteWindowTest.php | 44 +- 54 files changed, 2285 insertions(+), 726 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/Query/Formatter/FieldListIdentifierQuoter.php create mode 100644 src/Query/Formatter/IdentifierMarkerReplacer.php create mode 100644 src/Query/Formatter/SimpleIdentifierQuoter.php create mode 100644 tests/Integration/Mysql/CamelCaseIdentifierMySqlTest.php create mode 100644 tests/Integration/Postgres/CamelCaseIdentifierPostgresTest.php create mode 100644 tests/Integration/Sqlite/CamelCaseIdentifierSqliteTest.php create mode 100644 tests/Unit/Query/IdentifierAutoQuotingTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ae5d1df --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,58 @@ +# Changelog + +All notable changes to `jardissupport/dbquery` are documented in this file. +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +versioning follows [SemVer](https://semver.org/). + +## [Unreleased] + +### Added + +- **Identifier auto-quoting for simple identifiers.** Fields passed to + `where()`/`and()`/`or()`, `having()`, `orderBy()`, `groupBy()` and the + `select()` field list are now quoted with the dialect's identifier quoting + (MySQL/MariaDB/SQLite: backtick, PostgreSQL: double quote) when they are + SIMPLE identifiers — `ident` or `alias.ident`, boundary regex + `^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$`. Everything else + (functions, operators, `*`, already quoted strings, `Expression::raw()`) + is emitted byte-identical raw. `Expression::raw()` is the explicit escape + hatch and is never quoted, even for a bare identifier. + SQL literals and niladic functions matching the pattern are excluded + case-insensitively (`SimpleIdentifierQuoter::KEYWORD_EXCEPTIONS`): NULL, + TRUE, FALSE, DEFAULT, CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME, + LOCALTIME, LOCALTIMESTAMP, CURRENT_USER, SESSION_USER — UNION padding + (`select('id, NULL AS email')`) keeps `NULL` raw; a column literally named + `null` must be passed pre-quoted or via `Expression::raw()`. + New internals: `Query\Formatter\SimpleIdentifierQuoter`, + `Query\Formatter\IdentifierMarkerReplacer`, + `Query\Formatter\FieldListIdentifierQuoter`. + +### Fixed + +- camelCase columns created with quoted DDL (e.g. `"createdAt"`) failed on + PostgreSQL with error 42703 when referenced in WHERE/ORDER BY/GROUP BY/ + SELECT, while the same column arrived correctly quoted in INSERT/UPDATE SET. + The INSERT → WHERE roundtrip on quoted camelCase DDL now works on all + supported dialects (covered by integration tests on MySQL, PostgreSQL and + SQLite). + +### Changed + +- **Signatures of internal clause builders extended** (technically public + classes, resolved via `BuilderRegistry`): the `__invoke()` methods of + `Query\Builder\Clause\ConditionBuilder` (new `callable + $quoteMarkedIdentifiers` parameter), `SelectBuilder` (new `callable + $quoteFieldList`), `OrderByBuilder` and `GroupByBuilder` (new `callable + $quoteSimpleIdentifier`) take additional quoting callbacks. Custom code + or version overrides calling these builders directly must pass the new + parameters. Known accepted duplication: the quoting helper methods exist + once per SQL builder base class (`Query\SqlBuilder`, + `Command\Update\UpdateSqlBuilder`, `Command\Delete\DeleteSqlBuilder`), + following the existing structure of the JSON/subquery placeholder helpers. +- **Emitted SQL now contains quoted identifiers** in the positions listed + above (semantics unchanged for lowercase/snake_case identifiers on all + supported dialects). Code that string-compares generated SQL must be + adjusted. On PostgreSQL the written identifier is now case-significant: + it must match quoted DDL exactly, or be all-lowercase for unquoted DDL. + Declare aliases with an explicit `AS` so alias definition and references + stay consistent. diff --git a/README.md b/README.md index 4d98e59..d7757dc 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ A fluent SQL query builder for PHP that generates dialect-aware SQL for MySQL, M ## Features - **Dialect-Aware SQL** — generates correct syntax for MySQL, MariaDB, PostgreSQL, and SQLite from a single builder +- **Identifier Auto-Quoting** — simple identifiers (`ident`, `alias.ident`) in WHERE/HAVING/ORDER BY/GROUP BY/SELECT are quoted per dialect; expressions stay raw (see [Identifier Auto-Quoting](#identifier-auto-quoting)) - **CTEs** — `with()` and `withRecursive()` for common table expressions - **Window Functions** — `selectWindow()`, `window()`, and `selectWindowRef()` for analytics queries - **Subqueries** — subqueries in FROM, JOIN constraints, SELECT columns, and WHERE EXISTS / NOT EXISTS @@ -48,10 +49,52 @@ $query = (new DbQuery()) // Generate prepared SQL for MySQL $prepared = $query->sql('mysql', prepared: true); -// $prepared->sql() → "SELECT id, name, email FROM users WHERE status = ? AND created_at >= ? ORDER BY name ASC LIMIT 50" +// $prepared->sql() → "SELECT `id`, `name`, `email` FROM `users` WHERE `status` = ? AND `created_at` >= ? ORDER BY `name` ASC LIMIT 50" // $prepared->bindings() → ['active', '2024-01-01'] ``` +## Identifier Auto-Quoting + +Simple identifiers are quoted automatically with the dialect's identifier quoting — +MySQL/MariaDB/SQLite use backticks, PostgreSQL uses double quotes. This makes +case-sensitive column names (e.g. `createdAt` from quoted DDL) work on PostgreSQL, +which folds unquoted identifiers to lowercase (error 42703 before). + +**What is quoted** — a string is a *simple identifier* iff it matches +`^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$` (`ident` or `alias.ident`). +Simple identifiers are quoted in these positions: + +- `where()` / `and()` / `or()` condition fields +- `having()` fields +- `orderBy()` fields +- `groupBy()` columns +- the `select()` field list (per comma-separated item; for `expr AS alias` the + `expr` and the `alias` are each quoted when they are simple identifiers) + +**What stays raw (byte-identical)** — everything that is not a simple identifier: + +- SQL literals and niladic functions that would otherwise match the pattern + (case-insensitive): `NULL`, `TRUE`, `FALSE`, `DEFAULT`, `CURRENT_TIMESTAMP`, + `CURRENT_DATE`, `CURRENT_TIME`, `LOCALTIME`, `LOCALTIMESTAMP`, + `CURRENT_USER`, `SESSION_USER` — so UNION padding like + `select('id, NULL AS email')` keeps `NULL` raw. A column literally named + `null` must be passed pre-quoted (`` `null` ``/`"null"`) or via + `Expression::raw('"null"')`; qualified names (`t.null`) are always + treated as identifiers. +- expressions and functions (`YEAR(created)`, `price * 1.19`, `COUNT(*)`) +- `*` and `alias.*` +- already quoted strings (`` `createdAt` ``, `"createdAt"`) +- `Expression::raw(...)` — the explicit escape hatch: even a simple identifier + inside `Expression::raw()` is never quoted +- JOIN ON constraints, window specifications (`partitionBy()`, `windowOrderBy()`) + and CTE inner SQL other than what the inner builder itself quotes + +**Boundary** — quoting makes the written identifier case-significant on PostgreSQL. +The name you pass must match the DDL exactly when the DDL was quoted, or be +all-lowercase when the DDL was unquoted. Declare aliases with an explicit `AS` +(`COUNT(*) AS orderCount`) so alias definition and alias references are quoted +consistently; use `Expression::raw()` where the raw string is required. + ## Advanced Usage ```php diff --git a/src/Command/Delete/DeleteSqlBuilder.php b/src/Command/Delete/DeleteSqlBuilder.php index 9ce9e90..07a1d4d 100644 --- a/src/Command/Delete/DeleteSqlBuilder.php +++ b/src/Command/Delete/DeleteSqlBuilder.php @@ -13,7 +13,9 @@ use JardisSupport\DbQuery\Query\Builder\Clause\LimitBuilder; use JardisSupport\DbQuery\Query\Builder\Clause\OrderByBuilder; use JardisSupport\DbQuery\Factory\BuilderRegistry; +use JardisSupport\DbQuery\Query\Formatter\IdentifierMarkerReplacer; use JardisSupport\DbQuery\Query\Formatter\PlaceholderReplacer; +use JardisSupport\DbQuery\Query\Formatter\SimpleIdentifierQuoter; use JardisSupport\DbQuery\Query\Formatter\ValueFormatter; use JardisSupport\DbQuery\Query\Processor\JsonPlaceholderProcessor; use JardisSupport\DbQuery\Query\Validator\SqlInjectionValidator; @@ -143,6 +145,7 @@ protected function buildWhere(bool $prepared): string $prepared, fn(string $cond) => $this->processJsonPlaceholders($cond), fn(string $cond) => $this->replaceSubqueryPlaceholders($cond), + fn(string $cond) => $this->quoteMarkedIdentifiers($cond), $this->bindings ); } @@ -161,7 +164,40 @@ protected function buildOrderBy(): string /** @var OrderByBuilder $builder */ $builder = $this->registry->get(OrderByBuilder::class); - return $builder($this->state); + return $builder( + $this->state, + fn(string $id) => $this->quoteIfSimpleIdentifier($id) + ); + } + + /** + * Quote a candidate string when it is a simple identifier + * (`ident` or `alias.ident`), otherwise return it unchanged + * + * @param string $identifier The candidate string + * @return string The quoted identifier or the unchanged input + */ + protected function quoteIfSimpleIdentifier(string $identifier): string + { + /** @var SimpleIdentifierQuoter $quoter */ + $quoter = $this->registry->get(SimpleIdentifierQuoter::class); + + return $quoter($identifier, fn(string $part) => $this->quoteIdentifier($part)); + } + + /** + * Replace identifier markers in a condition string with + * dialect-quoted identifiers + * + * @param string $condition The condition string possibly containing markers + * @return string The condition string with all markers replaced + */ + protected function quoteMarkedIdentifiers(string $condition): string + { + /** @var IdentifierMarkerReplacer $replacer */ + $replacer = $this->registry->get(IdentifierMarkerReplacer::class); + + return $replacer($condition, fn(string $id) => $this->quoteIfSimpleIdentifier($id)); } /** diff --git a/src/Command/Update/UpdateSqlBuilder.php b/src/Command/Update/UpdateSqlBuilder.php index 3aa2e16..9aeecdb 100644 --- a/src/Command/Update/UpdateSqlBuilder.php +++ b/src/Command/Update/UpdateSqlBuilder.php @@ -13,7 +13,9 @@ use JardisSupport\DbQuery\Query\Builder\Clause\LimitBuilder; use JardisSupport\DbQuery\Query\Builder\Clause\OrderByBuilder; use JardisSupport\DbQuery\Factory\BuilderRegistry; +use JardisSupport\DbQuery\Query\Formatter\IdentifierMarkerReplacer; use JardisSupport\DbQuery\Query\Formatter\PlaceholderReplacer; +use JardisSupport\DbQuery\Query\Formatter\SimpleIdentifierQuoter; use JardisSupport\DbQuery\Query\Formatter\ValueFormatter; use JardisSupport\DbQuery\Query\Processor\JsonPlaceholderProcessor; use JardisSupport\DbQuery\Query\Validator\SqlInjectionValidator; @@ -216,6 +218,7 @@ protected function buildWhere(bool $prepared): string $prepared, fn(string $cond) => $this->processJsonPlaceholders($cond), fn(string $cond) => $this->replaceSubqueryPlaceholders($cond), + fn(string $cond) => $this->quoteMarkedIdentifiers($cond), $this->bindings ); } @@ -234,7 +237,40 @@ protected function buildOrderBy(): string /** @var OrderByBuilder $builder */ $builder = $this->registry->get(OrderByBuilder::class); - return $builder($this->state); + return $builder( + $this->state, + fn(string $id) => $this->quoteIfSimpleIdentifier($id) + ); + } + + /** + * Quote a candidate string when it is a simple identifier + * (`ident` or `alias.ident`), otherwise return it unchanged + * + * @param string $identifier The candidate string + * @return string The quoted identifier or the unchanged input + */ + protected function quoteIfSimpleIdentifier(string $identifier): string + { + /** @var SimpleIdentifierQuoter $quoter */ + $quoter = $this->registry->get(SimpleIdentifierQuoter::class); + + return $quoter($identifier, fn(string $part) => $this->quoteIdentifier($part)); + } + + /** + * Replace identifier markers in a condition string with + * dialect-quoted identifiers + * + * @param string $condition The condition string possibly containing markers + * @return string The condition string with all markers replaced + */ + protected function quoteMarkedIdentifiers(string $condition): string + { + /** @var IdentifierMarkerReplacer $replacer */ + $replacer = $this->registry->get(IdentifierMarkerReplacer::class); + + return $replacer($condition, fn(string $id) => $this->quoteIfSimpleIdentifier($id)); } /** diff --git a/src/DbQuery.php b/src/DbQuery.php index 5d1f261..47c5b19 100644 --- a/src/DbQuery.php +++ b/src/DbQuery.php @@ -305,7 +305,9 @@ public function groupBy(string ...$columns): self public function having(string $expression, ?string $openBracket = null): DbQueryConditionBuilderInterface { - $this->queryCondition->initCondition($openBracket ?? '', $expression, true); + $resolvedExpression = $this->registry->get(Method\ResolveField::class)($expression); + + $this->queryCondition->initCondition($openBracket ?? '', $resolvedExpression, true); return $this->queryCondition; } diff --git a/src/Query/Builder/Clause/ConditionBuilder.php b/src/Query/Builder/Clause/ConditionBuilder.php index 2972545..2dbb573 100644 --- a/src/Query/Builder/Clause/ConditionBuilder.php +++ b/src/Query/Builder/Clause/ConditionBuilder.php @@ -29,6 +29,8 @@ class ConditionBuilder * @param bool $prepared Whether to use prepared statement mode * @param callable $processJsonPlaceholders Callback to process JSON placeholders: fn(string): string * @param callable $replaceSubqueryPlaceholders Callback to replace subquery placeholders: fn(string): string + * @param callable $quoteMarkedIdentifiers Callback to replace identifier markers with + * dialect-quoted identifiers: fn(string): string * @param array &$bindings Reference to bindings array (modified in place) * @return string The processed conditions string */ @@ -38,6 +40,7 @@ public function __invoke( bool $prepared, callable $processJsonPlaceholders, callable $replaceSubqueryPlaceholders, + callable $quoteMarkedIdentifiers, array &$bindings ): string { $result = ''; @@ -47,6 +50,9 @@ public function __invoke( foreach ($conditions as $condition) { if (is_string($condition)) { + // Replace identifier markers with dialect-quoted identifiers + $condition = $quoteMarkedIdentifiers($condition); + // Process string conditions and replace subquery placeholders if in prepared mode if ($prepared) { $condition = $replaceSubqueryPlaceholders($condition); diff --git a/src/Query/Builder/Clause/GroupByBuilder.php b/src/Query/Builder/Clause/GroupByBuilder.php index 4b7a256..ba7663a 100644 --- a/src/Query/Builder/Clause/GroupByBuilder.php +++ b/src/Query/Builder/Clause/GroupByBuilder.php @@ -17,13 +17,24 @@ class GroupByBuilder /** * Builds GROUP BY clause * + * Columns that are simple identifiers are quoted; expressions stay untouched. + * * @param QueryState $state The query state containing groupBy columns + * @param callable $quoteSimpleIdentifier Callback quoting a simple identifier + * or returning the input unchanged: fn(string): string * @return string The GROUP BY clause */ - public function __invoke(QueryState $state): string + public function __invoke(QueryState $state, callable $quoteSimpleIdentifier): string { - return !empty($state->getGroupBy()) - ? ' GROUP BY ' . implode(', ', $state->getGroupBy()) - : ''; + if (empty($state->getGroupBy())) { + return ''; + } + + $columns = array_map( + static fn(string $column): string => $quoteSimpleIdentifier($column), + $state->getGroupBy() + ); + + return ' GROUP BY ' . implode(', ', $columns); } } diff --git a/src/Query/Builder/Clause/OrderByBuilder.php b/src/Query/Builder/Clause/OrderByBuilder.php index 8f0244b..6e438cb 100644 --- a/src/Query/Builder/Clause/OrderByBuilder.php +++ b/src/Query/Builder/Clause/OrderByBuilder.php @@ -17,13 +17,31 @@ class OrderByBuilder /** * Builds ORDER BY clause * + * Each entry is stored as ' '. The field part is quoted + * when it is a simple identifier; expressions stay untouched. + * * @param OrderByStateInterface $state The query state containing orderBy columns + * @param callable $quoteSimpleIdentifier Callback quoting a simple identifier + * or returning the input unchanged: fn(string): string * @return string The ORDER BY clause */ - public function __invoke(OrderByStateInterface $state): string + public function __invoke(OrderByStateInterface $state, callable $quoteSimpleIdentifier): string { - return !empty($state->getOrderBy()) - ? ' ORDER BY ' . implode(', ', $state->getOrderBy()) - : ''; + if (empty($state->getOrderBy())) { + return ''; + } + + $entries = array_map( + static function (string $entry) use ($quoteSimpleIdentifier): string { + if (preg_match('/^(.*\S)(\s+)(ASC|DESC)$/', $entry, $matches) === 1) { + return $quoteSimpleIdentifier($matches[1]) . $matches[2] . $matches[3]; + } + + return $entry; + }, + $state->getOrderBy() + ); + + return ' ORDER BY ' . implode(', ', $entries); } } diff --git a/src/Query/Builder/Clause/SelectBuilder.php b/src/Query/Builder/Clause/SelectBuilder.php index 7e975f3..f849853 100644 --- a/src/Query/Builder/Clause/SelectBuilder.php +++ b/src/Query/Builder/Clause/SelectBuilder.php @@ -22,6 +22,8 @@ class SelectBuilder * @param string $dialect The SQL dialect (mysql, postgres, sqlite) * @param bool $prepared Whether to use prepared statement mode * @param callable $quoteIdentifier Callback to quote identifiers: fn(string): string + * @param callable $quoteFieldList Callback quoting simple identifiers in the + * field list, leaving everything else untouched: fn(string): string * @param array &$bindings Reference to bindings array (modified in place) * @return string The SELECT clause */ @@ -30,10 +32,11 @@ public function __invoke( string $dialect, bool $prepared, callable $quoteIdentifier, + callable $quoteFieldList, array &$bindings ): string { $distinct = $state->isDistinct() ? 'DISTINCT ' : ''; - $selectFields = trim($state->getFields()); + $selectFields = $quoteFieldList(trim($state->getFields())); $additionalFields = []; diff --git a/src/Query/Builder/Method/ResolveField.php b/src/Query/Builder/Method/ResolveField.php index 7608607..15c8a22 100644 --- a/src/Query/Builder/Method/ResolveField.php +++ b/src/Query/Builder/Method/ResolveField.php @@ -4,12 +4,20 @@ namespace JardisSupport\DbQuery\Query\Builder\Method; +use JardisSupport\DbQuery\Query\Formatter\IdentifierMarkerReplacer; +use JardisSupport\DbQuery\Query\Formatter\SimpleIdentifierQuoter; use JardisSupport\Contract\DbQuery\ExpressionInterface; /** * Stateless helper to resolve field parameter * - * Converts ExpressionInterface to SQL string, leaves strings and null as-is. + * Converts ExpressionInterface to its raw SQL string (never quoted, never + * marked - Expression is the escape hatch for raw output). Plain strings + * that are SIMPLE identifiers (`ident` or `alias.ident`, see + * SimpleIdentifierQuoter::PATTERN) are wrapped in an identifier marker so + * they get dialect-quoted at build time. All other strings and null pass + * through unchanged. + * * Can be reused across multiple queries without side effects. */ class ResolveField @@ -18,13 +26,18 @@ class ResolveField * Resolve field to string * * @param string|ExpressionInterface|null $field The field to resolve - * @return string|null Resolved field or null + * @return string|null Resolved field (marked when a simple identifier) or null */ public function __invoke(string|ExpressionInterface|null $field): ?string { if ($field instanceof ExpressionInterface) { return $field->toSql(); } + + if ($field !== null && preg_match(SimpleIdentifierQuoter::PATTERN, $field) === 1) { + return IdentifierMarkerReplacer::PREFIX . $field . IdentifierMarkerReplacer::SUFFIX; + } + return $field; } } diff --git a/src/Query/Formatter/FieldListIdentifierQuoter.php b/src/Query/Formatter/FieldListIdentifierQuoter.php new file mode 100644 index 0000000..9af8a03 --- /dev/null +++ b/src/Query/Formatter/FieldListIdentifierQuoter.php @@ -0,0 +1,138 @@ +splitTopLevel($fieldList); + + $processed = array_map( + fn(string $segment): string => $this->processSegment($segment, $quoteSimpleIdentifier), + $segments + ); + + return implode(',', $processed); + } + + /** + * Process a single field list segment, preserving surrounding whitespace + * + * @param string $segment The raw segment including whitespace + * @param callable $quoteSimpleIdentifier Callback quoting a simple identifier + * @return string The processed segment + */ + private function processSegment(string $segment, callable $quoteSimpleIdentifier): string + { + if (preg_match('/^(\s*)(.*?)(\s*)$/s', $segment, $matches) !== 1) { + return $segment; + } + + [, $lead, $core, $trail] = $matches; + + if ($core === '') { + return $segment; + } + + // Simple identifier or alias.ident - quote directly + $quoted = $quoteSimpleIdentifier($core); + if ($quoted !== $core) { + return $lead . $quoted . $trail; + } + + // expr AS alias - greedy first group binds to the LAST top-level AS + if (preg_match('/^(.+)(\s+)([Aa][Ss])(\s+)([A-Za-z_][A-Za-z0-9_]*)$/s', $core, $asMatch) === 1) { + [, $expr, $wsBefore, $asToken, $wsAfter, $alias] = $asMatch; + + return $lead + . $quoteSimpleIdentifier($expr) + . $wsBefore . $asToken . $wsAfter + . $quoteSimpleIdentifier($alias) + . $trail; + } + + return $segment; + } + + /** + * Split a field list on top-level commas + * + * Commas inside parentheses, string literals ('...'), double-quoted + * identifiers ("...") or backtick-quoted identifiers (`...`) do not split. + * + * @param string $fieldList The raw field list + * @return array The raw segments without the separating commas + */ + private function splitTopLevel(string $fieldList): array + { + $segments = []; + $current = ''; + $depth = 0; + $inQuote = null; + + $length = strlen($fieldList); + for ($i = 0; $i < $length; $i++) { + $char = $fieldList[$i]; + + if ($inQuote !== null) { + $current .= $char; + if ($char === $inQuote) { + $inQuote = null; + } + continue; + } + + if ($char === "'" || $char === '"' || $char === '`') { + $inQuote = $char; + $current .= $char; + continue; + } + + if ($char === '(') { + $depth++; + } elseif ($char === ')') { + $depth--; + } elseif ($char === ',' && $depth === 0) { + $segments[] = $current; + $current = ''; + continue; + } + + $current .= $char; + } + + $segments[] = $current; + + return $segments; + } +} diff --git a/src/Query/Formatter/IdentifierMarkerReplacer.php b/src/Query/Formatter/IdentifierMarkerReplacer.php new file mode 100644 index 0000000..bfce995 --- /dev/null +++ b/src/Query/Formatter/IdentifierMarkerReplacer.php @@ -0,0 +1,55 @@ + $quoteSimpleIdentifier($matches[1]), + $sql + ); + + return is_string($result) ? $result : $sql; + } +} diff --git a/src/Query/Formatter/SimpleIdentifierQuoter.php b/src/Query/Formatter/SimpleIdentifierQuoter.php new file mode 100644 index 0000000..d141c05 --- /dev/null +++ b/src/Query/Formatter/SimpleIdentifierQuoter.php @@ -0,0 +1,78 @@ + $quoteIdentifier($part), + explode('.', $identifier) + ); + + return implode('.', $parts); + } +} diff --git a/src/Query/SqlBuilder.php b/src/Query/SqlBuilder.php index aff6a8a..8b59bbc 100644 --- a/src/Query/SqlBuilder.php +++ b/src/Query/SqlBuilder.php @@ -128,6 +128,7 @@ protected function buildSelect(bool $prepared): string $this->dialect, $prepared, fn(string $id) => $this->quoteIdentifier($id), + fn(string $fields) => $this->quoteSelectFieldList($fields), $this->bindings ); } @@ -184,7 +185,10 @@ protected function buildGroupBy(): string /** @var GroupByBuilder $builder */ $builder = $this->registry->get(GroupByBuilder::class); - return $builder($this->state); + return $builder( + $this->state, + fn(string $id) => $this->quoteIfSimpleIdentifier($id) + ); } /** @@ -221,6 +225,7 @@ private function buildConditions(array $conditions = [], bool $prepared = false) $prepared, fn(string $cond) => $this->processJsonPlaceholders($cond), fn(string $cond) => $this->replaceSubqueryPlaceholders($cond), + fn(string $cond) => $this->quoteMarkedIdentifiers($cond), $this->bindings ); } @@ -231,7 +236,10 @@ protected function buildOrderBy(): string /** @var OrderByBuilder $builder */ $builder = $this->registry->get(OrderByBuilder::class); - return $builder($this->state); + return $builder( + $this->state, + fn(string $id) => $this->quoteIfSimpleIdentifier($id) + ); } protected function buildLimitOffset(): string @@ -278,6 +286,50 @@ public function quoteIdentifier(string $identifier): string return '`' . str_replace('`', '``', $identifier) . '`'; } + /** + * Quote a candidate string when it is a simple identifier + * (`ident` or `alias.ident`), otherwise return it unchanged + * + * @param string $identifier The candidate string + * @return string The quoted identifier or the unchanged input + */ + protected function quoteIfSimpleIdentifier(string $identifier): string + { + /** @var Formatter\SimpleIdentifierQuoter $quoter */ + $quoter = $this->registry->get(Formatter\SimpleIdentifierQuoter::class); + + return $quoter($identifier, fn(string $part) => $this->quoteIdentifier($part)); + } + + /** + * Replace identifier markers in a condition string with + * dialect-quoted identifiers + * + * @param string $condition The condition string possibly containing markers + * @return string The condition string with all markers replaced + */ + protected function quoteMarkedIdentifiers(string $condition): string + { + /** @var Formatter\IdentifierMarkerReplacer $replacer */ + $replacer = $this->registry->get(Formatter\IdentifierMarkerReplacer::class); + + return $replacer($condition, fn(string $id) => $this->quoteIfSimpleIdentifier($id)); + } + + /** + * Quote simple identifiers in the SELECT field list + * + * @param string $fields The raw field list + * @return string The field list with simple identifiers quoted + */ + protected function quoteSelectFieldList(string $fields): string + { + /** @var Formatter\FieldListIdentifierQuoter $quoter */ + $quoter = $this->registry->get(Formatter\FieldListIdentifierQuoter::class); + + return $quoter($fields, fn(string $id) => $this->quoteIfSimpleIdentifier($id)); + } + /** * Determines if a join type should be skipped for this database dialect * Can be overridden by subclasses to filter unsupported join types diff --git a/tests/Integration/Mysql/CamelCaseIdentifierMySqlTest.php b/tests/Integration/Mysql/CamelCaseIdentifierMySqlTest.php new file mode 100644 index 0000000..e8a96be --- /dev/null +++ b/tests/Integration/Mysql/CamelCaseIdentifierMySqlTest.php @@ -0,0 +1,102 @@ +db = new DatabaseConnection(); + $this->connection = $this->db->getMysqlConnection(); + + $this->connection->exec('DROP TABLE IF EXISTS ' . self::TABLE); + $this->connection->exec( + 'CREATE TABLE ' . self::TABLE . ' (' + . '`id` INT AUTO_INCREMENT PRIMARY KEY, ' + . '`buyerName` VARCHAR(100) NOT NULL, ' + . '`createdAt` DATE NOT NULL, ' + . '`totalAmount` INT NOT NULL)' + ); + } + + protected function tearDown(): void + { + $this->connection->exec('DROP TABLE IF EXISTS ' . self::TABLE); + } + + private function execute(DbPreparedQueryInterface $prepared): array + { + $stmt = $this->connection->prepare($prepared->sql()); + // Type-aware binding: SQLite compares strictly by type, a string-bound + // integer would never satisfy an integer comparison (e.g. HAVING). + foreach (array_values($prepared->bindings()) as $index => $value) { + $stmt->bindValue($index + 1, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR); + } + $stmt->execute(); + + return $stmt->fetchAll(); + } + + public function testCamelCaseOverWhereOrderGroupSelect(): void + { + foreach ( + [ + ['Alice', '2024-03-01', 100], + ['Alice', '2024-03-02', 50], + ['Bob', '2024-01-15', 200], + ] as [$name, $date, $amount] + ) { + $prepared = (new DbInsert()) + ->into(self::TABLE) + ->set(['buyerName' => $name, 'createdAt' => $date, 'totalAmount' => $amount]) + ->sql('mysql'); + $stmt = $this->connection->prepare($prepared->sql()); + $stmt->execute($prepared->bindings()); + } + + $results = $this->execute( + (new DbQuery()) + ->select('buyerName, createdAt, totalAmount') + ->from(self::TABLE) + ->where('createdAt')->greaterEquals('2024-02-01') + ->orderBy('createdAt', 'DESC') + ->sql('mysql') + ); + + $this->assertCount(2, $results); + $this->assertEquals('2024-03-02', $results[0]['createdAt']); + + $results = $this->execute( + (new DbQuery()) + ->select('buyerName, COUNT(*) AS orderCount') + ->from(self::TABLE) + ->groupBy('buyerName') + ->having('orderCount')->greater(1) + ->sql('mysql') + ); + + $this->assertCount(1, $results); + $this->assertEquals('Alice', $results[0]['buyerName']); + $this->assertEquals(2, $results[0]['orderCount']); + } +} diff --git a/tests/Integration/Postgres/CamelCaseIdentifierPostgresTest.php b/tests/Integration/Postgres/CamelCaseIdentifierPostgresTest.php new file mode 100644 index 0000000..6810bbb --- /dev/null +++ b/tests/Integration/Postgres/CamelCaseIdentifierPostgresTest.php @@ -0,0 +1,137 @@ + WHERE roundtrip works. + */ +class CamelCaseIdentifierPostgresTest extends TestCase +{ + private const TABLE = 'camel_orders'; + + private DatabaseConnection $db; + private PDO $connection; + + protected function setUp(): void + { + $this->db = new DatabaseConnection(); + $this->connection = $this->db->getPostgresConnection(); + + $this->connection->exec('DROP TABLE IF EXISTS ' . self::TABLE); + // Quoted DDL - the case of "createdAt"/"buyerName" is preserved + $this->connection->exec( + 'CREATE TABLE ' . self::TABLE . ' (' + . '"id" SERIAL PRIMARY KEY, ' + . '"buyerName" VARCHAR(100) NOT NULL, ' + . '"createdAt" DATE NOT NULL, ' + . '"totalAmount" INT NOT NULL)' + ); + } + + protected function tearDown(): void + { + $this->connection->exec('DROP TABLE IF EXISTS ' . self::TABLE); + } + + private function execute(DbPreparedQueryInterface $prepared): array + { + $stmt = $this->connection->prepare($prepared->sql()); + // Type-aware binding: SQLite compares strictly by type, a string-bound + // integer would never satisfy an integer comparison (e.g. HAVING). + foreach (array_values($prepared->bindings()) as $index => $value) { + $stmt->bindValue($index + 1, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR); + } + $stmt->execute(); + + return $stmt->fetchAll(); + } + + public function testInsertToWhereRoundtripOnQuotedCamelCaseDdl(): void + { + // INSERT via DbInsert (columns arrive quoted, as before the fix) + $insert = (new DbInsert()) + ->into(self::TABLE) + ->set([ + 'buyerName' => 'Alice', + 'createdAt' => '2024-03-01', + 'totalAmount' => 100, + ]); + $prepared = $insert->sql('postgres'); + $stmt = $this->connection->prepare($prepared->sql()); + $stmt->execute($prepared->bindings()); + + // WHERE on the camelCase column - failed with 42703 before the fix + $results = $this->execute( + (new DbQuery()) + ->select('*') + ->from(self::TABLE) + ->where('buyerName')->equals('Alice') + ->sql('postgres') + ); + + $this->assertCount(1, $results); + $this->assertEquals('Alice', $results[0]['buyerName']); + } + + public function testCamelCaseOverWhereOrderGroupSelect(): void + { + foreach ( + [ + ['Alice', '2024-03-01', 100], + ['Alice', '2024-03-02', 50], + ['Bob', '2024-01-15', 200], + ] as [$name, $date, $amount] + ) { + $prepared = (new DbInsert()) + ->into(self::TABLE) + ->set(['buyerName' => $name, 'createdAt' => $date, 'totalAmount' => $amount]) + ->sql('postgres'); + $stmt = $this->connection->prepare($prepared->sql()); + $stmt->execute($prepared->bindings()); + } + + // SELECT field list + WHERE + ORDER BY + $results = $this->execute( + (new DbQuery()) + ->select('buyerName, createdAt, totalAmount') + ->from(self::TABLE) + ->where('createdAt')->greaterEquals('2024-02-01') + ->orderBy('createdAt', 'DESC') + ->sql('postgres') + ); + + $this->assertCount(2, $results); + $this->assertEquals('2024-03-02', $results[0]['createdAt']); + $this->assertArrayHasKey('buyerName', $results[0]); + + // GROUP BY + HAVING on aggregate alias-free expression + $results = $this->execute( + (new DbQuery()) + ->select('buyerName, COUNT(*) AS orderCount') + ->from(self::TABLE) + ->groupBy('buyerName') + ->having('COUNT(*)')->greater(1) + ->sql('postgres') + ); + + $this->assertCount(1, $results); + $this->assertEquals('Alice', $results[0]['buyerName']); + $this->assertEquals(2, $results[0]['orderCount']); + } +} diff --git a/tests/Integration/Sqlite/CamelCaseIdentifierSqliteTest.php b/tests/Integration/Sqlite/CamelCaseIdentifierSqliteTest.php new file mode 100644 index 0000000..50cff72 --- /dev/null +++ b/tests/Integration/Sqlite/CamelCaseIdentifierSqliteTest.php @@ -0,0 +1,103 @@ +db = new DatabaseConnection(); + $this->connection = $this->db->getSqliteConnection(); + + $this->connection->exec('DROP TABLE IF EXISTS ' . self::TABLE); + $this->connection->exec( + 'CREATE TABLE ' . self::TABLE . ' (' + . '"id" INTEGER PRIMARY KEY AUTOINCREMENT, ' + . '"buyerName" TEXT NOT NULL, ' + . '"createdAt" TEXT NOT NULL, ' + . '"totalAmount" INTEGER NOT NULL)' + ); + } + + protected function tearDown(): void + { + $this->connection->exec('DROP TABLE IF EXISTS ' . self::TABLE); + } + + private function execute(DbPreparedQueryInterface $prepared): array + { + $stmt = $this->connection->prepare($prepared->sql()); + // Type-aware binding: SQLite compares strictly by type, a string-bound + // integer would never satisfy an integer comparison (e.g. HAVING). + foreach (array_values($prepared->bindings()) as $index => $value) { + $stmt->bindValue($index + 1, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR); + } + $stmt->execute(); + + return $stmt->fetchAll(); + } + + public function testCamelCaseOverWhereOrderGroupSelect(): void + { + foreach ( + [ + ['Alice', '2024-03-01', 100], + ['Alice', '2024-03-02', 50], + ['Bob', '2024-01-15', 200], + ] as [$name, $date, $amount] + ) { + $prepared = (new DbInsert()) + ->into(self::TABLE) + ->set(['buyerName' => $name, 'createdAt' => $date, 'totalAmount' => $amount]) + ->sql('sqlite'); + $stmt = $this->connection->prepare($prepared->sql()); + $stmt->execute($prepared->bindings()); + } + + $results = $this->execute( + (new DbQuery()) + ->select('buyerName, createdAt, totalAmount') + ->from(self::TABLE) + ->where('createdAt')->greaterEquals('2024-02-01') + ->orderBy('createdAt', 'DESC') + ->sql('sqlite') + ); + + $this->assertCount(2, $results); + $this->assertEquals('2024-03-02', $results[0]['createdAt']); + + $results = $this->execute( + (new DbQuery()) + ->select('buyerName, COUNT(*) AS orderCount') + ->from(self::TABLE) + ->groupBy('buyerName') + ->having('orderCount')->greater(1) + ->sql('sqlite') + ); + + $this->assertCount(1, $results); + $this->assertEquals('Alice', $results[0]['buyerName']); + $this->assertEquals(2, $results[0]['orderCount']); + } +} diff --git a/tests/Unit/Command/Delete/DbDeleteMySqlTest.php b/tests/Unit/Command/Delete/DbDeleteMySqlTest.php index cf4e84b..411e42e 100644 --- a/tests/Unit/Command/Delete/DbDeleteMySqlTest.php +++ b/tests/Unit/Command/Delete/DbDeleteMySqlTest.php @@ -18,6 +18,14 @@ * * Tests: DELETE FROM, WHERE, JOIN, ORDER BY, LIMIT */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbDeleteMySqlTest extends TestCase { public function testConstructor(): void @@ -34,7 +42,7 @@ public function testSimpleDelete(): void ->where('id')->equals(1) ->sql('mysql', false); - $expected = "DELETE FROM `users` WHERE id = 1"; + $expected = "DELETE FROM `users` WHERE `id` = 1"; $this->assertEquals($expected, $sql); } @@ -47,7 +55,7 @@ public function testSimpleDeletePrepared(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("DELETE FROM `users` WHERE id = ?", $result->sql()); + $this->assertEquals("DELETE FROM `users` WHERE `id` = ?", $result->sql()); $this->assertEquals([1], $result->bindings()); } @@ -59,7 +67,7 @@ public function testDeleteWithAlias(): void ->where('u.id')->equals(1) ->sql('mysql', false); - $expected = "DELETE FROM `users` `u` WHERE u.id = 1"; + $expected = "DELETE FROM `users` `u` WHERE `u`.`id` = 1"; $this->assertEquals($expected, $sql); } @@ -72,7 +80,7 @@ public function testDeleteWithMultipleConditions(): void ->and('created_at')->lower('2020-01-01') ->sql('mysql', false); - $expected = "DELETE FROM `users` WHERE status = 'inactive' AND created_at < '2020-01-01'"; + $expected = "DELETE FROM `users` WHERE `status` = 'inactive' AND `created_at` < '2020-01-01'"; $this->assertEquals($expected, $sql); } @@ -85,7 +93,7 @@ public function testDeleteWithOrCondition(): void ->or('status')->equals('banned') ->sql('mysql', false); - $expected = "DELETE FROM `users` WHERE status = 'deleted' OR status = 'banned'"; + $expected = "DELETE FROM `users` WHERE `status` = 'deleted' OR `status` = 'banned'"; $this->assertEquals($expected, $sql); } @@ -98,7 +106,7 @@ public function testDeleteWithInCondition(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("DELETE FROM `users` WHERE status IN (?, ?, ?)", $result->sql()); + $this->assertEquals("DELETE FROM `users` WHERE `status` IN (?, ?, ?)", $result->sql()); $this->assertEquals(['deleted', 'banned', 'suspended'], $result->bindings()); } @@ -138,7 +146,7 @@ public function testDeleteWithEmptyInAndOtherCondition(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('DELETE FROM `users` WHERE status = ? AND 1=0', $result->sql()); + $this->assertEquals('DELETE FROM `users` WHERE `status` = ? AND 1=0', $result->sql()); $this->assertEquals(['banned'], $result->bindings()); } @@ -150,7 +158,7 @@ public function testDeleteWithBetweenCondition(): void ->where('created_at')->between('2020-01-01', '2020-12-31') ->sql('mysql', false); - $expected = "DELETE FROM `logs` WHERE created_at BETWEEN '2020-01-01' AND '2020-12-31'"; + $expected = "DELETE FROM `logs` WHERE `created_at` BETWEEN '2020-01-01' AND '2020-12-31'"; $this->assertEquals($expected, $sql); } @@ -162,7 +170,7 @@ public function testDeleteWithLikeCondition(): void ->where('email')->like('%@spam.com') ->sql('mysql', false); - $expected = "DELETE FROM `users` WHERE email LIKE '%@spam.com'"; + $expected = "DELETE FROM `users` WHERE `email` LIKE '%@spam.com'"; $this->assertEquals($expected, $sql); } @@ -174,7 +182,7 @@ public function testDeleteWithIsNull(): void ->where('deleted_at')->isNull() ->sql('mysql', false); - $expected = "DELETE FROM `users` WHERE deleted_at IS NULL"; + $expected = "DELETE FROM `users` WHERE `deleted_at` IS NULL"; $this->assertEquals($expected, $sql); } @@ -186,7 +194,7 @@ public function testDeleteWithIsNotNull(): void ->where('email_verified_at')->isNotNull() ->sql('mysql', false); - $expected = "DELETE FROM `users` WHERE email_verified_at IS NOT NULL"; + $expected = "DELETE FROM `users` WHERE `email_verified_at` IS NOT NULL"; $this->assertEquals($expected, $sql); } @@ -200,7 +208,7 @@ public function testDeleteWithBrackets(): void ->or('is_admin', '(')->equals(true, ')') ->sql('mysql', false); - $expected = "DELETE FROM `users` WHERE (status = 'active' AND age > 18) OR (is_admin = 1)"; + $expected = "DELETE FROM `users` WHERE (`status` = 'active' AND `age` > 18) OR (`is_admin` = 1)"; $this->assertEquals($expected, $sql); } @@ -213,7 +221,7 @@ public function testDeleteWithInnerJoin(): void ->where('users.status')->equals('deleted') ->sql('mysql', false); - $expected = "DELETE FROM `user_sessions` INNER JOIN `users` ON user_sessions.user_id = users.id WHERE users.status = 'deleted'"; + $expected = "DELETE FROM `user_sessions` INNER JOIN `users` ON user_sessions.user_id = users.id WHERE `users`.`status` = 'deleted'"; $this->assertEquals($expected, $sql); } @@ -226,7 +234,7 @@ public function testDeleteWithLeftJoin(): void ->where('p.id')->isNull() ->sql('mysql', false); - $expected = "DELETE `o` FROM `orphaned_records` `o` LEFT JOIN `parent_table` `p` ON o.parent_id = parent_table.id WHERE p.id IS NULL"; + $expected = "DELETE `o` FROM `orphaned_records` `o` LEFT JOIN `parent_table` `p` ON o.parent_id = parent_table.id WHERE `p`.`id` IS NULL"; $this->assertEquals($expected, $sql); } @@ -239,7 +247,7 @@ public function testDeleteWithOrderBy(): void ->orderBy('created_at', 'ASC') ->sql('mysql', false); - $expected = "DELETE FROM `logs` WHERE level = 'debug' ORDER BY created_at ASC"; + $expected = "DELETE FROM `logs` WHERE `level` = 'debug' ORDER BY `created_at` ASC"; $this->assertEquals($expected, $sql); } @@ -252,7 +260,7 @@ public function testDeleteWithLimit(): void ->limit(1000) ->sql('mysql', false); - $expected = "DELETE FROM `logs` WHERE level = 'info' LIMIT 1000"; + $expected = "DELETE FROM `logs` WHERE `level` = 'info' LIMIT 1000"; $this->assertEquals($expected, $sql); } @@ -266,7 +274,7 @@ public function testDeleteWithOrderByAndLimit(): void ->limit(500) ->sql('mysql', false); - $expected = "DELETE FROM `logs` WHERE created_at < '2020-01-01' ORDER BY created_at ASC LIMIT 500"; + $expected = "DELETE FROM `logs` WHERE `created_at` < '2020-01-01' ORDER BY `created_at` ASC LIMIT 500"; $this->assertEquals($expected, $sql); } @@ -283,7 +291,7 @@ public function testDeleteWithExists(): void ->where()->exists($subquery) ->sql('mysql', false); - $expected = "DELETE FROM `users` WHERE EXISTS (SELECT 1 FROM `orders` WHERE orders.user_id = users.id)"; + $expected = "DELETE FROM `users` WHERE EXISTS (SELECT 1 FROM `orders` WHERE `orders`.`user_id` = users.id)"; $this->assertEquals($expected, $sql); } @@ -300,7 +308,7 @@ public function testDeleteWithNotExists(): void ->where()->notExists($subquery) ->sql('mysql', false); - $expected = "DELETE FROM `users` WHERE NOT EXISTS (SELECT 1 FROM `orders` WHERE orders.user_id = users.id)"; + $expected = "DELETE FROM `users` WHERE NOT EXISTS (SELECT 1 FROM `orders` WHERE `orders`.`user_id` = users.id)"; $this->assertEquals($expected, $sql); } @@ -386,7 +394,7 @@ public function testDeleteWithComplexConditions(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $expected = "DELETE FROM `users` WHERE (status = ? AND last_login < ?) OR (email_verified_at IS NULL AND created_at < ?)"; + $expected = "DELETE FROM `users` WHERE (`status` = ? AND `last_login` < ?) OR (`email_verified_at` IS NULL AND `created_at` < ?)"; $this->assertEquals($expected, $result->sql()); $this->assertEquals(['inactive', '2020-01-01', '2019-01-01'], $result->bindings()); } @@ -404,7 +412,7 @@ public function testDeleteWithSubquery(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $expected = "DELETE FROM `users` WHERE id IN ?"; + $expected = "DELETE FROM `users` WHERE `id` IN ?"; $this->assertEquals($expected, $result->sql()); } } diff --git a/tests/Unit/Command/Delete/DbDeletePostgresTest.php b/tests/Unit/Command/Delete/DbDeletePostgresTest.php index 14332e2..d1fb30b 100644 --- a/tests/Unit/Command/Delete/DbDeletePostgresTest.php +++ b/tests/Unit/Command/Delete/DbDeletePostgresTest.php @@ -19,6 +19,14 @@ * Tests: DELETE FROM, WHERE (PostgreSQL dialect) * Note: PostgreSQL does not support JOIN, ORDER BY, or LIMIT in DELETE statements */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbDeletePostgresTest extends TestCase { public function testConstructor(): void @@ -35,7 +43,7 @@ public function testSimpleDelete(): void ->where('id')->equals(1) ->sql('postgres', false); - $expected = 'DELETE FROM "users" WHERE id = 1'; + $expected = 'DELETE FROM "users" WHERE "id" = 1'; $this->assertEquals($expected, $sql); } @@ -48,7 +56,7 @@ public function testSimpleDeletePrepared(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('DELETE FROM "users" WHERE id = ?', $result->sql()); + $this->assertEquals('DELETE FROM "users" WHERE "id" = ?', $result->sql()); $this->assertEquals([1], $result->bindings()); } @@ -60,7 +68,7 @@ public function testDeleteWithAlias(): void ->where('u.id')->equals(1) ->sql('postgres', false); - $expected = 'DELETE FROM "users" "u" WHERE u.id = 1'; + $expected = 'DELETE FROM "users" "u" WHERE "u"."id" = 1'; $this->assertEquals($expected, $sql); } @@ -73,7 +81,7 @@ public function testDeleteWithMultipleConditions(): void ->and('created_at')->lower('2020-01-01') ->sql('postgres', false); - $expected = "DELETE FROM \"users\" WHERE status = 'inactive' AND created_at < '2020-01-01'"; + $expected = "DELETE FROM \"users\" WHERE \"status\" = 'inactive' AND \"created_at\" < '2020-01-01'"; $this->assertEquals($expected, $sql); } @@ -86,7 +94,7 @@ public function testDeleteWithOrCondition(): void ->or('status')->equals('banned') ->sql('postgres', false); - $expected = "DELETE FROM \"users\" WHERE status = 'deleted' OR status = 'banned'"; + $expected = "DELETE FROM \"users\" WHERE \"status\" = 'deleted' OR \"status\" = 'banned'"; $this->assertEquals($expected, $sql); } @@ -99,7 +107,7 @@ public function testDeleteWithInCondition(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('DELETE FROM "users" WHERE status IN (?, ?, ?)', $result->sql()); + $this->assertEquals('DELETE FROM "users" WHERE "status" IN (?, ?, ?)', $result->sql()); $this->assertEquals(['deleted', 'banned', 'suspended'], $result->bindings()); } @@ -139,7 +147,7 @@ public function testDeleteWithEmptyInAndOtherCondition(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('DELETE FROM "users" WHERE status = ? AND 1=0', $result->sql()); + $this->assertEquals('DELETE FROM "users" WHERE "status" = ? AND 1=0', $result->sql()); $this->assertEquals(['banned'], $result->bindings()); } @@ -151,7 +159,7 @@ public function testDeleteWithBetweenCondition(): void ->where('created_at')->between('2020-01-01', '2020-12-31') ->sql('postgres', false); - $expected = "DELETE FROM \"logs\" WHERE created_at BETWEEN '2020-01-01' AND '2020-12-31'"; + $expected = "DELETE FROM \"logs\" WHERE \"created_at\" BETWEEN '2020-01-01' AND '2020-12-31'"; $this->assertEquals($expected, $sql); } @@ -163,7 +171,7 @@ public function testDeleteWithLikeCondition(): void ->where('email')->like('%@spam.com') ->sql('postgres', false); - $expected = "DELETE FROM \"users\" WHERE email LIKE '%@spam.com'"; + $expected = "DELETE FROM \"users\" WHERE \"email\" LIKE '%@spam.com'"; $this->assertEquals($expected, $sql); } @@ -175,7 +183,7 @@ public function testDeleteWithIsNull(): void ->where('deleted_at')->isNull() ->sql('postgres', false); - $expected = 'DELETE FROM "users" WHERE deleted_at IS NULL'; + $expected = 'DELETE FROM "users" WHERE "deleted_at" IS NULL'; $this->assertEquals($expected, $sql); } @@ -187,7 +195,7 @@ public function testDeleteWithIsNotNull(): void ->where('email_verified_at')->isNotNull() ->sql('postgres', false); - $expected = 'DELETE FROM "users" WHERE email_verified_at IS NOT NULL'; + $expected = 'DELETE FROM "users" WHERE "email_verified_at" IS NOT NULL'; $this->assertEquals($expected, $sql); } @@ -201,7 +209,7 @@ public function testDeleteWithBrackets(): void ->or('is_admin', '(')->equals(true, ')') ->sql('postgres', false); - $expected = "DELETE FROM \"users\" WHERE (status = 'active' AND age > 18) OR (is_admin = 1)"; + $expected = "DELETE FROM \"users\" WHERE (\"status\" = 'active' AND \"age\" > 18) OR (\"is_admin\" = 1)"; $this->assertEquals($expected, $sql); } @@ -218,7 +226,7 @@ public function testDeleteWithExists(): void ->where()->exists($subquery) ->sql('postgres', false); - $expected = 'DELETE FROM "users" WHERE EXISTS (SELECT 1 FROM "orders" WHERE orders.user_id = users.id)'; + $expected = 'DELETE FROM "users" WHERE EXISTS (SELECT 1 FROM "orders" WHERE "orders"."user_id" = users.id)'; $this->assertEquals($expected, $sql); } @@ -235,7 +243,7 @@ public function testDeleteWithNotExists(): void ->where()->notExists($subquery) ->sql('postgres', false); - $expected = 'DELETE FROM "users" WHERE NOT EXISTS (SELECT 1 FROM "orders" WHERE orders.user_id = users.id)'; + $expected = 'DELETE FROM "users" WHERE NOT EXISTS (SELECT 1 FROM "orders" WHERE "orders"."user_id" = users.id)'; $this->assertEquals($expected, $sql); } @@ -360,7 +368,7 @@ public function testDeleteWithComplexConditions(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $expected = 'DELETE FROM "users" WHERE (status = ? AND last_login < ?) OR (email_verified_at IS NULL AND created_at < ?)'; + $expected = 'DELETE FROM "users" WHERE ("status" = ? AND "last_login" < ?) OR ("email_verified_at" IS NULL AND "created_at" < ?)'; $this->assertEquals($expected, $result->sql()); $this->assertEquals(['inactive', '2020-01-01', '2019-01-01'], $result->bindings()); } @@ -378,7 +386,7 @@ public function testDeleteWithSubquery(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $expected = 'DELETE FROM "users" WHERE id IN ?'; + $expected = 'DELETE FROM "users" WHERE "id" IN ?'; $this->assertEquals($expected, $result->sql()); } @@ -395,7 +403,7 @@ public function testDeleteWithNotInSubquery(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $expected = 'DELETE FROM "users" WHERE id NOT IN ?'; + $expected = 'DELETE FROM "users" WHERE "id" NOT IN ?'; $this->assertEquals($expected, $result->sql()); } } diff --git a/tests/Unit/Command/Delete/DbDeleteSqliteTest.php b/tests/Unit/Command/Delete/DbDeleteSqliteTest.php index bf5d0a8..43cc879 100644 --- a/tests/Unit/Command/Delete/DbDeleteSqliteTest.php +++ b/tests/Unit/Command/Delete/DbDeleteSqliteTest.php @@ -19,6 +19,14 @@ * Tests: DELETE FROM, WHERE, ORDER BY, LIMIT (SQLite 3.24.0+) * Note: SQLite has limited JOIN support and does not support RIGHT/FULL OUTER JOIN */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbDeleteSqliteTest extends TestCase { public function testConstructor(): void @@ -35,7 +43,7 @@ public function testSimpleDelete(): void ->where('id')->equals(1) ->sql('sqlite', false); - $expected = "DELETE FROM `users` WHERE id = 1"; + $expected = "DELETE FROM `users` WHERE `id` = 1"; $this->assertEquals($expected, $sql); } @@ -48,7 +56,7 @@ public function testSimpleDeletePrepared(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("DELETE FROM `users` WHERE id = ?", $result->sql()); + $this->assertEquals("DELETE FROM `users` WHERE `id` = ?", $result->sql()); $this->assertEquals([1], $result->bindings()); } @@ -60,7 +68,7 @@ public function testDeleteWithAlias(): void ->where('u.id')->equals(1) ->sql('sqlite', false); - $expected = "DELETE FROM `users` `u` WHERE u.id = 1"; + $expected = "DELETE FROM `users` `u` WHERE `u`.`id` = 1"; $this->assertEquals($expected, $sql); } @@ -73,7 +81,7 @@ public function testDeleteWithMultipleConditions(): void ->and('created_at')->lower('2020-01-01') ->sql('sqlite', false); - $expected = "DELETE FROM `users` WHERE status = 'inactive' AND created_at < '2020-01-01'"; + $expected = "DELETE FROM `users` WHERE `status` = 'inactive' AND `created_at` < '2020-01-01'"; $this->assertEquals($expected, $sql); } @@ -86,7 +94,7 @@ public function testDeleteWithOrCondition(): void ->or('status')->equals('banned') ->sql('sqlite', false); - $expected = "DELETE FROM `users` WHERE status = 'deleted' OR status = 'banned'"; + $expected = "DELETE FROM `users` WHERE `status` = 'deleted' OR `status` = 'banned'"; $this->assertEquals($expected, $sql); } @@ -99,7 +107,7 @@ public function testDeleteWithInCondition(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("DELETE FROM `users` WHERE status IN (?, ?, ?)", $result->sql()); + $this->assertEquals("DELETE FROM `users` WHERE `status` IN (?, ?, ?)", $result->sql()); $this->assertEquals(['deleted', 'banned', 'suspended'], $result->bindings()); } @@ -139,7 +147,7 @@ public function testDeleteWithEmptyInAndOtherCondition(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('DELETE FROM `users` WHERE status = ? AND 1=0', $result->sql()); + $this->assertEquals('DELETE FROM `users` WHERE `status` = ? AND 1=0', $result->sql()); $this->assertEquals(['banned'], $result->bindings()); } @@ -151,7 +159,7 @@ public function testDeleteWithBetweenCondition(): void ->where('created_at')->between('2020-01-01', '2020-12-31') ->sql('sqlite', false); - $expected = "DELETE FROM `logs` WHERE created_at BETWEEN '2020-01-01' AND '2020-12-31'"; + $expected = "DELETE FROM `logs` WHERE `created_at` BETWEEN '2020-01-01' AND '2020-12-31'"; $this->assertEquals($expected, $sql); } @@ -163,7 +171,7 @@ public function testDeleteWithLikeCondition(): void ->where('email')->like('%@spam.com') ->sql('sqlite', false); - $expected = "DELETE FROM `users` WHERE email LIKE '%@spam.com'"; + $expected = "DELETE FROM `users` WHERE `email` LIKE '%@spam.com'"; $this->assertEquals($expected, $sql); } @@ -175,7 +183,7 @@ public function testDeleteWithIsNull(): void ->where('deleted_at')->isNull() ->sql('sqlite', false); - $expected = "DELETE FROM `users` WHERE deleted_at IS NULL"; + $expected = "DELETE FROM `users` WHERE `deleted_at` IS NULL"; $this->assertEquals($expected, $sql); } @@ -187,7 +195,7 @@ public function testDeleteWithIsNotNull(): void ->where('email_verified_at')->isNotNull() ->sql('sqlite', false); - $expected = "DELETE FROM `users` WHERE email_verified_at IS NOT NULL"; + $expected = "DELETE FROM `users` WHERE `email_verified_at` IS NOT NULL"; $this->assertEquals($expected, $sql); } @@ -201,7 +209,7 @@ public function testDeleteWithBrackets(): void ->or('is_admin', '(')->equals(true, ')') ->sql('sqlite', false); - $expected = "DELETE FROM `users` WHERE (status = 'active' AND age > 18) OR (is_admin = 1)"; + $expected = "DELETE FROM `users` WHERE (`status` = 'active' AND `age` > 18) OR (`is_admin` = 1)"; $this->assertEquals($expected, $sql); } @@ -258,7 +266,7 @@ public function testDeleteWithExists(): void ->where()->exists($subquery) ->sql('sqlite', false); - $expected = "DELETE FROM `users` WHERE EXISTS (SELECT 1 FROM `orders` WHERE orders.user_id = users.id)"; + $expected = "DELETE FROM `users` WHERE EXISTS (SELECT 1 FROM `orders` WHERE `orders`.`user_id` = users.id)"; $this->assertEquals($expected, $sql); } @@ -275,7 +283,7 @@ public function testDeleteWithNotExists(): void ->where()->notExists($subquery) ->sql('sqlite', false); - $expected = "DELETE FROM `users` WHERE NOT EXISTS (SELECT 1 FROM `orders` WHERE orders.user_id = users.id)"; + $expected = "DELETE FROM `users` WHERE NOT EXISTS (SELECT 1 FROM `orders` WHERE `orders`.`user_id` = users.id)"; $this->assertEquals($expected, $sql); } @@ -352,7 +360,7 @@ public function testDeleteWithComplexConditions(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $expected = "DELETE FROM `users` WHERE (status = ? AND last_login < ?) OR (email_verified_at IS NULL AND created_at < ?)"; + $expected = "DELETE FROM `users` WHERE (`status` = ? AND `last_login` < ?) OR (`email_verified_at` IS NULL AND `created_at` < ?)"; $this->assertEquals($expected, $result->sql()); $this->assertEquals(['inactive', '2020-01-01', '2019-01-01'], $result->bindings()); } @@ -370,7 +378,7 @@ public function testDeleteWithSubquery(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $expected = "DELETE FROM `users` WHERE id IN ?"; + $expected = "DELETE FROM `users` WHERE `id` IN ?"; $this->assertEquals($expected, $result->sql()); } diff --git a/tests/Unit/Command/Insert/DbInsertMySqlTest.php b/tests/Unit/Command/Insert/DbInsertMySqlTest.php index 7f19972..6aa5e2a 100644 --- a/tests/Unit/Command/Insert/DbInsertMySqlTest.php +++ b/tests/Unit/Command/Insert/DbInsertMySqlTest.php @@ -16,6 +16,14 @@ * * Tests: INSERT INTO, columns, values, set, fromSelect */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbInsertMySqlTest extends TestCase { public function testConstructor(): void @@ -175,7 +183,7 @@ public function testInsertFromSelect(): void ->fromSelect($selectQuery) ->sql('mysql', false); - $expected = "INSERT INTO `users` (`name`, `email`) SELECT name, email FROM `temp_users` WHERE status = 'pending'"; + $expected = "INSERT INTO `users` (`name`, `email`) SELECT `name`, `email` FROM `temp_users` WHERE `status` = 'pending'"; $this->assertEquals($expected, $sql); } @@ -194,7 +202,7 @@ public function testInsertFromSelectPrepared(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("INSERT INTO `users` (`name`, `email`) SELECT name, email FROM `temp_users` WHERE status = ?", $result->sql()); + $this->assertEquals("INSERT INTO `users` (`name`, `email`) SELECT `name`, `email` FROM `temp_users` WHERE `status` = ?", $result->sql()); $this->assertEquals(['pending'], $result->bindings()); } diff --git a/tests/Unit/Command/Insert/DbInsertPostgresTest.php b/tests/Unit/Command/Insert/DbInsertPostgresTest.php index f6c7c01..e8e25e5 100644 --- a/tests/Unit/Command/Insert/DbInsertPostgresTest.php +++ b/tests/Unit/Command/Insert/DbInsertPostgresTest.php @@ -16,6 +16,14 @@ * * Tests: INSERT INTO, columns, values, set, fromSelect (PostgreSQL dialect) */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbInsertPostgresTest extends TestCase { public function testConstructor(): void @@ -132,7 +140,7 @@ public function testInsertFromSelect(): void ->fromSelect($selectQuery) ->sql('postgres', false); - $expected = 'INSERT INTO "users" ("name", "email") SELECT name, email FROM "temp_users" WHERE status = \'pending\''; + $expected = 'INSERT INTO "users" ("name", "email") SELECT "name", "email" FROM "temp_users" WHERE "status" = \'pending\''; $this->assertEquals($expected, $sql); } @@ -151,7 +159,7 @@ public function testInsertFromSelectPrepared(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('INSERT INTO "users" ("name", "email") SELECT name, email FROM "temp_users" WHERE status = ?', $result->sql()); + $this->assertEquals('INSERT INTO "users" ("name", "email") SELECT "name", "email" FROM "temp_users" WHERE "status" = ?', $result->sql()); $this->assertEquals(['pending'], $result->bindings()); } diff --git a/tests/Unit/Command/Insert/DbInsertSqliteTest.php b/tests/Unit/Command/Insert/DbInsertSqliteTest.php index f9bfcf5..0fbadb0 100644 --- a/tests/Unit/Command/Insert/DbInsertSqliteTest.php +++ b/tests/Unit/Command/Insert/DbInsertSqliteTest.php @@ -16,6 +16,14 @@ * * Tests: INSERT INTO, columns, values, set, fromSelect (SQLite dialect) */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbInsertSqliteTest extends TestCase { public function testConstructor(): void @@ -132,7 +140,7 @@ public function testInsertFromSelect(): void ->fromSelect($selectQuery) ->sql('sqlite', false); - $expected = "INSERT INTO `users` (`name`, `email`) SELECT name, email FROM `temp_users` WHERE status = 'pending'"; + $expected = "INSERT INTO `users` (`name`, `email`) SELECT `name`, `email` FROM `temp_users` WHERE `status` = 'pending'"; $this->assertEquals($expected, $sql); } @@ -151,7 +159,7 @@ public function testInsertFromSelectPrepared(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("INSERT INTO `users` (`name`, `email`) SELECT name, email FROM `temp_users` WHERE status = ?", $result->sql()); + $this->assertEquals("INSERT INTO `users` (`name`, `email`) SELECT `name`, `email` FROM `temp_users` WHERE `status` = ?", $result->sql()); $this->assertEquals(['pending'], $result->bindings()); } diff --git a/tests/Unit/Command/Update/DbUpdateMySqlTest.php b/tests/Unit/Command/Update/DbUpdateMySqlTest.php index 163530f..8c0ef48 100644 --- a/tests/Unit/Command/Update/DbUpdateMySqlTest.php +++ b/tests/Unit/Command/Update/DbUpdateMySqlTest.php @@ -18,6 +18,14 @@ * * Tests: UPDATE, SET, WHERE, JOIN, ORDER BY, LIMIT */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbUpdateMySqlTest extends TestCase { public function testConstructor(): void @@ -35,7 +43,7 @@ public function testSimpleUpdateWithSet(): void ->where('id')->equals(1) ->sql('mysql', false); - $expected = "UPDATE `users` SET `name` = 'John Doe' WHERE id = 1"; + $expected = "UPDATE `users` SET `name` = 'John Doe' WHERE `id` = 1"; $this->assertEquals($expected, $sql); } @@ -49,7 +57,7 @@ public function testSimpleUpdateWithSetPrepared(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `users` SET `name` = ? WHERE id = ?", $result->sql()); + $this->assertEquals("UPDATE `users` SET `name` = ? WHERE `id` = ?", $result->sql()); $this->assertEquals(['John Doe', 1], $result->bindings()); } @@ -66,7 +74,7 @@ public function testUpdateWithSetMultiple(): void ->where('id')->equals(5) ->sql('mysql', false); - $expected = "UPDATE `users` SET `name` = 'Jane Smith', `email` = 'jane@example.com', `status` = 'active' WHERE id = 5"; + $expected = "UPDATE `users` SET `name` = 'Jane Smith', `email` = 'jane@example.com', `status` = 'active' WHERE `id` = 5"; $this->assertEquals($expected, $sql); } @@ -84,7 +92,7 @@ public function testUpdateWithSetMultiplePrepared(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `users` SET `name` = ?, `email` = ?, `status` = ? WHERE id = ?", $result->sql()); + $this->assertEquals("UPDATE `users` SET `name` = ?, `email` = ?, `status` = ? WHERE `id` = ?", $result->sql()); $this->assertEquals(['Jane Smith', 'jane@example.com', 'active', 5], $result->bindings()); } @@ -99,7 +107,7 @@ public function testUpdateWithMultipleSetCalls(): void ->where('id')->equals(1) ->sql('mysql', false); - $expected = "UPDATE `users` SET `name` = 'John Doe', `email` = 'john@example.com', `age` = 30 WHERE id = 1"; + $expected = "UPDATE `users` SET `name` = 'John Doe', `email` = 'john@example.com', `age` = 30 WHERE `id` = 1"; $this->assertEquals($expected, $sql); } @@ -117,7 +125,7 @@ public function testUpdateWithNullValue(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `users` SET `name` = ?, `email` = ?, `deleted_at` = ? WHERE id = ?", $result->sql()); + $this->assertEquals("UPDATE `users` SET `name` = ?, `email` = ?, `deleted_at` = ? WHERE `id` = ?", $result->sql()); $this->assertEquals(['John Doe', null, null, 1], $result->bindings()); } @@ -133,7 +141,7 @@ public function testUpdateWithBooleanValues(): void ->where('id')->equals(1) ->sql('mysql', false); - $expected = "UPDATE `settings` SET `is_active` = 1, `is_deleted` = 0 WHERE id = 1"; + $expected = "UPDATE `settings` SET `is_active` = 1, `is_deleted` = 0 WHERE `id` = 1"; $this->assertEquals($expected, $sql); } @@ -150,7 +158,7 @@ public function testUpdateWithNumericValues(): void ->where('id')->equals(10) ->sql('mysql', false); - $expected = "UPDATE `products` SET `price` = 19.99, `quantity` = 100, `discount` = 0 WHERE id = 10"; + $expected = "UPDATE `products` SET `price` = 19.99, `quantity` = 100, `discount` = 0 WHERE `id` = 10"; $this->assertEquals($expected, $sql); } @@ -168,7 +176,7 @@ public function testUpdateWithSubqueryValue(): void ->where('id')->equals(5) ->sql('mysql', false); - $expected = "UPDATE `departments` SET `max_salary` = (SELECT MAX(salary) FROM `employees` WHERE department_id = 5) WHERE id = 5"; + $expected = "UPDATE `departments` SET `max_salary` = (SELECT MAX(salary) FROM `employees` WHERE `department_id` = 5) WHERE `id` = 5"; $this->assertEquals($expected, $sql); } @@ -187,7 +195,7 @@ public function testUpdateWithSubqueryValuePrepared(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `departments` SET `max_salary` = (SELECT MAX(salary) FROM `employees` WHERE department_id = ?) WHERE id = ?", $result->sql()); + $this->assertEquals("UPDATE `departments` SET `max_salary` = (SELECT MAX(salary) FROM `employees` WHERE `department_id` = ?) WHERE `id` = ?", $result->sql()); $this->assertEquals([5, 5], $result->bindings()); } @@ -200,7 +208,7 @@ public function testUpdateWithWhereEquals(): void ->where('email')->equals('john@example.com') ->sql('mysql', false); - $expected = "UPDATE `users` SET `status` = 'active' WHERE email = 'john@example.com'"; + $expected = "UPDATE `users` SET `status` = 'active' WHERE `email` = 'john@example.com'"; $this->assertEquals($expected, $sql); } @@ -213,7 +221,7 @@ public function testUpdateWithWhereNotEquals(): void ->where('status')->notEquals('deleted') ->sql('mysql', false); - $expected = "UPDATE `users` SET `active` = 1 WHERE status != 'deleted'"; + $expected = "UPDATE `users` SET `active` = 1 WHERE `status` != 'deleted'"; $this->assertEquals($expected, $sql); } @@ -226,7 +234,7 @@ public function testUpdateWithWhereLike(): void ->where('email')->like('%@example.com') ->sql('mysql', false); - $expected = "UPDATE `users` SET `newsletter` = 1 WHERE email LIKE '%@example.com'"; + $expected = "UPDATE `users` SET `newsletter` = 1 WHERE `email` LIKE '%@example.com'"; $this->assertEquals($expected, $sql); } @@ -239,7 +247,7 @@ public function testUpdateWithWhereIn(): void ->where('id')->in([1, 2, 3, 4, 5]) ->sql('mysql', false); - $expected = "UPDATE `users` SET `status` = 'inactive' WHERE id IN (1, 2, 3, 4, 5)"; + $expected = "UPDATE `users` SET `status` = 'inactive' WHERE `id` IN (1, 2, 3, 4, 5)"; $this->assertEquals($expected, $sql); } @@ -282,7 +290,7 @@ public function testUpdateWithEmptyInAndOtherCondition(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('UPDATE `users` SET `status` = ? WHERE status = ? AND 1=0', $result->sql()); + $this->assertEquals('UPDATE `users` SET `status` = ? WHERE `status` = ? AND 1=0', $result->sql()); $this->assertEquals(['inactive', 'active'], $result->bindings()); } @@ -295,7 +303,7 @@ public function testUpdateWithWhereBetween(): void ->where('price')->between(100, 500) ->sql('mysql', false); - $expected = "UPDATE `products` SET `discount` = 10 WHERE price BETWEEN 100 AND 500"; + $expected = "UPDATE `products` SET `discount` = 10 WHERE `price` BETWEEN 100 AND 500"; $this->assertEquals($expected, $sql); } @@ -308,7 +316,7 @@ public function testUpdateWithWhereIsNull(): void ->where('verified_at')->isNull() ->sql('mysql', false); - $expected = "UPDATE `users` SET `verified` = 0 WHERE verified_at IS NULL"; + $expected = "UPDATE `users` SET `verified` = 0 WHERE `verified_at` IS NULL"; $this->assertEquals($expected, $sql); } @@ -321,7 +329,7 @@ public function testUpdateWithWhereIsNotNull(): void ->where('email')->isNotNull() ->sql('mysql', false); - $expected = "UPDATE `users` SET `active` = 1 WHERE email IS NOT NULL"; + $expected = "UPDATE `users` SET `active` = 1 WHERE `email` IS NOT NULL"; $this->assertEquals($expected, $sql); } @@ -335,7 +343,7 @@ public function testUpdateWithAndCondition(): void ->and('created_at')->lower('2020-01-01') ->sql('mysql', false); - $expected = "UPDATE `users` SET `status` = 'archived' WHERE active = 0 AND created_at < '2020-01-01'"; + $expected = "UPDATE `users` SET `status` = 'archived' WHERE `active` = 0 AND `created_at` < '2020-01-01'"; $this->assertEquals($expected, $sql); } @@ -349,7 +357,7 @@ public function testUpdateWithOrCondition(): void ->or('role')->equals('moderator') ->sql('mysql', false); - $expected = "UPDATE `users` SET `notification` = 1 WHERE role = 'admin' OR role = 'moderator'"; + $expected = "UPDATE `users` SET `notification` = 1 WHERE `role` = 'admin' OR `role` = 'moderator'"; $this->assertEquals($expected, $sql); } @@ -365,7 +373,7 @@ public function testUpdateWithComplexWhereConditions(): void ->and('verified')->equals(1, ')') ->sql('mysql', false); - $expected = "UPDATE `users` SET `priority` = 'high' WHERE (status = 'active' AND role = 'premium') OR (score > 1000 AND verified = 1)"; + $expected = "UPDATE `users` SET `priority` = 'high' WHERE (`status` = 'active' AND `role` = 'premium') OR (`score` > 1000 AND `verified` = 1)"; $this->assertEquals($expected, $sql); } @@ -392,7 +400,7 @@ public function testUpdateWithAndJsonCondition(): void ->andJson('settings')->extract('$.premium')->equals(true) ->sql('mysql', false); - $expected = "UPDATE `users` SET `featured` = 1 WHERE status = 'active' AND JSON_EXTRACT(`settings`, '$.premium') = 1"; + $expected = "UPDATE `users` SET `featured` = 1 WHERE `status` = 'active' AND JSON_EXTRACT(`settings`, '$.premium') = 1"; $this->assertEquals($expected, $sql); } @@ -406,7 +414,7 @@ public function testUpdateWithOrJsonCondition(): void ->orJson('attributes')->extract('$.discontinued')->equals(true) ->sql('mysql', false); - $expected = "UPDATE `products` SET `visible` = 0 WHERE stock = 0 OR JSON_EXTRACT(`attributes`, '$.discontinued') = 1"; + $expected = "UPDATE `products` SET `visible` = 0 WHERE `stock` = 0 OR JSON_EXTRACT(`attributes`, '$.discontinued') = 1"; $this->assertEquals($expected, $sql); } @@ -425,7 +433,7 @@ public function testUpdateWithExists(): void ->where()->exists($subquery) ->sql('mysql', false); - $expected = "UPDATE `users` SET `has_pending_orders` = 1 WHERE EXISTS (SELECT 1 FROM `orders` WHERE orders.user_id = users.id AND orders.status = 'pending')"; + $expected = "UPDATE `users` SET `has_pending_orders` = 1 WHERE EXISTS (SELECT 1 FROM `orders` WHERE `orders`.`user_id` = users.id AND `orders`.`status` = 'pending')"; $this->assertEquals($expected, $sql); } @@ -443,7 +451,7 @@ public function testUpdateWithNotExists(): void ->where()->notExists($subquery) ->sql('mysql', false); - $expected = "UPDATE `users` SET `no_orders` = 1 WHERE NOT EXISTS (SELECT 1 FROM `orders` WHERE orders.user_id = users.id)"; + $expected = "UPDATE `users` SET `no_orders` = 1 WHERE NOT EXISTS (SELECT 1 FROM `orders` WHERE `orders`.`user_id` = users.id)"; $this->assertEquals($expected, $sql); } @@ -457,7 +465,7 @@ public function testUpdateWithInnerJoin(): void ->where('o.status')->equals('cancelled') ->sql('mysql', false); - $expected = "UPDATE `users` `u` INNER JOIN `orders` `o` ON orders.user_id = u.id SET `u.total_orders` = 0 WHERE o.status = 'cancelled'"; + $expected = "UPDATE `users` `u` INNER JOIN `orders` `o` ON orders.user_id = u.id SET `u.total_orders` = 0 WHERE `o`.`status` = 'cancelled'"; $this->assertEquals($expected, $sql); } @@ -472,7 +480,7 @@ public function testUpdateWithLeftJoin(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `users` `u` LEFT JOIN `orders` `o` ON orders.user_id = u.id SET `u.last_order_date` = ? WHERE o.id IS NULL", $result->sql()); + $this->assertEquals("UPDATE `users` `u` LEFT JOIN `orders` `o` ON orders.user_id = u.id SET `u.last_order_date` = ? WHERE `o`.`id` IS NULL", $result->sql()); $this->assertEquals([null], $result->bindings()); } @@ -487,7 +495,7 @@ public function testUpdateWithMultipleJoins(): void ->where('u.vip')->equals(1) ->sql('mysql', false); - $expected = "UPDATE `orders` `o` INNER JOIN `users` `u` ON users.id = o.user_id LEFT JOIN `discounts` `d` ON discounts.user_id = u.id SET `o.discount` = 15 WHERE u.vip = 1"; + $expected = "UPDATE `orders` `o` INNER JOIN `users` `u` ON users.id = o.user_id LEFT JOIN `discounts` `d` ON discounts.user_id = u.id SET `o.discount` = 15 WHERE `u`.`vip` = 1"; $this->assertEquals($expected, $sql); } @@ -502,7 +510,7 @@ public function testUpdateWithOrderBy(): void ->limit(10) ->sql('mysql', false); - $expected = "UPDATE `users` SET `priority` = 1 WHERE status = 'active' ORDER BY created_at ASC LIMIT 10"; + $expected = "UPDATE `users` SET `priority` = 1 WHERE `status` = 'active' ORDER BY `created_at` ASC LIMIT 10"; $this->assertEquals($expected, $sql); } @@ -517,7 +525,7 @@ public function testUpdateWithOrderByDesc(): void ->limit(5) ->sql('mysql', false); - $expected = "UPDATE `posts` SET `featured` = 0 WHERE views < 100 ORDER BY views DESC LIMIT 5"; + $expected = "UPDATE `posts` SET `featured` = 0 WHERE `views` < 100 ORDER BY `views` DESC LIMIT 5"; $this->assertEquals($expected, $sql); } @@ -533,7 +541,7 @@ public function testUpdateWithMultipleOrderBy(): void ->limit(100) ->sql('mysql', false); - $expected = "UPDATE `users` SET `status` = 'archived' WHERE active = 0 ORDER BY last_login ASC, created_at DESC LIMIT 100"; + $expected = "UPDATE `users` SET `status` = 'archived' WHERE `active` = 0 ORDER BY `last_login` ASC, `created_at` DESC LIMIT 100"; $this->assertEquals($expected, $sql); } @@ -547,7 +555,7 @@ public function testUpdateWithLimit(): void ->limit(1000) ->sql('mysql', false); - $expected = "UPDATE `logs` SET `processed` = 1 WHERE processed = 0 LIMIT 1000"; + $expected = "UPDATE `logs` SET `processed` = 1 WHERE `processed` = 0 LIMIT 1000"; $this->assertEquals($expected, $sql); } @@ -564,7 +572,7 @@ public function testUpdateWithJoinWhereOrderByLimit(): void ->limit(50) ->sql('mysql', false); - $expected = "UPDATE `users` `u` INNER JOIN `orders` `o` ON orders.user_id = u.id SET `u.bonus` = 100 WHERE o.total > 1000 AND u.status = 'active' ORDER BY o.total DESC LIMIT 50"; + $expected = "UPDATE `users` `u` INNER JOIN `orders` `o` ON orders.user_id = u.id SET `u.bonus` = 100 WHERE `o`.`total` > 1000 AND `u`.`status` = 'active' ORDER BY `o`.`total` DESC LIMIT 50"; $this->assertEquals($expected, $sql); } @@ -577,7 +585,7 @@ public function testUpdateWithTableAlias(): void ->where('u.id')->equals(1) ->sql('mysql', false); - $expected = "UPDATE `users` `u` SET `u.updated_at` = '2024-01-01' WHERE u.id = 1"; + $expected = "UPDATE `users` `u` SET `u.updated_at` = '2024-01-01' WHERE `u`.`id` = 1"; $this->assertEquals($expected, $sql); } @@ -649,7 +657,7 @@ public function testUpdateWithExpressionInSet(): void ->where('id')->equals(1) ->sql('mysql', false); - $expected = "UPDATE `users` SET `updated_at` = NOW() WHERE id = 1"; + $expected = "UPDATE `users` SET `updated_at` = NOW() WHERE `id` = 1"; $this->assertEquals($expected, $sql); } @@ -664,7 +672,7 @@ public function testUpdateWithExpressionInSetPrepared(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `users` SET `updated_at` = NOW(), `name` = ? WHERE id = ?", $result->sql()); + $this->assertEquals("UPDATE `users` SET `updated_at` = NOW(), `name` = ? WHERE `id` = ?", $result->sql()); $this->assertEquals(['John', 1], $result->bindings()); } @@ -690,7 +698,7 @@ public function testIgnore(): void ->ignore() ->sql('mysql', false); - $expected = "UPDATE IGNORE `users` SET `name` = 'John' WHERE id = 1"; + $expected = "UPDATE IGNORE `users` SET `name` = 'John' WHERE `id` = 1"; $this->assertEquals($expected, $sql); } } diff --git a/tests/Unit/Command/Update/DbUpdatePostgresTest.php b/tests/Unit/Command/Update/DbUpdatePostgresTest.php index 39d2f77..e69d2b2 100644 --- a/tests/Unit/Command/Update/DbUpdatePostgresTest.php +++ b/tests/Unit/Command/Update/DbUpdatePostgresTest.php @@ -19,6 +19,14 @@ * Tests: UPDATE, SET, WHERE (PostgreSQL dialect) * Note: PostgreSQL does not support JOIN, ORDER BY, or LIMIT in UPDATE statements */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbUpdatePostgresTest extends TestCase { public function testConstructor(): void @@ -36,7 +44,7 @@ public function testSimpleUpdateWithSet(): void ->where('id')->equals(1) ->sql('postgres', false); - $expected = 'UPDATE "users" SET "name" = \'John Doe\' WHERE id = 1'; + $expected = 'UPDATE "users" SET "name" = \'John Doe\' WHERE "id" = 1'; $this->assertEquals($expected, $sql); } @@ -50,7 +58,7 @@ public function testSimpleUpdateWithSetPrepared(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('UPDATE "users" SET "name" = ? WHERE id = ?', $result->sql()); + $this->assertEquals('UPDATE "users" SET "name" = ? WHERE "id" = ?', $result->sql()); $this->assertEquals(['John Doe', 1], $result->bindings()); } @@ -67,7 +75,7 @@ public function testUpdateWithSetMultiple(): void ->where('id')->equals(5) ->sql('postgres', false); - $expected = 'UPDATE "users" SET "name" = \'Jane Smith\', "email" = \'jane@example.com\', "status" = \'active\' WHERE id = 5'; + $expected = 'UPDATE "users" SET "name" = \'Jane Smith\', "email" = \'jane@example.com\', "status" = \'active\' WHERE "id" = 5'; $this->assertEquals($expected, $sql); } @@ -85,7 +93,7 @@ public function testUpdateWithSetMultiplePrepared(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('UPDATE "users" SET "name" = ?, "email" = ?, "status" = ? WHERE id = ?', $result->sql()); + $this->assertEquals('UPDATE "users" SET "name" = ?, "email" = ?, "status" = ? WHERE "id" = ?', $result->sql()); $this->assertEquals(['Jane Smith', 'jane@example.com', 'active', 5], $result->bindings()); } @@ -100,7 +108,7 @@ public function testUpdateWithMultipleSetCalls(): void ->where('id')->equals(1) ->sql('postgres', false); - $expected = 'UPDATE "users" SET "name" = \'John Doe\', "email" = \'john@example.com\', "age" = 30 WHERE id = 1'; + $expected = 'UPDATE "users" SET "name" = \'John Doe\', "email" = \'john@example.com\', "age" = 30 WHERE "id" = 1'; $this->assertEquals($expected, $sql); } @@ -118,7 +126,7 @@ public function testUpdateWithNullValue(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('UPDATE "users" SET "name" = ?, "email" = ?, "deleted_at" = ? WHERE id = ?', $result->sql()); + $this->assertEquals('UPDATE "users" SET "name" = ?, "email" = ?, "deleted_at" = ? WHERE "id" = ?', $result->sql()); $this->assertEquals(['John Doe', null, null, 1], $result->bindings()); } @@ -135,7 +143,7 @@ public function testUpdateWithBooleanValues(): void ->sql('postgres', false); // PostgreSQL uses TRUE/FALSE for booleans - $expected = 'UPDATE "settings" SET "is_active" = TRUE, "is_deleted" = FALSE WHERE id = 1'; + $expected = 'UPDATE "settings" SET "is_active" = TRUE, "is_deleted" = FALSE WHERE "id" = 1'; $this->assertEquals($expected, $sql); } @@ -152,7 +160,7 @@ public function testUpdateWithNumericValues(): void ->where('id')->equals(10) ->sql('postgres', false); - $expected = 'UPDATE "products" SET "price" = 19.99, "quantity" = 100, "discount" = 0 WHERE id = 10'; + $expected = 'UPDATE "products" SET "price" = 19.99, "quantity" = 100, "discount" = 0 WHERE "id" = 10'; $this->assertEquals($expected, $sql); } @@ -170,7 +178,7 @@ public function testUpdateWithSubqueryValue(): void ->where('id')->equals(5) ->sql('postgres', false); - $expected = 'UPDATE "departments" SET "max_salary" = (SELECT MAX(salary) FROM "employees" WHERE department_id = 5) WHERE id = 5'; + $expected = 'UPDATE "departments" SET "max_salary" = (SELECT MAX(salary) FROM "employees" WHERE "department_id" = 5) WHERE "id" = 5'; $this->assertEquals($expected, $sql); } @@ -189,7 +197,7 @@ public function testUpdateWithSubqueryValuePrepared(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('UPDATE "departments" SET "max_salary" = (SELECT MAX(salary) FROM "employees" WHERE department_id = ?) WHERE id = ?', $result->sql()); + $this->assertEquals('UPDATE "departments" SET "max_salary" = (SELECT MAX(salary) FROM "employees" WHERE "department_id" = ?) WHERE "id" = ?', $result->sql()); $this->assertEquals([5, 5], $result->bindings()); } @@ -202,7 +210,7 @@ public function testUpdateWithWhereEquals(): void ->where('email')->equals('john@example.com') ->sql('postgres', false); - $expected = 'UPDATE "users" SET "status" = \'active\' WHERE email = \'john@example.com\''; + $expected = 'UPDATE "users" SET "status" = \'active\' WHERE "email" = \'john@example.com\''; $this->assertEquals($expected, $sql); } @@ -215,7 +223,7 @@ public function testUpdateWithWhereNotEquals(): void ->where('status')->notEquals('deleted') ->sql('postgres', false); - $expected = 'UPDATE "users" SET "active" = 1 WHERE status != \'deleted\''; + $expected = 'UPDATE "users" SET "active" = 1 WHERE "status" != \'deleted\''; $this->assertEquals($expected, $sql); } @@ -228,7 +236,7 @@ public function testUpdateWithWhereLike(): void ->where('email')->like('%@example.com') ->sql('postgres', false); - $expected = 'UPDATE "users" SET "newsletter" = 1 WHERE email LIKE \'%@example.com\''; + $expected = 'UPDATE "users" SET "newsletter" = 1 WHERE "email" LIKE \'%@example.com\''; $this->assertEquals($expected, $sql); } @@ -241,7 +249,7 @@ public function testUpdateWithWhereIn(): void ->where('id')->in([1, 2, 3, 4, 5]) ->sql('postgres', false); - $expected = 'UPDATE "users" SET "status" = \'inactive\' WHERE id IN (1, 2, 3, 4, 5)'; + $expected = 'UPDATE "users" SET "status" = \'inactive\' WHERE "id" IN (1, 2, 3, 4, 5)'; $this->assertEquals($expected, $sql); } @@ -284,7 +292,7 @@ public function testUpdateWithEmptyInAndOtherCondition(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('UPDATE "users" SET "status" = ? WHERE status = ? AND 1=0', $result->sql()); + $this->assertEquals('UPDATE "users" SET "status" = ? WHERE "status" = ? AND 1=0', $result->sql()); $this->assertEquals(['inactive', 'active'], $result->bindings()); } @@ -297,7 +305,7 @@ public function testUpdateWithWhereBetween(): void ->where('price')->between(100, 500) ->sql('postgres', false); - $expected = 'UPDATE "products" SET "discount" = 10 WHERE price BETWEEN 100 AND 500'; + $expected = 'UPDATE "products" SET "discount" = 10 WHERE "price" BETWEEN 100 AND 500'; $this->assertEquals($expected, $sql); } @@ -310,7 +318,7 @@ public function testUpdateWithWhereIsNull(): void ->where('verified_at')->isNull() ->sql('postgres', false); - $expected = 'UPDATE "users" SET "verified" = 0 WHERE verified_at IS NULL'; + $expected = 'UPDATE "users" SET "verified" = 0 WHERE "verified_at" IS NULL'; $this->assertEquals($expected, $sql); } @@ -323,7 +331,7 @@ public function testUpdateWithWhereIsNotNull(): void ->where('email')->isNotNull() ->sql('postgres', false); - $expected = 'UPDATE "users" SET "active" = 1 WHERE email IS NOT NULL'; + $expected = 'UPDATE "users" SET "active" = 1 WHERE "email" IS NOT NULL'; $this->assertEquals($expected, $sql); } @@ -337,7 +345,7 @@ public function testUpdateWithAndCondition(): void ->and('created_at')->lower('2020-01-01') ->sql('postgres', false); - $expected = 'UPDATE "users" SET "status" = \'archived\' WHERE active = 0 AND created_at < \'2020-01-01\''; + $expected = 'UPDATE "users" SET "status" = \'archived\' WHERE "active" = 0 AND "created_at" < \'2020-01-01\''; $this->assertEquals($expected, $sql); } @@ -351,7 +359,7 @@ public function testUpdateWithOrCondition(): void ->or('role')->equals('moderator') ->sql('postgres', false); - $expected = 'UPDATE "users" SET "notification" = 1 WHERE role = \'admin\' OR role = \'moderator\''; + $expected = 'UPDATE "users" SET "notification" = 1 WHERE "role" = \'admin\' OR "role" = \'moderator\''; $this->assertEquals($expected, $sql); } @@ -367,7 +375,7 @@ public function testUpdateWithComplexWhereConditions(): void ->and('verified')->equals(1, ')') ->sql('postgres', false); - $expected = 'UPDATE "users" SET "priority" = \'high\' WHERE (status = \'active\' AND role = \'premium\') OR (score > 1000 AND verified = 1)'; + $expected = 'UPDATE "users" SET "priority" = \'high\' WHERE ("status" = \'active\' AND "role" = \'premium\') OR ("score" > 1000 AND "verified" = 1)'; $this->assertEquals($expected, $sql); } @@ -394,7 +402,7 @@ public function testUpdateWithAndJsonCondition(): void ->andJson('settings')->extract('$.premium')->equals(true) ->sql('postgres', false); - $expected = 'UPDATE "users" SET "featured" = 1 WHERE status = \'active\' AND "settings"->>\'premium\' = TRUE'; + $expected = 'UPDATE "users" SET "featured" = 1 WHERE "status" = \'active\' AND "settings"->>\'premium\' = TRUE'; $this->assertEquals($expected, $sql); } @@ -408,7 +416,7 @@ public function testUpdateWithOrJsonCondition(): void ->orJson('attributes')->extract('$.discontinued')->equals(true) ->sql('postgres', false); - $expected = 'UPDATE "products" SET "visible" = 0 WHERE stock = 0 OR "attributes"->>\'discontinued\' = TRUE'; + $expected = 'UPDATE "products" SET "visible" = 0 WHERE "stock" = 0 OR "attributes"->>\'discontinued\' = TRUE'; $this->assertEquals($expected, $sql); } @@ -427,7 +435,7 @@ public function testUpdateWithExists(): void ->where()->exists($subquery) ->sql('postgres', false); - $expected = 'UPDATE "users" SET "has_pending_orders" = 1 WHERE EXISTS (SELECT 1 FROM "orders" WHERE orders.user_id = users.id AND orders.status = \'pending\')'; + $expected = 'UPDATE "users" SET "has_pending_orders" = 1 WHERE EXISTS (SELECT 1 FROM "orders" WHERE "orders"."user_id" = users.id AND "orders"."status" = \'pending\')'; $this->assertEquals($expected, $sql); } @@ -445,7 +453,7 @@ public function testUpdateWithNotExists(): void ->where()->notExists($subquery) ->sql('postgres', false); - $expected = 'UPDATE "users" SET "no_orders" = 1 WHERE NOT EXISTS (SELECT 1 FROM "orders" WHERE orders.user_id = users.id)'; + $expected = 'UPDATE "users" SET "no_orders" = 1 WHERE NOT EXISTS (SELECT 1 FROM "orders" WHERE "orders"."user_id" = users.id)'; $this->assertEquals($expected, $sql); } @@ -458,7 +466,7 @@ public function testUpdateWithExpressionInSet(): void ->where('id')->equals(1) ->sql('postgres', false); - $expected = 'UPDATE "users" SET "updated_at" = NOW() WHERE id = 1'; + $expected = 'UPDATE "users" SET "updated_at" = NOW() WHERE "id" = 1'; $this->assertEquals($expected, $sql); } @@ -473,7 +481,7 @@ public function testUpdateWithExpressionInSetPrepared(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('UPDATE "users" SET "updated_at" = NOW(), "name" = ? WHERE id = ?', $result->sql()); + $this->assertEquals('UPDATE "users" SET "updated_at" = NOW(), "name" = ? WHERE "id" = ?', $result->sql()); $this->assertEquals(['John', 1], $result->bindings()); } @@ -499,7 +507,7 @@ public function testUpdateWithTableAlias(): void ->where('u.id')->equals(1) ->sql('postgres', false); - $expected = 'UPDATE "users" "u" SET "u.updated_at" = \'2024-01-01\' WHERE u.id = 1'; + $expected = 'UPDATE "users" "u" SET "u.updated_at" = \'2024-01-01\' WHERE "u"."id" = 1'; $this->assertEquals($expected, $sql); } diff --git a/tests/Unit/Command/Update/DbUpdateSqliteTest.php b/tests/Unit/Command/Update/DbUpdateSqliteTest.php index 0d2b7dc..ddf0c24 100644 --- a/tests/Unit/Command/Update/DbUpdateSqliteTest.php +++ b/tests/Unit/Command/Update/DbUpdateSqliteTest.php @@ -19,6 +19,14 @@ * Tests: UPDATE, SET, WHERE (SQLite dialect) * Note: SQLite does not support JOIN, ORDER BY, or LIMIT in UPDATE statements */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbUpdateSqliteTest extends TestCase { public function testConstructor(): void @@ -36,7 +44,7 @@ public function testSimpleUpdateWithSet(): void ->where('id')->equals(1) ->sql('sqlite', false); - $expected = "UPDATE `users` SET `name` = 'John Doe' WHERE id = 1"; + $expected = "UPDATE `users` SET `name` = 'John Doe' WHERE `id` = 1"; $this->assertEquals($expected, $sql); } @@ -50,7 +58,7 @@ public function testSimpleUpdateWithSetPrepared(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `users` SET `name` = ? WHERE id = ?", $result->sql()); + $this->assertEquals("UPDATE `users` SET `name` = ? WHERE `id` = ?", $result->sql()); $this->assertEquals(['John Doe', 1], $result->bindings()); } @@ -67,7 +75,7 @@ public function testUpdateWithSetMultiple(): void ->where('id')->equals(5) ->sql('sqlite', false); - $expected = "UPDATE `users` SET `name` = 'Jane Smith', `email` = 'jane@example.com', `status` = 'active' WHERE id = 5"; + $expected = "UPDATE `users` SET `name` = 'Jane Smith', `email` = 'jane@example.com', `status` = 'active' WHERE `id` = 5"; $this->assertEquals($expected, $sql); } @@ -85,7 +93,7 @@ public function testUpdateWithSetMultiplePrepared(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `users` SET `name` = ?, `email` = ?, `status` = ? WHERE id = ?", $result->sql()); + $this->assertEquals("UPDATE `users` SET `name` = ?, `email` = ?, `status` = ? WHERE `id` = ?", $result->sql()); $this->assertEquals(['Jane Smith', 'jane@example.com', 'active', 5], $result->bindings()); } @@ -100,7 +108,7 @@ public function testUpdateWithMultipleSetCalls(): void ->where('id')->equals(1) ->sql('sqlite', false); - $expected = "UPDATE `users` SET `name` = 'John Doe', `email` = 'john@example.com', `age` = 30 WHERE id = 1"; + $expected = "UPDATE `users` SET `name` = 'John Doe', `email` = 'john@example.com', `age` = 30 WHERE `id` = 1"; $this->assertEquals($expected, $sql); } @@ -118,7 +126,7 @@ public function testUpdateWithNullValue(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `users` SET `name` = ?, `email` = ?, `deleted_at` = ? WHERE id = ?", $result->sql()); + $this->assertEquals("UPDATE `users` SET `name` = ?, `email` = ?, `deleted_at` = ? WHERE `id` = ?", $result->sql()); $this->assertEquals(['John Doe', null, null, 1], $result->bindings()); } @@ -135,7 +143,7 @@ public function testUpdateWithBooleanValues(): void ->sql('sqlite', false); // SQLite uses 1/0 for booleans - $expected = "UPDATE `settings` SET `is_active` = 1, `is_deleted` = 0 WHERE id = 1"; + $expected = "UPDATE `settings` SET `is_active` = 1, `is_deleted` = 0 WHERE `id` = 1"; $this->assertEquals($expected, $sql); } @@ -152,7 +160,7 @@ public function testUpdateWithNumericValues(): void ->where('id')->equals(10) ->sql('sqlite', false); - $expected = "UPDATE `products` SET `price` = 19.99, `quantity` = 100, `discount` = 0 WHERE id = 10"; + $expected = "UPDATE `products` SET `price` = 19.99, `quantity` = 100, `discount` = 0 WHERE `id` = 10"; $this->assertEquals($expected, $sql); } @@ -170,7 +178,7 @@ public function testUpdateWithSubqueryValue(): void ->where('id')->equals(5) ->sql('sqlite', false); - $expected = "UPDATE `departments` SET `max_salary` = (SELECT MAX(salary) FROM `employees` WHERE department_id = 5) WHERE id = 5"; + $expected = "UPDATE `departments` SET `max_salary` = (SELECT MAX(salary) FROM `employees` WHERE `department_id` = 5) WHERE `id` = 5"; $this->assertEquals($expected, $sql); } @@ -189,7 +197,7 @@ public function testUpdateWithSubqueryValuePrepared(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `departments` SET `max_salary` = (SELECT MAX(salary) FROM `employees` WHERE department_id = ?) WHERE id = ?", $result->sql()); + $this->assertEquals("UPDATE `departments` SET `max_salary` = (SELECT MAX(salary) FROM `employees` WHERE `department_id` = ?) WHERE `id` = ?", $result->sql()); $this->assertEquals([5, 5], $result->bindings()); } @@ -202,7 +210,7 @@ public function testUpdateWithWhereEquals(): void ->where('email')->equals('john@example.com') ->sql('sqlite', false); - $expected = "UPDATE `users` SET `status` = 'active' WHERE email = 'john@example.com'"; + $expected = "UPDATE `users` SET `status` = 'active' WHERE `email` = 'john@example.com'"; $this->assertEquals($expected, $sql); } @@ -215,7 +223,7 @@ public function testUpdateWithWhereNotEquals(): void ->where('status')->notEquals('deleted') ->sql('sqlite', false); - $expected = "UPDATE `users` SET `active` = 1 WHERE status != 'deleted'"; + $expected = "UPDATE `users` SET `active` = 1 WHERE `status` != 'deleted'"; $this->assertEquals($expected, $sql); } @@ -228,7 +236,7 @@ public function testUpdateWithWhereLike(): void ->where('email')->like('%@example.com') ->sql('sqlite', false); - $expected = "UPDATE `users` SET `newsletter` = 1 WHERE email LIKE '%@example.com'"; + $expected = "UPDATE `users` SET `newsletter` = 1 WHERE `email` LIKE '%@example.com'"; $this->assertEquals($expected, $sql); } @@ -241,7 +249,7 @@ public function testUpdateWithWhereIn(): void ->where('id')->in([1, 2, 3, 4, 5]) ->sql('sqlite', false); - $expected = "UPDATE `users` SET `status` = 'inactive' WHERE id IN (1, 2, 3, 4, 5)"; + $expected = "UPDATE `users` SET `status` = 'inactive' WHERE `id` IN (1, 2, 3, 4, 5)"; $this->assertEquals($expected, $sql); } @@ -284,7 +292,7 @@ public function testUpdateWithEmptyInAndOtherCondition(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals('UPDATE `users` SET `status` = ? WHERE status = ? AND 1=0', $result->sql()); + $this->assertEquals('UPDATE `users` SET `status` = ? WHERE `status` = ? AND 1=0', $result->sql()); $this->assertEquals(['inactive', 'active'], $result->bindings()); } @@ -297,7 +305,7 @@ public function testUpdateWithWhereBetween(): void ->where('price')->between(100, 500) ->sql('sqlite', false); - $expected = "UPDATE `products` SET `discount` = 10 WHERE price BETWEEN 100 AND 500"; + $expected = "UPDATE `products` SET `discount` = 10 WHERE `price` BETWEEN 100 AND 500"; $this->assertEquals($expected, $sql); } @@ -310,7 +318,7 @@ public function testUpdateWithWhereIsNull(): void ->where('verified_at')->isNull() ->sql('sqlite', false); - $expected = "UPDATE `users` SET `verified` = 0 WHERE verified_at IS NULL"; + $expected = "UPDATE `users` SET `verified` = 0 WHERE `verified_at` IS NULL"; $this->assertEquals($expected, $sql); } @@ -323,7 +331,7 @@ public function testUpdateWithWhereIsNotNull(): void ->where('email')->isNotNull() ->sql('sqlite', false); - $expected = "UPDATE `users` SET `active` = 1 WHERE email IS NOT NULL"; + $expected = "UPDATE `users` SET `active` = 1 WHERE `email` IS NOT NULL"; $this->assertEquals($expected, $sql); } @@ -337,7 +345,7 @@ public function testUpdateWithAndCondition(): void ->and('created_at')->lower('2020-01-01') ->sql('sqlite', false); - $expected = "UPDATE `users` SET `status` = 'archived' WHERE active = 0 AND created_at < '2020-01-01'"; + $expected = "UPDATE `users` SET `status` = 'archived' WHERE `active` = 0 AND `created_at` < '2020-01-01'"; $this->assertEquals($expected, $sql); } @@ -351,7 +359,7 @@ public function testUpdateWithOrCondition(): void ->or('role')->equals('moderator') ->sql('sqlite', false); - $expected = "UPDATE `users` SET `notification` = 1 WHERE role = 'admin' OR role = 'moderator'"; + $expected = "UPDATE `users` SET `notification` = 1 WHERE `role` = 'admin' OR `role` = 'moderator'"; $this->assertEquals($expected, $sql); } @@ -367,7 +375,7 @@ public function testUpdateWithComplexWhereConditions(): void ->and('verified')->equals(1, ')') ->sql('sqlite', false); - $expected = "UPDATE `users` SET `priority` = 'high' WHERE (status = 'active' AND role = 'premium') OR (score > 1000 AND verified = 1)"; + $expected = "UPDATE `users` SET `priority` = 'high' WHERE (`status` = 'active' AND `role` = 'premium') OR (`score` > 1000 AND `verified` = 1)"; $this->assertEquals($expected, $sql); } @@ -394,7 +402,7 @@ public function testUpdateWithAndJsonCondition(): void ->andJson('settings')->extract('$.premium')->equals(true) ->sql('sqlite', false); - $expected = "UPDATE `users` SET `featured` = 1 WHERE status = 'active' AND json_extract(`settings`, '$.premium') = 1"; + $expected = "UPDATE `users` SET `featured` = 1 WHERE `status` = 'active' AND json_extract(`settings`, '$.premium') = 1"; $this->assertEquals($expected, $sql); } @@ -408,7 +416,7 @@ public function testUpdateWithOrJsonCondition(): void ->orJson('attributes')->extract('$.discontinued')->equals(true) ->sql('sqlite', false); - $expected = "UPDATE `products` SET `visible` = 0 WHERE stock = 0 OR json_extract(`attributes`, '$.discontinued') = 1"; + $expected = "UPDATE `products` SET `visible` = 0 WHERE `stock` = 0 OR json_extract(`attributes`, '$.discontinued') = 1"; $this->assertEquals($expected, $sql); } @@ -427,7 +435,7 @@ public function testUpdateWithExists(): void ->where()->exists($subquery) ->sql('sqlite', false); - $expected = "UPDATE `users` SET `has_pending_orders` = 1 WHERE EXISTS (SELECT 1 FROM `orders` WHERE orders.user_id = users.id AND orders.status = 'pending')"; + $expected = "UPDATE `users` SET `has_pending_orders` = 1 WHERE EXISTS (SELECT 1 FROM `orders` WHERE `orders`.`user_id` = users.id AND `orders`.`status` = 'pending')"; $this->assertEquals($expected, $sql); } @@ -445,7 +453,7 @@ public function testUpdateWithNotExists(): void ->where()->notExists($subquery) ->sql('sqlite', false); - $expected = "UPDATE `users` SET `no_orders` = 1 WHERE NOT EXISTS (SELECT 1 FROM `orders` WHERE orders.user_id = users.id)"; + $expected = "UPDATE `users` SET `no_orders` = 1 WHERE NOT EXISTS (SELECT 1 FROM `orders` WHERE `orders`.`user_id` = users.id)"; $this->assertEquals($expected, $sql); } @@ -458,7 +466,7 @@ public function testUpdateWithExpressionInSet(): void ->where('id')->equals(1) ->sql('sqlite', false); - $expected = "UPDATE `users` SET `updated_at` = datetime('now') WHERE id = 1"; + $expected = "UPDATE `users` SET `updated_at` = datetime('now') WHERE `id` = 1"; $this->assertEquals($expected, $sql); } @@ -473,7 +481,7 @@ public function testUpdateWithExpressionInSetPrepared(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertEquals("UPDATE `users` SET `updated_at` = datetime('now'), `name` = ? WHERE id = ?", $result->sql()); + $this->assertEquals("UPDATE `users` SET `updated_at` = datetime('now'), `name` = ? WHERE `id` = ?", $result->sql()); $this->assertEquals(['John', 1], $result->bindings()); } @@ -499,7 +507,7 @@ public function testUpdateWithTableAlias(): void ->where('u.id')->equals(1) ->sql('sqlite', false); - $expected = "UPDATE `users` `u` SET `u.updated_at` = '2024-01-01' WHERE u.id = 1"; + $expected = "UPDATE `users` `u` SET `u.updated_at` = '2024-01-01' WHERE `u`.`id` = 1"; $this->assertEquals($expected, $sql); } diff --git a/tests/Unit/Query/IdentifierAutoQuotingTest.php b/tests/Unit/Query/IdentifierAutoQuotingTest.php new file mode 100644 index 0000000..03130c0 --- /dev/null +++ b/tests/Unit/Query/IdentifierAutoQuotingTest.php @@ -0,0 +1,380 @@ + + */ + public static function dialects(): array + { + return [ + 'mysql' => ['mysql', '`'], + 'postgres' => ['postgres', '"'], + 'sqlite' => ['sqlite', '`'], + ]; + } + + private function q(string $quoteChar, string $identifier): string + { + return $quoteChar . $identifier . $quoteChar; + } + + // ==================== camelCase over all clauses, all drivers ==================== + + #[DataProvider('dialects')] + public function testCamelCaseWhereIsQuoted(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('*') + ->from('orders') + ->where('createdAt')->greater('2024-01-01') + ->sql($dialect); + + $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); + $this->assertStringContainsString( + 'WHERE ' . $this->q($qc, 'createdAt') . ' > ?', + $prepared->sql() + ); + } + + #[DataProvider('dialects')] + public function testCamelCaseAndOrAreQuoted(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('*') + ->from('orders') + ->where('buyerName')->equals('X') + ->and('createdAt')->greater('2024-01-01') + ->or('updatedAt')->isNull() + ->sql($dialect); + + $sql = $prepared->sql(); + $this->assertStringContainsString('WHERE ' . $this->q($qc, 'buyerName') . ' = ?', $sql); + $this->assertStringContainsString('AND ' . $this->q($qc, 'createdAt') . ' > ?', $sql); + $this->assertStringContainsString('OR ' . $this->q($qc, 'updatedAt') . ' IS NULL', $sql); + } + + #[DataProvider('dialects')] + public function testCamelCaseOrderByIsQuoted(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('*') + ->from('orders') + ->orderBy('createdAt', 'DESC') + ->sql($dialect); + + $this->assertStringContainsString( + 'ORDER BY ' . $this->q($qc, 'createdAt') . ' DESC', + $prepared->sql() + ); + } + + #[DataProvider('dialects')] + public function testCamelCaseGroupByIsQuoted(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('buyerName, COUNT(*) AS orderCount') + ->from('orders') + ->groupBy('buyerName') + ->sql($dialect); + + $sql = $prepared->sql(); + $this->assertStringContainsString('GROUP BY ' . $this->q($qc, 'buyerName'), $sql); + // SELECT list: simple identifier quoted, function raw, alias quoted + $this->assertStringContainsString( + 'SELECT ' . $this->q($qc, 'buyerName') . ', COUNT(*) AS ' . $this->q($qc, 'orderCount'), + $sql + ); + } + + #[DataProvider('dialects')] + public function testCamelCaseSelectFieldListIsQuoted(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('id, buyerName, createdAt') + ->from('orders') + ->sql($dialect); + + $this->assertStringContainsString( + 'SELECT ' . $this->q($qc, 'id') . ', ' . $this->q($qc, 'buyerName') + . ', ' . $this->q($qc, 'createdAt'), + $prepared->sql() + ); + } + + #[DataProvider('dialects')] + public function testCamelCaseHavingIsQuoted(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('buyerName, COUNT(*) AS orderCount') + ->from('orders') + ->groupBy('buyerName') + ->having('orderCount')->greater(5) + ->sql($dialect); + + $this->assertStringContainsString( + 'HAVING ' . $this->q($qc, 'orderCount') . ' > ?', + $prepared->sql() + ); + } + + #[DataProvider('dialects')] + public function testCamelCaseUpdateWhereAndOrderByAreQuoted(string $dialect, string $qc): void + { + $update = (new DbUpdate()) + ->table('orders') + ->set('buyerName', 'X') + ->where('createdAt')->greater('2024-01-01'); + + if ($dialect === 'mysql') { + // ORDER BY in UPDATE is MySQL-only + $update->orderBy('createdAt', 'ASC')->limit(10); + } + + $prepared = $update->sql($dialect); + $sql = $prepared->sql(); + + $this->assertStringContainsString('WHERE ' . $this->q($qc, 'createdAt') . ' > ?', $sql); + if ($dialect === 'mysql') { + $this->assertStringContainsString('ORDER BY ' . $this->q($qc, 'createdAt') . ' ASC', $sql); + } + } + + #[DataProvider('dialects')] + public function testCamelCaseDeleteWhereIsQuoted(string $dialect, string $qc): void + { + $prepared = (new DbDelete()) + ->from('orders') + ->where('createdAt')->lower('2020-01-01') + ->sql($dialect); + + $this->assertStringContainsString( + 'WHERE ' . $this->q($qc, 'createdAt') . ' < ?', + $prepared->sql() + ); + } + + // ==================== alias.ident ==================== + + #[DataProvider('dialects')] + public function testAliasDotIdentIsQuotedPerPart(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('*') + ->from('orders', 'o') + ->where('o.createdAt')->greater('2024-01-01') + ->orderBy('o.createdAt') + ->sql($dialect); + + $sql = $prepared->sql(); + $qualified = $this->q($qc, 'o') . '.' . $this->q($qc, 'createdAt'); + $this->assertStringContainsString('WHERE ' . $qualified . ' > ?', $sql); + $this->assertStringContainsString('ORDER BY ' . $qualified . ' ASC', $sql); + } + + // ==================== expression regression: everything else stays raw ==================== + + #[DataProvider('dialects')] + public function testFunctionExpressionInWhereStaysRaw(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('*') + ->from('orders') + ->where('YEAR(created)')->equals(2024) + ->sql($dialect); + + $this->assertStringContainsString('WHERE YEAR(created) = ?', $prepared->sql()); + } + + #[DataProvider('dialects')] + public function testExpressionRawFieldStaysRawEvenWhenSimple(string $dialect, string $qc): void + { + // Expression::raw is the escape hatch: even a simple identifier + // must stay byte-identical raw. + $prepared = (new DbQuery()) + ->select('*') + ->from('orders') + ->where(Expression::raw('createdAt'))->greater('2024-01-01') + ->sql($dialect); + + $this->assertStringContainsString('WHERE createdAt > ?', $prepared->sql()); + } + + #[DataProvider('dialects')] + public function testAlreadyQuotedFieldStaysRaw(string $dialect, string $qc): void + { + $field = $qc . 'createdAt' . $qc; + $prepared = (new DbQuery()) + ->select('*') + ->from('orders') + ->where($field)->greater('2024-01-01') + ->sql($dialect); + + $this->assertStringContainsString('WHERE ' . $field . ' > ?', $prepared->sql()); + } + + #[DataProvider('dialects')] + public function testStarSelectStaysRaw(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('*') + ->from('orders') + ->sql($dialect); + + $this->assertStringContainsString('SELECT *', $prepared->sql()); + } + + #[DataProvider('dialects')] + public function testOrderByExpressionStaysRaw(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('*') + ->from('orders') + ->orderBy('YEAR(created)', 'DESC') + ->sql($dialect); + + $this->assertStringContainsString('ORDER BY YEAR(created) DESC', $prepared->sql()); + } + + #[DataProvider('dialects')] + public function testGroupByExpressionStaysRaw(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('*') + ->from('orders') + ->groupBy('YEAR(created)') + ->sql($dialect); + + $this->assertStringContainsString('GROUP BY YEAR(created)', $prepared->sql()); + } + + #[DataProvider('dialects')] + public function testHavingExpressionStaysRaw(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('dept, COUNT(*) AS cnt') + ->from('emp') + ->groupBy('dept') + ->having('COUNT(*)')->greater(5) + ->sql($dialect); + + $this->assertStringContainsString('HAVING COUNT(*) > ?', $prepared->sql()); + } + + // ==================== keyword exceptions ==================== + + #[DataProvider('dialects')] + public function testUnionPaddingNullLiteralStaysRaw(string $dialect, string $qc): void + { + // UNION padding: NULL is an SQL literal, not a column - must stay raw + // even though it matches the identifier pattern. + $left = (new DbQuery()) + ->select('id, NULL AS email') + ->from('guests'); + $prepared = $left + ->unionAll( + (new DbQuery())->select('id, email')->from('users') + ) + ->sql($dialect); + + $sql = $prepared->sql(); + $this->assertStringContainsString( + 'SELECT ' . $this->q($qc, 'id') . ', NULL AS ' . $this->q($qc, 'email'), + $sql + ); + $this->assertStringNotContainsString($this->q($qc, 'NULL'), $sql); + } + + #[DataProvider('dialects')] + public function testKeywordLiteralsStayRawInSelectAndWhere(string $dialect, string $qc): void + { + $prepared = (new DbQuery()) + ->select('id, CURRENT_TIMESTAMP AS fetchedAt, TRUE AS isActive') + ->from('orders') + ->where('createdAt')->lower(Expression::raw('CURRENT_DATE')) + ->sql($dialect); + + $sql = $prepared->sql(); + $this->assertStringContainsString('CURRENT_TIMESTAMP AS ' . $this->q($qc, 'fetchedAt'), $sql); + $this->assertStringContainsString('TRUE AS ' . $this->q($qc, 'isActive'), $sql); + $this->assertStringContainsString('WHERE ' . $this->q($qc, 'createdAt') . ' < CURRENT_DATE', $sql); + } + + #[DataProvider('dialects')] + public function testColumnLiterallyNamedNullStaysRawWithQuotedEscapeHatch(string $dialect, string $qc): void + { + // A column literally named "null" is NOT auto-quoted (keyword + // exception, case-insensitive) - it stays raw ... + $prepared = (new DbQuery()) + ->select('*') + ->from('t') + ->where('null')->equals(1) + ->sql($dialect); + $this->assertStringContainsString('WHERE null = ?', $prepared->sql()); + + // ... the escape hatch is passing it pre-quoted (stays byte-identical). + $preQuoted = $qc . 'null' . $qc; + $prepared = (new DbQuery()) + ->select('*') + ->from('t') + ->where($preQuoted)->equals(1) + ->sql($dialect); + $this->assertStringContainsString('WHERE ' . $preQuoted . ' = ?', $prepared->sql()); + } + + #[DataProvider('dialects')] + public function testQualifiedKeywordNameIsStillQuoted(string $dialect, string $qc): void + { + // Qualified names are always identifiers - `t.null` is a column. + $prepared = (new DbQuery()) + ->select('*') + ->from('t') + ->where('t.null')->equals(1) + ->sql($dialect); + + $this->assertStringContainsString( + 'WHERE ' . $this->q($qc, 't') . '.' . $this->q($qc, 'null') . ' = ?', + $prepared->sql() + ); + } + + #[DataProvider('dialects')] + public function testEmptyInWithEmbeddedBracketFieldKeepsBrackets(string $dialect, string $qc): void + { + // Field with embedded opening bracket is not a simple identifier; + // the bracket-preserving 1=0 replacement must keep working. + $prepared = (new DbQuery()) + ->select('*') + ->from('orders') + ->where('status')->equals('open') + ->and('(id')->in([], ')') + ->sql($dialect); + + $this->assertStringContainsString('AND (1=0)', $prepared->sql()); + } +} diff --git a/tests/Unit/Query/Mysql/DbQueryMySqlBasicTest.php b/tests/Unit/Query/Mysql/DbQueryMySqlBasicTest.php index 2ea3fea..e23b05d 100644 --- a/tests/Unit/Query/Mysql/DbQueryMySqlBasicTest.php +++ b/tests/Unit/Query/Mysql/DbQueryMySqlBasicTest.php @@ -13,6 +13,14 @@ * * Tests: Constructor, SELECT, FROM, DISTINCT, Alias */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryMySqlBasicTest extends TestCase { public function testConstructor(): void @@ -35,14 +43,14 @@ public function testSelectWithSingleField(): void { $query = new DbQuery(); $sql = $query->select('id')->from('users')->sql('mysql', false); - $this->assertEquals('SELECT id FROM `users`', $sql); + $this->assertEquals('SELECT `id` FROM `users`', $sql); } public function testSelectWithMultipleFields(): void { $query = new DbQuery(); $sql = $query->select('id, name, email')->from('users')->sql('mysql', false); - $this->assertEquals('SELECT id, name, email FROM `users`', $sql); + $this->assertEquals('SELECT `id`, `name`, `email` FROM `users`', $sql); } public function testDistinct(): void @@ -52,7 +60,7 @@ public function testDistinct(): void $this->assertSame($query, $result); $sql = $query->select('name')->from('users')->sql('mysql', false); - $this->assertEquals('SELECT DISTINCT name FROM `users`', $sql); + $this->assertEquals('SELECT DISTINCT `name` FROM `users`', $sql); } public function testFromWithTableName(): void diff --git a/tests/Unit/Query/Mysql/DbQueryMySqlCteTest.php b/tests/Unit/Query/Mysql/DbQueryMySqlCteTest.php index 2a7cd7d..172142d 100644 --- a/tests/Unit/Query/Mysql/DbQueryMySqlCteTest.php +++ b/tests/Unit/Query/Mysql/DbQueryMySqlCteTest.php @@ -20,6 +20,14 @@ * * Tests: WITH, WITH RECURSIVE */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryMySqlCteTest extends TestCase { public function testWithSingleCte(): void @@ -36,14 +44,14 @@ public function testWithSingleCte(): void ->from('active_users') ->sql('mysql', false); - $expected = "WITH `active_users` AS (SELECT id, name FROM `users` WHERE status = 'active') " + $expected = "WITH `active_users` AS (SELECT `id`, `name` FROM `users` WHERE `status` = 'active') " . "SELECT * FROM `active_users`"; $this->assertEquals($expected, $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "WITH `active_users` AS (SELECT id, name FROM `users` WHERE status = ?) " + $expectedPrepared = "WITH `active_users` AS (SELECT `id`, `name` FROM `users` WHERE `status` = ?) " . "SELECT * FROM `active_users`"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['active'], $prepared->bindings()); @@ -69,8 +77,8 @@ public function testWithMultipleCtes(): void ->from('active_users') ->sql('mysql', false); - $expected = "WITH `active_users` AS (SELECT id, name FROM `users` WHERE status = 'active'), " - . "`user_posts` AS (SELECT user_id, COUNT(*) as post_count FROM `posts` GROUP BY user_id) " + $expected = "WITH `active_users` AS (SELECT `id`, `name` FROM `users` WHERE `status` = 'active'), " + . "`user_posts` AS (SELECT `user_id`, COUNT(*) as `post_count` FROM `posts` GROUP BY `user_id`) " . "SELECT * FROM `active_users`"; $this->assertEquals($expected, $sql); @@ -91,8 +99,8 @@ public function testWithCteUsedInJoin(): void ->innerJoin('user_stats', 'u.id = s.user_id', 's') ->sql('mysql', false); - $expected = "WITH `user_stats` AS (SELECT user_id, COUNT(*) as post_count FROM `posts` GROUP BY user_id) " - . "SELECT u.name, s.post_count FROM `users` `u` INNER JOIN `user_stats` `s` ON u.id = s.user_id"; + $expected = "WITH `user_stats` AS (SELECT `user_id`, COUNT(*) as `post_count` FROM `posts` GROUP BY `user_id`) " + . "SELECT `u`.`name`, `s`.`post_count` FROM `users` `u` INNER JOIN `user_stats` `s` ON u.id = s.user_id"; $this->assertEquals($expected, $sql); } @@ -114,9 +122,9 @@ public function testWithCteContainingJoin(): void ->sql('mysql', false); $expected = "WITH `user_summary` AS " - . "(SELECT u.id, u.name, COUNT(p.id) as post_count FROM `users` `u` " - . "LEFT JOIN `posts` `p` ON u.id = p.user_id GROUP BY u.id, u.name) " - . "SELECT * FROM `user_summary` WHERE post_count > 5"; + . "(SELECT `u`.`id`, `u`.`name`, COUNT(p.id) as `post_count` FROM `users` `u` " + . "LEFT JOIN `posts` `p` ON u.id = p.user_id GROUP BY `u`.`id`, `u`.`name`) " + . "SELECT * FROM `user_summary` WHERE `post_count` > 5"; $this->assertEquals($expected, $sql); } @@ -137,16 +145,16 @@ public function testWithCteAndMainQueryWhere(): void ->sql('mysql', false); $expected = "WITH `recent_users` AS " - . "(SELECT * FROM `users` WHERE created_at > '2024-01-01') " - . "SELECT id, name FROM `recent_users` WHERE status = 'verified'"; + . "(SELECT * FROM `users` WHERE `created_at` > '2024-01-01') " + . "SELECT `id`, `name` FROM `recent_users` WHERE `status` = 'verified'"; $this->assertEquals($expected, $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $expectedPrepared = "WITH `recent_users` AS " - . "(SELECT * FROM `users` WHERE created_at > ?) " - . "SELECT id, name FROM `recent_users` WHERE status = ?"; + . "(SELECT * FROM `users` WHERE `created_at` > ?) " + . "SELECT `id`, `name` FROM `recent_users` WHERE `status` = ?"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['2024-01-01', 'verified'], $prepared->bindings()); } @@ -168,8 +176,8 @@ public function testWithCteAndOrderByLimit(): void ->sql('mysql', false); $expected = "WITH `user_totals` AS " - . "(SELECT user_id, SUM(amount) as total FROM `orders` GROUP BY user_id) " - . "SELECT * FROM `user_totals` ORDER BY total DESC LIMIT 10"; + . "(SELECT `user_id`, SUM(amount) as `total` FROM `orders` GROUP BY `user_id`) " + . "SELECT * FROM `user_totals` ORDER BY `total` DESC LIMIT 10"; $this->assertEquals($expected, $sql); } @@ -196,8 +204,8 @@ public function testWithMultipleCtesCascading(): void ->from('verified_users') ->sql('mysql', false); - $expected = "WITH `active_users` AS (SELECT id, name, status FROM `users` WHERE active = 1), " - . "`verified_users` AS (SELECT id, name FROM `active_users` WHERE status = 'verified') " + $expected = "WITH `active_users` AS (SELECT `id`, `name`, `status` FROM `users` WHERE `active` = 1), " + . "`verified_users` AS (SELECT `id`, `name` FROM `active_users` WHERE `status` = 'verified') " . "SELECT * FROM `verified_users`"; $this->assertEquals($expected, $sql); @@ -224,9 +232,9 @@ public function testWithCteAndMultipleJoins(): void ->innerJoin('post_counts', 'u.id = p.user_id', 'p') ->sql('mysql', false); - $expected = "WITH `active_users` AS (SELECT id, name FROM `users` WHERE active = 1), " - . "`post_counts` AS (SELECT user_id, COUNT(*) as count FROM `posts` GROUP BY user_id) " - . "SELECT u.name, p.count FROM `active_users` `u` INNER JOIN `post_counts` `p` ON u.id = p.user_id"; + $expected = "WITH `active_users` AS (SELECT `id`, `name` FROM `users` WHERE `active` = 1), " + . "`post_counts` AS (SELECT `user_id`, COUNT(*) as `count` FROM `posts` GROUP BY `user_id`) " + . "SELECT `u`.`name`, `p`.`count` FROM `active_users` `u` INNER JOIN `post_counts` `p` ON u.id = p.user_id"; $this->assertEquals($expected, $sql); } @@ -265,10 +273,10 @@ public function testWithCteContainingSubquery(): void ->sql('mysql', false); $this->assertStringContainsString('WITH `user_stats` AS', $sql); - $this->assertStringContainsString('SELECT id, name,', $sql); + $this->assertStringContainsString('SELECT `id`, `name`,', $sql); $this->assertStringContainsString('SELECT COUNT(*)', $sql); $this->assertStringContainsString('AS `post_count`', $sql); - $this->assertStringContainsString('FROM `user_stats` WHERE post_count > 10', $sql); + $this->assertStringContainsString('FROM `user_stats` WHERE `post_count` > 10', $sql); } // ==================== WITH RECURSIVE Tests ==================== @@ -295,8 +303,8 @@ public function testWithRecursiveSingleCte(): void ->sql('mysql', false); $this->assertStringStartsWith('WITH RECURSIVE `category_tree` AS', $sql); - $this->assertStringContainsString('SELECT id, name, parent_id, 1 as level', $sql); - $this->assertStringContainsString('WHERE parent_id IS NULL', $sql); + $this->assertStringContainsString('SELECT `id`, `name`, `parent_id`, 1 as `level`', $sql); + $this->assertStringContainsString('WHERE `parent_id` IS NULL', $sql); $this->assertStringContainsString('UNION', $sql); $this->assertStringContainsString('SELECT * FROM `category_tree`', $sql); } @@ -324,9 +332,9 @@ public function testWithRecursiveEmployeeHierarchy(): void ->sql('mysql', false); $this->assertStringContainsString('WITH RECURSIVE `hierarchy` AS', $sql); - $this->assertStringContainsString('0 as depth', $sql); + $this->assertStringContainsString('0 as `depth`', $sql); $this->assertStringContainsString('h.depth + 1', $sql); - $this->assertStringContainsString('ORDER BY depth ASC', $sql); + $this->assertStringContainsString('ORDER BY `depth` ASC', $sql); } public function testWithRecursiveMixedWithNormalCte(): void @@ -384,7 +392,7 @@ public function testWithRecursiveAndWhere(): void ->sql('mysql', false); $this->assertStringContainsString('WITH RECURSIVE `node_tree` AS', $sql); - $this->assertStringContainsString('FROM `node_tree` WHERE depth <= 3', $sql); + $this->assertStringContainsString('FROM `node_tree` WHERE `depth` <= 3', $sql); } public function testWithRecursiveAndLimit(): void @@ -447,7 +455,7 @@ public function testWithRecursivePathTracking(): void ->sql('mysql', false); $this->assertStringContainsString('WITH RECURSIVE `folder_paths` AS', $sql); - $this->assertStringContainsString('CAST(name AS CHAR(1000)) as path', $sql); + $this->assertStringContainsString('CAST(name AS CHAR(1000)) as `path`', $sql); $this->assertStringContainsString("CONCAT(p.path, '/', f.name)", $sql); } @@ -540,10 +548,10 @@ public function testWithRecursiveComplexHierarchyWithDepthLimit(): void ->sql('mysql', false); $this->assertStringContainsString('WITH RECURSIVE `org_hierarchy` AS', $sql); - $this->assertStringContainsString('1 as level', $sql); + $this->assertStringContainsString('1 as `level`', $sql); $this->assertStringContainsString('h.level + 1', $sql); - $this->assertStringContainsString('WHERE h.level < 5', $sql); - $this->assertStringContainsString('ORDER BY level ASC, name ASC', $sql); + $this->assertStringContainsString('WHERE `h`.`level` < 5', $sql); + $this->assertStringContainsString('ORDER BY `level` ASC, `name` ASC', $sql); } public function testRecursiveCteWithBindingsInPreparedMode(): void @@ -567,7 +575,7 @@ public function testRecursiveCteWithBindingsInPreparedMode(): void $this->assertStringContainsString('WITH RECURSIVE', $result->sql()); $this->assertStringContainsString('`emp_hierarchy` AS', $result->sql()); $this->assertStringContainsString('SELECT * FROM `departments`', $result->sql()); - $this->assertStringContainsString('WHERE id = ?', $result->sql()); + $this->assertStringContainsString('WHERE `id` = ?', $result->sql()); // Verify binding order: CTE bindings first, then main query bindings $bindings = $result->bindings(); diff --git a/tests/Unit/Query/Mysql/DbQueryMySqlExistsTest.php b/tests/Unit/Query/Mysql/DbQueryMySqlExistsTest.php index a1f2244..1e71449 100644 --- a/tests/Unit/Query/Mysql/DbQueryMySqlExistsTest.php +++ b/tests/Unit/Query/Mysql/DbQueryMySqlExistsTest.php @@ -15,6 +15,14 @@ * * Tests: EXISTS, NOT EXISTS, complex subqueries with EXISTS */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryMySqlExistsTest extends TestCase { public function testExistsSimple(): void @@ -31,7 +39,7 @@ public function testExistsSimple(): void ->exists($subquery) ->sql('mysql', false); - $expected = "SELECT * FROM `users` WHERE EXISTS (SELECT 1 FROM `posts` `p` WHERE p.user_id = users.id)"; + $expected = "SELECT * FROM `users` WHERE EXISTS (SELECT 1 FROM `posts` `p` WHERE `p`.`user_id` = users.id)"; $this->assertEquals($expected, $sql); } @@ -50,7 +58,7 @@ public function testNotExistsSimple(): void ->notExists($subquery) ->sql('mysql', false); - $expected = "SELECT * FROM `users` WHERE NOT EXISTS (SELECT 1 FROM `posts` `p` WHERE p.user_id = users.id)"; + $expected = "SELECT * FROM `users` WHERE NOT EXISTS (SELECT 1 FROM `posts` `p` WHERE `p`.`user_id` = users.id)"; $this->assertEquals($expected, $sql); } @@ -70,7 +78,7 @@ public function testPreparedExists(): void ->sql('mysql', true); $expected = "SELECT * FROM `users` " . - "WHERE active = ? AND EXISTS (SELECT 1 FROM `orders` WHERE orders.user_id = users.id AND orders.status = ?)"; + "WHERE `active` = ? AND EXISTS (SELECT 1 FROM `orders` WHERE `orders`.`user_id` = users.id AND `orders`.`status` = ?)"; $this->assertEquals($expected, $result->sql()); $this->assertEquals([1, 'completed'], $result->bindings()); @@ -92,17 +100,17 @@ public function testExistsWithComplexSubquery(): void ->exists($subquery) ->sql('mysql', false); - $expected = "SELECT u.id, u.name FROM `users` `u` " + $expected = "SELECT `u`.`id`, `u`.`name` FROM `users` `u` " . "WHERE EXISTS (SELECT 1 FROM `orders` `o` " - . "WHERE o.user_id = u.id AND o.status = 'completed' AND o.total > 1000)"; + . "WHERE `o`.`user_id` = u.id AND `o`.`status` = 'completed' AND `o`.`total` > 1000)"; $this->assertEquals($expected, $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT u.id, u.name FROM `users` `u` " + $expectedPrepared = "SELECT `u`.`id`, `u`.`name` FROM `users` `u` " . "WHERE EXISTS (SELECT 1 FROM `orders` `o` " - . "WHERE o.user_id = u.id AND o.status = ? AND o.total > ?)"; + . "WHERE `o`.`user_id` = u.id AND `o`.`status` = ? AND `o`.`total` > ?)"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['completed', 1000], $prepared->bindings()); } @@ -124,7 +132,7 @@ public function testNotExistsWithComplexSubquery(): void $expected = "SELECT * FROM `users` " . "WHERE NOT EXISTS (SELECT 1 FROM `violations` `v` " - . "WHERE v.user_id = users.id AND v.severity = 'critical')"; + . "WHERE `v`.`user_id` = users.id AND `v`.`severity` = 'critical')"; $this->assertEquals($expected, $sql); @@ -132,7 +140,7 @@ public function testNotExistsWithComplexSubquery(): void $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $expectedPrepared = "SELECT * FROM `users` " . "WHERE NOT EXISTS (SELECT 1 FROM `violations` `v` " - . "WHERE v.user_id = users.id AND v.severity = ?)"; + . "WHERE `v`.`user_id` = users.id AND `v`.`severity` = ?)"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['critical'], $prepared->bindings()); } @@ -155,12 +163,12 @@ public function testExistsWithJoinInSubquery(): void $this->assertStringContainsString('WHERE EXISTS (SELECT 1 FROM `posts` `p`', $sql); $this->assertStringContainsString('INNER JOIN `comments` `c`', $sql); - $this->assertStringContainsString("c.status = 'approved'", $sql); + $this->assertStringContainsString("`c`.`status` = 'approved'", $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $this->assertStringContainsString('WHERE EXISTS (SELECT 1 FROM `posts` `p`', $prepared->sql()); - $this->assertStringContainsString('c.status = ?', $prepared->sql()); + $this->assertStringContainsString('`c`.`status` = ?', $prepared->sql()); $this->assertEquals(['approved'], $prepared->bindings()); } @@ -180,14 +188,14 @@ public function testExistsCombinedWithWhereConditions(): void ->sql('mysql', false); $expected = "SELECT * FROM `users` `u` " - . "WHERE u.active = 1 AND EXISTS (SELECT 1 FROM `orders` `o` WHERE o.user_id = u.id)"; + . "WHERE `u`.`active` = 1 AND EXISTS (SELECT 1 FROM `orders` `o` WHERE `o`.`user_id` = u.id)"; $this->assertEquals($expected, $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $expectedPrepared = "SELECT * FROM `users` `u` " - . "WHERE u.active = ? AND EXISTS (SELECT 1 FROM `orders` `o` WHERE o.user_id = u.id)"; + . "WHERE `u`.`active` = ? AND EXISTS (SELECT 1 FROM `orders` `o` WHERE `o`.`user_id` = u.id)"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals([1], $prepared->bindings()); } @@ -208,14 +216,14 @@ public function testNotExistsCombinedWithWhereConditions(): void ->sql('mysql', false); $expected = "SELECT * FROM `users` `u` " - . "WHERE u.status = 'active' AND NOT EXISTS (SELECT 1 FROM `bans` `b` WHERE b.user_id = u.id)"; + . "WHERE `u`.`status` = 'active' AND NOT EXISTS (SELECT 1 FROM `bans` `b` WHERE `b`.`user_id` = u.id)"; $this->assertEquals($expected, $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $expectedPrepared = "SELECT * FROM `users` `u` " - . "WHERE u.status = ? AND NOT EXISTS (SELECT 1 FROM `bans` `b` WHERE b.user_id = u.id)"; + . "WHERE `u`.`status` = ? AND NOT EXISTS (SELECT 1 FROM `bans` `b` WHERE `b`.`user_id` = u.id)"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['active'], $prepared->bindings()); } @@ -235,12 +243,12 @@ public function testExistsWithBrackets(): void ->exists($subquery, ')') ->sql('mysql', false); - $this->assertStringContainsString('WHERE (u.age > 18 AND EXISTS', $sql); + $this->assertStringContainsString('WHERE (`u`.`age` > 18 AND EXISTS', $sql); $this->assertStringContainsString('))', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertStringContainsString('WHERE (u.age > ?', $prepared->sql()); + $this->assertStringContainsString('WHERE (`u`.`age` > ?', $prepared->sql()); $this->assertEquals([18], $prepared->bindings()); } @@ -259,11 +267,11 @@ public function testNotExistsWithBrackets(): void ->notExists($subquery, ')') ->sql('mysql', false); - $this->assertStringContainsString('WHERE (u.verified = 1 AND NOT EXISTS', $sql); + $this->assertStringContainsString('WHERE (`u`.`verified` = 1 AND NOT EXISTS', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertStringContainsString('WHERE (u.verified = ?', $prepared->sql()); + $this->assertStringContainsString('WHERE (`u`.`verified` = ?', $prepared->sql()); $this->assertEquals([1], $prepared->bindings()); } @@ -364,7 +372,7 @@ public function testExistsWithAggregateInSubquery(): void ->sql('mysql', false); $this->assertStringContainsString('WHERE EXISTS (SELECT 1 FROM `orders`', $sql); - $this->assertStringContainsString('GROUP BY o.user_id', $sql); + $this->assertStringContainsString('GROUP BY `o`.`user_id`', $sql); $this->assertStringContainsString('HAVING COUNT(*) > 5', $sql); $prepared = $query->sql('mysql', true); @@ -436,12 +444,12 @@ public function testNotExistsWithNestedSubquery(): void ->sql('mysql', false); $this->assertStringContainsString('WHERE NOT EXISTS', $sql); - $this->assertStringContainsString('IN (SELECT order_id FROM', $sql); + $this->assertStringContainsString('IN (SELECT `order_id` FROM', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $this->assertStringContainsString('WHERE NOT EXISTS', $prepared->sql()); - $this->assertStringContainsString('IN (SELECT order_id FROM', $prepared->sql()); + $this->assertStringContainsString('IN (SELECT `order_id` FROM', $prepared->sql()); $this->assertEquals(['approved'], $prepared->bindings()); } @@ -463,13 +471,13 @@ public function testExistsWithMultipleCorrelations(): void ->sql('mysql', false); $this->assertStringContainsString('WHERE EXISTS', $sql); - $this->assertStringContainsString('up.user_id = u.id', $sql); - $this->assertStringContainsString('up.resource_type = r.type', $sql); + $this->assertStringContainsString('`up`.`user_id` = u.id', $sql); + $this->assertStringContainsString('`up`.`resource_type` = r.type', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $this->assertStringContainsString('WHERE EXISTS', $prepared->sql()); - $this->assertStringContainsString('up.permission = ?', $prepared->sql()); + $this->assertStringContainsString('`up`.`permission` = ?', $prepared->sql()); $this->assertEquals(['write'], $prepared->bindings()); } } diff --git a/tests/Unit/Query/Mysql/DbQueryMySqlGroupOrderLimitTest.php b/tests/Unit/Query/Mysql/DbQueryMySqlGroupOrderLimitTest.php index 0143be7..f0c7e33 100644 --- a/tests/Unit/Query/Mysql/DbQueryMySqlGroupOrderLimitTest.php +++ b/tests/Unit/Query/Mysql/DbQueryMySqlGroupOrderLimitTest.php @@ -13,6 +13,14 @@ * * Tests: GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryMySqlGroupOrderLimitTest extends TestCase { public function testGroupBySingleColumn(): void @@ -24,7 +32,7 @@ public function testGroupBySingleColumn(): void ->groupBy('status') ->sql('mysql', false); - $this->assertEquals('SELECT status, COUNT(*) as count FROM `users` GROUP BY status', $sql); + $this->assertEquals('SELECT `status`, COUNT(*) as `count` FROM `users` GROUP BY `status`', $sql); } public function testGroupByMultipleColumns(): void @@ -36,7 +44,7 @@ public function testGroupByMultipleColumns(): void ->groupBy('status', 'role') ->sql('mysql', false); - $this->assertEquals('SELECT status, role, COUNT(*) FROM `users` GROUP BY status, role', $sql); + $this->assertEquals('SELECT `status`, `role`, COUNT(*) FROM `users` GROUP BY `status`, `role`', $sql); } public function testHavingWithCount(): void @@ -49,11 +57,11 @@ public function testHavingWithCount(): void ->having('COUNT(*)')->greater(5) ->sql('mysql', false); - $this->assertEquals('SELECT status, COUNT(*) as count FROM `users` GROUP BY status HAVING COUNT(*) > 5', $sql); + $this->assertEquals('SELECT `status`, COUNT(*) as `count` FROM `users` GROUP BY `status` HAVING COUNT(*) > 5', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT status, COUNT(*) as count FROM `users` GROUP BY status HAVING COUNT(*) > ?', $prepared->sql()); + $this->assertEquals('SELECT `status`, COUNT(*) as `count` FROM `users` GROUP BY `status` HAVING COUNT(*) > ?', $prepared->sql()); $this->assertEquals([5], $prepared->bindings()); } @@ -68,11 +76,11 @@ public function testHavingWithMultipleConditions(): void ->and('COUNT(*)')->lower(100) ->sql('mysql', false); - $this->assertEquals('SELECT status, COUNT(*) as count FROM `users` GROUP BY status HAVING COUNT(*) > 5 AND COUNT(*) < 100', $sql); + $this->assertEquals('SELECT `status`, COUNT(*) as `count` FROM `users` GROUP BY `status` HAVING COUNT(*) > 5 AND COUNT(*) < 100', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT status, COUNT(*) as count FROM `users` GROUP BY status HAVING COUNT(*) > ? AND COUNT(*) < ?', $prepared->sql()); + $this->assertEquals('SELECT `status`, COUNT(*) as `count` FROM `users` GROUP BY `status` HAVING COUNT(*) > ? AND COUNT(*) < ?', $prepared->sql()); $this->assertEquals([5, 100], $prepared->bindings()); } @@ -85,7 +93,7 @@ public function testOrderBySingleColumn(): void ->orderBy('name') ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` ORDER BY name ASC', $sql); + $this->assertEquals('SELECT * FROM `users` ORDER BY `name` ASC', $sql); } public function testOrderByDescending(): void @@ -97,7 +105,7 @@ public function testOrderByDescending(): void ->orderBy('created_at', 'DESC') ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` ORDER BY created_at DESC', $sql); + $this->assertEquals('SELECT * FROM `users` ORDER BY `created_at` DESC', $sql); } public function testOrderByMultipleColumns(): void @@ -110,7 +118,7 @@ public function testOrderByMultipleColumns(): void ->orderBy('name', 'DESC') ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` ORDER BY status ASC, name DESC', $sql); + $this->assertEquals('SELECT * FROM `users` ORDER BY `status` ASC, `name` DESC', $sql); } public function testLimitOnly(): void @@ -151,24 +159,24 @@ public function testCompleteQueryWithAllClauses(): void ->limit(10) ->sql('mysql', false); - $expected = "SELECT u.id, u.name, COUNT(p.id) as post_count FROM `users` `u` " + $expected = "SELECT `u`.`id`, `u`.`name`, COUNT(p.id) as `post_count` FROM `users` `u` " . "LEFT JOIN `posts` `p` ON u.id = p.user_id " - . "WHERE u.status = 'active' " - . "GROUP BY u.id, u.name " + . "WHERE `u`.`status` = 'active' " + . "GROUP BY `u`.`id`, `u`.`name` " . "HAVING COUNT(p.id) > 5 " - . "ORDER BY post_count DESC " + . "ORDER BY `post_count` DESC " . "LIMIT 10"; $this->assertEquals($expected, $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT u.id, u.name, COUNT(p.id) as post_count FROM `users` `u` " + $expectedPrepared = "SELECT `u`.`id`, `u`.`name`, COUNT(p.id) as `post_count` FROM `users` `u` " . "LEFT JOIN `posts` `p` ON u.id = p.user_id " - . "WHERE u.status = ? " - . "GROUP BY u.id, u.name " + . "WHERE `u`.`status` = ? " + . "GROUP BY `u`.`id`, `u`.`name` " . "HAVING COUNT(p.id) > ? " - . "ORDER BY post_count DESC " + . "ORDER BY `post_count` DESC " . "LIMIT 10"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['active', 5], $prepared->bindings()); diff --git a/tests/Unit/Query/Mysql/DbQueryMySqlJoinTest.php b/tests/Unit/Query/Mysql/DbQueryMySqlJoinTest.php index 9938ac5..45fb04c 100644 --- a/tests/Unit/Query/Mysql/DbQueryMySqlJoinTest.php +++ b/tests/Unit/Query/Mysql/DbQueryMySqlJoinTest.php @@ -13,6 +13,14 @@ * * Tests: INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryMySqlJoinTest extends TestCase { public function testInnerJoinWithConstraint(): void @@ -138,9 +146,9 @@ public function testJoinSubqueryWithBindingsInPreparedMode(): void $this->assertStringContainsString('FROM `users` `u`', $result->sql()); $this->assertStringContainsString('LEFT JOIN (SELECT', $result->sql()); $this->assertStringContainsString('FROM `orders`', $result->sql()); - $this->assertStringContainsString('WHERE status = ?', $result->sql()); + $this->assertStringContainsString('WHERE `status` = ?', $result->sql()); $this->assertStringContainsString(') `o` ON u.id = o.user_id', $result->sql()); - $this->assertStringContainsString('WHERE u.active = ?', $result->sql()); + $this->assertStringContainsString('WHERE `u`.`active` = ?', $result->sql()); // Verify binding order: JOIN subquery bindings first, then main query bindings $bindings = $result->bindings(); @@ -169,8 +177,8 @@ public function testJoinSubqueryInNonPreparedMode(): void $this->assertIsString($sql); $this->assertStringContainsString('FROM `users` `u`', $sql); - $this->assertStringContainsString('LEFT JOIN (SELECT user_id, COUNT(*) as cnt FROM `orders` WHERE status = \'paid\'', $sql); + $this->assertStringContainsString('LEFT JOIN (SELECT `user_id`, COUNT(*) as `cnt` FROM `orders` WHERE `status` = \'paid\'', $sql); $this->assertStringContainsString('`o` ON u.id = o.user_id', $sql); - $this->assertStringContainsString('WHERE u.active = 1', $sql); + $this->assertStringContainsString('WHERE `u`.`active` = 1', $sql); } } diff --git a/tests/Unit/Query/Mysql/DbQueryMySqlSubqueryTest.php b/tests/Unit/Query/Mysql/DbQueryMySqlSubqueryTest.php index eca012e..d78afa9 100644 --- a/tests/Unit/Query/Mysql/DbQueryMySqlSubqueryTest.php +++ b/tests/Unit/Query/Mysql/DbQueryMySqlSubqueryTest.php @@ -15,6 +15,14 @@ * * Tests: SELECT subqueries, correlated subqueries, nested subqueries */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryMySqlSubqueryTest extends TestCase { public function testSelectSubquerySingleSimple(): void @@ -31,12 +39,12 @@ public function testSelectSubquerySingleSimple(): void ->from('users') ->sql('mysql', false); - $expected = "SELECT id, name, (SELECT COUNT(*) FROM `posts` WHERE user_id = 1) AS `post_count` FROM `users`"; + $expected = "SELECT `id`, `name`, (SELECT COUNT(*) FROM `posts` WHERE `user_id` = 1) AS `post_count` FROM `users`"; $this->assertEquals($expected, $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals("SELECT id, name, (SELECT COUNT(*) FROM `posts` WHERE user_id = ?) AS `post_count` FROM `users`", $prepared->sql()); + $this->assertEquals("SELECT `id`, `name`, (SELECT COUNT(*) FROM `posts` WHERE `user_id` = ?) AS `post_count` FROM `users`", $prepared->sql()); $this->assertEquals([1], $prepared->bindings()); } @@ -54,7 +62,7 @@ public function testSelectSubqueryWithCorrelatedCondition(): void ->from('users') ->sql('mysql', false); - $expected = "SELECT id, name, (SELECT COUNT(*) FROM `posts` `p` WHERE p.user_id = users.id) AS `post_count` FROM `users`"; + $expected = "SELECT `id`, `name`, (SELECT COUNT(*) FROM `posts` `p` WHERE `p`.`user_id` = users.id) AS `post_count` FROM `users`"; $this->assertEquals($expected, $sql); } @@ -78,9 +86,9 @@ public function testSelectSubqueryMultiple(): void ->from('users') ->sql('mysql', false); - $expected = "SELECT id, name, " - . "(SELECT COUNT(*) FROM `posts` `p` WHERE p.user_id = users.id) AS `post_count`, " - . "(SELECT COUNT(*) FROM `comments` `c` WHERE c.user_id = users.id) AS `comment_count` " + $expected = "SELECT `id`, `name`, " + . "(SELECT COUNT(*) FROM `posts` `p` WHERE `p`.`user_id` = users.id) AS `post_count`, " + . "(SELECT COUNT(*) FROM `comments` `c` WHERE `c`.`user_id` = users.id) AS `comment_count` " . "FROM `users`"; $this->assertEquals($expected, $sql); @@ -119,17 +127,17 @@ public function testSelectSubqueryWithComplexSubquery(): void ->where('active')->equals(1) ->sql('mysql', false); - $expected = "SELECT id, name, email, " - . "(SELECT MAX(p.created_at) FROM `posts` `p` WHERE p.user_id = users.id AND p.status = 'published') AS `last_post_date` " - . "FROM `users` WHERE active = 1"; + $expected = "SELECT `id`, `name`, `email`, " + . "(SELECT MAX(p.created_at) FROM `posts` `p` WHERE `p`.`user_id` = users.id AND `p`.`status` = 'published') AS `last_post_date` " + . "FROM `users` WHERE `active` = 1"; $this->assertEquals($expected, $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT id, name, email, " - . "(SELECT MAX(p.created_at) FROM `posts` `p` WHERE p.user_id = users.id AND p.status = ?) AS `last_post_date` " - . "FROM `users` WHERE active = ?"; + $expectedPrepared = "SELECT `id`, `name`, `email`, " + . "(SELECT MAX(p.created_at) FROM `posts` `p` WHERE `p`.`user_id` = users.id AND `p`.`status` = ?) AS `last_post_date` " + . "FROM `users` WHERE `active` = ?"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['published', 1], $prepared->bindings()); } @@ -149,8 +157,8 @@ public function testSelectSubqueryWithJoinInSubquery(): void ->from('users') ->sql('mysql', false); - $expected = "SELECT id, name, " - . "(SELECT COUNT(DISTINCT c.id) FROM `posts` `p` INNER JOIN `comments` `c` ON p.id = c.post_id WHERE p.user_id = users.id) AS `comment_count` " + $expected = "SELECT `id`, `name`, " + . "(SELECT COUNT(DISTINCT c.id) FROM `posts` `p` INNER JOIN `comments` `c` ON p.id = c.post_id WHERE `p`.`user_id` = users.id) AS `comment_count` " . "FROM `users`"; $this->assertEquals($expected, $sql); @@ -171,8 +179,8 @@ public function testSelectSubqueryWithMainQueryJoin(): void ->innerJoin('profiles', 'u.id = prof.user_id', 'prof') ->sql('mysql', false); - $expected = "SELECT u.id, u.name, " - . "(SELECT COUNT(*) FROM `posts` `p` WHERE p.user_id = u.id) AS `post_count` " + $expected = "SELECT `u`.`id`, `u`.`name`, " + . "(SELECT COUNT(*) FROM `posts` `p` WHERE `p`.`user_id` = u.id) AS `post_count` " . "FROM `users` `u` INNER JOIN `profiles` `prof` ON u.id = prof.user_id"; $this->assertEquals($expected, $sql); @@ -194,9 +202,9 @@ public function testSelectSubqueryWithOrderByAndLimit(): void ->limit(10) ->sql('mysql', false); - $expected = "SELECT id, name, " - . "(SELECT COUNT(*) FROM `posts` `p` WHERE p.user_id = users.id) AS `post_count` " - . "FROM `users` ORDER BY name ASC LIMIT 10"; + $expected = "SELECT `id`, `name`, " + . "(SELECT COUNT(*) FROM `posts` `p` WHERE `p`.`user_id` = users.id) AS `post_count` " + . "FROM `users` ORDER BY `name` ASC LIMIT 10"; $this->assertEquals($expected, $sql); } @@ -217,25 +225,25 @@ public function testSelectSubqueryWithGroupByAndHaving(): void ->having('COUNT(*)')->greater(5) ->sql('mysql', false); - $expected = "SELECT c.id, c.name, COUNT(*) as total, " - . "(SELECT AVG(p.rating) FROM `posts` `p` WHERE p.category_id = c.id) AS `avg_rating` " - . "FROM `categories` `c` GROUP BY c.id, c.name HAVING COUNT(*) > 5"; + $expected = "SELECT `c`.`id`, `c`.`name`, COUNT(*) as `total`, " + . "(SELECT AVG(p.rating) FROM `posts` `p` WHERE `p`.`category_id` = c.id) AS `avg_rating` " + . "FROM `categories` `c` GROUP BY `c`.`id`, `c`.`name` HAVING COUNT(*) > 5"; $this->assertEquals($expected, $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT c.id, c.name, COUNT(*) as total, " - . "(SELECT AVG(p.rating) FROM `posts` `p` WHERE p.category_id = c.id) AS `avg_rating` " - . "FROM `categories` `c` GROUP BY c.id, c.name HAVING COUNT(*) > ?"; + $expectedPrepared = "SELECT `c`.`id`, `c`.`name`, COUNT(*) as `total`, " + . "(SELECT AVG(p.rating) FROM `posts` `p` WHERE `p`.`category_id` = c.id) AS `avg_rating` " + . "FROM `categories` `c` GROUP BY `c`.`id`, `c`.`name` HAVING COUNT(*) > ?"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals([5], $prepared->bindings()); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT c.id, c.name, COUNT(*) as total, " - . "(SELECT AVG(p.rating) FROM `posts` `p` WHERE p.category_id = c.id) AS `avg_rating` " - . "FROM `categories` `c` GROUP BY c.id, c.name HAVING COUNT(*) > ?"; + $expectedPrepared = "SELECT `c`.`id`, `c`.`name`, COUNT(*) as `total`, " + . "(SELECT AVG(p.rating) FROM `posts` `p` WHERE `p`.`category_id` = c.id) AS `avg_rating` " + . "FROM `categories` `c` GROUP BY `c`.`id`, `c`.`name` HAVING COUNT(*) > ?"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals([5], $prepared->bindings()); } @@ -275,7 +283,7 @@ public function testSelectSubqueryWithNestedSubquery(): void ->sql('mysql', false); // Note: This creates a very complex nested structure - $this->assertStringContainsString('SELECT id, name,', $sql); + $this->assertStringContainsString('SELECT `id`, `name`,', $sql); $this->assertStringContainsString('SELECT COUNT(*)', $sql); $this->assertStringContainsString('SELECT MAX(created_at)', $sql); $this->assertStringContainsString('AS `post_info`', $sql); @@ -300,9 +308,9 @@ public function testFromSubqueryWithBindingsInPreparedMode(): void $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); $this->assertStringContainsString('FROM (SELECT', $result->sql()); $this->assertStringContainsString('FROM `users`', $result->sql()); - $this->assertStringContainsString('WHERE status = ?', $result->sql()); + $this->assertStringContainsString('WHERE `status` = ?', $result->sql()); $this->assertStringContainsString(') `sub`', $result->sql()); - $this->assertStringContainsString('WHERE sub.id > ?', $result->sql()); + $this->assertStringContainsString('WHERE `sub`.`id` > ?', $result->sql()); // Verify binding order: FROM subquery bindings first, then main query bindings $bindings = $result->bindings(); @@ -328,8 +336,8 @@ public function testFromSubqueryInNonPreparedMode(): void $sql = $query->sql('mysql', false); $this->assertIsString($sql); - $this->assertStringContainsString('FROM (SELECT id, name FROM `users` WHERE status = \'active\')', $sql); + $this->assertStringContainsString('FROM (SELECT `id`, `name` FROM `users` WHERE `status` = \'active\')', $sql); $this->assertStringContainsString('`sub`', $sql); - $this->assertStringContainsString('WHERE sub.id > 5', $sql); + $this->assertStringContainsString('WHERE `sub`.`id` > 5', $sql); } } diff --git a/tests/Unit/Query/Mysql/DbQueryMySqlUnionTest.php b/tests/Unit/Query/Mysql/DbQueryMySqlUnionTest.php index c6cdb0c..8cb27a0 100644 --- a/tests/Unit/Query/Mysql/DbQueryMySqlUnionTest.php +++ b/tests/Unit/Query/Mysql/DbQueryMySqlUnionTest.php @@ -14,6 +14,14 @@ * * Tests: UNION, UNION ALL */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryMySqlUnionTest extends TestCase { public function testUnionSimple(): void @@ -31,22 +39,22 @@ public function testUnionSimple(): void ->union($secondQuery) ->sql('mysql', false); - $expected = "SELECT id, name FROM `customers` WHERE country = 'Germany' " - . "UNION SELECT id, name FROM `suppliers` WHERE country = 'Germany'"; + $expected = "SELECT `id`, `name` FROM `customers` WHERE `country` = 'Germany' " + . "UNION SELECT `id`, `name` FROM `suppliers` WHERE `country` = 'Germany'"; $this->assertEquals($expected, $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT id, name FROM `customers` WHERE country = ? " - . "UNION SELECT id, name FROM `suppliers` WHERE country = ?"; + $expectedPrepared = "SELECT `id`, `name` FROM `customers` WHERE `country` = ? " + . "UNION SELECT `id`, `name` FROM `suppliers` WHERE `country` = ?"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['Germany', 'Germany'], $prepared->bindings()); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT id, name FROM `customers` WHERE country = ? " - . "UNION SELECT id, name FROM `suppliers` WHERE country = ?"; + $expectedPrepared = "SELECT `id`, `name` FROM `customers` WHERE `country` = ? " + . "UNION SELECT `id`, `name` FROM `suppliers` WHERE `country` = ?"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['Germany', 'Germany'], $prepared->bindings()); } @@ -64,7 +72,7 @@ public function testUnionAllSimple(): void ->unionAll($secondQuery) ->sql('mysql', false); - $expected = "SELECT name FROM `active_users` UNION ALL SELECT name FROM `archived_users`"; + $expected = "SELECT `name` FROM `active_users` UNION ALL SELECT `name` FROM `archived_users`"; $this->assertEquals($expected, $sql); } @@ -87,9 +95,9 @@ public function testUnionMultipleQueries(): void ->union($thirdQuery) ->sql('mysql', false); - $expected = "SELECT email FROM `customers` " - . "UNION SELECT email FROM `suppliers` " - . "UNION SELECT email FROM `partners`"; + $expected = "SELECT `email` FROM `customers` " + . "UNION SELECT `email` FROM `suppliers` " + . "UNION SELECT `email` FROM `partners`"; $this->assertEquals($expected, $sql); } @@ -113,9 +121,9 @@ public function testUnionAllMultipleQueries(): void ->unionAll($q3) ->sql('mysql', false); - $expected = "SELECT id, type FROM `quotes` " - . "UNION ALL SELECT id, type FROM `orders` WHERE status = 'completed' " - . "UNION ALL SELECT id, type FROM `invoices`"; + $expected = "SELECT `id`, `type` FROM `quotes` " + . "UNION ALL SELECT `id`, `type` FROM `orders` WHERE `status` = 'completed' " + . "UNION ALL SELECT `id`, `type` FROM `invoices`"; $this->assertEquals($expected, $sql); } @@ -138,9 +146,9 @@ public function testUnionMixedWithUnionAll(): void ->unionAll($q3) ->sql('mysql', false); - $expected = "SELECT name FROM `table1` " - . "UNION SELECT name FROM `table2` " - . "UNION ALL SELECT name FROM `table3`"; + $expected = "SELECT `name` FROM `table1` " + . "UNION SELECT `name` FROM `table2` " + . "UNION ALL SELECT `name` FROM `table3`"; $this->assertEquals($expected, $sql); } @@ -160,10 +168,10 @@ public function testUnionWithOrderBy(): void ->sql('mysql', false); // Verify components are present in correct order - $this->assertStringContainsString('SELECT name, created_at FROM `active_posts`', $sql); + $this->assertStringContainsString('SELECT `name`, `created_at` FROM `active_posts`', $sql); $this->assertStringContainsString('UNION', $sql); - $this->assertStringContainsString('SELECT name, created_at FROM `archived_posts`', $sql); - $this->assertStringContainsString('ORDER BY created_at DESC', $sql); + $this->assertStringContainsString('SELECT `name`, `created_at` FROM `archived_posts`', $sql); + $this->assertStringContainsString('ORDER BY `created_at` DESC', $sql); // Verify ORDER BY comes after UNION $unionPos = strpos($sql, 'UNION'); @@ -185,8 +193,8 @@ public function testUnionWithLimit(): void ->limit(20) ->sql('mysql', false); - $expected = "SELECT title FROM `news_posts` " - . "UNION SELECT title FROM `blog_posts` " + $expected = "SELECT `title` FROM `news_posts` " + . "UNION SELECT `title` FROM `blog_posts` " . "LIMIT 20"; $this->assertEquals($expected, $sql); @@ -208,9 +216,9 @@ public function testUnionWithOrderByAndLimit(): void ->limit(50) ->sql('mysql', false); - $expected = "SELECT id, priority, title FROM `active_tasks` " - . "UNION SELECT id, priority, title FROM `pending_tasks` " - . "ORDER BY priority DESC, title ASC " + $expected = "SELECT `id`, `priority`, `title` FROM `active_tasks` " + . "UNION SELECT `id`, `priority`, `title` FROM `pending_tasks` " + . "ORDER BY `priority` DESC, `title` ASC " . "LIMIT 50"; $this->assertEquals($expected, $sql); @@ -244,10 +252,10 @@ public function testUnionWithComplexQueries(): void ->sql('mysql', false); $this->assertStringContainsString('UNION', $sql); - $this->assertStringContainsString("u.type = 'premium'", $sql); - $this->assertStringContainsString("u.type = 'standard'", $sql); + $this->assertStringContainsString("`u`.`type` = 'premium'", $sql); + $this->assertStringContainsString("`u`.`type` = 'standard'", $sql); $this->assertStringContainsString('GROUP BY', $sql); - $this->assertStringContainsString('ORDER BY order_count DESC', $sql); + $this->assertStringContainsString('ORDER BY `order_count` DESC', $sql); } public function testUnionReturnsQueryBuilder(): void @@ -296,8 +304,8 @@ public function testUnionWithDifferentColumns(): void ->union($secondQuery) ->sql('mysql', false); - $expected = "SELECT customer_id as id, customer_name as name FROM `customers` " - . "UNION SELECT supplier_id as id, company_name as name FROM `suppliers`"; + $expected = "SELECT `customer_id` as `id`, `customer_name` as `name` FROM `customers` " + . "UNION SELECT `supplier_id` as `id`, `company_name` as `name` FROM `suppliers`"; $this->assertEquals($expected, $sql); } @@ -319,18 +327,18 @@ public function testUnionWithWhereInBothQueries(): void ->union($secondQuery) ->sql('mysql', false); - $this->assertStringContainsString("WHERE status = 'active' AND created_at > '2024-01-01'", $sql); - $this->assertStringContainsString("WHERE status = 'inactive' AND created_at > '2023-01-01'", $sql); + $this->assertStringContainsString("WHERE `status` = 'active' AND `created_at` > '2024-01-01'", $sql); + $this->assertStringContainsString("WHERE `status` = 'inactive' AND `created_at` > '2023-01-01'", $sql); $this->assertStringContainsString('UNION', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertStringContainsString('WHERE status = ? AND created_at > ?', $prepared->sql()); + $this->assertStringContainsString('WHERE `status` = ? AND `created_at` > ?', $prepared->sql()); $this->assertEquals(['active', '2024-01-01', 'inactive', '2023-01-01'], $prepared->bindings()); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertStringContainsString('WHERE status = ? AND created_at > ?', $prepared->sql()); + $this->assertStringContainsString('WHERE `status` = ? AND `created_at` > ?', $prepared->sql()); $this->assertStringContainsString('UNION', $prepared->sql()); $this->assertEquals(['active', '2024-01-01', 'inactive', '2023-01-01'], $prepared->bindings()); } @@ -352,11 +360,11 @@ public function testUnionFiveQueries(): void ->unionAll($q5) ->sql('mysql', false); - $expected = "SELECT name FROM `t1` " - . "UNION SELECT name FROM `t2` " - . "UNION SELECT name FROM `t3` " - . "UNION ALL SELECT name FROM `t4` " - . "UNION ALL SELECT name FROM `t5`"; + $expected = "SELECT `name` FROM `t1` " + . "UNION SELECT `name` FROM `t2` " + . "UNION SELECT `name` FROM `t3` " + . "UNION ALL SELECT `name` FROM `t4` " + . "UNION ALL SELECT `name` FROM `t5`"; $this->assertEquals($expected, $sql); } @@ -379,10 +387,10 @@ public function testUnionAllWithBindingsInPreparedMode(): void $result = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertStringContainsString('SELECT id, name FROM `employees`', $result->sql()); - $this->assertStringContainsString('WHERE country = ?', $result->sql()); + $this->assertStringContainsString('SELECT `id`, `name` FROM `employees`', $result->sql()); + $this->assertStringContainsString('WHERE `country` = ?', $result->sql()); $this->assertStringContainsString('UNION ALL', $result->sql()); - $this->assertStringContainsString('SELECT id, name FROM `suppliers`', $result->sql()); + $this->assertStringContainsString('SELECT `id`, `name` FROM `suppliers`', $result->sql()); // Verify binding order: main query bindings first, then UNION ALL bindings $bindings = $result->bindings(); diff --git a/tests/Unit/Query/Mysql/DbQueryMySqlWhereTest.php b/tests/Unit/Query/Mysql/DbQueryMySqlWhereTest.php index e0327cf..98faf7c 100644 --- a/tests/Unit/Query/Mysql/DbQueryMySqlWhereTest.php +++ b/tests/Unit/Query/Mysql/DbQueryMySqlWhereTest.php @@ -14,6 +14,14 @@ * * Tests: WHERE conditions, operators, AND, OR, brackets */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryMySqlWhereTest extends TestCase { public function testWhereEquals(): void @@ -25,11 +33,11 @@ public function testWhereEquals(): void ->where('id')->equals(123) ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` WHERE id = 123', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `id` = 123', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE id = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `id` = ?', $prepared->sql()); $this->assertEquals([123], $prepared->bindings()); } @@ -42,11 +50,11 @@ public function testWhereWithString(): void ->where('name')->equals('John') ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE name = 'John'", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `name` = 'John'", $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE name = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `name` = ?', $prepared->sql()); $this->assertEquals(['John'], $prepared->bindings()); } @@ -59,11 +67,11 @@ public function testWhereGreater(): void ->where('age')->greater(18) ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` WHERE age > 18', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `age` > 18', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age > ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` > ?', $prepared->sql()); $this->assertEquals([18], $prepared->bindings()); } @@ -76,11 +84,11 @@ public function testWhereLower(): void ->where('age')->lower(65) ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` WHERE age < 65', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `age` < 65', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age < ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` < ?', $prepared->sql()); $this->assertEquals([65], $prepared->bindings()); } @@ -93,11 +101,11 @@ public function testWhereBetween(): void ->where('age')->between(18, 65) ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` WHERE age BETWEEN 18 AND 65', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `age` BETWEEN 18 AND 65', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age BETWEEN ? AND ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` BETWEEN ? AND ?', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); } @@ -110,11 +118,11 @@ public function testWhereIn(): void ->where('status')->in(['active', 'pending']) ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE status IN ('active', 'pending')", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` IN ('active', 'pending')", $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE status IN (?, ?)', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `status` IN (?, ?)', $prepared->sql()); $this->assertEquals(['active', 'pending'], $prepared->bindings()); } @@ -162,10 +170,10 @@ public function testWhereInEmptyArrayWithOtherConditions(): void ->and('id')->in([]) ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE status = 'active' AND 1=0", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` = 'active' AND 1=0", $sql); $prepared = $query->sql('mysql', true); - $this->assertEquals('SELECT * FROM `users` WHERE status = ? AND 1=0', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `status` = ? AND 1=0', $prepared->sql()); $this->assertEquals(['active'], $prepared->bindings()); } @@ -179,7 +187,7 @@ public function testWhereInEmptyArrayWithOrCondition(): void ->or('id')->in([]) ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE status = 'active' OR 1=0", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` = 'active' OR 1=0", $sql); } public function testWhereInEmptyArrayWithBrackets(): void @@ -192,7 +200,7 @@ public function testWhereInEmptyArrayWithBrackets(): void ->and('(id')->in([], ')') ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE status = 'active' AND (1=0)", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` = 'active' AND (1=0)", $sql); } public function testWhereNotInEmptyArrayWithOtherConditions(): void @@ -205,7 +213,7 @@ public function testWhereNotInEmptyArrayWithOtherConditions(): void ->and('id')->notIn([]) ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE status = 'active' AND 1=1", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` = 'active' AND 1=1", $sql); } public function testWhereLike(): void @@ -217,11 +225,11 @@ public function testWhereLike(): void ->where('name')->like('%John%') ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE name LIKE '%John%'", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `name` LIKE '%John%'", $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE name LIKE ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `name` LIKE ?', $prepared->sql()); $this->assertEquals(['%John%'], $prepared->bindings()); } @@ -234,7 +242,7 @@ public function testWhereIsNull(): void ->where('deleted_at')->isNull() ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` WHERE deleted_at IS NULL', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `deleted_at` IS NULL', $sql); } public function testWhereIsNotNull(): void @@ -246,7 +254,7 @@ public function testWhereIsNotNull(): void ->where('email')->isNotNull() ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` WHERE email IS NOT NULL', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `email` IS NOT NULL', $sql); } public function testMultipleWhereWithAnd(): void @@ -259,11 +267,11 @@ public function testMultipleWhereWithAnd(): void ->and('status')->equals('active') ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE age > 18 AND status = 'active'", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `age` > 18 AND `status` = 'active'", $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age > ? AND status = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` > ? AND `status` = ?', $prepared->sql()); $this->assertEquals([18, 'active'], $prepared->bindings()); } @@ -277,11 +285,11 @@ public function testWhereWithOr(): void ->or('age')->greater(65) ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` WHERE age < 18 OR age > 65', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `age` < 18 OR `age` > 65', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age < ? OR age > ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` < ? OR `age` > ?', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); } @@ -295,11 +303,11 @@ public function testWhereWithBrackets(): void ->and('age')->lower(65, ')') ->sql('mysql', false); - $this->assertEquals('SELECT * FROM `users` WHERE (age > 18 AND age < 65)', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE (age > 18 AND `age` < 65)', $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE (age > ? AND age < ?)', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE (age > ? AND `age` < ?)', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); } @@ -314,11 +322,11 @@ public function testComplexWhereConditions(): void ->or('role')->equals('admin') ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE (status = 'active' AND age > 18) OR role = 'admin'", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE (status = 'active' AND `age` > 18) OR `role` = 'admin'", $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE (status = ? AND age > ?) OR role = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE (status = ? AND `age` > ?) OR `role` = ?', $prepared->sql()); $this->assertEquals(['active', 18, 'admin'], $prepared->bindings()); } @@ -340,7 +348,7 @@ public function testWhereWithSubqueryInNonPreparedMode(): void $this->assertIsString($sql); $this->assertStringContainsString('SELECT * FROM `users`', $sql); - $this->assertStringContainsString('WHERE id IN (SELECT user_id FROM `orders` WHERE status = \'completed\')', $sql); + $this->assertStringContainsString('WHERE `id` IN (SELECT `user_id` FROM `orders` WHERE `status` = \'completed\')', $sql); } public function testWhereJsonExtract(): void @@ -366,7 +374,7 @@ public function testAndJsonExtract(): void $sql = $query->sql('mysql', false); - $this->assertStringContainsString('WHERE active = 1', $sql); + $this->assertStringContainsString('WHERE `active` = 1', $sql); $this->assertStringContainsString('AND JSON_EXTRACT(`settings`, \'$.theme\') = \'dark\'', $sql); } @@ -393,7 +401,7 @@ public function testOrJsonExtract(): void $sql = $query->sql('mysql', false); - $this->assertStringContainsString('WHERE status = \'active\'', $sql); + $this->assertStringContainsString('WHERE `status` = \'active\'', $sql); $this->assertStringContainsString('OR JSON_EXTRACT(`preferences`, \'$.notifications\') = 1', $sql); } @@ -407,7 +415,7 @@ public function testHavingJson(): void $sql = $query->sql('mysql', false); - $this->assertStringContainsString('GROUP BY user_id', $sql); + $this->assertStringContainsString('GROUP BY `user_id`', $sql); $this->assertStringContainsString('HAVING JSON_EXTRACT(`metadata`, \'$.priority\') = \'high\'', $sql); } @@ -452,11 +460,11 @@ public function testAndWithExpressionField(): void ->and(Expression::raw('YEAR(created_at)'))->equals(2024) ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE status = 'active' AND YEAR(created_at) = 2024", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` = 'active' AND YEAR(created_at) = 2024", $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE status = ? AND YEAR(created_at) = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `status` = ? AND YEAR(created_at) = ?', $prepared->sql()); $this->assertEquals(['active', 2024], $prepared->bindings()); } @@ -470,11 +478,11 @@ public function testOrWithExpressionField(): void ->or(Expression::raw('LOWER(email)'))->like('%john%') ->sql('mysql', false); - $this->assertEquals("SELECT * FROM `users` WHERE name LIKE '%John%' OR LOWER(email) LIKE '%john%'", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `name` LIKE '%John%' OR LOWER(email) LIKE '%john%'", $sql); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE name LIKE ? OR LOWER(email) LIKE ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `name` LIKE ? OR LOWER(email) LIKE ?', $prepared->sql()); $this->assertEquals(['%John%', '%john%'], $prepared->bindings()); } @@ -541,14 +549,14 @@ public function testComplexExpressionConditions(): void ->sql('mysql', false); $this->assertEquals( - 'SELECT * FROM `products` WHERE (stock_level > 0 AND price * 0.8 < 100) OR featured = 1', + 'SELECT * FROM `products` WHERE (stock_level > 0 AND price * 0.8 < 100) OR `featured` = 1', $sql ); $prepared = $query->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $this->assertEquals( - 'SELECT * FROM `products` WHERE (stock_level > ? AND price * 0.8 < ?) OR featured = ?', + 'SELECT * FROM `products` WHERE (stock_level > ? AND price * 0.8 < ?) OR `featured` = ?', $prepared->sql() ); $this->assertEquals([0, 100, 1], $prepared->bindings()); diff --git a/tests/Unit/Query/Mysql/DbQueryMySqlWindowTest.php b/tests/Unit/Query/Mysql/DbQueryMySqlWindowTest.php index 9d23823..0a408d1 100644 --- a/tests/Unit/Query/Mysql/DbQueryMySqlWindowTest.php +++ b/tests/Unit/Query/Mysql/DbQueryMySqlWindowTest.php @@ -14,6 +14,14 @@ * * Tests: Window functions (ROW_NUMBER, RANK, SUM, etc.), Named windows, PARTITION BY, ORDER BY, Frames */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryMySqlWindowTest extends TestCase { public function testSelectWindowWithRowNumber(): void @@ -29,7 +37,7 @@ public function testSelectWindowWithRowNumber(): void $this->assertInstanceOf(DbQuery::class, $result); $sql = $query->sql('mysql', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees`'; + $expected = 'SELECT `id`, `name`, `salary`, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees`'; $this->assertEquals($expected, $sql); } @@ -43,7 +51,7 @@ public function testSelectWindowWithRank(): void ->from('players') ->sql('mysql', false); - $expected = 'SELECT id, score, RANK() OVER (ORDER BY score DESC) AS `rank` FROM `players`'; + $expected = 'SELECT `id`, `score`, RANK() OVER (ORDER BY score DESC) AS `rank` FROM `players`'; $this->assertEquals($expected, $sql); } @@ -57,7 +65,7 @@ public function testSelectWindowWithSumAndArgs(): void ->from('transactions') ->sql('mysql', false); - $expected = 'SELECT date, amount, SUM(amount) OVER (ORDER BY date ASC) AS `running_total` FROM `transactions`'; + $expected = 'SELECT `date`, `amount`, SUM(amount) OVER (ORDER BY date ASC) AS `running_total` FROM `transactions`'; $this->assertEquals($expected, $sql); } @@ -71,7 +79,7 @@ public function testSelectWindowWithLag(): void ->from('stock_prices') ->sql('mysql', false); - $expected = 'SELECT date, price, LAG(price, 1) OVER (ORDER BY date ASC) AS `prev_price` FROM `stock_prices`'; + $expected = 'SELECT `date`, `price`, LAG(price, 1) OVER (ORDER BY date ASC) AS `prev_price` FROM `stock_prices`'; $this->assertEquals($expected, $sql); } @@ -86,7 +94,7 @@ public function testSelectWindowWithMultiplePartitions(): void ->from('employees') ->sql('mysql', false); - $expected = 'SELECT id, name, ROW_NUMBER() OVER (PARTITION BY department, location ORDER BY hire_date ASC) AS `row_num` FROM `employees`'; + $expected = 'SELECT `id`, `name`, ROW_NUMBER() OVER (PARTITION BY department, location ORDER BY hire_date ASC) AS `row_num` FROM `employees`'; $this->assertEquals($expected, $sql); } @@ -102,7 +110,7 @@ public function testSelectWindowWithMultipleOrderBy(): void ->from('employees') ->sql('mysql', false); - $expected = 'SELECT id, name, salary, RANK() OVER (PARTITION BY department ORDER BY salary DESC, hire_date ASC) AS `rank` FROM `employees`'; + $expected = 'SELECT `id`, `name`, `salary`, RANK() OVER (PARTITION BY department ORDER BY salary DESC, hire_date ASC) AS `rank` FROM `employees`'; $this->assertEquals($expected, $sql); } @@ -117,7 +125,7 @@ public function testSelectWindowWithFrame(): void ->from('sales') ->sql('mysql', false); - $expected = 'SELECT date, amount, AVG(amount) OVER (ORDER BY date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS `moving_avg` FROM `sales`'; + $expected = 'SELECT `date`, `amount`, AVG(amount) OVER (ORDER BY date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS `moving_avg` FROM `sales`'; $this->assertEquals($expected, $sql); } @@ -132,7 +140,7 @@ public function testSelectWindowWithRangeFrame(): void ->from('data') ->sql('mysql', false); - $expected = 'SELECT id, value, SUM(value) OVER (ORDER BY id ASC RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS `total` FROM `data`'; + $expected = 'SELECT `id`, `value`, SUM(value) OVER (ORDER BY id ASC RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS `total` FROM `data`'; $this->assertEquals($expected, $sql); } @@ -151,7 +159,7 @@ public function testMultipleWindowFunctions(): void ->from('employees') ->sql('mysql', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num`, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS `rank` FROM `employees`'; + $expected = 'SELECT `id`, `name`, `salary`, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num`, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS `rank` FROM `employees`'; $this->assertEquals($expected, $sql); } @@ -171,7 +179,7 @@ public function testNamedWindow(): void ->from('employees') ->sql('mysql', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER dept_window AS `row_num`, RANK() OVER dept_window AS `rank` FROM `employees` WINDOW dept_window AS (PARTITION BY department ORDER BY salary DESC)'; + $expected = 'SELECT `id`, `name`, `salary`, ROW_NUMBER() OVER dept_window AS `row_num`, RANK() OVER dept_window AS `rank` FROM `employees` WINDOW dept_window AS (PARTITION BY department ORDER BY salary DESC)'; $this->assertEquals($expected, $sql); } @@ -187,7 +195,7 @@ public function testNamedWindowWithArgs(): void ->from('metrics') ->sql('mysql', false); - $expected = 'SELECT timestamp, value, SUM(value) OVER time_window AS `running_total`, LAG(value, 1) OVER time_window AS `prev_value` FROM `metrics` WINDOW time_window AS (ORDER BY timestamp ASC)'; + $expected = 'SELECT `timestamp`, `value`, SUM(value) OVER time_window AS `running_total`, LAG(value, 1) OVER time_window AS `prev_value` FROM `metrics` WINDOW time_window AS (ORDER BY timestamp ASC)'; $this->assertEquals($expected, $sql); } @@ -208,7 +216,7 @@ public function testMultipleNamedWindows(): void ->from('employees') ->sql('mysql', false); - $expected = 'SELECT id, name, ROW_NUMBER() OVER by_dept AS `dept_seniority`, RANK() OVER by_team AS `team_rank` FROM `employees` WINDOW by_dept AS (PARTITION BY department ORDER BY hire_date ASC), by_team AS (PARTITION BY team ORDER BY performance DESC)'; + $expected = 'SELECT `id`, `name`, ROW_NUMBER() OVER by_dept AS `dept_seniority`, RANK() OVER by_team AS `team_rank` FROM `employees` WINDOW by_dept AS (PARTITION BY department ORDER BY hire_date ASC), by_team AS (PARTITION BY team ORDER BY performance DESC)'; $this->assertEquals($expected, $sql); } @@ -232,7 +240,7 @@ public function testWindowWithWhereClause(): void ->where('active')->equals(1) ->sql('mysql', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees` WHERE active = 1'; + $expected = 'SELECT `id`, `name`, `salary`, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees` WHERE `active` = 1'; $this->assertEquals($expected, $sql); } @@ -248,7 +256,7 @@ public function testWindowWithGroupByAndHaving(): void ->having('COUNT(*)')->greater(5) ->sql('mysql', false); - $expected = 'SELECT department, COUNT(*) as emp_count, RANK() OVER (ORDER BY COUNT(*) DESC) AS `dept_rank` FROM `employees` GROUP BY department HAVING COUNT(*) > 5'; + $expected = 'SELECT `department`, COUNT(*) as `emp_count`, RANK() OVER (ORDER BY COUNT(*) DESC) AS `dept_rank` FROM `employees` GROUP BY `department` HAVING COUNT(*) > 5'; $this->assertEquals($expected, $sql); } @@ -265,7 +273,7 @@ public function testWindowFunctionWithPreparedStatement(): void ->sql('mysql', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees` WHERE salary > ?'; + $expected = 'SELECT `id`, `name`, `salary`, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees` WHERE `salary` > ?'; $this->assertEquals($expected, $result->sql()); $this->assertEquals([50000], $result->bindings()); } @@ -280,7 +288,7 @@ public function testWindowWithOnlyPartitionBy(): void ->from('employees') ->sql('mysql', false); - $expected = 'SELECT id, department, COUNT(*) OVER (PARTITION BY department) AS `dept_count` FROM `employees`'; + $expected = 'SELECT `id`, `department`, COUNT(*) OVER (PARTITION BY department) AS `dept_count` FROM `employees`'; $this->assertEquals($expected, $sql); } @@ -294,7 +302,7 @@ public function testWindowWithOnlyOrderBy(): void ->from('data') ->sql('mysql', false); - $expected = 'SELECT id, value, SUM(value) OVER (ORDER BY id ASC) AS `cumulative` FROM `data`'; + $expected = 'SELECT `id`, `value`, SUM(value) OVER (ORDER BY id ASC) AS `cumulative` FROM `data`'; $this->assertEquals($expected, $sql); } @@ -311,7 +319,7 @@ public function testComplexWindowWithAllFeatures(): void ->where('category')->notEquals('void') ->sql('mysql', false); - $expected = 'SELECT date, amount, category, AVG(amount) OVER (PARTITION BY category ORDER BY date ASC ROWS BETWEEN 3 PRECEDING AND 1 FOLLOWING) AS `moving_avg` FROM `transactions` WHERE category != \'void\''; + $expected = 'SELECT `date`, `amount`, `category`, AVG(amount) OVER (PARTITION BY category ORDER BY date ASC ROWS BETWEEN 3 PRECEDING AND 1 FOLLOWING) AS `moving_avg` FROM `transactions` WHERE `category` != \'void\''; $this->assertEquals($expected, $sql); } } diff --git a/tests/Unit/Query/Postgres/DbQueryPostgresBasicTest.php b/tests/Unit/Query/Postgres/DbQueryPostgresBasicTest.php index 764ba4d..f33f442 100644 --- a/tests/Unit/Query/Postgres/DbQueryPostgresBasicTest.php +++ b/tests/Unit/Query/Postgres/DbQueryPostgresBasicTest.php @@ -15,6 +15,14 @@ * * PostgreSQL uses " (double quotes) for identifiers */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryPostgresBasicTest extends TestCase { public function testConstructor(): void @@ -37,14 +45,14 @@ public function testSelectWithSingleField(): void { $query = new DbQuery(); $sql = $query->select('id')->from('users')->sql('postgres', false); - $this->assertEquals('SELECT id FROM "users"', $sql); + $this->assertEquals('SELECT "id" FROM "users"', $sql); } public function testSelectWithMultipleFields(): void { $query = new DbQuery(); $sql = $query->select('id, name, email')->from('users')->sql('postgres', false); - $this->assertEquals('SELECT id, name, email FROM "users"', $sql); + $this->assertEquals('SELECT "id", "name", "email" FROM "users"', $sql); } public function testDistinct(): void @@ -54,7 +62,7 @@ public function testDistinct(): void $this->assertSame($query, $result); $sql = $query->select('name')->from('users')->sql('postgres', false); - $this->assertEquals('SELECT DISTINCT name FROM "users"', $sql); + $this->assertEquals('SELECT DISTINCT "name" FROM "users"', $sql); } public function testFromWithTableName(): void diff --git a/tests/Unit/Query/Postgres/DbQueryPostgresCteTest.php b/tests/Unit/Query/Postgres/DbQueryPostgresCteTest.php index 4a8c71e..09ee035 100644 --- a/tests/Unit/Query/Postgres/DbQueryPostgresCteTest.php +++ b/tests/Unit/Query/Postgres/DbQueryPostgresCteTest.php @@ -20,6 +20,14 @@ * Tests: WITH, WITH RECURSIVE * PostgreSQL uses VARCHAR instead of CHAR for CAST */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryPostgresCteTest extends TestCase { public function testWithSingleCte(): void @@ -36,7 +44,7 @@ public function testWithSingleCte(): void ->from('active_users') ->sql('postgres', false); - $expected = "WITH \"active_users\" AS (SELECT id, name FROM \"users\" WHERE status = 'active') " + $expected = "WITH \"active_users\" AS (SELECT \"id\", \"name\" FROM \"users\" WHERE \"status\" = 'active') " . "SELECT * FROM \"active_users\""; $this->assertEquals($expected, $sql); @@ -62,8 +70,8 @@ public function testWithMultipleCtes(): void ->from('active_users') ->sql('postgres', false); - $expected = "WITH \"active_users\" AS (SELECT id, name FROM \"users\" WHERE status = 'active'), " - . "\"user_posts\" AS (SELECT user_id, COUNT(*) as post_count FROM \"posts\" GROUP BY user_id) " + $expected = "WITH \"active_users\" AS (SELECT \"id\", \"name\" FROM \"users\" WHERE \"status\" = 'active'), " + . "\"user_posts\" AS (SELECT \"user_id\", COUNT(*) as \"post_count\" FROM \"posts\" GROUP BY \"user_id\") " . "SELECT * FROM \"active_users\""; $this->assertEquals($expected, $sql); @@ -84,8 +92,8 @@ public function testWithCteUsedInJoin(): void ->innerJoin('user_stats', 'u.id = s.user_id', 's') ->sql('postgres', false); - $expected = "WITH \"user_stats\" AS (SELECT user_id, COUNT(*) as post_count FROM \"posts\" GROUP BY user_id) " - . "SELECT u.name, s.post_count FROM \"users\" \"u\" INNER JOIN \"user_stats\" \"s\" ON u.id = s.user_id"; + $expected = "WITH \"user_stats\" AS (SELECT \"user_id\", COUNT(*) as \"post_count\" FROM \"posts\" GROUP BY \"user_id\") " + . "SELECT \"u\".\"name\", \"s\".\"post_count\" FROM \"users\" \"u\" INNER JOIN \"user_stats\" \"s\" ON u.id = s.user_id"; $this->assertEquals($expected, $sql); } @@ -107,9 +115,9 @@ public function testWithCteContainingJoin(): void ->sql('postgres', false); $expected = "WITH \"user_summary\" AS " - . "(SELECT u.id, u.name, COUNT(p.id) as post_count FROM \"users\" \"u\" " - . "LEFT JOIN \"posts\" \"p\" ON u.id = p.user_id GROUP BY u.id, u.name) " - . "SELECT * FROM \"user_summary\" WHERE post_count > 5"; + . "(SELECT \"u\".\"id\", \"u\".\"name\", COUNT(p.id) as \"post_count\" FROM \"users\" \"u\" " + . "LEFT JOIN \"posts\" \"p\" ON u.id = p.user_id GROUP BY \"u\".\"id\", \"u\".\"name\") " + . "SELECT * FROM \"user_summary\" WHERE \"post_count\" > 5"; $this->assertEquals($expected, $sql); } @@ -148,8 +156,8 @@ public function testWithRecursiveSingleCte(): void ->sql('postgres', false); $this->assertStringStartsWith('WITH RECURSIVE "category_tree" AS', $sql); - $this->assertStringContainsString('SELECT id, name, parent_id, 1 as level', $sql); - $this->assertStringContainsString('WHERE parent_id IS NULL', $sql); + $this->assertStringContainsString('SELECT "id", "name", "parent_id", 1 as "level"', $sql); + $this->assertStringContainsString('WHERE "parent_id" IS NULL', $sql); $this->assertStringContainsString('UNION', $sql); $this->assertStringContainsString('SELECT * FROM "category_tree"', $sql); } @@ -175,7 +183,7 @@ public function testWithRecursivePathTracking(): void ->sql('postgres', false); $this->assertStringContainsString('WITH RECURSIVE "folder_paths" AS', $sql); - $this->assertStringContainsString('CAST(name AS VARCHAR(1000)) as path', $sql); + $this->assertStringContainsString('CAST(name AS VARCHAR(1000)) as "path"', $sql); $this->assertStringContainsString("CONCAT(p.path, '/', f.name)", $sql); } @@ -213,7 +221,7 @@ public function testRecursiveCteWithBindingsInPreparedMode(): void $this->assertStringContainsString('WITH RECURSIVE', $result->sql()); $this->assertStringContainsString('"emp_hierarchy" AS', $result->sql()); $this->assertStringContainsString('SELECT * FROM "departments"', $result->sql()); - $this->assertStringContainsString('WHERE id = ?', $result->sql()); + $this->assertStringContainsString('WHERE "id" = ?', $result->sql()); // Verify binding order: CTE bindings first, then main query bindings $bindings = $result->bindings(); diff --git a/tests/Unit/Query/Postgres/DbQueryPostgresExistsTest.php b/tests/Unit/Query/Postgres/DbQueryPostgresExistsTest.php index 8443604..dec25af 100644 --- a/tests/Unit/Query/Postgres/DbQueryPostgresExistsTest.php +++ b/tests/Unit/Query/Postgres/DbQueryPostgresExistsTest.php @@ -15,6 +15,14 @@ * * Tests: EXISTS, NOT EXISTS, complex subqueries with EXISTS */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryPostgresExistsTest extends TestCase { public function testExistsSimple(): void @@ -31,7 +39,7 @@ public function testExistsSimple(): void ->exists($subquery) ->sql('postgres', false); - $expected = "SELECT * FROM \"users\" WHERE EXISTS (SELECT 1 FROM \"posts\" \"p\" WHERE p.user_id = users.id)"; + $expected = "SELECT * FROM \"users\" WHERE EXISTS (SELECT 1 FROM \"posts\" \"p\" WHERE \"p\".\"user_id\" = users.id)"; $this->assertEquals($expected, $sql); } @@ -50,7 +58,7 @@ public function testNotExistsSimple(): void ->notExists($subquery) ->sql('postgres', false); - $expected = "SELECT * FROM \"users\" WHERE NOT EXISTS (SELECT 1 FROM \"posts\" \"p\" WHERE p.user_id = users.id)"; + $expected = "SELECT * FROM \"users\" WHERE NOT EXISTS (SELECT 1 FROM \"posts\" \"p\" WHERE \"p\".\"user_id\" = users.id)"; $this->assertEquals($expected, $sql); } @@ -71,25 +79,25 @@ public function testExistsWithComplexSubquery(): void ->exists($subquery) ->sql('postgres', false); - $expected = "SELECT u.id, u.name FROM \"users\" \"u\" " + $expected = "SELECT \"u\".\"id\", \"u\".\"name\" FROM \"users\" \"u\" " . "WHERE EXISTS (SELECT 1 FROM \"orders\" \"o\" " - . "WHERE o.user_id = u.id AND o.status = 'completed' AND o.total > 1000)"; + . "WHERE \"o\".\"user_id\" = u.id AND \"o\".\"status\" = 'completed' AND \"o\".\"total\" > 1000)"; $this->assertEquals($expected, $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT u.id, u.name FROM \"users\" \"u\" " + $expectedPrepared = "SELECT \"u\".\"id\", \"u\".\"name\" FROM \"users\" \"u\" " . "WHERE EXISTS (SELECT 1 FROM \"orders\" \"o\" " - . "WHERE o.user_id = u.id AND o.status = ? AND o.total > ?)"; + . "WHERE \"o\".\"user_id\" = u.id AND \"o\".\"status\" = ? AND \"o\".\"total\" > ?)"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['completed', 1000], $prepared->bindings()); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT u.id, u.name FROM \"users\" \"u\" " + $expectedPrepared = "SELECT \"u\".\"id\", \"u\".\"name\" FROM \"users\" \"u\" " . "WHERE EXISTS (SELECT 1 FROM \"orders\" \"o\" " - . "WHERE o.user_id = u.id AND o.status = ? AND o.total > ?)"; + . "WHERE \"o\".\"user_id\" = u.id AND \"o\".\"status\" = ? AND \"o\".\"total\" > ?)"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['completed', 1000], $prepared->bindings()); } @@ -110,21 +118,21 @@ public function testExistsCombinedWithWhereConditions(): void ->sql('postgres', false); $expected = "SELECT * FROM \"users\" \"u\" " - . "WHERE u.active = 1 AND EXISTS (SELECT 1 FROM \"orders\" \"o\" WHERE o.user_id = u.id)"; + . "WHERE \"u\".\"active\" = 1 AND EXISTS (SELECT 1 FROM \"orders\" \"o\" WHERE \"o\".\"user_id\" = u.id)"; $this->assertEquals($expected, $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $expectedPrepared = "SELECT * FROM \"users\" \"u\" " - . "WHERE u.active = ? AND EXISTS (SELECT 1 FROM \"orders\" \"o\" WHERE o.user_id = u.id)"; + . "WHERE \"u\".\"active\" = ? AND EXISTS (SELECT 1 FROM \"orders\" \"o\" WHERE \"o\".\"user_id\" = u.id)"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals([1], $prepared->bindings()); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $expectedPrepared = "SELECT * FROM \"users\" \"u\" " - . "WHERE u.active = ? AND EXISTS (SELECT 1 FROM \"orders\" \"o\" WHERE o.user_id = u.id)"; + . "WHERE \"u\".\"active\" = ? AND EXISTS (SELECT 1 FROM \"orders\" \"o\" WHERE \"o\".\"user_id\" = u.id)"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals([1], $prepared->bindings()); } @@ -145,21 +153,21 @@ public function testNotExistsCombinedWithWhereConditions(): void ->sql('postgres', false); $expected = "SELECT * FROM \"users\" \"u\" " - . "WHERE u.status = 'active' AND NOT EXISTS (SELECT 1 FROM \"bans\" \"b\" WHERE b.user_id = u.id)"; + . "WHERE \"u\".\"status\" = 'active' AND NOT EXISTS (SELECT 1 FROM \"bans\" \"b\" WHERE \"b\".\"user_id\" = u.id)"; $this->assertEquals($expected, $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $expectedPrepared = "SELECT * FROM \"users\" \"u\" " - . "WHERE u.status = ? AND NOT EXISTS (SELECT 1 FROM \"bans\" \"b\" WHERE b.user_id = u.id)"; + . "WHERE \"u\".\"status\" = ? AND NOT EXISTS (SELECT 1 FROM \"bans\" \"b\" WHERE \"b\".\"user_id\" = u.id)"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['active'], $prepared->bindings()); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $expectedPrepared = "SELECT * FROM \"users\" \"u\" " - . "WHERE u.status = ? AND NOT EXISTS (SELECT 1 FROM \"bans\" \"b\" WHERE b.user_id = u.id)"; + . "WHERE \"u\".\"status\" = ? AND NOT EXISTS (SELECT 1 FROM \"bans\" \"b\" WHERE \"b\".\"user_id\" = u.id)"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['active'], $prepared->bindings()); } diff --git a/tests/Unit/Query/Postgres/DbQueryPostgresGroupOrderLimitTest.php b/tests/Unit/Query/Postgres/DbQueryPostgresGroupOrderLimitTest.php index 92e975d..2a7b2f1 100644 --- a/tests/Unit/Query/Postgres/DbQueryPostgresGroupOrderLimitTest.php +++ b/tests/Unit/Query/Postgres/DbQueryPostgresGroupOrderLimitTest.php @@ -13,6 +13,14 @@ * * Tests: GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryPostgresGroupOrderLimitTest extends TestCase { public function testGroupBySingleColumn(): void @@ -24,7 +32,7 @@ public function testGroupBySingleColumn(): void ->groupBy('status') ->sql('postgres', false); - $this->assertEquals('SELECT status, COUNT(*) as count FROM "users" GROUP BY status', $sql); + $this->assertEquals('SELECT "status", COUNT(*) as "count" FROM "users" GROUP BY "status"', $sql); } public function testGroupByMultipleColumns(): void @@ -36,7 +44,7 @@ public function testGroupByMultipleColumns(): void ->groupBy('status', 'role') ->sql('postgres', false); - $this->assertEquals('SELECT status, role, COUNT(*) FROM "users" GROUP BY status, role', $sql); + $this->assertEquals('SELECT "status", "role", COUNT(*) FROM "users" GROUP BY "status", "role"', $sql); } public function testHavingWithCount(): void @@ -49,11 +57,11 @@ public function testHavingWithCount(): void ->having('COUNT(*)')->greater(5) ->sql('postgres', false); - $this->assertEquals('SELECT status, COUNT(*) as count FROM "users" GROUP BY status HAVING COUNT(*) > 5', $sql); + $this->assertEquals('SELECT "status", COUNT(*) as "count" FROM "users" GROUP BY "status" HAVING COUNT(*) > 5', $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT status, COUNT(*) as count FROM "users" GROUP BY status HAVING COUNT(*) > ?', $prepared->sql()); + $this->assertEquals('SELECT "status", COUNT(*) as "count" FROM "users" GROUP BY "status" HAVING COUNT(*) > ?', $prepared->sql()); $this->assertEquals([5], $prepared->bindings()); } @@ -68,11 +76,11 @@ public function testHavingWithMultipleConditions(): void ->and('COUNT(*)')->lower(100) ->sql('postgres', false); - $this->assertEquals('SELECT status, COUNT(*) as count FROM "users" GROUP BY status HAVING COUNT(*) > 5 AND COUNT(*) < 100', $sql); + $this->assertEquals('SELECT "status", COUNT(*) as "count" FROM "users" GROUP BY "status" HAVING COUNT(*) > 5 AND COUNT(*) < 100', $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT status, COUNT(*) as count FROM "users" GROUP BY status HAVING COUNT(*) > ? AND COUNT(*) < ?', $prepared->sql()); + $this->assertEquals('SELECT "status", COUNT(*) as "count" FROM "users" GROUP BY "status" HAVING COUNT(*) > ? AND COUNT(*) < ?', $prepared->sql()); $this->assertEquals([5, 100], $prepared->bindings()); } @@ -85,7 +93,7 @@ public function testOrderBySingleColumn(): void ->orderBy('name') ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" ORDER BY name ASC', $sql); + $this->assertEquals('SELECT * FROM "users" ORDER BY "name" ASC', $sql); } public function testOrderByDescending(): void @@ -97,7 +105,7 @@ public function testOrderByDescending(): void ->orderBy('created_at', 'DESC') ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" ORDER BY created_at DESC', $sql); + $this->assertEquals('SELECT * FROM "users" ORDER BY "created_at" DESC', $sql); } public function testOrderByMultipleColumns(): void @@ -110,7 +118,7 @@ public function testOrderByMultipleColumns(): void ->orderBy('name', 'DESC') ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" ORDER BY status ASC, name DESC', $sql); + $this->assertEquals('SELECT * FROM "users" ORDER BY "status" ASC, "name" DESC', $sql); } public function testLimitOnly(): void @@ -151,24 +159,24 @@ public function testCompleteQueryWithAllClauses(): void ->limit(10) ->sql('postgres', false); - $expected = "SELECT u.id, u.name, COUNT(p.id) as post_count FROM \"users\" \"u\" " + $expected = "SELECT \"u\".\"id\", \"u\".\"name\", COUNT(p.id) as \"post_count\" FROM \"users\" \"u\" " . "LEFT JOIN \"posts\" \"p\" ON u.id = p.user_id " - . "WHERE u.status = 'active' " - . "GROUP BY u.id, u.name " + . "WHERE \"u\".\"status\" = 'active' " + . "GROUP BY \"u\".\"id\", \"u\".\"name\" " . "HAVING COUNT(p.id) > 5 " - . "ORDER BY post_count DESC " + . "ORDER BY \"post_count\" DESC " . "LIMIT 10"; $this->assertEquals($expected, $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT u.id, u.name, COUNT(p.id) as post_count FROM \"users\" \"u\" " + $expectedPrepared = "SELECT \"u\".\"id\", \"u\".\"name\", COUNT(p.id) as \"post_count\" FROM \"users\" \"u\" " . "LEFT JOIN \"posts\" \"p\" ON u.id = p.user_id " - . "WHERE u.status = ? " - . "GROUP BY u.id, u.name " + . "WHERE \"u\".\"status\" = ? " + . "GROUP BY \"u\".\"id\", \"u\".\"name\" " . "HAVING COUNT(p.id) > ? " - . "ORDER BY post_count DESC " + . "ORDER BY \"post_count\" DESC " . "LIMIT 10"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['active', 5], $prepared->bindings()); diff --git a/tests/Unit/Query/Postgres/DbQueryPostgresJoinTest.php b/tests/Unit/Query/Postgres/DbQueryPostgresJoinTest.php index 72ceeb1..2388dc7 100644 --- a/tests/Unit/Query/Postgres/DbQueryPostgresJoinTest.php +++ b/tests/Unit/Query/Postgres/DbQueryPostgresJoinTest.php @@ -14,6 +14,14 @@ * Tests: INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN * PostgreSQL supports FULL OUTER JOIN ✅ */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryPostgresJoinTest extends TestCase { public function testInnerJoinWithConstraint(): void @@ -138,9 +146,9 @@ public function testJoinSubqueryWithBindingsInPreparedMode(): void $this->assertStringContainsString('FROM "users" "u"', $result->sql()); $this->assertStringContainsString('LEFT JOIN (SELECT', $result->sql()); $this->assertStringContainsString('FROM "orders"', $result->sql()); - $this->assertStringContainsString('WHERE status = ?', $result->sql()); + $this->assertStringContainsString('WHERE "status" = ?', $result->sql()); $this->assertStringContainsString(') "o" ON u.id = o.user_id', $result->sql()); - $this->assertStringContainsString('WHERE u.active = ?', $result->sql()); + $this->assertStringContainsString('WHERE "u"."active" = ?', $result->sql()); // Verify binding order: JOIN subquery bindings first, then main query bindings $bindings = $result->bindings(); @@ -169,8 +177,8 @@ public function testJoinSubqueryInNonPreparedMode(): void $this->assertIsString($sql); $this->assertStringContainsString('FROM "users" "u"', $sql); - $this->assertStringContainsString('LEFT JOIN (SELECT user_id, COUNT(*) as cnt FROM "orders" WHERE status = \'paid\'', $sql); + $this->assertStringContainsString('LEFT JOIN (SELECT "user_id", COUNT(*) as "cnt" FROM "orders" WHERE "status" = \'paid\'', $sql); $this->assertStringContainsString('"o" ON u.id = o.user_id', $sql); - $this->assertStringContainsString('WHERE u.active = 1', $sql); + $this->assertStringContainsString('WHERE "u"."active" = 1', $sql); } } diff --git a/tests/Unit/Query/Postgres/DbQueryPostgresSubqueryTest.php b/tests/Unit/Query/Postgres/DbQueryPostgresSubqueryTest.php index e7855ed..b035058 100644 --- a/tests/Unit/Query/Postgres/DbQueryPostgresSubqueryTest.php +++ b/tests/Unit/Query/Postgres/DbQueryPostgresSubqueryTest.php @@ -15,6 +15,14 @@ * * Tests: SELECT subqueries, correlated subqueries, nested subqueries */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryPostgresSubqueryTest extends TestCase { public function testSelectSubquerySingleSimple(): void @@ -31,12 +39,12 @@ public function testSelectSubquerySingleSimple(): void ->from('users') ->sql('postgres', false); - $expected = "SELECT id, name, (SELECT COUNT(*) FROM \"posts\" WHERE user_id = 1) AS \"post_count\" FROM \"users\""; + $expected = "SELECT \"id\", \"name\", (SELECT COUNT(*) FROM \"posts\" WHERE \"user_id\" = 1) AS \"post_count\" FROM \"users\""; $this->assertEquals($expected, $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals("SELECT id, name, (SELECT COUNT(*) FROM \"posts\" WHERE user_id = ?) AS \"post_count\" FROM \"users\"", $prepared->sql()); + $this->assertEquals("SELECT \"id\", \"name\", (SELECT COUNT(*) FROM \"posts\" WHERE \"user_id\" = ?) AS \"post_count\" FROM \"users\"", $prepared->sql()); $this->assertEquals([1], $prepared->bindings()); } @@ -54,7 +62,7 @@ public function testSelectSubqueryWithCorrelatedCondition(): void ->from('users') ->sql('postgres', false); - $expected = "SELECT id, name, (SELECT COUNT(*) FROM \"posts\" \"p\" WHERE p.user_id = users.id) AS \"post_count\" FROM \"users\""; + $expected = "SELECT \"id\", \"name\", (SELECT COUNT(*) FROM \"posts\" \"p\" WHERE \"p\".\"user_id\" = users.id) AS \"post_count\" FROM \"users\""; $this->assertEquals($expected, $sql); } @@ -78,9 +86,9 @@ public function testSelectSubqueryMultiple(): void ->from('users') ->sql('postgres', false); - $expected = "SELECT id, name, " - . "(SELECT COUNT(*) FROM \"posts\" \"p\" WHERE p.user_id = users.id) AS \"post_count\", " - . "(SELECT COUNT(*) FROM \"comments\" \"c\" WHERE c.user_id = users.id) AS \"comment_count\" " + $expected = "SELECT \"id\", \"name\", " + . "(SELECT COUNT(*) FROM \"posts\" \"p\" WHERE \"p\".\"user_id\" = users.id) AS \"post_count\", " + . "(SELECT COUNT(*) FROM \"comments\" \"c\" WHERE \"c\".\"user_id\" = users.id) AS \"comment_count\" " . "FROM \"users\""; $this->assertEquals($expected, $sql); @@ -119,17 +127,17 @@ public function testSelectSubqueryWithComplexSubquery(): void ->where('active')->equals(1) ->sql('postgres', false); - $expected = "SELECT id, name, email, " - . "(SELECT MAX(p.created_at) FROM \"posts\" \"p\" WHERE p.user_id = users.id AND p.status = 'published') AS \"last_post_date\" " - . "FROM \"users\" WHERE active = 1"; + $expected = "SELECT \"id\", \"name\", \"email\", " + . "(SELECT MAX(p.created_at) FROM \"posts\" \"p\" WHERE \"p\".\"user_id\" = users.id AND \"p\".\"status\" = 'published') AS \"last_post_date\" " + . "FROM \"users\" WHERE \"active\" = 1"; $this->assertEquals($expected, $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT id, name, email, " - . "(SELECT MAX(p.created_at) FROM \"posts\" \"p\" WHERE p.user_id = users.id AND p.status = ?) AS \"last_post_date\" " - . "FROM \"users\" WHERE active = ?"; + $expectedPrepared = "SELECT \"id\", \"name\", \"email\", " + . "(SELECT MAX(p.created_at) FROM \"posts\" \"p\" WHERE \"p\".\"user_id\" = users.id AND \"p\".\"status\" = ?) AS \"last_post_date\" " + . "FROM \"users\" WHERE \"active\" = ?"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['published', 1], $prepared->bindings()); } @@ -149,8 +157,8 @@ public function testSelectSubqueryWithJoinInSubquery(): void ->from('users') ->sql('postgres', false); - $expected = "SELECT id, name, " - . "(SELECT COUNT(DISTINCT c.id) FROM \"posts\" \"p\" INNER JOIN \"comments\" \"c\" ON p.id = c.post_id WHERE p.user_id = users.id) AS \"comment_count\" " + $expected = "SELECT \"id\", \"name\", " + . "(SELECT COUNT(DISTINCT c.id) FROM \"posts\" \"p\" INNER JOIN \"comments\" \"c\" ON p.id = c.post_id WHERE \"p\".\"user_id\" = users.id) AS \"comment_count\" " . "FROM \"users\""; $this->assertEquals($expected, $sql); @@ -171,8 +179,8 @@ public function testSelectSubqueryWithMainQueryJoin(): void ->innerJoin('profiles', 'u.id = prof.user_id', 'prof') ->sql('postgres', false); - $expected = "SELECT u.id, u.name, " - . "(SELECT COUNT(*) FROM \"posts\" \"p\" WHERE p.user_id = u.id) AS \"post_count\" " + $expected = "SELECT \"u\".\"id\", \"u\".\"name\", " + . "(SELECT COUNT(*) FROM \"posts\" \"p\" WHERE \"p\".\"user_id\" = u.id) AS \"post_count\" " . "FROM \"users\" \"u\" INNER JOIN \"profiles\" \"prof\" ON u.id = prof.user_id"; $this->assertEquals($expected, $sql); @@ -194,9 +202,9 @@ public function testSelectSubqueryWithOrderByAndLimit(): void ->limit(10) ->sql('postgres', false); - $expected = "SELECT id, name, " - . "(SELECT COUNT(*) FROM \"posts\" \"p\" WHERE p.user_id = users.id) AS \"post_count\" " - . "FROM \"users\" ORDER BY name ASC LIMIT 10"; + $expected = "SELECT \"id\", \"name\", " + . "(SELECT COUNT(*) FROM \"posts\" \"p\" WHERE \"p\".\"user_id\" = users.id) AS \"post_count\" " + . "FROM \"users\" ORDER BY \"name\" ASC LIMIT 10"; $this->assertEquals($expected, $sql); } @@ -217,17 +225,17 @@ public function testSelectSubqueryWithGroupByAndHaving(): void ->having('COUNT(*)')->greater(5) ->sql('postgres', false); - $expected = "SELECT c.id, c.name, COUNT(*) as total, " - . "(SELECT AVG(p.rating) FROM \"posts\" \"p\" WHERE p.category_id = c.id) AS \"avg_rating\" " - . "FROM \"categories\" \"c\" GROUP BY c.id, c.name HAVING COUNT(*) > 5"; + $expected = "SELECT \"c\".\"id\", \"c\".\"name\", COUNT(*) as \"total\", " + . "(SELECT AVG(p.rating) FROM \"posts\" \"p\" WHERE \"p\".\"category_id\" = c.id) AS \"avg_rating\" " + . "FROM \"categories\" \"c\" GROUP BY \"c\".\"id\", \"c\".\"name\" HAVING COUNT(*) > 5"; $this->assertEquals($expected, $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT c.id, c.name, COUNT(*) as total, " - . "(SELECT AVG(p.rating) FROM \"posts\" \"p\" WHERE p.category_id = c.id) AS \"avg_rating\" " - . "FROM \"categories\" \"c\" GROUP BY c.id, c.name HAVING COUNT(*) > ?"; + $expectedPrepared = "SELECT \"c\".\"id\", \"c\".\"name\", COUNT(*) as \"total\", " + . "(SELECT AVG(p.rating) FROM \"posts\" \"p\" WHERE \"p\".\"category_id\" = c.id) AS \"avg_rating\" " + . "FROM \"categories\" \"c\" GROUP BY \"c\".\"id\", \"c\".\"name\" HAVING COUNT(*) > ?"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals([5], $prepared->bindings()); } @@ -265,7 +273,7 @@ public function testSelectSubqueryWithNestedSubquery(): void ->from('users') ->sql('postgres', false); - $this->assertStringContainsString('SELECT id, name,', $sql); + $this->assertStringContainsString('SELECT "id", "name",', $sql); $this->assertStringContainsString('SELECT COUNT(*)', $sql); $this->assertStringContainsString('SELECT MAX(created_at)', $sql); $this->assertStringContainsString('AS "post_info"', $sql); @@ -290,9 +298,9 @@ public function testFromSubqueryWithBindingsInPreparedMode(): void $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); $this->assertStringContainsString('FROM (SELECT', $result->sql()); $this->assertStringContainsString('FROM "users"', $result->sql()); - $this->assertStringContainsString('WHERE status = ?', $result->sql()); + $this->assertStringContainsString('WHERE "status" = ?', $result->sql()); $this->assertStringContainsString(') "sub"', $result->sql()); - $this->assertStringContainsString('WHERE sub.id > ?', $result->sql()); + $this->assertStringContainsString('WHERE "sub"."id" > ?', $result->sql()); // Verify binding order: FROM subquery bindings first, then main query bindings $bindings = $result->bindings(); @@ -318,8 +326,8 @@ public function testFromSubqueryInNonPreparedMode(): void $sql = $query->sql('postgres', false); $this->assertIsString($sql); - $this->assertStringContainsString('FROM (SELECT id, name FROM "users" WHERE status = \'active\')', $sql); + $this->assertStringContainsString('FROM (SELECT "id", "name" FROM "users" WHERE "status" = \'active\')', $sql); $this->assertStringContainsString('"sub"', $sql); - $this->assertStringContainsString('WHERE sub.id > 5', $sql); + $this->assertStringContainsString('WHERE "sub"."id" > 5', $sql); } } diff --git a/tests/Unit/Query/Postgres/DbQueryPostgresUnionTest.php b/tests/Unit/Query/Postgres/DbQueryPostgresUnionTest.php index 8bdceab..8818bc4 100644 --- a/tests/Unit/Query/Postgres/DbQueryPostgresUnionTest.php +++ b/tests/Unit/Query/Postgres/DbQueryPostgresUnionTest.php @@ -14,6 +14,14 @@ * * Tests: UNION, UNION ALL */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryPostgresUnionTest extends TestCase { public function testUnionSimple(): void @@ -31,8 +39,8 @@ public function testUnionSimple(): void ->union($secondQuery) ->sql('postgres', false); - $expected = "SELECT id, name FROM \"customers\" WHERE country = 'Germany' " - . "UNION SELECT id, name FROM \"suppliers\" WHERE country = 'Germany'"; + $expected = "SELECT \"id\", \"name\" FROM \"customers\" WHERE \"country\" = 'Germany' " + . "UNION SELECT \"id\", \"name\" FROM \"suppliers\" WHERE \"country\" = 'Germany'"; $this->assertEquals($expected, $sql); } @@ -50,7 +58,7 @@ public function testUnionAllSimple(): void ->unionAll($secondQuery) ->sql('postgres', false); - $expected = "SELECT name FROM \"active_users\" UNION ALL SELECT name FROM \"archived_users\""; + $expected = "SELECT \"name\" FROM \"active_users\" UNION ALL SELECT \"name\" FROM \"archived_users\""; $this->assertEquals($expected, $sql); } @@ -73,9 +81,9 @@ public function testUnionMultipleQueries(): void ->union($thirdQuery) ->sql('postgres', false); - $expected = "SELECT email FROM \"customers\" " - . "UNION SELECT email FROM \"suppliers\" " - . "UNION SELECT email FROM \"partners\""; + $expected = "SELECT \"email\" FROM \"customers\" " + . "UNION SELECT \"email\" FROM \"suppliers\" " + . "UNION SELECT \"email\" FROM \"partners\""; $this->assertEquals($expected, $sql); } @@ -94,10 +102,10 @@ public function testUnionWithOrderBy(): void ->orderBy('created_at', 'DESC') ->sql('postgres', false); - $this->assertStringContainsString('SELECT name, created_at FROM "active_posts"', $sql); + $this->assertStringContainsString('SELECT "name", "created_at" FROM "active_posts"', $sql); $this->assertStringContainsString('UNION', $sql); - $this->assertStringContainsString('SELECT name, created_at FROM "archived_posts"', $sql); - $this->assertStringContainsString('ORDER BY created_at DESC', $sql); + $this->assertStringContainsString('SELECT "name", "created_at" FROM "archived_posts"', $sql); + $this->assertStringContainsString('ORDER BY "created_at" DESC', $sql); $unionPos = strpos($sql, 'UNION'); $orderByPos = strpos($sql, 'ORDER BY'); @@ -118,8 +126,8 @@ public function testUnionWithLimit(): void ->limit(20) ->sql('postgres', false); - $expected = "SELECT title FROM \"news_posts\" " - . "UNION SELECT title FROM \"blog_posts\" " + $expected = "SELECT \"title\" FROM \"news_posts\" " + . "UNION SELECT \"title\" FROM \"blog_posts\" " . "LIMIT 20"; $this->assertEquals($expected, $sql); @@ -175,11 +183,11 @@ public function testUnionAllWithBindingsInPreparedMode(): void $result = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertStringContainsString('SELECT id, name FROM "employees"', $result->sql()); - $this->assertStringContainsString('WHERE country = ?', $result->sql()); + $this->assertStringContainsString('SELECT "id", "name" FROM "employees"', $result->sql()); + $this->assertStringContainsString('WHERE "country" = ?', $result->sql()); $this->assertStringContainsString('UNION ALL', $result->sql()); - $this->assertStringContainsString('SELECT id, name FROM "suppliers"', $result->sql()); - $this->assertStringContainsString('WHERE country = ?', $result->sql()); + $this->assertStringContainsString('SELECT "id", "name" FROM "suppliers"', $result->sql()); + $this->assertStringContainsString('WHERE "country" = ?', $result->sql()); // Verify binding order: main query bindings first, then UNION ALL bindings $bindings = $result->bindings(); diff --git a/tests/Unit/Query/Postgres/DbQueryPostgresWhereTest.php b/tests/Unit/Query/Postgres/DbQueryPostgresWhereTest.php index 8b142cd..692ba10 100644 --- a/tests/Unit/Query/Postgres/DbQueryPostgresWhereTest.php +++ b/tests/Unit/Query/Postgres/DbQueryPostgresWhereTest.php @@ -14,6 +14,14 @@ * * Tests: WHERE conditions, operators, AND, OR, brackets */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryPostgresWhereTest extends TestCase { public function testWhereEquals(): void @@ -25,11 +33,11 @@ public function testWhereEquals(): void ->where('id')->equals(123) ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" WHERE id = 123', $sql); + $this->assertEquals('SELECT * FROM "users" WHERE "id" = 123', $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE id = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "id" = ?', $prepared->sql()); $this->assertEquals([123], $prepared->bindings()); } @@ -42,11 +50,11 @@ public function testWhereWithString(): void ->where('name')->equals('John') ->sql('postgres', false); - $this->assertEquals("SELECT * FROM \"users\" WHERE name = 'John'", $sql); + $this->assertEquals("SELECT * FROM \"users\" WHERE \"name\" = 'John'", $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE name = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "name" = ?', $prepared->sql()); $this->assertEquals(['John'], $prepared->bindings()); } @@ -59,11 +67,11 @@ public function testWhereGreater(): void ->where('age')->greater(18) ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" WHERE age > 18', $sql); + $this->assertEquals('SELECT * FROM "users" WHERE "age" > 18', $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE age > ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "age" > ?', $prepared->sql()); $this->assertEquals([18], $prepared->bindings()); } @@ -76,11 +84,11 @@ public function testWhereLower(): void ->where('age')->lower(65) ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" WHERE age < 65', $sql); + $this->assertEquals('SELECT * FROM "users" WHERE "age" < 65', $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE age < ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "age" < ?', $prepared->sql()); $this->assertEquals([65], $prepared->bindings()); } @@ -93,11 +101,11 @@ public function testWhereBetween(): void ->where('age')->between(18, 65) ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" WHERE age BETWEEN 18 AND 65', $sql); + $this->assertEquals('SELECT * FROM "users" WHERE "age" BETWEEN 18 AND 65', $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE age BETWEEN ? AND ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "age" BETWEEN ? AND ?', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); } @@ -110,11 +118,11 @@ public function testWhereIn(): void ->where('status')->in(['active', 'pending']) ->sql('postgres', false); - $this->assertEquals("SELECT * FROM \"users\" WHERE status IN ('active', 'pending')", $sql); + $this->assertEquals("SELECT * FROM \"users\" WHERE \"status\" IN ('active', 'pending')", $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE status IN (?, ?)', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "status" IN (?, ?)', $prepared->sql()); $this->assertEquals(['active', 'pending'], $prepared->bindings()); } @@ -162,10 +170,10 @@ public function testWhereInEmptyArrayWithOtherConditions(): void ->and('id')->in([]) ->sql('postgres', false); - $this->assertEquals("SELECT * FROM \"users\" WHERE status = 'active' AND 1=0", $sql); + $this->assertEquals("SELECT * FROM \"users\" WHERE \"status\" = 'active' AND 1=0", $sql); $prepared = $query->sql('postgres', true); - $this->assertEquals('SELECT * FROM "users" WHERE status = ? AND 1=0', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "status" = ? AND 1=0', $prepared->sql()); $this->assertEquals(['active'], $prepared->bindings()); } @@ -179,7 +187,7 @@ public function testWhereInEmptyArrayWithOrCondition(): void ->or('id')->in([]) ->sql('postgres', false); - $this->assertEquals("SELECT * FROM \"users\" WHERE status = 'active' OR 1=0", $sql); + $this->assertEquals("SELECT * FROM \"users\" WHERE \"status\" = 'active' OR 1=0", $sql); } public function testWhereInEmptyArrayWithBrackets(): void @@ -192,7 +200,7 @@ public function testWhereInEmptyArrayWithBrackets(): void ->and('(id')->in([], ')') ->sql('postgres', false); - $this->assertEquals("SELECT * FROM \"users\" WHERE status = 'active' AND (1=0)", $sql); + $this->assertEquals("SELECT * FROM \"users\" WHERE \"status\" = 'active' AND (1=0)", $sql); } public function testWhereNotInEmptyArrayWithOtherConditions(): void @@ -205,7 +213,7 @@ public function testWhereNotInEmptyArrayWithOtherConditions(): void ->and('id')->notIn([]) ->sql('postgres', false); - $this->assertEquals("SELECT * FROM \"users\" WHERE status = 'active' AND 1=1", $sql); + $this->assertEquals("SELECT * FROM \"users\" WHERE \"status\" = 'active' AND 1=1", $sql); } public function testWhereLike(): void @@ -217,11 +225,11 @@ public function testWhereLike(): void ->where('name')->like('%John%') ->sql('postgres', false); - $this->assertEquals("SELECT * FROM \"users\" WHERE name LIKE '%John%'", $sql); + $this->assertEquals("SELECT * FROM \"users\" WHERE \"name\" LIKE '%John%'", $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE name LIKE ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "name" LIKE ?', $prepared->sql()); $this->assertEquals(['%John%'], $prepared->bindings()); } @@ -234,7 +242,7 @@ public function testWhereIsNull(): void ->where('deleted_at')->isNull() ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" WHERE deleted_at IS NULL', $sql); + $this->assertEquals('SELECT * FROM "users" WHERE "deleted_at" IS NULL', $sql); } public function testWhereIsNotNull(): void @@ -246,7 +254,7 @@ public function testWhereIsNotNull(): void ->where('email')->isNotNull() ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" WHERE email IS NOT NULL', $sql); + $this->assertEquals('SELECT * FROM "users" WHERE "email" IS NOT NULL', $sql); } public function testMultipleWhereWithAnd(): void @@ -259,11 +267,11 @@ public function testMultipleWhereWithAnd(): void ->and('status')->equals('active') ->sql('postgres', false); - $this->assertEquals("SELECT * FROM \"users\" WHERE age > 18 AND status = 'active'", $sql); + $this->assertEquals("SELECT * FROM \"users\" WHERE \"age\" > 18 AND \"status\" = 'active'", $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE age > ? AND status = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "age" > ? AND "status" = ?', $prepared->sql()); $this->assertEquals([18, 'active'], $prepared->bindings()); } @@ -277,11 +285,11 @@ public function testWhereWithOr(): void ->or('age')->greater(65) ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" WHERE age < 18 OR age > 65', $sql); + $this->assertEquals('SELECT * FROM "users" WHERE "age" < 18 OR "age" > 65', $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE age < ? OR age > ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "age" < ? OR "age" > ?', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); } @@ -295,11 +303,11 @@ public function testWhereWithBrackets(): void ->and('age')->lower(65, ')') ->sql('postgres', false); - $this->assertEquals('SELECT * FROM "users" WHERE (age > 18 AND age < 65)', $sql); + $this->assertEquals('SELECT * FROM "users" WHERE (age > 18 AND "age" < 65)', $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE (age > ? AND age < ?)', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE (age > ? AND "age" < ?)', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); } @@ -314,11 +322,11 @@ public function testComplexWhereConditions(): void ->or('role')->equals('admin') ->sql('postgres', false); - $this->assertEquals("SELECT * FROM \"users\" WHERE (status = 'active' AND age > 18) OR role = 'admin'", $sql); + $this->assertEquals("SELECT * FROM \"users\" WHERE (status = 'active' AND \"age\" > 18) OR \"role\" = 'admin'", $sql); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE (status = ? AND age > ?) OR role = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE (status = ? AND "age" > ?) OR "role" = ?', $prepared->sql()); $this->assertEquals(['active', 18, 'admin'], $prepared->bindings()); } @@ -340,7 +348,7 @@ public function testWhereWithSubqueryInNonPreparedMode(): void $this->assertIsString($sql); $this->assertStringContainsString('SELECT * FROM "users"', $sql); - $this->assertStringContainsString('WHERE id IN (SELECT user_id FROM "orders" WHERE status = \'completed\')', $sql); + $this->assertStringContainsString('WHERE "id" IN (SELECT "user_id" FROM "orders" WHERE "status" = \'completed\')', $sql); } public function testWhereJsonExtract(): void @@ -366,7 +374,7 @@ public function testAndJsonExtract(): void $sql = $query->sql('postgres', false); - $this->assertStringContainsString('WHERE active = 1', $sql); + $this->assertStringContainsString('WHERE "active" = 1', $sql); $this->assertStringContainsString('AND "settings"->>\'theme\' = \'dark\'', $sql); } @@ -380,7 +388,7 @@ public function testOrJsonExtract(): void $sql = $query->sql('postgres', false); - $this->assertStringContainsString('WHERE status = \'active\'', $sql); + $this->assertStringContainsString('WHERE "status" = \'active\'', $sql); $this->assertStringContainsString('OR "preferences"->>\'notifications\' = TRUE', $sql); } @@ -394,7 +402,7 @@ public function testHavingJson(): void $sql = $query->sql('postgres', false); - $this->assertStringContainsString('GROUP BY user_id', $sql); + $this->assertStringContainsString('GROUP BY "user_id"', $sql); $this->assertStringContainsString('HAVING "metadata"->>\'priority\' = \'high\'', $sql); } @@ -440,14 +448,14 @@ public function testAndWithExpressionField(): void ->sql('postgres', false); $this->assertEquals( - 'SELECT * FROM "users" WHERE status = \'active\' AND EXTRACT(YEAR FROM created_at) = 2024', + 'SELECT * FROM "users" WHERE "status" = \'active\' AND EXTRACT(YEAR FROM created_at) = 2024', $sql ); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $this->assertEquals( - 'SELECT * FROM "users" WHERE status = ? AND EXTRACT(YEAR FROM created_at) = ?', + 'SELECT * FROM "users" WHERE "status" = ? AND EXTRACT(YEAR FROM created_at) = ?', $prepared->sql() ); $this->assertEquals(['active', 2024], $prepared->bindings()); @@ -464,13 +472,13 @@ public function testOrWithExpressionField(): void ->sql('postgres', false); $this->assertEquals( - 'SELECT * FROM "users" WHERE name LIKE \'%John%\' OR LOWER(email) LIKE \'%john%\'', + 'SELECT * FROM "users" WHERE "name" LIKE \'%John%\' OR LOWER(email) LIKE \'%john%\'', $sql ); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM "users" WHERE name LIKE ? OR LOWER(email) LIKE ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM "users" WHERE "name" LIKE ? OR LOWER(email) LIKE ?', $prepared->sql()); $this->assertEquals(['%John%', '%john%'], $prepared->bindings()); } @@ -537,14 +545,14 @@ public function testComplexExpressionConditions(): void ->sql('postgres', false); $this->assertEquals( - 'SELECT * FROM "products" WHERE (stock_level > 0 AND price * 0.8 < 100) OR featured = 1', + 'SELECT * FROM "products" WHERE (stock_level > 0 AND price * 0.8 < 100) OR "featured" = 1', $sql ); $prepared = $query->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $this->assertEquals( - 'SELECT * FROM "products" WHERE (stock_level > ? AND price * 0.8 < ?) OR featured = ?', + 'SELECT * FROM "products" WHERE (stock_level > ? AND price * 0.8 < ?) OR "featured" = ?', $prepared->sql() ); $this->assertEquals([0, 100, 1], $prepared->bindings()); diff --git a/tests/Unit/Query/Postgres/DbQueryPostgresWindowTest.php b/tests/Unit/Query/Postgres/DbQueryPostgresWindowTest.php index b8aafab..56aeb49 100644 --- a/tests/Unit/Query/Postgres/DbQueryPostgresWindowTest.php +++ b/tests/Unit/Query/Postgres/DbQueryPostgresWindowTest.php @@ -14,6 +14,14 @@ * * Tests: Window functions (ROW_NUMBER, RANK, SUM, etc.), Named windows, PARTITION BY, ORDER BY, Frames, GROUPS */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQueryPostgresWindowTest extends TestCase { public function testSelectWindowWithRowNumber(): void @@ -29,7 +37,7 @@ public function testSelectWindowWithRowNumber(): void $this->assertInstanceOf(DbQuery::class, $result); $sql = $query->sql('postgres', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS "row_num" FROM "employees"'; + $expected = 'SELECT "id", "name", "salary", ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS "row_num" FROM "employees"'; $this->assertEquals($expected, $sql); } @@ -43,7 +51,7 @@ public function testSelectWindowWithRank(): void ->from('players') ->sql('postgres', false); - $expected = 'SELECT id, score, RANK() OVER (ORDER BY score DESC) AS "rank" FROM "players"'; + $expected = 'SELECT "id", "score", RANK() OVER (ORDER BY score DESC) AS "rank" FROM "players"'; $this->assertEquals($expected, $sql); } @@ -57,7 +65,7 @@ public function testSelectWindowWithSumAndArgs(): void ->from('transactions') ->sql('postgres', false); - $expected = 'SELECT date, amount, SUM(amount) OVER (ORDER BY date ASC) AS "running_total" FROM "transactions"'; + $expected = 'SELECT "date", "amount", SUM(amount) OVER (ORDER BY date ASC) AS "running_total" FROM "transactions"'; $this->assertEquals($expected, $sql); } @@ -71,7 +79,7 @@ public function testSelectWindowWithLead(): void ->from('stock_prices') ->sql('postgres', false); - $expected = 'SELECT date, price, LEAD(price, 1) OVER (ORDER BY date ASC) AS "next_price" FROM "stock_prices"'; + $expected = 'SELECT "date", "price", LEAD(price, 1) OVER (ORDER BY date ASC) AS "next_price" FROM "stock_prices"'; $this->assertEquals($expected, $sql); } @@ -86,7 +94,7 @@ public function testSelectWindowWithMultiplePartitions(): void ->from('employees') ->sql('postgres', false); - $expected = 'SELECT id, name, ROW_NUMBER() OVER (PARTITION BY department, location ORDER BY hire_date ASC) AS "row_num" FROM "employees"'; + $expected = 'SELECT "id", "name", ROW_NUMBER() OVER (PARTITION BY department, location ORDER BY hire_date ASC) AS "row_num" FROM "employees"'; $this->assertEquals($expected, $sql); } @@ -101,7 +109,7 @@ public function testSelectWindowWithFrame(): void ->from('sales') ->sql('postgres', false); - $expected = 'SELECT date, amount, AVG(amount) OVER (ORDER BY date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS "moving_avg" FROM "sales"'; + $expected = 'SELECT "date", "amount", AVG(amount) OVER (ORDER BY date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS "moving_avg" FROM "sales"'; $this->assertEquals($expected, $sql); } @@ -116,7 +124,7 @@ public function testSelectWindowWithGroupsFrame(): void ->from('data') ->sql('postgres', false); - $expected = 'SELECT id, value, SUM(value) OVER (ORDER BY id ASC GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS "total" FROM "data"'; + $expected = 'SELECT "id", "value", SUM(value) OVER (ORDER BY id ASC GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS "total" FROM "data"'; $this->assertEquals($expected, $sql); } @@ -135,7 +143,7 @@ public function testMultipleWindowFunctions(): void ->from('employees') ->sql('postgres', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS "row_num", DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS "dense_rank" FROM "employees"'; + $expected = 'SELECT "id", "name", "salary", ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS "row_num", DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS "dense_rank" FROM "employees"'; $this->assertEquals($expected, $sql); } @@ -155,7 +163,7 @@ public function testNamedWindow(): void ->from('employees') ->sql('postgres', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER dept_window AS "row_num", RANK() OVER dept_window AS "rank" FROM "employees" WINDOW dept_window AS (PARTITION BY department ORDER BY salary DESC)'; + $expected = 'SELECT "id", "name", "salary", ROW_NUMBER() OVER dept_window AS "row_num", RANK() OVER dept_window AS "rank" FROM "employees" WINDOW dept_window AS (PARTITION BY department ORDER BY salary DESC)'; $this->assertEquals($expected, $sql); } @@ -171,7 +179,7 @@ public function testNamedWindowWithArgs(): void ->from('metrics') ->sql('postgres', false); - $expected = 'SELECT timestamp, value, SUM(value) OVER time_window AS "running_total", FIRST_VALUE(value) OVER time_window AS "first_value" FROM "metrics" WINDOW time_window AS (ORDER BY timestamp ASC)'; + $expected = 'SELECT "timestamp", "value", SUM(value) OVER time_window AS "running_total", FIRST_VALUE(value) OVER time_window AS "first_value" FROM "metrics" WINDOW time_window AS (ORDER BY timestamp ASC)'; $this->assertEquals($expected, $sql); } @@ -192,7 +200,7 @@ public function testMultipleNamedWindows(): void ->from('employees') ->sql('postgres', false); - $expected = 'SELECT id, name, ROW_NUMBER() OVER by_dept AS "dept_seniority", RANK() OVER by_team AS "team_rank" FROM "employees" WINDOW by_dept AS (PARTITION BY department ORDER BY hire_date ASC), by_team AS (PARTITION BY team ORDER BY performance DESC)'; + $expected = 'SELECT "id", "name", ROW_NUMBER() OVER by_dept AS "dept_seniority", RANK() OVER by_team AS "team_rank" FROM "employees" WINDOW by_dept AS (PARTITION BY department ORDER BY hire_date ASC), by_team AS (PARTITION BY team ORDER BY performance DESC)'; $this->assertEquals($expected, $sql); } @@ -216,7 +224,7 @@ public function testWindowWithWhereClause(): void ->where('active')->equals(true) ->sql('postgres', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS "row_num" FROM "employees" WHERE active = TRUE'; + $expected = 'SELECT "id", "name", "salary", ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS "row_num" FROM "employees" WHERE "active" = TRUE'; $this->assertEquals($expected, $sql); } @@ -232,7 +240,7 @@ public function testWindowWithGroupByAndHaving(): void ->having('COUNT(*)')->greater(5) ->sql('postgres', false); - $expected = 'SELECT department, COUNT(*) as emp_count, RANK() OVER (ORDER BY COUNT(*) DESC) AS "dept_rank" FROM "employees" GROUP BY department HAVING COUNT(*) > 5'; + $expected = 'SELECT "department", COUNT(*) as "emp_count", RANK() OVER (ORDER BY COUNT(*) DESC) AS "dept_rank" FROM "employees" GROUP BY "department" HAVING COUNT(*) > 5'; $this->assertEquals($expected, $sql); } @@ -249,7 +257,7 @@ public function testWindowFunctionWithPreparedStatement(): void ->sql('postgres', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS "row_num" FROM "employees" WHERE salary > ?'; + $expected = 'SELECT "id", "name", "salary", ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS "row_num" FROM "employees" WHERE "salary" > ?'; $this->assertEquals($expected, $result->sql()); $this->assertEquals([50000], $result->bindings()); } @@ -265,7 +273,7 @@ public function testSelectWindowWithNthValue(): void ->from('data') ->sql('postgres', false); - $expected = 'SELECT id, value, NTH_VALUE(value, 3) OVER (ORDER BY id ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS "third_value" FROM "data"'; + $expected = 'SELECT "id", "value", NTH_VALUE(value, 3) OVER (ORDER BY id ASC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS "third_value" FROM "data"'; $this->assertEquals($expected, $sql); } @@ -280,7 +288,7 @@ public function testSelectWindowWithLastValue(): void ->from('transactions') ->sql('postgres', false); - $expected = 'SELECT date, amount, LAST_VALUE(amount) OVER (ORDER BY date ASC RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS "last_amount" FROM "transactions"'; + $expected = 'SELECT "date", "amount", LAST_VALUE(amount) OVER (ORDER BY date ASC RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS "last_amount" FROM "transactions"'; $this->assertEquals($expected, $sql); } @@ -298,7 +306,7 @@ public function testWindowWithCTE(): void ->from('active_emp') ->sql('postgres', false); - $expected = 'WITH "active_emp" AS (SELECT * FROM "employees" WHERE active = TRUE) SELECT id, name, ROW_NUMBER() OVER (ORDER BY id ASC) AS "row_num" FROM "active_emp"'; + $expected = 'WITH "active_emp" AS (SELECT * FROM "employees" WHERE "active" = TRUE) SELECT "id", "name", ROW_NUMBER() OVER (ORDER BY id ASC) AS "row_num" FROM "active_emp"'; $this->assertEquals($expected, $sql); } } diff --git a/tests/Unit/Query/Sqlite/DbQuerySqliteBasicTest.php b/tests/Unit/Query/Sqlite/DbQuerySqliteBasicTest.php index 2021a48..c74709b 100644 --- a/tests/Unit/Query/Sqlite/DbQuerySqliteBasicTest.php +++ b/tests/Unit/Query/Sqlite/DbQuerySqliteBasicTest.php @@ -13,6 +13,14 @@ * * Tests: Constructor, SELECT, FROM, DISTINCT, Alias */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQuerySqliteBasicTest extends TestCase { public function testConstructor(): void @@ -35,14 +43,14 @@ public function testSelectWithSingleField(): void { $query = new DbQuery(); $sql = $query->select('id')->from('users')->sql('sqlite', false); - $this->assertEquals('SELECT id FROM `users`', $sql); + $this->assertEquals('SELECT `id` FROM `users`', $sql); } public function testSelectWithMultipleFields(): void { $query = new DbQuery(); $sql = $query->select('id, name, email')->from('users')->sql('sqlite', false); - $this->assertEquals('SELECT id, name, email FROM `users`', $sql); + $this->assertEquals('SELECT `id`, `name`, `email` FROM `users`', $sql); } public function testDistinct(): void @@ -52,7 +60,7 @@ public function testDistinct(): void $this->assertSame($query, $result); $sql = $query->select('name')->from('users')->sql('sqlite', false); - $this->assertEquals('SELECT DISTINCT name FROM `users`', $sql); + $this->assertEquals('SELECT DISTINCT `name` FROM `users`', $sql); } public function testFromWithTableName(): void diff --git a/tests/Unit/Query/Sqlite/DbQuerySqliteCteTest.php b/tests/Unit/Query/Sqlite/DbQuerySqliteCteTest.php index bb62ea4..44bbdf4 100644 --- a/tests/Unit/Query/Sqlite/DbQuerySqliteCteTest.php +++ b/tests/Unit/Query/Sqlite/DbQuerySqliteCteTest.php @@ -20,6 +20,14 @@ * * Tests: WITH, WITH RECURSIVE */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQuerySqliteCteTest extends TestCase { public function testWithSingleCte(): void @@ -36,7 +44,7 @@ public function testWithSingleCte(): void ->from('active_users') ->sql('sqlite', false); - $expected = "WITH `active_users` AS (SELECT id, name FROM `users` WHERE status = 'active') " + $expected = "WITH `active_users` AS (SELECT `id`, `name` FROM `users` WHERE `status` = 'active') " . "SELECT * FROM `active_users`"; $this->assertEquals($expected, $sql); @@ -62,8 +70,8 @@ public function testWithMultipleCtes(): void ->from('active_users') ->sql('sqlite', false); - $expected = "WITH `active_users` AS (SELECT id, name FROM `users` WHERE status = 'active'), " - . "`user_posts` AS (SELECT user_id, COUNT(*) as post_count FROM `posts` GROUP BY user_id) " + $expected = "WITH `active_users` AS (SELECT `id`, `name` FROM `users` WHERE `status` = 'active'), " + . "`user_posts` AS (SELECT `user_id`, COUNT(*) as `post_count` FROM `posts` GROUP BY `user_id`) " . "SELECT * FROM `active_users`"; $this->assertEquals($expected, $sql); @@ -84,8 +92,8 @@ public function testWithCteUsedInJoin(): void ->innerJoin('user_stats', 'u.id = s.user_id', 's') ->sql('sqlite', false); - $expected = "WITH `user_stats` AS (SELECT user_id, COUNT(*) as post_count FROM `posts` GROUP BY user_id) " - . "SELECT u.name, s.post_count FROM `users` `u` INNER JOIN `user_stats` `s` ON u.id = s.user_id"; + $expected = "WITH `user_stats` AS (SELECT `user_id`, COUNT(*) as `post_count` FROM `posts` GROUP BY `user_id`) " + . "SELECT `u`.`name`, `s`.`post_count` FROM `users` `u` INNER JOIN `user_stats` `s` ON u.id = s.user_id"; $this->assertEquals($expected, $sql); } @@ -107,9 +115,9 @@ public function testWithCteContainingJoin(): void ->sql('sqlite', false); $expected = "WITH `user_summary` AS " - . "(SELECT u.id, u.name, COUNT(p.id) as post_count FROM `users` `u` " - . "LEFT JOIN `posts` `p` ON u.id = p.user_id GROUP BY u.id, u.name) " - . "SELECT * FROM `user_summary` WHERE post_count > 5"; + . "(SELECT `u`.`id`, `u`.`name`, COUNT(p.id) as `post_count` FROM `users` `u` " + . "LEFT JOIN `posts` `p` ON u.id = p.user_id GROUP BY `u`.`id`, `u`.`name`) " + . "SELECT * FROM `user_summary` WHERE `post_count` > 5"; $this->assertEquals($expected, $sql); } @@ -130,8 +138,8 @@ public function testWithCteAndMainQueryWhere(): void ->sql('sqlite', false); $expected = "WITH `recent_users` AS " - . "(SELECT * FROM `users` WHERE created_at > '2024-01-01') " - . "SELECT id, name FROM `recent_users` WHERE status = 'verified'"; + . "(SELECT * FROM `users` WHERE `created_at` > '2024-01-01') " + . "SELECT `id`, `name` FROM `recent_users` WHERE `status` = 'verified'"; $this->assertEquals($expected, $sql); } @@ -153,8 +161,8 @@ public function testWithCteAndOrderByLimit(): void ->sql('sqlite', false); $expected = "WITH `user_totals` AS " - . "(SELECT user_id, SUM(amount) as total FROM `orders` GROUP BY user_id) " - . "SELECT * FROM `user_totals` ORDER BY total DESC LIMIT 10"; + . "(SELECT `user_id`, SUM(amount) as `total` FROM `orders` GROUP BY `user_id`) " + . "SELECT * FROM `user_totals` ORDER BY `total` DESC LIMIT 10"; $this->assertEquals($expected, $sql); } @@ -181,8 +189,8 @@ public function testWithMultipleCtesCascading(): void ->from('verified_users') ->sql('sqlite', false); - $expected = "WITH `active_users` AS (SELECT id, name, status FROM `users` WHERE active = 1), " - . "`verified_users` AS (SELECT id, name FROM `active_users` WHERE status = 'verified') " + $expected = "WITH `active_users` AS (SELECT `id`, `name`, `status` FROM `users` WHERE `active` = 1), " + . "`verified_users` AS (SELECT `id`, `name` FROM `active_users` WHERE `status` = 'verified') " . "SELECT * FROM `verified_users`"; $this->assertEquals($expected, $sql); @@ -209,9 +217,9 @@ public function testWithCteAndMultipleJoins(): void ->innerJoin('post_counts', 'u.id = p.user_id', 'p') ->sql('sqlite', false); - $expected = "WITH `active_users` AS (SELECT id, name FROM `users` WHERE active = 1), " - . "`post_counts` AS (SELECT user_id, COUNT(*) as count FROM `posts` GROUP BY user_id) " - . "SELECT u.name, p.count FROM `active_users` `u` INNER JOIN `post_counts` `p` ON u.id = p.user_id"; + $expected = "WITH `active_users` AS (SELECT `id`, `name` FROM `users` WHERE `active` = 1), " + . "`post_counts` AS (SELECT `user_id`, COUNT(*) as `count` FROM `posts` GROUP BY `user_id`) " + . "SELECT `u`.`name`, `p`.`count` FROM `active_users` `u` INNER JOIN `post_counts` `p` ON u.id = p.user_id"; $this->assertEquals($expected, $sql); } @@ -250,10 +258,10 @@ public function testWithCteContainingSubquery(): void ->sql('sqlite', false); $this->assertStringContainsString('WITH `user_stats` AS', $sql); - $this->assertStringContainsString('SELECT id, name,', $sql); + $this->assertStringContainsString('SELECT `id`, `name`,', $sql); $this->assertStringContainsString('SELECT COUNT(*)', $sql); $this->assertStringContainsString('AS `post_count`', $sql); - $this->assertStringContainsString('FROM `user_stats` WHERE post_count > 10', $sql); + $this->assertStringContainsString('FROM `user_stats` WHERE `post_count` > 10', $sql); } // ==================== WITH RECURSIVE Tests ==================== @@ -280,8 +288,8 @@ public function testWithRecursiveSingleCte(): void ->sql('sqlite', false); $this->assertStringStartsWith('WITH RECURSIVE `category_tree` AS', $sql); - $this->assertStringContainsString('SELECT id, name, parent_id, 1 as level', $sql); - $this->assertStringContainsString('WHERE parent_id IS NULL', $sql); + $this->assertStringContainsString('SELECT `id`, `name`, `parent_id`, 1 as `level`', $sql); + $this->assertStringContainsString('WHERE `parent_id` IS NULL', $sql); $this->assertStringContainsString('UNION', $sql); $this->assertStringContainsString('SELECT * FROM `category_tree`', $sql); } @@ -309,9 +317,9 @@ public function testWithRecursiveEmployeeHierarchy(): void ->sql('sqlite', false); $this->assertStringContainsString('WITH RECURSIVE `hierarchy` AS', $sql); - $this->assertStringContainsString('0 as depth', $sql); + $this->assertStringContainsString('0 as `depth`', $sql); $this->assertStringContainsString('h.depth + 1', $sql); - $this->assertStringContainsString('ORDER BY depth ASC', $sql); + $this->assertStringContainsString('ORDER BY `depth` ASC', $sql); } public function testWithRecursiveMixedWithNormalCte(): void @@ -369,7 +377,7 @@ public function testWithRecursiveAndWhere(): void ->sql('sqlite', false); $this->assertStringContainsString('WITH RECURSIVE `node_tree` AS', $sql); - $this->assertStringContainsString('FROM `node_tree` WHERE depth <= 3', $sql); + $this->assertStringContainsString('FROM `node_tree` WHERE `depth` <= 3', $sql); } public function testWithRecursiveAndLimit(): void @@ -432,7 +440,7 @@ public function testWithRecursivePathTracking(): void ->sql('sqlite', false); $this->assertStringContainsString('WITH RECURSIVE `folder_paths` AS', $sql); - $this->assertStringContainsString('CAST(name AS TEXT) as path', $sql); + $this->assertStringContainsString('CAST(name AS TEXT) as `path`', $sql); $this->assertStringContainsString("p.path || '/' || f.name", $sql); } @@ -496,7 +504,7 @@ public function testWithRecursiveWithJoinInMainQuery(): void ->sql('sqlite', false); $this->assertStringContainsString('WITH RECURSIVE `cat_hierarchy` AS', $sql); - $this->assertStringContainsString('SELECT h.*, p.title', $sql); + $this->assertStringContainsString('SELECT h.*, `p`.`title`', $sql); $this->assertStringContainsString('INNER JOIN `products`', $sql); } @@ -521,7 +529,7 @@ public function testRecursiveCteWithBindingsInPreparedMode(): void $this->assertStringContainsString('WITH RECURSIVE', $result->sql()); $this->assertStringContainsString('`emp_hierarchy` AS', $result->sql()); $this->assertStringContainsString('SELECT * FROM `departments`', $result->sql()); - $this->assertStringContainsString('WHERE id = ?', $result->sql()); + $this->assertStringContainsString('WHERE `id` = ?', $result->sql()); // Verify binding order: CTE bindings first, then main query bindings $bindings = $result->bindings(); diff --git a/tests/Unit/Query/Sqlite/DbQuerySqliteExistsTest.php b/tests/Unit/Query/Sqlite/DbQuerySqliteExistsTest.php index 954e7d8..d01af1b 100644 --- a/tests/Unit/Query/Sqlite/DbQuerySqliteExistsTest.php +++ b/tests/Unit/Query/Sqlite/DbQuerySqliteExistsTest.php @@ -14,6 +14,14 @@ * * Tests: EXISTS, NOT EXISTS, andExists, orExists, andNotExists, orNotExists */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQuerySqliteExistsTest extends TestCase { public function testExistsSimple(): void @@ -30,7 +38,7 @@ public function testExistsSimple(): void ->exists($subquery) ->sql('sqlite', false); - $expected = "SELECT * FROM `users` WHERE EXISTS (SELECT 1 FROM `posts` `p` WHERE p.user_id = users.id)"; + $expected = "SELECT * FROM `users` WHERE EXISTS (SELECT 1 FROM `posts` `p` WHERE `p`.`user_id` = users.id)"; $this->assertEquals($expected, $sql); } @@ -49,7 +57,7 @@ public function testNotExistsSimple(): void ->notExists($subquery) ->sql('sqlite', false); - $expected = "SELECT * FROM `users` WHERE NOT EXISTS (SELECT 1 FROM `posts` `p` WHERE p.user_id = users.id)"; + $expected = "SELECT * FROM `users` WHERE NOT EXISTS (SELECT 1 FROM `posts` `p` WHERE `p`.`user_id` = users.id)"; $this->assertEquals($expected, $sql); } @@ -70,7 +78,7 @@ public function testExistsCombinedWithWhereConditions(): void ->sql('sqlite', false); $expected = "SELECT * FROM `users` `u` " - . "WHERE u.active = 1 AND EXISTS (SELECT 1 FROM `orders` `o` WHERE o.user_id = u.id)"; + . "WHERE `u`.`active` = 1 AND EXISTS (SELECT 1 FROM `orders` `o` WHERE `o`.`user_id` = u.id)"; $this->assertEquals($expected, $sql); } @@ -91,7 +99,7 @@ public function testNotExistsCombinedWithWhereConditions(): void ->sql('sqlite', false); $expected = "SELECT * FROM `users` `u` " - . "WHERE u.status = 'active' AND NOT EXISTS (SELECT 1 FROM `bans` `b` WHERE b.user_id = u.id)"; + . "WHERE `u`.`status` = 'active' AND NOT EXISTS (SELECT 1 FROM `bans` `b` WHERE `b`.`user_id` = u.id)"; $this->assertEquals($expected, $sql); } @@ -118,9 +126,9 @@ public function testAndExists(): void ->sql('sqlite', false); $expected = "SELECT * FROM `users` `u` " - . "WHERE u.active = 1 " - . "AND EXISTS (SELECT 1 FROM `posts` `p` WHERE p.user_id = u.id) " - . "AND EXISTS (SELECT 1 FROM `comments` `c` WHERE c.user_id = u.id)"; + . "WHERE `u`.`active` = 1 " + . "AND EXISTS (SELECT 1 FROM `posts` `p` WHERE `p`.`user_id` = u.id) " + . "AND EXISTS (SELECT 1 FROM `comments` `c` WHERE `c`.`user_id` = u.id)"; $this->assertEquals($expected, $sql); } @@ -146,8 +154,8 @@ public function testOrExists(): void ->sql('sqlite', false); $expected = "SELECT * FROM `users` `u` " - . "WHERE EXISTS (SELECT 1 FROM `posts` `p` WHERE p.user_id = u.id) " - . "OR EXISTS (SELECT 1 FROM `drafts` `d` WHERE d.user_id = u.id)"; + . "WHERE EXISTS (SELECT 1 FROM `posts` `p` WHERE `p`.`user_id` = u.id) " + . "OR EXISTS (SELECT 1 FROM `drafts` `d` WHERE `d`.`user_id` = u.id)"; $this->assertEquals($expected, $sql); } @@ -174,9 +182,9 @@ public function testAndNotExists(): void ->sql('sqlite', false); $expected = "SELECT * FROM `users` `u` " - . "WHERE u.active = 1 " - . "AND NOT EXISTS (SELECT 1 FROM `bans` `b` WHERE b.user_id = u.id) " - . "AND NOT EXISTS (SELECT 1 FROM `suspensions` `s` WHERE s.user_id = u.id)"; + . "WHERE `u`.`active` = 1 " + . "AND NOT EXISTS (SELECT 1 FROM `bans` `b` WHERE `b`.`user_id` = u.id) " + . "AND NOT EXISTS (SELECT 1 FROM `suspensions` `s` WHERE `s`.`user_id` = u.id)"; $this->assertEquals($expected, $sql); } @@ -202,8 +210,8 @@ public function testOrNotExists(): void ->sql('sqlite', false); $expected = "SELECT * FROM `users` `u` " - . "WHERE NOT EXISTS (SELECT 1 FROM `posts` `p` WHERE p.user_id = u.id) " - . "OR NOT EXISTS (SELECT 1 FROM `comments` `c` WHERE c.user_id = u.id)"; + . "WHERE NOT EXISTS (SELECT 1 FROM `posts` `p` WHERE `p`.`user_id` = u.id) " + . "OR NOT EXISTS (SELECT 1 FROM `comments` `c` WHERE `c`.`user_id` = u.id)"; $this->assertEquals($expected, $sql); } @@ -262,8 +270,8 @@ public function testExistsWithMultipleConditions(): void ->sql('sqlite', false); $expected = "SELECT * FROM `users` `u` " - . "WHERE u.active = 1 " - . "AND EXISTS (SELECT 1 FROM `orders` `o` WHERE o.user_id = u.id AND o.status = 'completed' AND o.total > 100)"; + . "WHERE `u`.`active` = 1 " + . "AND EXISTS (SELECT 1 FROM `orders` `o` WHERE `o`.`user_id` = u.id AND `o`.`status` = 'completed' AND `o`.`total` > 100)"; $this->assertEquals($expected, $sql); } @@ -284,7 +292,7 @@ public function testNotExistsWithJoinInSubquery(): void ->sql('sqlite', false); $expected = "SELECT * FROM `users` `u` " - . "WHERE NOT EXISTS (SELECT 1 FROM `posts` `p` INNER JOIN `spam_reports` `sr` ON p.id = sr.post_id WHERE p.user_id = u.id)"; + . "WHERE NOT EXISTS (SELECT 1 FROM `posts` `p` INNER JOIN `spam_reports` `sr` ON p.id = sr.post_id WHERE `p`.`user_id` = u.id)"; $this->assertEquals($expected, $sql); } diff --git a/tests/Unit/Query/Sqlite/DbQuerySqliteGroupOrderLimitTest.php b/tests/Unit/Query/Sqlite/DbQuerySqliteGroupOrderLimitTest.php index 8a8621e..542d3d7 100644 --- a/tests/Unit/Query/Sqlite/DbQuerySqliteGroupOrderLimitTest.php +++ b/tests/Unit/Query/Sqlite/DbQuerySqliteGroupOrderLimitTest.php @@ -13,6 +13,14 @@ * * Tests: GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQuerySqliteGroupOrderLimitTest extends TestCase { public function testGroupBySingleColumn(): void @@ -24,7 +32,7 @@ public function testGroupBySingleColumn(): void ->groupBy('status') ->sql('sqlite', false); - $this->assertEquals('SELECT status, COUNT(*) as count FROM `users` GROUP BY status', $sql); + $this->assertEquals('SELECT `status`, COUNT(*) as `count` FROM `users` GROUP BY `status`', $sql); } public function testGroupByMultipleColumns(): void @@ -36,7 +44,7 @@ public function testGroupByMultipleColumns(): void ->groupBy('status', 'role') ->sql('sqlite', false); - $this->assertEquals('SELECT status, role, COUNT(*) FROM `users` GROUP BY status, role', $sql); + $this->assertEquals('SELECT `status`, `role`, COUNT(*) FROM `users` GROUP BY `status`, `role`', $sql); } public function testHavingWithCount(): void @@ -49,11 +57,11 @@ public function testHavingWithCount(): void ->having('COUNT(*)')->greater(5) ->sql('sqlite', false); - $this->assertEquals('SELECT status, COUNT(*) as count FROM `users` GROUP BY status HAVING COUNT(*) > 5', $sql); + $this->assertEquals('SELECT `status`, COUNT(*) as `count` FROM `users` GROUP BY `status` HAVING COUNT(*) > 5', $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT status, COUNT(*) as count FROM `users` GROUP BY status HAVING COUNT(*) > ?', $prepared->sql()); + $this->assertEquals('SELECT `status`, COUNT(*) as `count` FROM `users` GROUP BY `status` HAVING COUNT(*) > ?', $prepared->sql()); $this->assertEquals([5], $prepared->bindings()); } @@ -68,11 +76,11 @@ public function testHavingWithMultipleConditions(): void ->and('COUNT(*)')->lower(100) ->sql('sqlite', false); - $this->assertEquals('SELECT status, COUNT(*) as count FROM `users` GROUP BY status HAVING COUNT(*) > 5 AND COUNT(*) < 100', $sql); + $this->assertEquals('SELECT `status`, COUNT(*) as `count` FROM `users` GROUP BY `status` HAVING COUNT(*) > 5 AND COUNT(*) < 100', $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT status, COUNT(*) as count FROM `users` GROUP BY status HAVING COUNT(*) > ? AND COUNT(*) < ?', $prepared->sql()); + $this->assertEquals('SELECT `status`, COUNT(*) as `count` FROM `users` GROUP BY `status` HAVING COUNT(*) > ? AND COUNT(*) < ?', $prepared->sql()); $this->assertEquals([5, 100], $prepared->bindings()); } @@ -85,7 +93,7 @@ public function testOrderBySingleColumn(): void ->orderBy('name') ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` ORDER BY name ASC', $sql); + $this->assertEquals('SELECT * FROM `users` ORDER BY `name` ASC', $sql); } public function testOrderByDescending(): void @@ -97,7 +105,7 @@ public function testOrderByDescending(): void ->orderBy('created_at', 'DESC') ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` ORDER BY created_at DESC', $sql); + $this->assertEquals('SELECT * FROM `users` ORDER BY `created_at` DESC', $sql); } public function testOrderByMultipleColumns(): void @@ -110,7 +118,7 @@ public function testOrderByMultipleColumns(): void ->orderBy('name', 'DESC') ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` ORDER BY status ASC, name DESC', $sql); + $this->assertEquals('SELECT * FROM `users` ORDER BY `status` ASC, `name` DESC', $sql); } public function testLimitOnly(): void @@ -151,24 +159,24 @@ public function testCompleteQueryWithAllClauses(): void ->limit(10) ->sql('sqlite', false); - $expected = "SELECT u.id, u.name, COUNT(p.id) as post_count FROM `users` `u` " + $expected = "SELECT `u`.`id`, `u`.`name`, COUNT(p.id) as `post_count` FROM `users` `u` " . "LEFT JOIN `posts` `p` ON u.id = p.user_id " - . "WHERE u.status = 'active' " - . "GROUP BY u.id, u.name " + . "WHERE `u`.`status` = 'active' " + . "GROUP BY `u`.`id`, `u`.`name` " . "HAVING COUNT(p.id) > 5 " - . "ORDER BY post_count DESC " + . "ORDER BY `post_count` DESC " . "LIMIT 10"; $this->assertEquals($expected, $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $expectedPrepared = "SELECT u.id, u.name, COUNT(p.id) as post_count FROM `users` `u` " + $expectedPrepared = "SELECT `u`.`id`, `u`.`name`, COUNT(p.id) as `post_count` FROM `users` `u` " . "LEFT JOIN `posts` `p` ON u.id = p.user_id " - . "WHERE u.status = ? " - . "GROUP BY u.id, u.name " + . "WHERE `u`.`status` = ? " + . "GROUP BY `u`.`id`, `u`.`name` " . "HAVING COUNT(p.id) > ? " - . "ORDER BY post_count DESC " + . "ORDER BY `post_count` DESC " . "LIMIT 10"; $this->assertEquals($expectedPrepared, $prepared->sql()); $this->assertEquals(['active', 5], $prepared->bindings()); diff --git a/tests/Unit/Query/Sqlite/DbQuerySqliteJoinTest.php b/tests/Unit/Query/Sqlite/DbQuerySqliteJoinTest.php index 568f095..7d10838 100644 --- a/tests/Unit/Query/Sqlite/DbQuerySqliteJoinTest.php +++ b/tests/Unit/Query/Sqlite/DbQuerySqliteJoinTest.php @@ -13,6 +13,14 @@ * * Tests: INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQuerySqliteJoinTest extends TestCase { public function testInnerJoinWithConstraint(): void @@ -135,9 +143,9 @@ public function testJoinSubqueryWithBindingsInPreparedMode(): void $this->assertStringContainsString('FROM `users` `u`', $result->sql()); $this->assertStringContainsString('LEFT JOIN (SELECT', $result->sql()); $this->assertStringContainsString('FROM `orders`', $result->sql()); - $this->assertStringContainsString('WHERE status = ?', $result->sql()); + $this->assertStringContainsString('WHERE `status` = ?', $result->sql()); $this->assertStringContainsString(') `o` ON u.id = o.user_id', $result->sql()); - $this->assertStringContainsString('WHERE u.active = ?', $result->sql()); + $this->assertStringContainsString('WHERE `u`.`active` = ?', $result->sql()); // Verify binding order: JOIN subquery bindings first, then main query bindings $bindings = $result->bindings(); @@ -166,8 +174,8 @@ public function testJoinSubqueryInNonPreparedMode(): void $this->assertIsString($sql); $this->assertStringContainsString('FROM `users` `u`', $sql); - $this->assertStringContainsString('LEFT JOIN (SELECT user_id, COUNT(*) as cnt FROM `orders` WHERE status = \'paid\'', $sql); + $this->assertStringContainsString('LEFT JOIN (SELECT `user_id`, COUNT(*) as `cnt` FROM `orders` WHERE `status` = \'paid\'', $sql); $this->assertStringContainsString('`o` ON u.id = o.user_id', $sql); - $this->assertStringContainsString('WHERE u.active = 1', $sql); + $this->assertStringContainsString('WHERE `u`.`active` = 1', $sql); } } diff --git a/tests/Unit/Query/Sqlite/DbQuerySqliteSubqueryTest.php b/tests/Unit/Query/Sqlite/DbQuerySqliteSubqueryTest.php index 12c1df3..614abc8 100644 --- a/tests/Unit/Query/Sqlite/DbQuerySqliteSubqueryTest.php +++ b/tests/Unit/Query/Sqlite/DbQuerySqliteSubqueryTest.php @@ -15,6 +15,14 @@ * * Tests: SELECT subqueries, correlated subqueries, nested subqueries */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQuerySqliteSubqueryTest extends TestCase { public function testSelectSubquerySingleSimple(): void @@ -31,7 +39,7 @@ public function testSelectSubquerySingleSimple(): void ->from('users') ->sql('sqlite', false); - $expected = "SELECT id, name, (SELECT COUNT(*) FROM `posts` WHERE user_id = 1) AS `post_count` FROM `users`"; + $expected = "SELECT `id`, `name`, (SELECT COUNT(*) FROM `posts` WHERE `user_id` = 1) AS `post_count` FROM `users`"; $this->assertEquals($expected, $sql); } @@ -49,7 +57,7 @@ public function testSelectSubqueryWithCorrelatedCondition(): void ->from('users') ->sql('sqlite', false); - $expected = "SELECT id, name, (SELECT COUNT(*) FROM `posts` `p` WHERE p.user_id = users.id) AS `post_count` FROM `users`"; + $expected = "SELECT `id`, `name`, (SELECT COUNT(*) FROM `posts` `p` WHERE `p`.`user_id` = users.id) AS `post_count` FROM `users`"; $this->assertEquals($expected, $sql); } @@ -73,9 +81,9 @@ public function testSelectSubqueryMultiple(): void ->from('users') ->sql('sqlite', false); - $expected = "SELECT id, name, " - . "(SELECT COUNT(*) FROM `posts` `p` WHERE p.user_id = users.id) AS `post_count`, " - . "(SELECT COUNT(*) FROM `comments` `c` WHERE c.user_id = users.id) AS `comment_count` " + $expected = "SELECT `id`, `name`, " + . "(SELECT COUNT(*) FROM `posts` `p` WHERE `p`.`user_id` = users.id) AS `post_count`, " + . "(SELECT COUNT(*) FROM `comments` `c` WHERE `c`.`user_id` = users.id) AS `comment_count` " . "FROM `users`"; $this->assertEquals($expected, $sql); @@ -114,9 +122,9 @@ public function testSelectSubqueryWithComplexSubquery(): void ->where('active')->equals(1) ->sql('sqlite', false); - $expected = "SELECT id, name, email, " - . "(SELECT MAX(p.created_at) FROM `posts` `p` WHERE p.user_id = users.id AND p.status = 'published') AS `last_post_date` " - . "FROM `users` WHERE active = 1"; + $expected = "SELECT `id`, `name`, `email`, " + . "(SELECT MAX(p.created_at) FROM `posts` `p` WHERE `p`.`user_id` = users.id AND `p`.`status` = 'published') AS `last_post_date` " + . "FROM `users` WHERE `active` = 1"; $this->assertEquals($expected, $sql); } @@ -136,8 +144,8 @@ public function testSelectSubqueryWithJoinInSubquery(): void ->from('users') ->sql('sqlite', false); - $expected = "SELECT id, name, " - . "(SELECT COUNT(DISTINCT c.id) FROM `posts` `p` INNER JOIN `comments` `c` ON p.id = c.post_id WHERE p.user_id = users.id) AS `comment_count` " + $expected = "SELECT `id`, `name`, " + . "(SELECT COUNT(DISTINCT c.id) FROM `posts` `p` INNER JOIN `comments` `c` ON p.id = c.post_id WHERE `p`.`user_id` = users.id) AS `comment_count` " . "FROM `users`"; $this->assertEquals($expected, $sql); @@ -158,8 +166,8 @@ public function testSelectSubqueryWithMainQueryJoin(): void ->innerJoin('profiles', 'u.id = prof.user_id', 'prof') ->sql('sqlite', false); - $expected = "SELECT u.id, u.name, " - . "(SELECT COUNT(*) FROM `posts` `p` WHERE p.user_id = u.id) AS `post_count` " + $expected = "SELECT `u`.`id`, `u`.`name`, " + . "(SELECT COUNT(*) FROM `posts` `p` WHERE `p`.`user_id` = u.id) AS `post_count` " . "FROM `users` `u` INNER JOIN `profiles` `prof` ON u.id = prof.user_id"; $this->assertEquals($expected, $sql); @@ -181,9 +189,9 @@ public function testSelectSubqueryWithOrderByAndLimit(): void ->limit(10) ->sql('sqlite', false); - $expected = "SELECT id, name, " - . "(SELECT COUNT(*) FROM `posts` `p` WHERE p.user_id = users.id) AS `post_count` " - . "FROM `users` ORDER BY name ASC LIMIT 10"; + $expected = "SELECT `id`, `name`, " + . "(SELECT COUNT(*) FROM `posts` `p` WHERE `p`.`user_id` = users.id) AS `post_count` " + . "FROM `users` ORDER BY `name` ASC LIMIT 10"; $this->assertEquals($expected, $sql); } @@ -204,9 +212,9 @@ public function testSelectSubqueryWithGroupByAndHaving(): void ->having('COUNT(*)')->greater(5) ->sql('sqlite', false); - $expected = "SELECT c.id, c.name, COUNT(*) as total, " - . "(SELECT AVG(p.rating) FROM `posts` `p` WHERE p.category_id = c.id) AS `avg_rating` " - . "FROM `categories` `c` GROUP BY c.id, c.name HAVING COUNT(*) > 5"; + $expected = "SELECT `c`.`id`, `c`.`name`, COUNT(*) as `total`, " + . "(SELECT AVG(p.rating) FROM `posts` `p` WHERE `p`.`category_id` = c.id) AS `avg_rating` " + . "FROM `categories` `c` GROUP BY `c`.`id`, `c`.`name` HAVING COUNT(*) > 5"; $this->assertEquals($expected, $sql); } @@ -246,7 +254,7 @@ public function testSelectSubqueryWithNestedSubquery(): void ->sql('sqlite', false); // Note: This creates a very complex nested structure - $this->assertStringContainsString('SELECT id, name,', $sql); + $this->assertStringContainsString('SELECT `id`, `name`,', $sql); $this->assertStringContainsString('SELECT COUNT(*)', $sql); $this->assertStringContainsString('SELECT MAX(created_at)', $sql); $this->assertStringContainsString('AS `post_info`', $sql); @@ -271,9 +279,9 @@ public function testFromSubqueryWithBindingsInPreparedMode(): void $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); $this->assertStringContainsString('FROM (SELECT', $result->sql()); $this->assertStringContainsString('FROM `users`', $result->sql()); - $this->assertStringContainsString('WHERE status = ?', $result->sql()); + $this->assertStringContainsString('WHERE `status` = ?', $result->sql()); $this->assertStringContainsString(') `sub`', $result->sql()); - $this->assertStringContainsString('WHERE sub.id > ?', $result->sql()); + $this->assertStringContainsString('WHERE `sub`.`id` > ?', $result->sql()); // Verify binding order: FROM subquery bindings first, then main query bindings $bindings = $result->bindings(); @@ -299,8 +307,8 @@ public function testFromSubqueryInNonPreparedMode(): void $sql = $query->sql('sqlite', false); $this->assertIsString($sql); - $this->assertStringContainsString('FROM (SELECT id, name FROM `users` WHERE status = \'active\')', $sql); + $this->assertStringContainsString('FROM (SELECT `id`, `name` FROM `users` WHERE `status` = \'active\')', $sql); $this->assertStringContainsString('`sub`', $sql); - $this->assertStringContainsString('WHERE sub.id > 5', $sql); + $this->assertStringContainsString('WHERE `sub`.`id` > 5', $sql); } } diff --git a/tests/Unit/Query/Sqlite/DbQuerySqliteUnionTest.php b/tests/Unit/Query/Sqlite/DbQuerySqliteUnionTest.php index 448ab68..674c2c5 100644 --- a/tests/Unit/Query/Sqlite/DbQuerySqliteUnionTest.php +++ b/tests/Unit/Query/Sqlite/DbQuerySqliteUnionTest.php @@ -14,6 +14,14 @@ * * Tests: UNION, UNION ALL */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQuerySqliteUnionTest extends TestCase { public function testUnionSimple(): void @@ -31,8 +39,8 @@ public function testUnionSimple(): void ->union($secondQuery) ->sql('sqlite', false); - $expected = "SELECT id, name FROM `customers` WHERE country = 'Germany' " - . "UNION SELECT id, name FROM `suppliers` WHERE country = 'Germany'"; + $expected = "SELECT `id`, `name` FROM `customers` WHERE `country` = 'Germany' " + . "UNION SELECT `id`, `name` FROM `suppliers` WHERE `country` = 'Germany'"; $this->assertEquals($expected, $sql); } @@ -50,7 +58,7 @@ public function testUnionAllSimple(): void ->unionAll($secondQuery) ->sql('sqlite', false); - $expected = "SELECT name FROM `active_users` UNION ALL SELECT name FROM `archived_users`"; + $expected = "SELECT `name` FROM `active_users` UNION ALL SELECT `name` FROM `archived_users`"; $this->assertEquals($expected, $sql); } @@ -73,9 +81,9 @@ public function testUnionMultipleQueries(): void ->union($thirdQuery) ->sql('sqlite', false); - $expected = "SELECT email FROM `customers` " - . "UNION SELECT email FROM `suppliers` " - . "UNION SELECT email FROM `partners`"; + $expected = "SELECT `email` FROM `customers` " + . "UNION SELECT `email` FROM `suppliers` " + . "UNION SELECT `email` FROM `partners`"; $this->assertEquals($expected, $sql); } @@ -94,10 +102,10 @@ public function testUnionWithOrderBy(): void ->orderBy('created_at', 'DESC') ->sql('sqlite', false); - $this->assertStringContainsString('SELECT name, created_at FROM `active_posts`', $sql); + $this->assertStringContainsString('SELECT `name`, `created_at` FROM `active_posts`', $sql); $this->assertStringContainsString('UNION', $sql); - $this->assertStringContainsString('SELECT name, created_at FROM `archived_posts`', $sql); - $this->assertStringContainsString('ORDER BY created_at DESC', $sql); + $this->assertStringContainsString('SELECT `name`, `created_at` FROM `archived_posts`', $sql); + $this->assertStringContainsString('ORDER BY `created_at` DESC', $sql); $unionPos = strpos($sql, 'UNION'); $orderByPos = strpos($sql, 'ORDER BY'); @@ -118,8 +126,8 @@ public function testUnionWithLimit(): void ->limit(20) ->sql('sqlite', false); - $expected = "SELECT title FROM `news_posts` " - . "UNION SELECT title FROM `blog_posts` " + $expected = "SELECT `title` FROM `news_posts` " + . "UNION SELECT `title` FROM `blog_posts` " . "LIMIT 20"; $this->assertEquals($expected, $sql); @@ -142,12 +150,12 @@ public function testUnionWithOrderByAndLimit(): void ->limit(50) ->sql('sqlite', false); - $this->assertStringContainsString('SELECT id, name, created_at FROM `active_items`', $sql); - $this->assertStringContainsString("WHERE status = 'open'", $sql); + $this->assertStringContainsString('SELECT `id`, `name`, `created_at` FROM `active_items`', $sql); + $this->assertStringContainsString("WHERE `status` = 'open'", $sql); $this->assertStringContainsString('UNION', $sql); - $this->assertStringContainsString('SELECT id, name, created_at FROM `archived_items`', $sql); - $this->assertStringContainsString("WHERE status = 'closed'", $sql); - $this->assertStringContainsString('ORDER BY created_at DESC', $sql); + $this->assertStringContainsString('SELECT `id`, `name`, `created_at` FROM `archived_items`', $sql); + $this->assertStringContainsString("WHERE `status` = 'closed'", $sql); + $this->assertStringContainsString('ORDER BY `created_at` DESC', $sql); $this->assertStringContainsString('LIMIT 50', $sql); } @@ -170,14 +178,14 @@ public function testUnionWithComplexQueries(): void ->limit(100) ->sql('sqlite', false); - $this->assertStringContainsString('SELECT u.id, u.email', $sql); + $this->assertStringContainsString('SELECT `u`.`id`, `u`.`email`', $sql); $this->assertStringContainsString('FROM `active_users` `u`', $sql); $this->assertStringContainsString('INNER JOIN `profiles`', $sql); - $this->assertStringContainsString("WHERE u.status = 'verified'", $sql); + $this->assertStringContainsString("WHERE `u`.`status` = 'verified'", $sql); $this->assertStringContainsString('UNION', $sql); $this->assertStringContainsString('FROM `inactive_users` `u`', $sql); $this->assertStringContainsString('LEFT JOIN `profiles`', $sql); - $this->assertStringContainsString("WHERE u.last_login < '2023-01-01'", $sql); + $this->assertStringContainsString("WHERE `u`.`last_login` < '2023-01-01'", $sql); $this->assertStringContainsString('LIMIT 100', $sql); } @@ -199,9 +207,9 @@ public function testUnionAllWithMultipleQueries(): void ->unionAll($thirdQuery) ->sql('sqlite', false); - $expected = "SELECT email, created_at FROM `active_subscribers` " - . "UNION ALL SELECT email, created_at FROM `pending_subscribers` " - . "UNION ALL SELECT email, created_at FROM `archived_subscribers`"; + $expected = "SELECT `email`, `created_at` FROM `active_subscribers` " + . "UNION ALL SELECT `email`, `created_at` FROM `pending_subscribers` " + . "UNION ALL SELECT `email`, `created_at` FROM `archived_subscribers`"; $this->assertEquals($expected, $sql); } @@ -224,9 +232,9 @@ public function testUnionMixedWithUnionAll(): void ->unionAll($thirdQuery) ->sql('sqlite', false); - $expected = "SELECT name FROM `customers` " - . "UNION SELECT name FROM `suppliers` " - . "UNION ALL SELECT name FROM `partners`"; + $expected = "SELECT `name` FROM `customers` " + . "UNION SELECT `name` FROM `suppliers` " + . "UNION ALL SELECT `name` FROM `partners`"; $this->assertEquals($expected, $sql); } @@ -265,10 +273,10 @@ public function testUnionAllWithBindingsInPreparedMode(): void $result = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $this->assertStringContainsString('SELECT id, name FROM `employees`', $result->sql()); - $this->assertStringContainsString('WHERE country = ?', $result->sql()); + $this->assertStringContainsString('SELECT `id`, `name` FROM `employees`', $result->sql()); + $this->assertStringContainsString('WHERE `country` = ?', $result->sql()); $this->assertStringContainsString('UNION ALL', $result->sql()); - $this->assertStringContainsString('SELECT id, name FROM `suppliers`', $result->sql()); + $this->assertStringContainsString('SELECT `id`, `name` FROM `suppliers`', $result->sql()); // Verify binding order: main query bindings first, then UNION ALL bindings $bindings = $result->bindings(); diff --git a/tests/Unit/Query/Sqlite/DbQuerySqliteWhereTest.php b/tests/Unit/Query/Sqlite/DbQuerySqliteWhereTest.php index 6d867bb..3d8f578 100644 --- a/tests/Unit/Query/Sqlite/DbQuerySqliteWhereTest.php +++ b/tests/Unit/Query/Sqlite/DbQuerySqliteWhereTest.php @@ -14,6 +14,14 @@ * * Tests: WHERE conditions, operators, AND, OR, brackets */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQuerySqliteWhereTest extends TestCase { public function testWhereEquals(): void @@ -25,16 +33,16 @@ public function testWhereEquals(): void ->where('id')->equals(123) ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` WHERE id = 123', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `id` = 123', $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE id = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `id` = ?', $prepared->sql()); $this->assertEquals([123], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE id = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `id` = ?', $prepared->sql()); $this->assertEquals([123], $prepared->bindings()); } @@ -47,16 +55,16 @@ public function testWhereWithString(): void ->where('name')->equals('John') ->sql('sqlite', false); - $this->assertEquals("SELECT * FROM `users` WHERE name = 'John'", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `name` = 'John'", $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE name = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `name` = ?', $prepared->sql()); $this->assertEquals(['John'], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE name = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `name` = ?', $prepared->sql()); $this->assertEquals(['John'], $prepared->bindings()); } @@ -69,16 +77,16 @@ public function testWhereGreater(): void ->where('age')->greater(18) ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` WHERE age > 18', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `age` > 18', $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age > ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` > ?', $prepared->sql()); $this->assertEquals([18], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age > ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` > ?', $prepared->sql()); $this->assertEquals([18], $prepared->bindings()); } @@ -91,16 +99,16 @@ public function testWhereLower(): void ->where('age')->lower(65) ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` WHERE age < 65', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `age` < 65', $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age < ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` < ?', $prepared->sql()); $this->assertEquals([65], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age < ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` < ?', $prepared->sql()); $this->assertEquals([65], $prepared->bindings()); } @@ -113,16 +121,16 @@ public function testWhereGreaterEquals(): void ->where('age')->greaterEquals(18) ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` WHERE age >= 18', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `age` >= 18', $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age >= ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` >= ?', $prepared->sql()); $this->assertEquals([18], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age >= ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` >= ?', $prepared->sql()); $this->assertEquals([18], $prepared->bindings()); } @@ -135,16 +143,16 @@ public function testWhereLowerEquals(): void ->where('age')->lowerEquals(65) ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` WHERE age <= 65', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `age` <= 65', $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age <= ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` <= ?', $prepared->sql()); $this->assertEquals([65], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age <= ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` <= ?', $prepared->sql()); $this->assertEquals([65], $prepared->bindings()); } @@ -157,16 +165,16 @@ public function testWhereBetween(): void ->where('age')->between(18, 65) ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` WHERE age BETWEEN 18 AND 65', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `age` BETWEEN 18 AND 65', $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age BETWEEN ? AND ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` BETWEEN ? AND ?', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age BETWEEN ? AND ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` BETWEEN ? AND ?', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); } @@ -179,16 +187,16 @@ public function testWhereIn(): void ->where('status')->in(['active', 'pending']) ->sql('sqlite', false); - $this->assertEquals("SELECT * FROM `users` WHERE status IN ('active', 'pending')", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` IN ('active', 'pending')", $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE status IN (?, ?)', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `status` IN (?, ?)', $prepared->sql()); $this->assertEquals(['active', 'pending'], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE status IN (?, ?)', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `status` IN (?, ?)', $prepared->sql()); $this->assertEquals(['active', 'pending'], $prepared->bindings()); } @@ -236,10 +244,10 @@ public function testWhereInEmptyArrayWithOtherConditions(): void ->and('id')->in([]) ->sql('sqlite', false); - $this->assertEquals("SELECT * FROM `users` WHERE status = 'active' AND 1=0", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` = 'active' AND 1=0", $sql); $prepared = $query->sql('sqlite', true); - $this->assertEquals('SELECT * FROM `users` WHERE status = ? AND 1=0', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `status` = ? AND 1=0', $prepared->sql()); $this->assertEquals(['active'], $prepared->bindings()); } @@ -253,7 +261,7 @@ public function testWhereInEmptyArrayWithOrCondition(): void ->or('id')->in([]) ->sql('sqlite', false); - $this->assertEquals("SELECT * FROM `users` WHERE status = 'active' OR 1=0", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` = 'active' OR 1=0", $sql); } public function testWhereInEmptyArrayWithBrackets(): void @@ -266,7 +274,7 @@ public function testWhereInEmptyArrayWithBrackets(): void ->and('(id')->in([], ')') ->sql('sqlite', false); - $this->assertEquals("SELECT * FROM `users` WHERE status = 'active' AND (1=0)", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` = 'active' AND (1=0)", $sql); } public function testWhereNotInEmptyArrayWithOtherConditions(): void @@ -279,7 +287,7 @@ public function testWhereNotInEmptyArrayWithOtherConditions(): void ->and('id')->notIn([]) ->sql('sqlite', false); - $this->assertEquals("SELECT * FROM `users` WHERE status = 'active' AND 1=1", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `status` = 'active' AND 1=1", $sql); } public function testWhereLike(): void @@ -291,16 +299,16 @@ public function testWhereLike(): void ->where('name')->like('%John%') ->sql('sqlite', false); - $this->assertEquals("SELECT * FROM `users` WHERE name LIKE '%John%'", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `name` LIKE '%John%'", $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE name LIKE ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `name` LIKE ?', $prepared->sql()); $this->assertEquals(['%John%'], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE name LIKE ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `name` LIKE ?', $prepared->sql()); $this->assertEquals(['%John%'], $prepared->bindings()); } @@ -313,7 +321,7 @@ public function testWhereIsNull(): void ->where('deleted_at')->isNull() ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` WHERE deleted_at IS NULL', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `deleted_at` IS NULL', $sql); } public function testWhereIsNotNull(): void @@ -325,7 +333,7 @@ public function testWhereIsNotNull(): void ->where('email')->isNotNull() ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` WHERE email IS NOT NULL', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `email` IS NOT NULL', $sql); } public function testMultipleWhereWithAnd(): void @@ -338,16 +346,16 @@ public function testMultipleWhereWithAnd(): void ->and('status')->equals('active') ->sql('sqlite', false); - $this->assertEquals("SELECT * FROM `users` WHERE age > 18 AND status = 'active'", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE `age` > 18 AND `status` = 'active'", $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age > ? AND status = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` > ? AND `status` = ?', $prepared->sql()); $this->assertEquals([18, 'active'], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age > ? AND status = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` > ? AND `status` = ?', $prepared->sql()); $this->assertEquals([18, 'active'], $prepared->bindings()); } @@ -361,16 +369,16 @@ public function testWhereWithOr(): void ->or('age')->greater(65) ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` WHERE age < 18 OR age > 65', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE `age` < 18 OR `age` > 65', $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age < ? OR age > ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` < ? OR `age` > ?', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE age < ? OR age > ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `age` < ? OR `age` > ?', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); } @@ -384,16 +392,16 @@ public function testWhereWithBrackets(): void ->and('age')->lower(65, ')') ->sql('sqlite', false); - $this->assertEquals('SELECT * FROM `users` WHERE (age > 18 AND age < 65)', $sql); + $this->assertEquals('SELECT * FROM `users` WHERE (age > 18 AND `age` < 65)', $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE (age > ? AND age < ?)', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE (age > ? AND `age` < ?)', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE (age > ? AND age < ?)', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE (age > ? AND `age` < ?)', $prepared->sql()); $this->assertEquals([18, 65], $prepared->bindings()); } @@ -408,16 +416,16 @@ public function testComplexWhereConditions(): void ->or('role')->equals('admin') ->sql('sqlite', false); - $this->assertEquals("SELECT * FROM `users` WHERE (status = 'active' AND age > 18) OR role = 'admin'", $sql); + $this->assertEquals("SELECT * FROM `users` WHERE (status = 'active' AND `age` > 18) OR `role` = 'admin'", $sql); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE (status = ? AND age > ?) OR role = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE (status = ? AND `age` > ?) OR `role` = ?', $prepared->sql()); $this->assertEquals(['active', 18, 'admin'], $prepared->bindings()); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE (status = ? AND age > ?) OR role = ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE (status = ? AND `age` > ?) OR `role` = ?', $prepared->sql()); $this->assertEquals(['active', 18, 'admin'], $prepared->bindings()); } @@ -439,7 +447,7 @@ public function testWhereWithSubqueryInNonPreparedMode(): void $this->assertIsString($sql); $this->assertStringContainsString('SELECT * FROM `users`', $sql); - $this->assertStringContainsString('WHERE id IN (SELECT user_id FROM `orders` WHERE status = \'completed\')', $sql); + $this->assertStringContainsString('WHERE `id` IN (SELECT `user_id` FROM `orders` WHERE `status` = \'completed\')', $sql); } public function testWhereJsonExtract(): void @@ -465,7 +473,7 @@ public function testAndJsonExtract(): void $sql = $query->sql('sqlite', false); - $this->assertStringContainsString('WHERE active = 1', $sql); + $this->assertStringContainsString('WHERE `active` = 1', $sql); $this->assertStringContainsString('AND json_extract(`settings`, \'$.theme\') = \'dark\'', $sql); } @@ -479,7 +487,7 @@ public function testOrJsonExtract(): void $sql = $query->sql('sqlite', false); - $this->assertStringContainsString('WHERE status = \'active\'', $sql); + $this->assertStringContainsString('WHERE `status` = \'active\'', $sql); $this->assertStringContainsString('OR json_extract(`preferences`, \'$.notifications\') = 1', $sql); } @@ -493,7 +501,7 @@ public function testHavingJson(): void $sql = $query->sql('sqlite', false); - $this->assertStringContainsString('GROUP BY user_id', $sql); + $this->assertStringContainsString('GROUP BY `user_id`', $sql); $this->assertStringContainsString('HAVING json_extract(`metadata`, \'$.priority\') = \'high\'', $sql); } @@ -539,14 +547,14 @@ public function testAndWithExpressionField(): void ->sql('sqlite', false); $this->assertEquals( - 'SELECT * FROM `users` WHERE status = \'active\' AND strftime("%Y", created_at) = \'2024\'', + 'SELECT * FROM `users` WHERE `status` = \'active\' AND strftime("%Y", created_at) = \'2024\'', $sql ); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $this->assertEquals( - 'SELECT * FROM `users` WHERE status = ? AND strftime("%Y", created_at) = ?', + 'SELECT * FROM `users` WHERE `status` = ? AND strftime("%Y", created_at) = ?', $prepared->sql() ); $this->assertEquals(['active', '2024'], $prepared->bindings()); @@ -563,13 +571,13 @@ public function testOrWithExpressionField(): void ->sql('sqlite', false); $this->assertEquals( - "SELECT * FROM `users` WHERE name LIKE '%John%' OR LOWER(email) LIKE '%john%'", + "SELECT * FROM `users` WHERE `name` LIKE '%John%' OR LOWER(email) LIKE '%john%'", $sql ); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); - $this->assertEquals('SELECT * FROM `users` WHERE name LIKE ? OR LOWER(email) LIKE ?', $prepared->sql()); + $this->assertEquals('SELECT * FROM `users` WHERE `name` LIKE ? OR LOWER(email) LIKE ?', $prepared->sql()); $this->assertEquals(['%John%', '%john%'], $prepared->bindings()); } @@ -636,14 +644,14 @@ public function testComplexExpressionConditions(): void ->sql('sqlite', false); $this->assertEquals( - 'SELECT * FROM `products` WHERE (stock_level > 0 AND price * 0.8 < 100) OR featured = 1', + 'SELECT * FROM `products` WHERE (stock_level > 0 AND price * 0.8 < 100) OR `featured` = 1', $sql ); $prepared = $query->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $prepared); $this->assertEquals( - 'SELECT * FROM `products` WHERE (stock_level > ? AND price * 0.8 < ?) OR featured = ?', + 'SELECT * FROM `products` WHERE (stock_level > ? AND price * 0.8 < ?) OR `featured` = ?', $prepared->sql() ); $this->assertEquals([0, 100, 1], $prepared->bindings()); diff --git a/tests/Unit/Query/Sqlite/DbQuerySqliteWindowTest.php b/tests/Unit/Query/Sqlite/DbQuerySqliteWindowTest.php index 9e17bb4..8b65c89 100644 --- a/tests/Unit/Query/Sqlite/DbQuerySqliteWindowTest.php +++ b/tests/Unit/Query/Sqlite/DbQuerySqliteWindowTest.php @@ -15,6 +15,14 @@ * Tests: Window functions (ROW_NUMBER, RANK, SUM, etc.), Named windows, PARTITION BY, ORDER BY, Frames * Note: SQLite 3.25.0+ required for window functions */ +/* + * SQL-Pins am 2026-08-10 an das Auto-Quoting einfacher Identifier angepasst: + * WHERE/AND/OR-, HAVING-, ORDER-BY-, GROUP-BY-Felder und die SELECT-Feldliste + * quoten `ident` bzw. `alias.ident` jetzt dialektgerecht (MySQL/SQLite: Backtick, + * PostgreSQL: Double-Quote). Ausdruecke, '*', bereits Gequotetes und + * Expression::raw() bleiben byte-identisch roh. Alle Aenderungen in dieser + * Datei sind reine Quote-Zeichen-Diffs in erwarteten SQL-Strings. + */ class DbQuerySqliteWindowTest extends TestCase { public function testSelectWindowWithRowNumber(): void @@ -30,7 +38,7 @@ public function testSelectWindowWithRowNumber(): void $this->assertInstanceOf(DbQuery::class, $result); $sql = $query->sql('sqlite', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees`'; + $expected = 'SELECT `id`, `name`, `salary`, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees`'; $this->assertEquals($expected, $sql); } @@ -44,7 +52,7 @@ public function testSelectWindowWithRank(): void ->from('players') ->sql('sqlite', false); - $expected = 'SELECT id, score, RANK() OVER (ORDER BY score DESC) AS `rank` FROM `players`'; + $expected = 'SELECT `id`, `score`, RANK() OVER (ORDER BY score DESC) AS `rank` FROM `players`'; $this->assertEquals($expected, $sql); } @@ -58,7 +66,7 @@ public function testSelectWindowWithSumAndArgs(): void ->from('transactions') ->sql('sqlite', false); - $expected = 'SELECT date, amount, SUM(amount) OVER (ORDER BY date ASC) AS `running_total` FROM `transactions`'; + $expected = 'SELECT `date`, `amount`, SUM(amount) OVER (ORDER BY date ASC) AS `running_total` FROM `transactions`'; $this->assertEquals($expected, $sql); } @@ -72,7 +80,7 @@ public function testSelectWindowWithLag(): void ->from('stock_prices') ->sql('sqlite', false); - $expected = 'SELECT date, price, LAG(price, 1) OVER (ORDER BY date ASC) AS `prev_price` FROM `stock_prices`'; + $expected = 'SELECT `date`, `price`, LAG(price, 1) OVER (ORDER BY date ASC) AS `prev_price` FROM `stock_prices`'; $this->assertEquals($expected, $sql); } @@ -87,7 +95,7 @@ public function testSelectWindowWithMultiplePartitions(): void ->from('employees') ->sql('sqlite', false); - $expected = 'SELECT id, name, ROW_NUMBER() OVER (PARTITION BY department, location ORDER BY hire_date ASC) AS `row_num` FROM `employees`'; + $expected = 'SELECT `id`, `name`, ROW_NUMBER() OVER (PARTITION BY department, location ORDER BY hire_date ASC) AS `row_num` FROM `employees`'; $this->assertEquals($expected, $sql); } @@ -102,7 +110,7 @@ public function testSelectWindowWithFrame(): void ->from('sales') ->sql('sqlite', false); - $expected = 'SELECT date, amount, AVG(amount) OVER (ORDER BY date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS `moving_avg` FROM `sales`'; + $expected = 'SELECT `date`, `amount`, AVG(amount) OVER (ORDER BY date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS `moving_avg` FROM `sales`'; $this->assertEquals($expected, $sql); } @@ -117,7 +125,7 @@ public function testSelectWindowWithRangeFrame(): void ->from('data') ->sql('sqlite', false); - $expected = 'SELECT id, value, SUM(value) OVER (ORDER BY id ASC RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS `total` FROM `data`'; + $expected = 'SELECT `id`, `value`, SUM(value) OVER (ORDER BY id ASC RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS `total` FROM `data`'; $this->assertEquals($expected, $sql); } @@ -136,7 +144,7 @@ public function testMultipleWindowFunctions(): void ->from('employees') ->sql('sqlite', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num`, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS `rank` FROM `employees`'; + $expected = 'SELECT `id`, `name`, `salary`, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num`, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS `rank` FROM `employees`'; $this->assertEquals($expected, $sql); } @@ -156,7 +164,7 @@ public function testNamedWindow(): void ->from('employees') ->sql('sqlite', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER dept_window AS `row_num`, RANK() OVER dept_window AS `rank` FROM `employees` WINDOW dept_window AS (PARTITION BY department ORDER BY salary DESC)'; + $expected = 'SELECT `id`, `name`, `salary`, ROW_NUMBER() OVER dept_window AS `row_num`, RANK() OVER dept_window AS `rank` FROM `employees` WINDOW dept_window AS (PARTITION BY department ORDER BY salary DESC)'; $this->assertEquals($expected, $sql); } @@ -172,7 +180,7 @@ public function testNamedWindowWithArgs(): void ->from('metrics') ->sql('sqlite', false); - $expected = 'SELECT timestamp, value, SUM(value) OVER time_window AS `running_total`, LAG(value, 1) OVER time_window AS `prev_value` FROM `metrics` WINDOW time_window AS (ORDER BY timestamp ASC)'; + $expected = 'SELECT `timestamp`, `value`, SUM(value) OVER time_window AS `running_total`, LAG(value, 1) OVER time_window AS `prev_value` FROM `metrics` WINDOW time_window AS (ORDER BY timestamp ASC)'; $this->assertEquals($expected, $sql); } @@ -193,7 +201,7 @@ public function testMultipleNamedWindows(): void ->from('employees') ->sql('sqlite', false); - $expected = 'SELECT id, name, ROW_NUMBER() OVER by_dept AS `dept_seniority`, RANK() OVER by_team AS `team_rank` FROM `employees` WINDOW by_dept AS (PARTITION BY department ORDER BY hire_date ASC), by_team AS (PARTITION BY team ORDER BY performance DESC)'; + $expected = 'SELECT `id`, `name`, ROW_NUMBER() OVER by_dept AS `dept_seniority`, RANK() OVER by_team AS `team_rank` FROM `employees` WINDOW by_dept AS (PARTITION BY department ORDER BY hire_date ASC), by_team AS (PARTITION BY team ORDER BY performance DESC)'; $this->assertEquals($expected, $sql); } @@ -217,7 +225,7 @@ public function testWindowWithWhereClause(): void ->where('active')->equals(1) ->sql('sqlite', false); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees` WHERE active = 1'; + $expected = 'SELECT `id`, `name`, `salary`, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees` WHERE `active` = 1'; $this->assertEquals($expected, $sql); } @@ -233,7 +241,7 @@ public function testWindowWithGroupByAndHaving(): void ->having('COUNT(*)')->greater(5) ->sql('sqlite', false); - $expected = 'SELECT department, COUNT(*) as emp_count, RANK() OVER (ORDER BY COUNT(*) DESC) AS `dept_rank` FROM `employees` GROUP BY department HAVING COUNT(*) > 5'; + $expected = 'SELECT `department`, COUNT(*) as `emp_count`, RANK() OVER (ORDER BY COUNT(*) DESC) AS `dept_rank` FROM `employees` GROUP BY `department` HAVING COUNT(*) > 5'; $this->assertEquals($expected, $sql); } @@ -250,7 +258,7 @@ public function testWindowFunctionWithPreparedStatement(): void ->sql('sqlite', true); $this->assertInstanceOf(DbPreparedQueryInterface::class, $result); - $expected = 'SELECT id, name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees` WHERE salary > ?'; + $expected = 'SELECT `id`, `name`, `salary`, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS `row_num` FROM `employees` WHERE `salary` > ?'; $this->assertEquals($expected, $result->sql()); $this->assertEquals([50000], $result->bindings()); } @@ -265,7 +273,7 @@ public function testWindowWithOnlyPartitionBy(): void ->from('employees') ->sql('sqlite', false); - $expected = 'SELECT id, department, COUNT(*) OVER (PARTITION BY department) AS `dept_count` FROM `employees`'; + $expected = 'SELECT `id`, `department`, COUNT(*) OVER (PARTITION BY department) AS `dept_count` FROM `employees`'; $this->assertEquals($expected, $sql); } @@ -279,7 +287,7 @@ public function testWindowWithDenseRank(): void ->from('players') ->sql('sqlite', false); - $expected = 'SELECT id, score, DENSE_RANK() OVER (ORDER BY score DESC) AS `dense_rank` FROM `players`'; + $expected = 'SELECT `id`, `score`, DENSE_RANK() OVER (ORDER BY score DESC) AS `dense_rank` FROM `players`'; $this->assertEquals($expected, $sql); } @@ -293,7 +301,7 @@ public function testWindowWithNtile(): void ->from('employees') ->sql('sqlite', false); - $expected = 'SELECT id, salary, NTILE(4) OVER (ORDER BY salary ASC) AS `quartile` FROM `employees`'; + $expected = 'SELECT `id`, `salary`, NTILE(4) OVER (ORDER BY salary ASC) AS `quartile` FROM `employees`'; $this->assertEquals($expected, $sql); } @@ -310,7 +318,7 @@ public function testComplexWindowWithAllFeatures(): void ->where('category')->notEquals('void') ->sql('sqlite', false); - $expected = 'SELECT date, amount, category, AVG(amount) OVER (PARTITION BY category ORDER BY date ASC ROWS BETWEEN 3 PRECEDING AND 1 FOLLOWING) AS `moving_avg` FROM `transactions` WHERE category != \'void\''; + $expected = 'SELECT `date`, `amount`, `category`, AVG(amount) OVER (PARTITION BY category ORDER BY date ASC ROWS BETWEEN 3 PRECEDING AND 1 FOLLOWING) AS `moving_avg` FROM `transactions` WHERE `category` != \'void\''; $this->assertEquals($expected, $sql); } }