packages/ztd-query-mysql/tests/Unit/Rewrite/Transformer/UpdateTransformerTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Rewrite\Transformer;
6
7use PhpMyAdmin\SqlParser\Parser;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use ZtdQuery\Platform\MySql\Rewrite\Transformer\SelectTransformer;
12use ZtdQuery\Platform\MySql\Rewrite\Transformer\UpdateTransformer;
13use ZtdQuery\Platform\MySql\Sql\Dml\DmlWhereClauseExtractor;
14use ZtdQuery\Platform\MySql\Sql\Dml\UpdateAssignmentExtractor;
15use ZtdQuery\Platform\MySql\Sql\Dml\UpdateSourceExtractor;
16use ZtdQuery\Platform\MySql\Sql\MySqlIdentifierQuoter;
17use ZtdQuery\Platform\MySql\Sql\MySqlParser;
18use ZtdQuery\Platform\MySql\Sql\Value\MySqlCastRenderer;
19
20#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Partition\MySqlPartitionSelectionRewriter::class)]
21#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Dml\AssignmentExpression::class)]
22#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Cte\HeaderParser::class)]
23#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Cte\IdentifierReferences::class)]
24#[UsesClass(\ZtdQuery\Platform\MySql\Sql\OptionalInsertIntoNormalizer::class)]
25#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Relation\ReferenceReader::class)]
26#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\FullText\ExpressionEditor::class)]
27#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Partition\SelectionReader::class)]
28#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Partition\SourceProjection::class)]
29#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Set\OrderRewriter::class)]
30#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Set\ValueNormalizer::class)]
31#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Shadow\CteRows::class)]
32
33
34#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\CastTypeResolver::class)]
35#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Type\RankEdits::class)]
36#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\ScalarExpression::class)]
37#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\StringCoercion::class)]
38#[CoversClass(UpdateTransformer::class)]
39#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Relation\MySqlSelectRelationParser::class)]
40#[UsesClass(MySqlParser::class)]
41#[UsesClass(SelectTransformer::class)]
42#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\FullText\MySqlFullTextSearchRewriter::class)]
43#[UsesClass(MySqlCastRenderer::class)]
44#[UsesClass(MySqlIdentifierQuoter::class)]
45#[UsesClass(DmlWhereClauseExtractor::class)]
46#[UsesClass(UpdateAssignmentExtractor::class)]
47#[UsesClass(UpdateSourceExtractor::class)]
48#[UsesClass(\ZtdQuery\Platform\MySql\Sql\Value\MySqlValueRenderer::class)]
49#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Type\MySqlTypeSemantics::class)]
50#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\Cte\MySqlCteShadowComposer::class)]
51#[UsesClass(\ZtdQuery\Platform\MySql\Rewrite\GeneratedColumn\MySqlGeneratedColumnProjector::class)]
52#[UsesClass(\ZtdQuery\Platform\MySql\Sql\MySqlLexerProfile::class)]
53#[CoversClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Update\ResultSelect::class)]
54#[CoversClass(\ZtdQuery\Platform\MySql\Rewrite\Transformer\Update\TargetProjection::class)]
55final class UpdateTransformerTest extends TestCase
56{
57    public function testBuildProjectionPreservesDerivedJoinSource(): void
58    {
59        $sql = 'UPDATE summary s JOIN (SELECT category, COUNT(*) AS cnt, MIN(price) AS mn FROM products GROUP BY category) p ON s.category = p.category SET s.min_price = p.mn, s.item_count = p.cnt';
60        $parser = new Parser($sql);
61        $statement = $parser->statements[0];
62        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
63        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
64
65        $result = $transformer->buildProjection(
66            $statement,
67            ['id', 'category', 'min_price', 'item_count'],
68            ['id'],
69            [],
70            ['p.mn', 'p.cnt'],
71            null,
72            (new UpdateSourceExtractor())->extract($sql),
73        );
74
75        self::assertSame(
76            'SELECT p.mn AS `min_price`, p.cnt AS `item_count`, `s`.`id`, `s`.`category`, `s`.`id` AS `__ztd_original_id` FROM summary s JOIN (SELECT category, COUNT(*) AS cnt, MIN(price) AS mn FROM products GROUP BY category) p ON s.category = p.category',
77            $result['sql'],
78        );
79    }
80
81    public function testTransformPreservesIntroducedHexLiteral(): void
82    {
83        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
84        $tables = [
85            'data' => [
86                'rows' => [['id' => 1, 'payload' => 'Hello']],
87                'columns' => ['id', 'payload'],
88                'columnTypes' => [],
89            ],
90        ];
91
92        $result = $transformer->transform("UPDATE data SET payload = X'576F726C64' WHERE id = 1", $tables);
93
94        self::assertStringContainsString("X'576F726C64' AS `payload`", $result);
95        self::assertStringNotContainsString('SELECT X AS `payload`', $result);
96    }
97
98    public function testTransformPreservesIntroducedLiteralsForEveryUpdateTarget(): void
99    {
100        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
101        $tables = [
102            'data' => [
103                'rows' => [['id' => 1, 'payload' => 'old']],
104                'columns' => ['id', 'payload'],
105                'columnTypes' => [],
106                'primaryKeys' => ['id'],
107            ],
108            'audit' => [
109                'rows' => [['id' => 1, 'bits' => '0']],
110                'columns' => ['id', 'bits'],
111                'columnTypes' => [],
112                'primaryKeys' => ['id'],
113            ],
114        ];
115
116        $result = $transformer->transform(
117            "UPDATE data d, audit a SET d.payload = X'00', a.bits = B'01' WHERE d.id = a.id",
118            $tables,
119        );
120
121        self::assertStringContainsString("X'00' AS `__ztd_multi_0_value_1`", $result);
122        self::assertStringContainsString("B'01' AS `__ztd_multi_1_value_1`", $result);
123    }
124
125    public function testTransformPreservesIntervalUnitInAssignment(): void
126    {
127        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
128        $tables = [
129            'tasks' => [
130                'rows' => [['id' => 1, 'created_at' => '2025-01-01 10:00:00', 'due_at' => null]],
131                'columns' => ['id', 'created_at', 'due_at'],
132                'columnTypes' => [],
133            ],
134        ];
135
136        $result = $transformer->transform(
137            'UPDATE tasks SET due_at = created_at + INTERVAL 30 DAY WHERE id = 1',
138            $tables,
139        );
140
141        self::assertStringContainsString('created_at + INTERVAL 30 DAY AS `due_at`', $result);
142    }
143
144    public function testTransformPreservesCaseWhereExpression(): void
145    {
146        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
147        $tables = [
148            't' => [
149                'rows' => [['id' => 1, 'score' => 85], ['id' => 2, 'score' => 60]],
150                'columns' => ['id', 'score'],
151                'columnTypes' => [],
152            ],
153        ];
154
155        $result = $transformer->transform(
156            'UPDATE t SET score = 0 WHERE CASE WHEN score > 80 THEN 1 ELSE 0 END = 1',
157            $tables,
158        );
159
160        self::assertStringContainsString('WHERE CASE WHEN score > 80 THEN 1 ELSE 0 END = 1', $result);
161    }
162
163    public function testBuildUpdateSelectUsesAliasAndColumns(): void
164    {
165        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
166        $sql = "UPDATE users u SET name = 'Bob' WHERE id = 1";
167        $parser = new Parser($sql);
168        $statement = $parser->statements[0];
169        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
170
171        $result = $transformer->buildProjection($statement, ['id', 'name']);
172
173        self::assertStringContainsString("SELECT 'Bob' AS `name`", $result['sql']);
174        self::assertStringContainsString('`u`.`id`', $result['sql']);
175        self::assertStringContainsString('FROM `users` AS u', $result['sql']);
176        self::assertStringContainsString('WHERE id = 1', $result['sql']);
177        self::assertSame('users', $result['table']);
178        self::assertCount(1, $result['tables']);
179    }
180
181    public function testBuildUpdateSelectWithQualifiedColumnName(): void
182    {
183        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
184        $sql = "UPDATE products SET `products`.`name` = 'Widget' WHERE id = 1";
185        $parser = new Parser($sql);
186        $statement = $parser->statements[0];
187        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
188
189        $result = $transformer->buildProjection($statement, ['id', 'name', 'price']);
190
191        self::assertStringContainsString('AS `name`', $result['sql']);
192        self::assertStringNotContainsString('``name``', $result['sql']);
193        self::assertStringContainsString('`products`.`id`', $result['sql']);
194        self::assertStringContainsString('`products`.`price`', $result['sql']);
195    }
196
197    public function testBuildUpdateSelectWithUnqualifiedColumnName(): void
198    {
199        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
200        $sql = "UPDATE products SET name = 'Widget' WHERE id = 1";
201        $parser = new Parser($sql);
202        $statement = $parser->statements[0];
203        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
204
205        $result = $transformer->buildProjection($statement, ['id', 'name', 'price']);
206
207        self::assertStringContainsString('AS `name`', $result['sql']);
208        self::assertStringNotContainsString('``', $result['sql']);
209        self::assertStringContainsString('`products`.`id`', $result['sql']);
210        self::assertStringContainsString('`products`.`price`', $result['sql']);
211    }
212
213    public function testBuildUpdateSelectWithBacktickedUnqualifiedColumn(): void
214    {
215        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
216        $sql = "UPDATE products SET `name` = 'Widget' WHERE id = 1";
217        $parser = new Parser($sql);
218        $statement = $parser->statements[0];
219        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
220
221        $result = $transformer->buildProjection($statement, ['id', 'name']);
222
223        self::assertStringContainsString('AS `name`', $result['sql']);
224        self::assertStringNotContainsString('``name``', $result['sql']);
225    }
226
227    public function testBuildUpdateSelectWithJoin(): void
228    {
229        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
230        $sql = "UPDATE users u JOIN orders o ON u.id = o.user_id SET u.name = 'Updated' WHERE o.amount > 100";
231        $parser = new Parser($sql);
232        $statement = $parser->statements[0];
233        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
234
235        $result = $transformer->buildProjection($statement, ['id', 'name']);
236
237        self::assertStringContainsString("SELECT 'Updated' AS `name`", $result['sql']);
238        self::assertStringContainsString('`u`.`id`', $result['sql']);
239        self::assertStringContainsString('FROM `users` AS u', $result['sql']);
240        self::assertStringContainsString('JOIN `orders` AS o', $result['sql']);
241        self::assertStringContainsString('ON u.id = o.user_id', $result['sql']);
242        self::assertStringContainsString('WHERE o.amount > 100', $result['sql']);
243    }
244
245    public function testBuildUpdateSelectWithLeftJoin(): void
246    {
247        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
248        $sql = "UPDATE users u LEFT JOIN orders o ON u.id = o.user_id SET u.status = 'inactive' WHERE o.id IS NULL";
249        $parser = new Parser($sql);
250        $statement = $parser->statements[0];
251        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
252
253        $result = $transformer->buildProjection($statement, ['id', 'status']);
254
255        self::assertStringContainsString("SELECT 'inactive' AS `status`", $result['sql']);
256        self::assertStringContainsString('LEFT JOIN `orders` AS o', $result['sql']);
257        self::assertStringContainsString('ON u.id = o.user_id', $result['sql']);
258    }
259
260    public function testBuildUpdateSelectWithMultipleJoins(): void
261    {
262        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
263        $sql = "UPDATE users u JOIN orders o ON u.id = o.user_id JOIN products p ON o.product_id = p.id SET u.name = 'VIP' WHERE p.price > 1000";
264        $parser = new Parser($sql);
265        $statement = $parser->statements[0];
266        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
267
268        $result = $transformer->buildProjection($statement, ['id', 'name']);
269
270        self::assertStringContainsString('JOIN `orders` AS o', $result['sql']);
271        self::assertStringContainsString('JOIN `products` AS p', $result['sql']);
272        self::assertStringContainsString('ON u.id = o.user_id', $result['sql']);
273        self::assertStringContainsString('ON o.product_id = p.id', $result['sql']);
274    }
275
276    public function testBuildMultiTableUpdate(): void
277    {
278        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
279        $sql = "UPDATE users u, orders o SET u.name = 'Updated', o.status = 'processed' WHERE u.id = o.user_id";
280        $parser = new Parser($sql);
281        $statement = $parser->statements[0];
282        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
283
284        $result = $transformer->buildProjection($statement, ['id', 'name']);
285
286        self::assertStringContainsString('FROM `users`', $result['sql']);
287        self::assertStringContainsString('`orders`', $result['sql']);
288        self::assertSame('users', $result['table']);
289        self::assertCount(2, $result['tables']);
290        self::assertArrayHasKey('users', $result['tables']);
291        self::assertArrayHasKey('orders', $result['tables']);
292    }
293
294    public function testTransformThrowsForNonUpdateStatement(): void
295    {
296        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
297
298        self::expectException(\ZtdQuery\Exception\UnsupportedSqlException::class);
299        $transformer->transform('SELECT 1', []);
300    }
301
302    public function testTransformSimpleUpdateReturnsSelect(): void
303    {
304        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
305        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
306        $tables = [
307            'users' => [
308                'rows' => [],
309                'columns' => ['id', 'name'],
310                'columnTypes' => [],
311                'primaryKeys' => ['id'],
312            ],
313        ];
314
315        $result = $transformer->transform($sql, $tables);
316        self::assertStringContainsString('SELECT', $result);
317        self::assertStringContainsString("'Bob' AS `name`", $result);
318        self::assertStringContainsString('`users`.`id` AS `__ztd_original_id`', $result);
319    }
320
321    public function testTransformUpdateWithPartitionThrows(): void
322    {
323        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
324
325        self::expectException(\ZtdQuery\Exception\UnsupportedSqlException::class);
326        self::expectExceptionMessage('PARTITION');
327        $transformer->transform("UPDATE users PARTITION (p0) SET name = 'Bob' WHERE id = 1", []);
328    }
329
330    public function testBuildUpdateSelectWithOrderByAndLimit(): void
331    {
332        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
333        $sql = "UPDATE users SET name = 'Bob' WHERE id > 5 ORDER BY id LIMIT 10";
334        $parser = new Parser($sql);
335        $statement = $parser->statements[0];
336        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
337
338        $result = $transformer->buildProjection($statement, ['id', 'name']);
339
340        self::assertStringContainsString('ORDER BY', $result['sql']);
341        self::assertStringContainsString('LIMIT', $result['sql']);
342    }
343
344    public function testBuildUpdateSelectWithOnlyOrderBySelectsRowsBeforeProjecting(): void
345    {
346        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
347        $parser = new Parser("UPDATE users SET name = 'Bob' ORDER BY id");
348        $statement = $parser->statements[0];
349        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
350
351        $result = $transformer->buildProjection($statement, ['id', 'name']);
352
353        self::assertSame(
354            "SELECT 'Bob' AS `name`, `users`.`id` FROM (SELECT * FROM `users` ORDER BY id ASC) AS `users`",
355            $result['sql'],
356        );
357    }
358
359    public function testBuildUpdateSelectWithOnlyLimitSelectsRowsBeforeProjecting(): void
360    {
361        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
362        $parser = new Parser("UPDATE users SET name = 'Bob' LIMIT 2");
363        $statement = $parser->statements[0];
364        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
365
366        $result = $transformer->buildProjection($statement, ['id', 'name']);
367
368        self::assertSame(
369            "SELECT 'Bob' AS `name`, `users`.`id` FROM (SELECT * FROM `users` LIMIT 0, 2) AS `users`",
370            $result['sql'],
371        );
372    }
373
374    public function testBuildUpdateSelectWithJoinAndLimitKeepsJoinInResultSelect(): void
375    {
376        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
377        $parser = new Parser("UPDATE users JOIN orders ON users.id = orders.user_id SET users.name = 'Bob' LIMIT 2");
378        $statement = $parser->statements[0];
379        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
380
381        $result = $transformer->buildProjection($statement, ['id', 'name']);
382
383        self::assertSame(
384            "SELECT 'Bob' AS `name`, `users`.`id` FROM `users` JOIN `orders` ON users.id = orders.user_id LIMIT 0, 2",
385            $result['sql'],
386        );
387    }
388
389    public function testBuildUpdateSelectWithEmptyColumnsUsesWildcard(): void
390    {
391        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
392        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
393        $parser = new Parser($sql);
394        $statement = $parser->statements[0];
395        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
396
397        $result = $transformer->buildProjection($statement, []);
398
399        self::assertStringContainsString("'Bob' AS `name`", $result['sql']);
400    }
401
402    public function testTransformUpdateWithShadowDataAddsCte(): void
403    {
404        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
405        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
406        $tables = [
407            'users' => [
408                'rows' => [['id' => 1, 'name' => 'Alice']],
409                'columns' => ['id', 'name'],
410                'columnTypes' => [],
411            ],
412        ];
413
414        $result = $transformer->transform($sql, $tables);
415        self::assertStringContainsString('WITH', $result);
416    }
417
418    public function testBuildUpdateSelectExactFormat(): void
419    {
420        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
421        $sql = 'UPDATE t SET a = 1 WHERE b = 2';
422        $parser = new Parser($sql);
423        $statement = $parser->statements[0];
424        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
425
426        $result = $transformer->buildProjection($statement, ['a', 'b']);
427        self::assertStringStartsWith('SELECT 1 AS `a`, `t`.`b` FROM `t`', $result['sql']);
428        self::assertStringContainsString('WHERE b = 2', $result['sql']);
429    }
430
431    public function testBuildUpdateSelectWithoutColumnsIncludesSetValuesOnly(): void
432    {
433        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
434        $sql = 'UPDATE t SET a = 1';
435        $parser = new Parser($sql);
436        $statement = $parser->statements[0];
437        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
438
439        $result = $transformer->buildProjection($statement, []);
440        self::assertStringStartsWith('SELECT 1 AS `a` FROM `t`', $result['sql']);
441    }
442
443    public function testBuildUpdateSelectAliasClause(): void
444    {
445        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
446        $sql = "UPDATE users u SET u.name = 'X' WHERE u.id = 1";
447        $parser = new Parser($sql);
448        $statement = $parser->statements[0];
449        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
450
451        $result = $transformer->buildProjection($statement, ['id', 'name']);
452        self::assertStringContainsString('FROM `users` AS u', $result['sql']);
453        self::assertStringContainsString('`u`.`id`', $result['sql']);
454    }
455
456    public function testBuildMultiTableUpdateAdditionalTableAlias(): void
457    {
458        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
459        $sql = "UPDATE users u, orders o SET u.name = 'X' WHERE u.id = o.user_id";
460        $parser = new Parser($sql);
461        $statement = $parser->statements[0];
462        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
463
464        $result = $transformer->buildProjection($statement, ['id', 'name']);
465        self::assertStringContainsString(', `orders` AS o', $result['sql']);
466        self::assertSame('users', $result['table']);
467    }
468
469    public function testBuildUpdateSelectCoveredColNotDuplicated(): void
470    {
471        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
472        $sql = 'UPDATE t SET a = 1, b = 2 WHERE c = 3';
473        $parser = new Parser($sql);
474        $statement = $parser->statements[0];
475        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
476
477        $result = $transformer->buildProjection($statement, ['a', 'b', 'c']);
478        self::assertSame(1, substr_count($result['sql'], 'AS `a`'));
479        self::assertSame(1, substr_count($result['sql'], 'AS `b`'));
480        self::assertStringContainsString('`t`.`c`', $result['sql']);
481    }
482
483    public function testBuildUpdateWithJoinUsingClause(): void
484    {
485        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
486        $sql = "UPDATE users JOIN orders USING (id) SET users.name = 'X' WHERE orders.amount > 100";
487        $parser = new Parser($sql);
488        $statement = $parser->statements[0];
489        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
490
491        $result = $transformer->buildProjection($statement, ['id', 'name']);
492        self::assertStringContainsString('JOIN', $result['sql']);
493    }
494
495    public function testBuildUpdateSelectOrderByFormatContainsKeyword(): void
496    {
497        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
498        $sql = 'UPDATE t SET a = 1 ORDER BY b DESC LIMIT 5';
499        $parser = new Parser($sql);
500        $statement = $parser->statements[0];
501        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
502
503        $result = $transformer->buildProjection($statement, ['a', 'b']);
504        self::assertStringContainsString('ORDER BY', $result['sql']);
505        self::assertStringContainsString('b DESC', $result['sql']);
506        self::assertStringContainsString('LIMIT', $result['sql']);
507    }
508
509    public function testTransformWithLowercasePartitionThrows(): void
510    {
511        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
512
513        self::expectException(\ZtdQuery\Exception\UnsupportedSqlException::class);
514        self::expectExceptionMessage('PARTITION');
515        $transformer->transform("UPDATE users partition (p0) SET name = 'Bob' WHERE id = 1", []);
516    }
517
518    public function testBuildProjectionOrderByExactFormat(): void
519    {
520        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
521        $sql = 'UPDATE t SET a = 1 ORDER BY b ASC LIMIT 3';
522        $parser = new Parser($sql);
523        $statement = $parser->statements[0];
524        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
525
526        $result = $transformer->buildProjection($statement, ['a', 'b']);
527        self::assertStringContainsString(' ORDER BY b ASC', $result['sql']);
528        self::assertStringContainsString(' LIMIT ', $result['sql']);
529        self::assertMatchesRegularExpression('/ORDER BY b ASC LIMIT/', $result['sql']);
530    }
531
532    public function testBuildProjectionColumnsNotEmpty(): void
533    {
534        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
535        $sql = 'UPDATE t SET a = 1 WHERE id = 1';
536        $parser = new Parser($sql);
537        $statement = $parser->statements[0];
538        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
539
540        $tables = ['t' => ['columns' => ['a', 'b'], 'rows' => [], 'columnTypes' => []]];
541        $result = $transformer->transform($sql, $tables);
542        self::assertStringContainsString('AS `a`', $result);
543        self::assertStringContainsString('`t`.`b`', $result);
544    }
545
546    public function testBuildProjectionCoveredColIsNotDuplicatedWhenTrue(): void
547    {
548        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
549        $sql = 'UPDATE t SET a = 99 WHERE id = 1';
550        $parser = new Parser($sql);
551        $statement = $parser->statements[0];
552        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
553
554        $result = $transformer->buildProjection($statement, ['a', 'b']);
555        self::assertSame(1, substr_count($result['sql'], '`a`'));
556    }
557
558    public function testBuildProjectionMultiTableReturnsAllTables(): void
559    {
560        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
561        $sql = "UPDATE users u, orders o SET u.name = 'X' WHERE u.id = o.user_id";
562        $parser = new Parser($sql);
563        $statement = $parser->statements[0];
564        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
565
566        $result = $transformer->buildProjection($statement, ['id', 'name']);
567        self::assertSame('u', $result['tables']['users']['alias']);
568        self::assertSame('o', $result['tables']['orders']['alias']);
569    }
570
571    public function testBuildProjectionJoinOnClause(): void
572    {
573        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
574        $sql = "UPDATE users u JOIN orders o ON u.id = o.user_id SET u.name = 'X'";
575        $parser = new Parser($sql);
576        $statement = $parser->statements[0];
577        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
578
579        $result = $transformer->buildProjection($statement, ['id', 'name']);
580        self::assertStringContainsString('ON u.id = o.user_id', $result['sql']);
581        self::assertStringContainsString('JOIN `orders`', $result['sql']);
582    }
583
584    public function testBuildProjectionJoinTypeIsIncluded(): void
585    {
586        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
587        $sql = "UPDATE users u LEFT JOIN orders o ON u.id = o.user_id SET u.name = 'X'";
588        $parser = new Parser($sql);
589        $statement = $parser->statements[0];
590        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
591
592        $result = $transformer->buildProjection($statement, ['id', 'name']);
593        self::assertStringContainsString('LEFT JOIN', $result['sql']);
594    }
595
596    public function testBuildProjectionWithNoWhereReturnsNoWhereClause(): void
597    {
598        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
599        $sql = 'UPDATE t SET a = 1';
600        $parser = new Parser($sql);
601        $statement = $parser->statements[0];
602        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
603
604        $result = $transformer->buildProjection($statement, ['a']);
605        self::assertStringNotContainsString('WHERE', $result['sql']);
606    }
607
608    public function testBuildProjectionWithNoOrderByReturnsNoOrderClause(): void
609    {
610        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
611        $sql = 'UPDATE t SET a = 1 WHERE id = 1';
612        $parser = new Parser($sql);
613        $statement = $parser->statements[0];
614        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
615
616        $result = $transformer->buildProjection($statement, ['a']);
617        self::assertStringNotContainsString('ORDER BY', $result['sql']);
618        self::assertStringNotContainsString('LIMIT', $result['sql']);
619    }
620
621    public function testBuildProjectionCoveredColIsTrueNotFalse(): void
622    {
623        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
624        $sql = 'UPDATE t SET a = 99 WHERE id = 1';
625        $parser = new Parser($sql);
626        $statement = $parser->statements[0];
627        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
628
629        $result = $transformer->buildProjection($statement, ['a', 'b']);
630        self::assertStringNotContainsString('`t`.`a`', $result['sql']);
631        self::assertStringContainsString('99 AS `a`', $result['sql']);
632        self::assertStringContainsString('`t`.`b`', $result['sql']);
633    }
634
635    public function testBuildProjectionLimitContainsActualValue(): void
636    {
637        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
638        $sql = 'UPDATE t SET a = 1 ORDER BY b LIMIT 5';
639        $parser = new Parser($sql);
640        $statement = $parser->statements[0];
641        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
642
643        $result = $transformer->buildProjection($statement, ['a', 'b']);
644        self::assertMatchesRegularExpression('/LIMIT\s+\d/', $result['sql']);
645    }
646
647    public function testBuildProjectionSingleTableNoAdditionalTables(): void
648    {
649        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
650        $sql = 'UPDATE t SET a = 1 WHERE id = 1';
651        $parser = new Parser($sql);
652        $statement = $parser->statements[0];
653        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
654
655        $result = $transformer->buildProjection($statement, ['a']);
656        $fromPos = strpos($result['sql'], 'FROM `t`');
657        self::assertNotFalse($fromPos);
658        $afterFrom = substr($result['sql'], $fromPos + strlen('FROM `t`'));
659        self::assertStringNotContainsString('`t`', $afterFrom);
660    }
661
662    public function testBuildProjectionMultiTableAdditionalTableSameNameNoAlias(): void
663    {
664        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
665        $sql = "UPDATE users, orders SET users.name = 'X' WHERE users.id = orders.user_id";
666        $parser = new Parser($sql);
667        $statement = $parser->statements[0];
668        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
669
670        $result = $transformer->buildProjection($statement, ['id', 'name']);
671        self::assertStringContainsString('`orders`', $result['sql']);
672    }
673
674    public function testBuildProjectionJoinUsingClauseExactFormat(): void
675    {
676        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
677        $sql = "UPDATE users JOIN orders USING (id) SET users.name = 'X'";
678        $parser = new Parser($sql);
679        $statement = $parser->statements[0];
680        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
681
682        $result = $transformer->buildProjection($statement, ['id', 'name']);
683        self::assertStringContainsString('USING', $result['sql']);
684        self::assertStringContainsString('(', $result['sql']);
685        self::assertStringContainsString(')', $result['sql']);
686    }
687
688    public function testBuildProjectionJoinOnConditionFormatted(): void
689    {
690        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
691        $sql = "UPDATE users u JOIN orders o ON u.id = o.user_id SET u.name = 'X' WHERE o.amount > 50";
692        $parser = new Parser($sql);
693        $statement = $parser->statements[0];
694        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
695
696        $result = $transformer->buildProjection($statement, ['id', 'name']);
697        self::assertStringContainsString(' ON ', $result['sql']);
698        self::assertStringContainsString('u.id = o.user_id', $result['sql']);
699    }
700
701    public function testBuildProjectionJoinAliasPresentInOutput(): void
702    {
703        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
704        $sql = "UPDATE users u JOIN orders o ON u.id = o.user_id SET u.name = 'X'";
705        $parser = new Parser($sql);
706        $statement = $parser->statements[0];
707        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
708
709        $result = $transformer->buildProjection($statement, ['id', 'name']);
710        self::assertStringContainsString('AS o', $result['sql']);
711    }
712
713    public function testBuildProjectionExactSqlForSimpleUpdate(): void
714    {
715        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
716        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
717        $parser = new Parser($sql);
718        $statement = $parser->statements[0];
719        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
720
721        $result = $transformer->buildProjection($statement, ['id', 'name']);
722        self::assertSame("SELECT 'Bob' AS `name`, `users`.`id` FROM `users` WHERE id = 1", $result['sql']);
723        self::assertSame('users', $result['table']);
724        self::assertSame(['users' => ['alias' => 'users']], $result['tables']);
725    }
726
727    public function testBuildProjectionExactSqlForUpdateWithAlias(): void
728    {
729        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
730        $sql = "UPDATE users u SET u.name = 'Bob' WHERE u.id = 1";
731        $parser = new Parser($sql);
732        $statement = $parser->statements[0];
733        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
734
735        $result = $transformer->buildProjection($statement, ['id', 'name']);
736        self::assertSame("SELECT 'Bob' AS `name`, `u`.`id` FROM `users` AS u WHERE u.id = 1", $result['sql']);
737        self::assertSame('users', $result['table']);
738        self::assertSame(['users' => ['alias' => 'u']], $result['tables']);
739    }
740
741    public function testBuildProjectionExactSqlForMultiTableUpdate(): void
742    {
743        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
744        $sql = "UPDATE users u, orders o SET u.name = 'X', o.status = 'done' WHERE u.id = o.user_id";
745        $parser = new Parser($sql);
746        $statement = $parser->statements[0];
747        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
748
749        $result = $transformer->buildProjection($statement, ['id', 'name']);
750        self::assertSame("SELECT 'X' AS `name`, 'done' AS `status`, `u`.`id` FROM `users` AS u, `orders` AS o WHERE u.id = o.user_id", $result['sql']);
751        self::assertSame('users', $result['table']);
752        self::assertSame(['users' => ['alias' => 'u'], 'orders' => ['alias' => 'o']], $result['tables']);
753    }
754
755    public function testBuildProjectionExactSqlForUpdateWithOrderByAndLimit(): void
756    {
757        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
758        $sql = "UPDATE users SET name = 'X' ORDER BY id LIMIT 5";
759        $parser = new Parser($sql);
760        $statement = $parser->statements[0];
761        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
762
763        $result = $transformer->buildProjection($statement, ['id', 'name']);
764        self::assertSame("SELECT 'X' AS `name`, `users`.`id` FROM (SELECT * FROM `users` ORDER BY id ASC LIMIT 0, 5) AS `users`", $result['sql']);
765    }
766
767    public function testBuildProjectionExactSqlForUpdateWithJoin(): void
768    {
769        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
770        $sql = "UPDATE users u JOIN orders o ON u.id = o.user_id SET u.name = 'X' WHERE o.status = 'pending'";
771        $parser = new Parser($sql);
772        $statement = $parser->statements[0];
773        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
774
775        $result = $transformer->buildProjection($statement, ['id', 'name']);
776        self::assertSame("SELECT 'X' AS `name`, `u`.`id` FROM `users` AS u JOIN `orders` AS o ON u.id = o.user_id WHERE o.status = 'pending'", $result['sql']);
777        self::assertSame('users', $result['table']);
778        self::assertSame(['users' => ['alias' => 'u']], $result['tables']);
779    }
780
781    public function testTransformExactOutputWithShadowData(): void
782    {
783        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
784        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
785        $tables = [
786            'users' => [
787                'rows' => [['id' => 1, 'name' => 'Alice']],
788                'columns' => ['id', 'name'],
789                'columnTypes' => [],
790            ],
791        ];
792
793        $result = $transformer->transform($sql, $tables);
794        self::assertSame(
795            'WITH `users` AS (SELECT CAST(1 AS SIGNED) AS `id`, CAST(' . "'" . 'Alice' . "'" . ' AS CHAR) AS `name`)' . "\n"
796            . "SELECT 'Bob' AS `name`, `users`.`id` FROM users WHERE id = 1",
797            $result
798        );
799    }
800
801    public function testTransformExactOutputWithEmptyRows(): void
802    {
803        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
804        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
805        $tables = [
806            'users' => [
807                'rows' => [],
808                'columns' => ['id', 'name'],
809                'columnTypes' => [],
810            ],
811        ];
812
813        $result = $transformer->transform($sql, $tables);
814        self::assertSame(
815            'WITH `users` AS (SELECT CAST(NULL AS CHAR) AS `id`, CAST(NULL AS CHAR) AS `name` FROM DUAL WHERE 0)' . "\n"
816            . "SELECT 'Bob' AS `name`, `users`.`id` FROM users WHERE id = 1",
817            $result
818        );
819    }
820
821    public function testBuildProjectionWithNoColumnsUsesWildcard(): void
822    {
823        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
824        $sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
825        $parser = new Parser($sql);
826        $statement = $parser->statements[0];
827        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
828
829        $result = $transformer->buildProjection($statement, []);
830        self::assertSame("SELECT 'Bob' AS `name` FROM `users` WHERE id = 1", $result['sql']);
831    }
832
833    public function testBuildProjectionJoinUsingExactColumnInOutput(): void
834    {
835        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
836        $sql = "UPDATE users JOIN orders USING (user_id) SET users.name = 'X'";
837        $parser = new Parser($sql);
838        $statement = $parser->statements[0];
839        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
840
841        $result = $transformer->buildProjection($statement, ['user_id', 'name']);
842        self::assertStringContainsString('USING (user_id)', $result['sql']);
843    }
844
845    public function testBuildProjectionMultiTableNoAliasSameNameNotDuplicated(): void
846    {
847        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
848        $sql = "UPDATE users, orders SET users.name = 'X' WHERE users.id = orders.user_id";
849        $parser = new Parser($sql);
850        $statement = $parser->statements[0];
851        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
852
853        $result = $transformer->buildProjection($statement, ['id', 'name']);
854        self::assertStringNotContainsString('AS orders', $result['sql']);
855        self::assertStringContainsString('`orders`', $result['sql']);
856    }
857
858    public function testBuildProjectionMultiTableWithAliasDifferentFromTableName(): void
859    {
860        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
861        $sql = "UPDATE users u, orders o SET u.name = 'Y' WHERE u.id = o.user_id";
862        $parser = new Parser($sql);
863        $statement = $parser->statements[0];
864        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
865
866        $result = $transformer->buildProjection($statement, ['id', 'name']);
867        self::assertStringContainsString('`orders` AS o', $result['sql']);
868    }
869
870    public function testBuildProjectionExactSqlForUpdateWithJoinUsing(): void
871    {
872        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
873        $sql = "UPDATE users JOIN orders USING (id) SET users.name = 'Z' WHERE orders.amount > 10";
874        $parser = new Parser($sql);
875        $statement = $parser->statements[0];
876        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
877
878        $result = $transformer->buildProjection($statement, ['id', 'name']);
879        self::assertStringContainsString('JOIN `orders`', $result['sql']);
880        self::assertStringContainsString('USING (id)', $result['sql']);
881        self::assertStringContainsString("'Z' AS `name`", $result['sql']);
882        self::assertStringContainsString('WHERE orders.amount > 10', $result['sql']);
883    }
884
885    public function testBuildProjectionExactSqlForUpdateWithLeftJoinOn(): void
886    {
887        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
888        $sql = 'UPDATE t1 LEFT JOIN t2 ON t1.id = t2.t1_id SET t1.a = 99';
889        $parser = new Parser($sql);
890        $statement = $parser->statements[0];
891        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
892
893        $result = $transformer->buildProjection($statement, ['id', 'a']);
894        self::assertStringContainsString('LEFT JOIN `t2`', $result['sql']);
895        self::assertStringContainsString('ON t1.id = t2.t1_id', $result['sql']);
896        self::assertStringContainsString('99 AS `a`', $result['sql']);
897    }
898
899    public function testTransformExactOutputForUpdateWithJoinUsing(): void
900    {
901        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
902        $sql = "UPDATE users JOIN orders USING (id) SET users.name = 'Z'";
903        $tables = [
904            'users' => [
905                'rows' => [],
906                'columns' => ['id', 'name'],
907                'columnTypes' => [],
908            ],
909        ];
910
911        $result = $transformer->transform($sql, $tables);
912        self::assertStringContainsString('USING (id)', $result);
913        self::assertStringContainsString("'Z' AS `name`", $result);
914    }
915
916    public function testBuildProjectionCoveredColBlocksColumnFromRemainder(): void
917    {
918        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
919        $sql = 'UPDATE t SET x = 10, y = 20 WHERE id = 1';
920        $parser = new Parser($sql);
921        $statement = $parser->statements[0];
922        self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\UpdateStatement::class, $statement);
923
924        $result = $transformer->buildProjection($statement, ['x', 'y', 'z']);
925        self::assertStringContainsString('10 AS `x`', $result['sql']);
926        self::assertStringContainsString('20 AS `y`', $result['sql']);
927        self::assertStringContainsString('`t`.`z`', $result['sql']);
928        self::assertStringNotContainsString('`t`.`x`', $result['sql']);
929        self::assertStringNotContainsString('`t`.`y`', $result['sql']);
930    }
931
932    public function testTransformMultiUpdateProjectsValuesAndOriginalIdentityPerTarget(): void
933    {
934        $transformer = new UpdateTransformer(new MySqlParser(), new SelectTransformer());
935        $tables = [
936            'users' => [
937                'rows' => [['id' => 2, 'name' => 'Bob']],
938                'columns' => ['id', 'name'],
939                'columnTypes' => [],
940                'primaryKeys' => ['id'],
941            ],
942            'orders' => [
943                'rows' => [['order_id' => 9, 'user_id' => 2]],
944                'columns' => ['order_id', 'user_id'],
945                'columnTypes' => [],
946                'primaryKeys' => ['order_id'],
947            ],
948        ];
949
950        $result = $transformer->transform(
951            "UPDATE users u, orders o SET `u`.`name` = 'Updated', `o`.`user_id` = 7 WHERE u.id = o.user_id",
952            $tables,
953        );
954
955        self::assertStringContainsString(
956            "SELECT `u`.`id` AS `__ztd_multi_0_value_0`, 'Updated' AS `__ztd_multi_0_value_1`, `u`.`id` AS `__ztd_multi_0_identity_0`",
957            $result,
958        );
959        self::assertStringContainsString(
960            '`o`.`order_id` AS `__ztd_multi_1_value_0`, 7 AS `__ztd_multi_1_value_1`, `o`.`order_id` AS `__ztd_multi_1_identity_0`',
961            $result,
962        );
963    }
964}
965