packages/ztd-query-sqlite/src/Rewrite/Transformer/InsertSelectRenderer.php

1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Platform\Sqlite\Rewrite\Transformer;
6
7use InvalidArgumentException;
8use ZtdQuery\Platform\Sqlite\Sql\SqliteIdentifierQuoter;
9use ZtdQuery\Rewrite\InsertSelectProjectionPlanner;
10
11/**
12 * Projects INSERT SELECT results onto target table columns.
13 */
14final class InsertSelectRenderer
15{
16    private SqliteIdentifierQuoter $quoter;
17    private InsertSelectProjectionPlanner $projectionPlanner;
18
19    /**
20     * Binds the dependencies used by this operation.
21     */
22    public function __construct()
23    {
24        $this->quoter = new SqliteIdentifierQuoter();
25        $this->projectionPlanner = new InsertSelectProjectionPlanner();
26    }
27
28    /**
29     * @param list<string> $tableColumns
30     * @param list<string> $insertColumns
31     * @param array<string, string> $defaults
32     * @param array<string, int> $generatedIdentityStarts
33     */
34    public function render(
35        string $selectSql,
36        array $tableColumns,
37        array $insertColumns,
38        array $defaults,
39        array $generatedIdentityStarts = [],
40    ): string {
41        $sourceColumns = [];
42        foreach ($insertColumns as $index => $column) {
43            $sourceColumns[] = $this->quoter->quote('__ztd_insert_' . $index);
44        }
45
46        $selects = [];
47        foreach ($this->projectionPlanner->plan($tableColumns, $insertColumns, $defaults, $generatedIdentityStarts) as $projection) {
48            $sourceIndex = $projection->sourceIndex();
49            $generatedIdentityStart = $projection->generatedIdentityStart();
50            if ($sourceIndex !== null) {
51                $expression = $this->quoter->quote('__ztd_insert_' . $sourceIndex);
52            } elseif ($generatedIdentityStart !== null) {
53                $expression = $this->renderGeneratedIdentity($generatedIdentityStart);
54            } else {
55                $expression = $projection->defaultExpressionValue() ?? 'NULL';
56            }
57            $selects[] = $expression . ' AS ' . $this->quoter->quote($projection->targetColumn());
58        }
59
60        $sourceName = $this->quoter->quote('__ztd_insert_source');
61
62        return 'WITH ' . $sourceName . ' (' . implode(', ', $sourceColumns) . ') AS ('
63            . $selectSql . ') SELECT ' . implode(', ', $selects) . ' FROM ' . $sourceName;
64    }
65
66    /**
67     * Returns render generated identity.
68     * @throws InvalidArgumentException
69     */
70    public function renderGeneratedIdentity(int $start): string
71    {
72        if ($start < 1) {
73            throw new InvalidArgumentException('Generated identity start must be positive.');
74        }
75
76        return $start . ' + ROW_NUMBER() OVER () - 1';
77    }
78}
79