packages/ztd-query-mysql/src/Rewrite/Transformer/InsertSelectRenderer.php
1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Platform\MySql\Rewrite\Transformer;
6
7use InvalidArgumentException;
8use RuntimeException;
9use ZtdQuery\Platform\MySql\Sql\MySqlIdentifierQuoter;
10use ZtdQuery\Rewrite\InsertSelectProjectionPlanner;
11
12/**
13 * Implements the Insert Select Renderer contract for MySQL.
14 */
15final class InsertSelectRenderer
16{
17 private MySqlIdentifierQuoter $quoter;
18 private InsertSelectProjectionPlanner $projectionPlanner;
19 private MySqlSelectListAliaser $selectListAliaser;
20
21 /**
22 * Configure the dependencies used by this operation.
23 */
24 public function __construct()
25 {
26 $this->quoter = new MySqlIdentifierQuoter();
27 $this->projectionPlanner = new InsertSelectProjectionPlanner();
28 $this->selectListAliaser = new MySqlSelectListAliaser();
29 }
30
31 /**
32 * @param list<string> $tableColumns
33 * @param list<string> $insertColumns
34 * @param array<string, string> $defaults
35 * @param array<string, int> $generatedIdentityStarts
36 * @throws RuntimeException
37 */
38 public function render(
39 string $selectSql,
40 array $tableColumns,
41 array $insertColumns,
42 array $defaults,
43 array $generatedIdentityStarts = [],
44 ): string {
45 $projectionCount = $this->selectListAliaser->projectionCount($selectSql);
46 if ($projectionCount !== null && $projectionCount !== count($insertColumns)) {
47 throw new RuntimeException('INSERT column count does not match SELECT column count.');
48 }
49 $selectSql = $this->selectListAliaser->alias($selectSql);
50
51 $sourceColumns = [];
52 foreach ($insertColumns as $index => $column) {
53 $sourceColumns[] = $this->quoter->quote('__ztd_insert_' . $index);
54 }
55
56 $selects = [];
57 foreach ($this->projectionPlanner->plan($tableColumns, $insertColumns, $defaults, $generatedIdentityStarts) as $projection) {
58 $sourceIndex = $projection->sourceIndex();
59 $generatedIdentityStart = $projection->generatedIdentityStart();
60 if ($sourceIndex !== null) {
61 $expression = $this->quoter->quote('__ztd_insert_' . $sourceIndex);
62 } elseif ($generatedIdentityStart !== null) {
63 $expression = $this->renderGeneratedIdentity($generatedIdentityStart);
64 } else {
65 $expression = $projection->defaultExpressionValue() ?? 'NULL';
66 }
67 $selects[] = $expression . ' AS ' . $this->quoter->quote($projection->targetColumn());
68 }
69
70 $sourceName = $this->quoter->quote('__ztd_insert_source');
71
72 return 'WITH ' . $sourceName . ' (' . implode(', ', $sourceColumns) . ') AS ('
73 . $selectSql . ') SELECT ' . implode(', ', $selects) . ' FROM ' . $sourceName;
74 }
75
76 /**
77 * Render Generated Identity for the supplied MySQL input.
78 * @throws InvalidArgumentException
79 */
80 public function renderGeneratedIdentity(int $start): string
81 {
82 if ($start < 1) {
83 throw new InvalidArgumentException('Generated identity start must be positive.');
84 }
85
86 return $start . ' + ROW_NUMBER() OVER () - 1';
87 }
88}
89