Skip to content

Commit 4e4091e

Browse files
committed
analyse INSERT/REPLACE/DELETE ... RETURNING
1 parent c32e8f7 commit 4e4091e

File tree

8 files changed

+1065
-44
lines changed

8 files changed

+1065
-44
lines changed

src/Analyser/AnalyserState.php

Lines changed: 61 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use MariaStan\Ast\Query\UpdateQuery;
3737
use MariaStan\Ast\SelectExpr\AllColumns;
3838
use MariaStan\Ast\SelectExpr\RegularExpr;
39+
use MariaStan\Ast\SelectExpr\SelectExpr;
3940
use MariaStan\Ast\SelectExpr\SelectExprTypeEnum;
4041
use MariaStan\Database\FunctionInfo\FunctionInfoHelper;
4142
use MariaStan\Database\FunctionInfo\FunctionInfoRegistry;
@@ -380,43 +381,8 @@ private function analyseSingleSelectQuery(SimpleSelectQuery $select): array
380381
$this->columnResolver->addKnowledge($whereResult->knowledgeBase);
381382
}
382383

383-
$fields = [];
384384
$this->fieldBehavior = ColumnResolverFieldBehaviorEnum::FIELD_LIST;
385-
386-
foreach ($select->select as $selectExpr) {
387-
switch ($selectExpr::getSelectExprType()) {
388-
case SelectExprTypeEnum::REGULAR_EXPR:
389-
assert($selectExpr instanceof RegularExpr);
390-
$expr = $this->removeUnaryPlusPrefix($selectExpr->expr);
391-
$resolvedExpr = $this->resolveExprType($expr, null, $select->groupBy !== null, true);
392-
$resolvedField = new QueryResultField(
393-
$selectExpr->alias ?? $this->getDefaultFieldNameForExpr($expr),
394-
$resolvedExpr,
395-
);
396-
$fields[] = $resolvedField;
397-
$this->columnResolver->registerField(
398-
$resolvedField,
399-
$selectExpr->expr::getExprType() === Expr\ExprTypeEnum::COLUMN,
400-
);
401-
break;
402-
case SelectExprTypeEnum::ALL_COLUMNS:
403-
assert($selectExpr instanceof AllColumns);
404-
$allFields = $this->columnResolver->resolveAllColumns(
405-
$selectExpr->tableName?->name,
406-
$selectExpr->tableName?->databaseName,
407-
);
408-
409-
foreach ($allFields as $field) {
410-
$this->columnResolver->registerField($field, true);
411-
$this->recordColumnReference($field->exprType->column);
412-
}
413-
414-
$fields = array_merge($fields, $allFields);
415-
unset($allFields);
416-
break;
417-
}
418-
}
419-
385+
$fields = $this->analyseSelectExpressions($select->select, $select->groupBy !== null);
420386
$this->fieldBehavior = ColumnResolverFieldBehaviorEnum::GROUP_BY;
421387

422388
foreach ($select->groupBy->expressions ?? [] as $groupByExpr) {
@@ -467,6 +433,52 @@ private function analyseSingleSelectQuery(SimpleSelectQuery $select): array
467433
return [$fields, $rowCount];
468434
}
469435

436+
/**
437+
* @param list<SelectExpr> $selectExpressions
438+
* @return list<QueryResultField>
439+
* @throws AnalyserException
440+
*/
441+
private function analyseSelectExpressions(array $selectExpressions, bool $isNonEmptyAggResultSet): array
442+
{
443+
$fields = [];
444+
445+
foreach ($selectExpressions as $selectExpr) {
446+
switch ($selectExpr::getSelectExprType()) {
447+
case SelectExprTypeEnum::REGULAR_EXPR:
448+
assert($selectExpr instanceof RegularExpr);
449+
$expr = $this->removeUnaryPlusPrefix($selectExpr->expr);
450+
$resolvedExpr = $this->resolveExprType($expr, null, $isNonEmptyAggResultSet, true);
451+
$resolvedField = new QueryResultField(
452+
$selectExpr->alias ?? $this->getDefaultFieldNameForExpr($expr),
453+
$resolvedExpr,
454+
);
455+
$fields[] = $resolvedField;
456+
$this->columnResolver->registerField(
457+
$resolvedField,
458+
$selectExpr->expr::getExprType() === Expr\ExprTypeEnum::COLUMN,
459+
);
460+
break;
461+
case SelectExprTypeEnum::ALL_COLUMNS:
462+
assert($selectExpr instanceof AllColumns);
463+
$allFields = $this->columnResolver->resolveAllColumns(
464+
$selectExpr->tableName?->name,
465+
$selectExpr->tableName?->databaseName,
466+
);
467+
468+
foreach ($allFields as $field) {
469+
$this->columnResolver->registerField($field, true);
470+
$this->recordColumnReference($field->exprType->column);
471+
}
472+
473+
$fields = array_merge($fields, $allFields);
474+
unset($allFields);
475+
break;
476+
}
477+
}
478+
479+
return $fields;
480+
}
481+
470482
/** @throws AnalyserException|DbReflectionException */
471483
private function analyseTableReference(TableReference $fromClause, ColumnResolver $columnResolver): ColumnResolver
472484
{
@@ -595,8 +607,7 @@ private function analyseDeleteQuery(DeleteQuery $query): array
595607
$this->resolveExprType($query->limit);
596608
}
597609

598-
// TODO: DELETE ... RETURNING
599-
return [];
610+
return $this->analyseSelectExpressions($query->returning, false);
600611
}
601612

602613
/** @throws AnalyserException */
@@ -764,9 +775,17 @@ private function analyseInsertOrReplaceQuery(InsertQuery|ReplaceQuery $query): a
764775
$this->errors[] = AnalyserErrorBuilder::createMissingValueForColumnError($name);
765776
}
766777

767-
// TODO: INSERT ... ON DUPLICATE KEY
768-
// TODO: INSERT ... RETURNING
769-
return [];
778+
$this->fieldBehavior = ColumnResolverFieldBehaviorEnum::FIELD_LIST;
779+
780+
if ($query->returning === [] || $tableSchema === null) {
781+
return [];
782+
}
783+
784+
// Make sure that the RETURNING clause doesn't have access to any table other than the insert target.
785+
$cleanAnalyser = $this->getSubqueryAnalyser($query);
786+
$cleanAnalyser->columnResolver->registerInsertReplaceTargetTable($tableSchema, $tableSchema->database);
787+
788+
return $cleanAnalyser->analyseSelectExpressions($query->returning, false);
770789
}
771790

772791
/** @throws AnalyserException */
@@ -1675,7 +1694,7 @@ private function getNodeContent(Node $node): string
16751694
return $node->getStartPosition()->findSubstringToEndPosition($this->query, $node->getEndPosition());
16761695
}
16771696

1678-
private function getSubqueryAnalyser(SelectQuery $subquery, bool $canReferenceGrandParent = false): self
1697+
private function getSubqueryAnalyser(Query $subquery, bool $canReferenceGrandParent = false): self
16791698
{
16801699
$other = new self(
16811700
$this->dbReflection,

src/Analyser/ColumnResolver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ private function findUniqueItemInFieldList(string $name): ?QueryResultField
639639
}
640640

641641
/**
642-
* @return array<QueryResultField>
642+
* @return list<QueryResultField>
643643
* @throws AnalyserException
644644
*/
645645
public function resolveAllColumns(?string $table, ?string $database = null): array

src/Ast/Query/SelectQuery/SimpleSelectQuery.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
final class SimpleSelectQuery extends BaseSelectQuery
1717
{
18-
/** @param non-empty-array<SelectExpr> $select */
18+
/** @param non-empty-list<SelectExpr> $select */
1919
public function __construct(
2020
Position $startPosition,
2121
Position $endPosition,

tests/Analyser/AnalyserTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,18 @@ private static function provideValidInsertTestData(): iterable
12571257
'query' => $type . ' INTO analyser_test SET id = 999, name = "abcd"',
12581258
];
12591259

1260+
yield "{$type} ... RETURNING *" => [
1261+
'query' => $type . ' INTO analyser_test SET name = "abcd" RETURNING *, CONCAT(name, "foo")',
1262+
];
1263+
1264+
yield "{$type} ... SELECT ... JOIN ... RETURNING *" => [
1265+
'query' => $type . ' INTO analyser_test (name)
1266+
SELECT t1.name
1267+
FROM analyser_test t1, analyser_test t2
1268+
RETURNING *
1269+
',
1270+
];
1271+
12601272
yield "{$type} ... VALUES, explicitly set id" => [
12611273
'query' => $type . ' INTO analyser_test VALUES (999, "abcd")',
12621274
];
@@ -1409,6 +1421,10 @@ private static function provideValidDeleteTestData(): iterable
14091421
'query' => 'DELETE FROM analyser_test_truncate WHERE id > 5 ORDER BY id, name DESC LIMIT 5',
14101422
];
14111423

1424+
yield 'DELETE ... RETURNING' => [
1425+
'query' => 'DELETE FROM analyser_test_truncate RETURNING *, CONCAT(name, "foo")',
1426+
];
1427+
14121428
yield 'DELETE - placeholders' => [
14131429
'query' => 'DELETE t1 FROM analyser_test_truncate t1 JOIN (SELECT 1 id) t2 ON ? WHERE t1.id > ?',
14141430
'params' => [1, 2],
@@ -3225,6 +3241,24 @@ private static function provideInvalidInsertTestData(): iterable
32253241
'DB error code' => MariaDbErrorCodes::ER_NO_SUCH_TABLE,
32263242
];
32273243

3244+
yield "{$type} INTO missing_table ... RETURNING" => [
3245+
'query' => "{$type} INTO missing_table SET col = 'value' RETURNING *",
3246+
'error' => [
3247+
AnalyserErrorBuilder::createTableDoesntExistError('missing_table'),
3248+
AnalyserErrorBuilder::createUnknownColumnError('col'),
3249+
],
3250+
'DB error code' => MariaDbErrorCodes::ER_NO_SUCH_TABLE,
3251+
];
3252+
3253+
yield "{$type} INTO ... RETURNING missing_column" => [
3254+
'query' => "
3255+
{$type} INTO analyser_test (id, name) SELECT 999, 'adasd'
3256+
RETURNING missing_column
3257+
",
3258+
'error' => AnalyserErrorBuilder::createUnknownColumnError('missing_column'),
3259+
'DB error code' => MariaDbErrorCodes::ER_BAD_FIELD_ERROR,
3260+
];
3261+
32283262
yield "{$type} INTO ... SET missing_column" => [
32293263
'query' => "{$type} INTO analyser_test SET missing_column = 'value'",
32303264
'error' => AnalyserErrorBuilder::createUnknownColumnError('missing_column'),
@@ -3345,6 +3379,16 @@ private static function provideInvalidInsertTestData(): iterable
33453379
'DB error code' => MariaDbErrorCodes::ER_NON_UNIQ_ERROR,
33463380
];
33473381

3382+
yield 'INSERT ... ON DUPLICATE KEY UPDATE - reference column from SELECT - ambiguous column, same table' => [
3383+
'query' => '
3384+
INSERT INTO analyse_test_insert (id, val_string_not_null_no_default)
3385+
SELECT id, "abcd" FROM analyse_test_insert
3386+
ON DUPLICATE KEY UPDATE analyse_test_insert.id = id
3387+
',
3388+
'error' => AnalyserErrorBuilder::createAmbiguousColumnError('id'),
3389+
'DB error code' => MariaDbErrorCodes::ER_NON_UNIQ_ERROR,
3390+
];
3391+
33483392
yield 'INSERT ... ON DUPLICATE KEY UPDATE - writing into the SELECT table' => [
33493393
'query' => '
33503394
INSERT INTO analyse_test_insert (id, val_string_not_null_no_default)
@@ -3469,6 +3513,18 @@ private static function provideInvalidDeleteTestData(): iterable
34693513
'DB error code' => MariaDbErrorCodes::ER_NO_SUCH_TABLE,
34703514
];
34713515

3516+
yield 'DELETE FROM missing_table RETURNING *' => [
3517+
'query' => 'DELETE FROM missing_table RETURNING *',
3518+
'error' => AnalyserErrorBuilder::createTableDoesntExistError('missing_table'),
3519+
'DB error code' => MariaDbErrorCodes::ER_NO_SUCH_TABLE,
3520+
];
3521+
3522+
yield 'DELETE ... RETURNING missing_column' => [
3523+
'query' => 'DELETE FROM analyser_test_truncate RETURNING missing_column',
3524+
'error' => AnalyserErrorBuilder::createUnknownColumnError('missing_column'),
3525+
'DB error code' => MariaDbErrorCodes::ER_BAD_FIELD_ERROR,
3526+
];
3527+
34723528
yield 'DELETE - wrong alias' => [
34733529
'query' => 'DELETE t_miss FROM analyser_test_truncate t1',
34743530
'error' => AnalyserErrorBuilder::createTableDoesntExistError('t_miss'),

tests/Analyser/data/golden/invalid/delete.test

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,68 @@ MariaStan\Analyser\AnalyserResult
2323
#######
2424
1146: Table 'mariastan_test.missing_table' doesn't exist
2525
-----
26+
DELETE FROM missing_table RETURNING *
27+
-----
28+
MariaStan\Analyser\AnalyserResult
29+
(
30+
[resultFields] => Array
31+
(
32+
)
33+
[errors] => Array
34+
(
35+
MariaStan\Analyser\AnalyserError
36+
(
37+
[message] => Table 'missing_table' doesn't exist
38+
[type] => MariaStan\Analyser\AnalyserErrorTypeEnum::UNKNOWN_TABLE
39+
)
40+
)
41+
[positionalPlaceholderCount] => 0
42+
[referencedSymbols] => Array
43+
(
44+
)
45+
)
46+
#######
47+
1146: Table 'mariastan_test.missing_table' doesn't exist
48+
-----
49+
DELETE FROM analyser_test_truncate RETURNING missing_column
50+
-----
51+
MariaStan\Analyser\AnalyserResult
52+
(
53+
[resultFields] => Array
54+
(
55+
MariaStan\Analyser\QueryResultField
56+
(
57+
[name] => missing_column
58+
[exprType] => MariaStan\Analyser\ExprTypeResult
59+
(
60+
[type] => MariaStan\Schema\DbType\MixedType
61+
(
62+
)
63+
[isNullable] => true
64+
)
65+
)
66+
)
67+
[errors] => Array
68+
(
69+
MariaStan\Analyser\AnalyserError
70+
(
71+
[message] => Unknown column 'missing_column'
72+
[type] => MariaStan\Analyser\AnalyserErrorTypeEnum::UNKNOWN_COLUMN
73+
)
74+
)
75+
[positionalPlaceholderCount] => 0
76+
[referencedSymbols] => Array
77+
(
78+
MariaStan\Analyser\ReferencedSymbol\Table
79+
(
80+
[name] => analyser_test_truncate
81+
[database] => mariastan_test
82+
)
83+
)
84+
)
85+
#######
86+
1054: Unknown column 'missing_column' in 'RETURNING'
87+
-----
2688
DELETE t_miss FROM analyser_test_truncate t1
2789
-----
2890
MariaStan\Analyser\AnalyserResult

0 commit comments

Comments
 (0)