Skip to content

Commit

Permalink
Merge branch 'master' into fix_batch_insert_with_associative_arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov committed Nov 4, 2023
2 parents 13c2b97 + 4f1dbb9 commit cdb5f5a
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 27 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

- Bug #746: Refactor `DMLQueryBuilder` and fix: unique indexes in `upsert()`, column names with table name and brackets, `batchInsert()` with associative arrays, enhanced documentation of `batchInsert()` and `update()` (@Tigrov)
- Bug #751: Fix collected debug actions (@xepozz)
- Chg #755: Deprecate `TableSchemaInterface::compositeForeignKey()` (@Tigrov)
- Enh #756: Refactor `Quoter` (@Tigrov)
- Bug #756: Fix `Quoter::quoteSql()` for SQL containing table with prefix (@Tigrov)
- Bug #756: Fix `Quoter::getTableNameParts()` for cases when different quotes for tables and columns (@Tigrov)
- Bug #756: Fix `Quoter::quoteTableName()` for sub-query with alias (@Tigrov)
- Bug #761: Quote aliases of CTE in `WITH` queries (@Tigrov)
- Chg #765: Deprecate `SchemaInterface::TYPE_JSONB` (@Tigrov)

## 1.1.1 August 16, 2023

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"spatie/phpunit-watcher": "^1.23",
"vimeo/psalm": "^4.30|^5.12",
"yiisoft/aliases": "^3.0",
"yiisoft/cache-file": "^2.0",
"yiisoft/cache-file": "^3.1",
"yiisoft/di": "^1.0",
"yiisoft/event-dispatcher": "^1.0",
"yiisoft/json": "^1.0",
Expand Down
2 changes: 1 addition & 1 deletion src/Query/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ public function where(array|string|ExpressionInterface|null $condition, array $p
return $this;
}

public function withQuery(QueryInterface|string $query, string $alias, bool $recursive = false): static
public function withQuery(QueryInterface|string $query, ExpressionInterface|string $alias, bool $recursive = false): static
{
$this->withQueries[] = ['query' => $query, 'alias' => $alias, 'recursive' => $recursive];
return $this;
Expand Down
5 changes: 3 additions & 2 deletions src/Query/QueryPartsInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -645,10 +645,11 @@ public function where(array|string|ExpressionInterface|null $condition, array $p
* Prepends an SQL statement using `WITH` syntax.
*
* @param QueryInterface|string $query The SQL statement to append using `UNION`.
* @param string $alias The query alias in `WITH` construction.
* @param ExpressionInterface|string $alias The query alias in `WITH` construction.
* To specify the alias in plain SQL, you may pass an instance of {@see ExpressionInterface}.
* @param bool $recursive Its `true` if using `WITH RECURSIVE` and `false` if using `WITH`.
*/
public function withQuery(QueryInterface|string $query, string $alias, bool $recursive = false): static;
public function withQuery(QueryInterface|string $query, ExpressionInterface|string $alias, bool $recursive = false): static;

/**
* Specifies the `WITH` query clause for the query.
Expand Down
34 changes: 32 additions & 2 deletions src/QueryBuilder/AbstractDQLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ public function buildWithQueries(array $withs, array &$params): string
$recursive = false;
$result = [];

/** @psalm-var array<array-key, array{query:string|Query, alias:string, recursive:bool}> $withs */
/** @psalm-var array{query:string|Query, alias:ExpressionInterface|string, recursive:bool}[] $withs */
foreach ($withs as $with) {
if ($with['recursive']) {
$recursive = true;
Expand All @@ -421,7 +421,9 @@ public function buildWithQueries(array $withs, array &$params): string
[$with['query'], $params] = $this->build($with['query'], $params);
}

$result[] = $with['alias'] . ' AS (' . $with['query'] . ')';
$quotedAlias = $this->quoteCteAlias($with['alias']);

$result[] = $quotedAlias . ' AS (' . $with['query'] . ')';
}

return 'WITH ' . ($recursive ? 'RECURSIVE ' : '') . implode(', ', $result);
Expand Down Expand Up @@ -610,4 +612,32 @@ private function quoteTableNames(array $tables, array &$params): array

return $tables;
}

/**
* Quotes an alias of Common Table Expressions (CTE)
*
* @param ExpressionInterface|string $name The alias name with or without column names to quote.
*
* @return string The quoted alias.
*/
private function quoteCteAlias(ExpressionInterface|string $name): string
{
if ($name instanceof ExpressionInterface) {
return $this->buildExpression($name);
}

if (!str_contains($name, '(')) {
return $this->quoter->quoteTableName($name);
}

if (!str_ends_with($name, ')')) {
return $name;
}

/** @psalm-suppress PossiblyUndefinedArrayOffset */
[$name, $columns] = explode('(', substr($name, 0, -1), 2);
$name = trim($name);

return $this->quoter->quoteTableName($name) . '(' . $this->buildColumns($columns) . ')';
}
}
33 changes: 22 additions & 11 deletions src/Schema/Quoter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
use Yiisoft\Db\Expression\ExpressionInterface;

use function addcslashes;
use function array_slice;
use function count;
use function explode;
use function implode;
use function is_string;
use function preg_match;
use function preg_replace;
use function preg_replace_callback;
use function str_contains;
use function str_replace;
Expand Down Expand Up @@ -44,7 +48,7 @@ public function cleanUpTableNames(array $tableNames): array
{
$cleanedUpTableNames = [];
$pattern = <<<PATTERN
~^\s*((?:['"`\[]|{{).*?(?:['"`\]]|}})|\(.*?\)|.*?)(?:(?:\s+(?:as\s+)?)((?:['"`\[]|{{).*?(?:['"`\]]|}})|.*?))?\s*$~iux
~^\s*((?:['"`\[]|{{).*?(?:['"`\]]|}})|\(.*?\)|.*?)(?:\s+(?:as\s+)?((?:['"`\[]|{{).*?(?:['"`\]]|}})|.*?))?\s*$~iux
PATTERN;

/** @psalm-var array<array-key, ExpressionInterface|string> $tableNames */
Expand Down Expand Up @@ -104,7 +108,7 @@ public function ensureColumnName(string $name): string
$name = $parts[count($parts) - 1];
}

return preg_replace('|^\[\[([_\w\-. ]+)\]\]$|', '\1', $name);
return preg_replace('|^\[\[([\w\-. ]+)]]$|', '\1', $name);
}

public function quoteColumnName(string $name): string
Expand Down Expand Up @@ -135,8 +139,9 @@ public function quoteSimpleColumnName(string $name): string
[$startingCharacter, $endingCharacter] = $this->columnQuoteCharacter;
}

return $name === '*' || str_contains($name, $startingCharacter) ? $name : $startingCharacter . $name
. $endingCharacter;
return $name === '*' || str_starts_with($name, $startingCharacter)
? $name
: $startingCharacter . $name . $endingCharacter;
}

public function quoteSimpleTableName(string $name): string
Expand All @@ -147,13 +152,15 @@ public function quoteSimpleTableName(string $name): string
[$startingCharacter, $endingCharacter] = $this->tableQuoteCharacter;
}

return str_contains($name, $startingCharacter) ? $name : $startingCharacter . $name . $endingCharacter;
return str_starts_with($name, $startingCharacter)
? $name
: $startingCharacter . $name . $endingCharacter;
}

public function quoteSql(string $sql): string
{
return preg_replace_callback(
'/({{(%?[\w\-. ]+%?)}}|\\[\\[([\w\-. ]+)]])/',
'/({{(%?[\w\-. ]+)%?}}|\\[\\[([\w\-. ]+)]])/',
function ($matches) {
if (isset($matches[3])) {
return $this->quoteColumnName($matches[3]);
Expand All @@ -167,7 +174,7 @@ function ($matches) {

public function quoteTableName(string $name): string
{
if (str_starts_with($name, '(') && str_ends_with($name, ')')) {
if (str_starts_with($name, '(')) {
return $name;
}

Expand All @@ -194,7 +201,7 @@ public function quoteValue(mixed $value): mixed
return $value;
}

return '\'' . str_replace('\'', '\'\'', addcslashes($value, "\000\032")) . '\'';
return "'" . str_replace("'", "''", addcslashes($value, "\000\032")) . "'";
}

public function unquoteSimpleColumnName(string $name): string
Expand All @@ -205,7 +212,9 @@ public function unquoteSimpleColumnName(string $name): string
$startingCharacter = $this->columnQuoteCharacter[0];
}

return !str_contains($name, $startingCharacter) ? $name : substr($name, 1, -1);
return !str_starts_with($name, $startingCharacter)
? $name
: substr($name, 1, -1);
}

public function unquoteSimpleTableName(string $name): string
Expand All @@ -216,7 +225,9 @@ public function unquoteSimpleTableName(string $name): string
$startingCharacter = $this->tableQuoteCharacter[0];
}

return !str_contains($name, $startingCharacter) ? $name : substr($name, 1, -1);
return !str_starts_with($name, $startingCharacter)
? $name
: substr($name, 1, -1);
}

/**
Expand All @@ -229,7 +240,7 @@ protected function unquoteParts(array $parts, bool $withColumn): array
$lastKey = count($parts) - 1;

foreach ($parts as $k => &$part) {
$part = ($withColumn || $lastKey === $k) ?
$part = ($withColumn && $lastKey === $k) ?
$this->unquoteSimpleColumnName($part) :
$this->unquoteSimpleTableName($part);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Schema/SchemaInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ interface SchemaInterface extends ConstraintSchemaInterface
public const TYPE_JSON = 'json';
/**
* Define the abstract column type as `jsonb`.
*
* @deprecated will be removed in version 2.0.0. Use `SchemaInterface::TYPE_JSON` instead.
*/
public const TYPE_JSONB = 'jsonb';

Expand Down
2 changes: 2 additions & 0 deletions src/Schema/TableSchemaInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ public function foreignKey(string|int $id, array $to): void;
* @param string $to The column name in foreign table.
*
* @throws NotSupportedException
*
* @deprecated will be removed in version 2.0.0
*/
public function compositeForeignKey(int $id, string $from, string $to): void;
}
45 changes: 35 additions & 10 deletions tests/AbstractQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ public function testBuildWithQueries(): void
$this->assertSame(
DbHelper::replaceQuotes(
<<<SQL
WITH cte AS (SELECT * FROM [[admin_profile]])
WITH [[cte]] AS (SELECT * FROM [[admin_profile]])
SQL,
$db->getDriverName(),
),
Expand Down Expand Up @@ -1131,7 +1131,7 @@ public function testBuildWithQuery(): void
$this->assertSame(
DbHelper::replaceQuotes(
<<<SQL
WITH a1 AS (SELECT [[id]] FROM [[t1]] WHERE expr = 1), a2 AS ((SELECT [[id]] FROM [[t2]] INNER JOIN [[a1]] ON t2.id = a1.id WHERE expr = 2) UNION ( SELECT [[id]] FROM [[t3]] WHERE expr = 3 )) SELECT * FROM [[a2]]
WITH [[a1]] AS (SELECT [[id]] FROM [[t1]] WHERE expr = 1), [[a2]] AS ((SELECT [[id]] FROM [[t2]] INNER JOIN [[a1]] ON t2.id = a1.id WHERE expr = 2) UNION ( SELECT [[id]] FROM [[t3]] WHERE expr = 3 )) SELECT * FROM [[a2]]
SQL,
$db->getDriverName(),
),
Expand All @@ -1157,15 +1157,40 @@ public function testBuildWithQueryRecursive(): void
[$sql, $params] = $qb->build($query);
$this->assertSame(
DbHelper::replaceQuotes(
<<<SQL
WITH RECURSIVE a1 AS (SELECT [[id]] FROM [[t1]] WHERE expr = 1) SELECT * FROM [[a1]]
SQL,
$db->getDriverName(),
),
$sql,
$expected = DbHelper::replaceQuotes(
<<<SQL
WITH RECURSIVE [[a1]] AS (SELECT [[id]] FROM [[t1]] WHERE expr = 1) SELECT * FROM [[a1]]
SQL,
$db->getDriverName(),
);
if (in_array($db->getDriverName(), ['oci', 'sqlsrv'], true)) {
$expected = str_replace('WITH RECURSIVE ', 'WITH ', $expected);
}
$this->assertSame($expected, $sql);
$this->assertSame([], $params);
}
/** @dataProvider \Yiisoft\Db\Tests\Provider\QueryBuilderProvider::cteAliases */
public function testBuildWithQueryAlias($alias, $expected)
{
$db = $this->getConnection();
$qb = $db->getQueryBuilder();
$withQuery = (new Query($db))->from('t');
$query = (new Query($db))->withQuery($withQuery, $alias)->from('t');
[$sql, $params] = $qb->build($query);
$expectedSql = DbHelper::replaceQuotes(
<<<SQL
WITH $expected AS (SELECT * FROM [[t]]) SELECT * FROM [[t]]
SQL,
$db->getDriverName(),
);
$this->assertSame($expectedSql, $sql);
$this->assertSame([], $params);
}
Expand Down
47 changes: 47 additions & 0 deletions tests/Common/CommonQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Yiisoft\Db\Tests\Common;

use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Query\Query;
use Yiisoft\Db\Tests\AbstractQueryTest;

Expand Down Expand Up @@ -34,4 +35,50 @@ public function testColumnIndexByWithClosure()

$db->close();
}

public function testWithQuery()
{
$db = $this->getConnection(true);

$with = (new Query($db))
->distinct()
->select(['status'])
->from('customer');

$query = (new Query($db))
->withQuery($with, 'statuses')
->from('statuses');

$this->assertEquals(2, $query->count());

$db->close();
}

public function testWithQueryRecursive()
{
$db = $this->getConnection();
$quoter = $db->getQuoter();
$isOracle = $db->getDriverName() === 'oci';

/** Sum 1 to 10 equals 55 */
$quotedName = $quoter->quoteColumnName('n');
$union = (new Query($db))
->select(new Expression($quotedName . ' + 1'))
->from('t')
->where(['<', 'n', 10]);

$with = (new Query($db))
->select(new Expression('1'))
->from($isOracle ? new Expression('DUAL') : [])
->union($union, true);

$sum = (new Query($db))
->withQuery($with, 't(n)', true)
->from('t')
->sum($quotedName);

$this->assertEquals(55, $sum);

$db->close();
}
}
23 changes: 23 additions & 0 deletions tests/Db/Schema/QuoterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,27 @@ public function testCleanUpTableNamesWithCastException(): void
['tableAlias' => 123],
);
}

public function testGetTableNamePartsWithDifferentQuotes(): void
{
$quoter = new Quoter('`', '"');

$this->assertSame(['schema', 'table'], $quoter->getTableNameParts('"schema"."table"'));
}

public function testQuoteSqlWithTablePrefix(): void
{
$quoter = new Quoter('`', '`', 'prefix_');
$sql = 'SELECT * FROM {{%table%}}';

$this->assertSame('SELECT * FROM `prefix_table`', $quoter->quoteSql($sql));
}

public function testQuoteTableNameWithQueryAlias()
{
$quoter = new Quoter('`', '`');
$name = '(SELECT * FROM table) alias';

$this->assertSame($name, $quoter->quoteTableName($name));
}
}
11 changes: 11 additions & 0 deletions tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -1250,4 +1250,15 @@ public static function upsert(): array
],
];
}

public static function cteAliases(): array
{
return [
'simple' => ['a', '[[a]]'],
'with one column' => ['a(b)', '[[a]]([[b]])'],
'with columns' => ['a(b,c,d)', '[[a]]([[b]], [[c]], [[d]])'],
'with extra space' => ['a(b,c,d) ', 'a(b,c,d) '],
'expression' => [new Expression('a(b,c,d)'), 'a(b,c,d)'],
];
}
}

0 comments on commit cdb5f5a

Please sign in to comment.