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

1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Platform\Sqlite\Rewrite\Transformer;
6
7use ZtdQuery\Exception\UnsupportedSqlException;
8use ZtdQuery\Platform\CastRenderer;
9use ZtdQuery\Platform\Sqlite\Rewrite\Cte\SqliteCteShadowComposer;
10use ZtdQuery\Platform\Sqlite\Rewrite\Upsert\SqliteNativeUpsertProjector;
11use ZtdQuery\Platform\Sqlite\Sql\SqliteParser;
12use ZtdQuery\Platform\Sqlite\Sql\Value\SqliteCastRenderer;
13use ZtdQuery\Rewrite\ShadowIdentityAllocator;
14use ZtdQuery\Rewrite\SqlTransformer;
15
16/**
17 * Transforms INSERT/REPLACE statements into SELECT queries that return the inserted rows.
18 * Applies CTE shadowing via the SelectTransformer delegate.
19 *
20 * Handles:
21 * - INSERT INTO ... VALUES (...)
22 * - INSERT OR REPLACE INTO ... VALUES (...)
23 * - REPLACE INTO ... VALUES (...)
24 * - INSERT INTO ... SELECT ...
25 * - INSERT INTO ... ON CONFLICT ... DO UPDATE SET ...
26 */
27final class InsertTransformer implements SqlTransformer
28{
29    private SqliteParser $parser;
30    private SelectTransformer $selectTransformer;
31    private CastRenderer $castRenderer;
32    private InsertRowRenderer $rowRenderer;
33    private ShadowIdentityAllocator $identityAllocator;
34    private InsertSelectRenderer $insertSelectRenderer;
35    private SqliteCteShadowComposer $cteComposer;
36    private SqliteNativeUpsertProjector $upsertProjector;
37
38    /**
39     * Binds the dependencies used by this operation.
40     */
41    public function __construct(
42        SqliteParser $parser,
43        SelectTransformer $selectTransformer,
44        ?CastRenderer $castRenderer = null,
45    ) {
46        $this->parser = $parser;
47        $this->selectTransformer = $selectTransformer;
48        $this->castRenderer = $castRenderer ?? new SqliteCastRenderer();
49        $this->rowRenderer = new InsertRowRenderer();
50        $this->identityAllocator = new ShadowIdentityAllocator();
51        $this->insertSelectRenderer = new InsertSelectRenderer();
52        $this->cteComposer = new SqliteCteShadowComposer();
53        $this->upsertProjector = new SqliteNativeUpsertProjector();
54    }
55
56    /**
57     * {@inheritDoc}
58     * @throws UnsupportedSqlException
59     */
60    public function transform(string $sql, array $tables): string
61    {
62        $this->identityAllocator->beginProjection();
63        $type = $this->parser->classifyStatement($sql);
64        if ($type !== 'INSERT') {
65            throw new UnsupportedSqlException($sql, 'Expected INSERT/REPLACE statement');
66        }
67
68        $tableName = $this->parser->extractTargetTable($sql);
69        if ($tableName === null) {
70            throw new UnsupportedSqlException($sql, 'Cannot resolve INSERT target');
71        }
72
73        $insertColumns = Insert\InsertProjectionBuilder::orderedValues($this->parser->extractInsertColumns($sql));
74        $tableColumns = Insert\InsertProjectionBuilder::orderedValues($tables[$tableName]['columns'] ?? $insertColumns);
75        if ($tableColumns === []) {
76            throw new UnsupportedSqlException($sql, 'Cannot determine columns');
77        }
78
79        $columnTypes = $tables[$tableName]['columnTypes'] ?? [];
80        $columnDefaults = $tables[$tableName]['columnDefaults'] ?? [];
81        $identityStrategies = $tables[$tableName]['identityStrategies'] ?? [];
82        $existingRows = $tables[$tableName]['rows'] ?? [];
83        $selectSql = (new Insert\InsertProjectionBuilder($this->castRenderer, $this->identityAllocator, $this->insertSelectRenderer, $this->parser, $this->rowRenderer))->buildInsertSelect(
84            $sql,
85            $tableName,
86            $tableColumns,
87            $insertColumns,
88            $columnTypes,
89            $columnDefaults,
90            $identityStrategies,
91            $existingRows,
92        );
93        $selectSql = $this->upsertProjector->project(
94            $selectSql,
95            $tableName,
96            $tableColumns,
97            isset($tables[$tableName]['candidateKeys']) ? $tables[$tableName]['candidateKeys'] : [],
98            $this->parser->extractOnConflictUpdates($sql),
99            $this->parser->extractOnConflictUpdateWhere($sql),
100        );
101
102        return $this->selectTransformer->transform(
103            $this->cteComposer->carryPrefix($sql, $selectSql),
104            $tables,
105        );
106    }
107
108    /**
109     * Commits the transformer state after a rewrite completes.
110     */
111    public function commitRewriteState(): void
112    {
113        $this->identityAllocator->commitProjection();
114    }
115
116}
117