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