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