packages/ztd-query-postgres/src/Rewrite/Transformer/SelectTransformer.php

1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Platform\Postgres\Rewrite\Transformer;
6
7use ZtdQuery\Platform\CastRenderer;
8use ZtdQuery\Platform\IdentifierQuoter;
9use ZtdQuery\Platform\Postgres\Rewrite\Cte\PgSqlCteShadowComposer;
10use ZtdQuery\Platform\Postgres\Rewrite\GeneratedColumn\PgSqlGeneratedColumnProjector;
11use ZtdQuery\Platform\Postgres\Rewrite\Sampling\PgSqlTableSampleRewriter;
12use ZtdQuery\Platform\Postgres\Sql\PgSqlIdentifierQuoter;
13use ZtdQuery\Platform\Postgres\Sql\Value\PgSqlCastRenderer;
14use ZtdQuery\Platform\ValueRenderer;
15use ZtdQuery\Rewrite\SqlTransformer;
16use ZtdQuery\Schema\ColumnDeclaration;
17
18/**
19 * Applies CTE shadowing to SELECT statements for PostgreSQL.
20 *
21 * Key differences from MySQL SelectTransformer:
22 * - Uses double-quote identifiers ("table") instead of backticks
23 * - Uses AS MATERIALIZED for CTE definition (PG 12+ inline prevention)
24 * - Uses VALUES clause for multi-row CTEs instead of UNION ALL chains
25 * - Uses WHERE FALSE for empty CTEs instead of FROM DUAL WHERE 0
26 * - Uses PostgreSQL CAST types (INTEGER, TEXT, BOOLEAN, etc.)
27 */
28final class SelectTransformer implements SqlTransformer
29{
30    private CastRenderer $castRenderer;
31    private IdentifierQuoter $quoter;
32    private ValueRenderer $valueRenderer;
33    private PgSqlCteShadowComposer $cteComposer;
34    private PgSqlGeneratedColumnProjector $generatedColumnProjector;
35    private PgSqlTableSampleRewriter $tableSampleRewriter;
36
37    /**
38     * Initializes the collaborators and state used by this select transformer.
39     */
40    public function __construct(
41        ?CastRenderer $castRenderer = null,
42        ?IdentifierQuoter $quoter = null,
43        ?ValueRenderer $valueRenderer = null,
44    ) {
45        $this->castRenderer = $castRenderer ?? new PgSqlCastRenderer();
46        $this->quoter = $quoter ?? new PgSqlIdentifierQuoter();
47        $this->valueRenderer = $valueRenderer ?? new \ZtdQuery\Platform\Postgres\Sql\Value\PgSqlValueRenderer($this->castRenderer);
48        $this->cteComposer = new PgSqlCteShadowComposer();
49        $this->generatedColumnProjector = new PgSqlGeneratedColumnProjector();
50        $this->tableSampleRewriter = new PgSqlTableSampleRewriter();
51    }
52
53    /**
54     * {@inheritDoc}
55     * @throws \ZtdQuery\Exception\UnsupportedSqlException when a shadow relation uses an unsupported TABLESAMPLE method.
56     */
57    public function transform(string $sql, array $tables): string
58    {
59        $sql = $this->tableSampleRewriter->rewrite($sql, $tables);
60        $ctes = [];
61        foreach ($tables as $tableName => $tableContext) {
62            if (isset($tableContext['viewSql'])) {
63                $ctes[$tableName] = $this->quoter->quote($tableName) . " AS MATERIALIZED ({$tableContext['viewSql']})";
64                continue;
65            }
66            if (isset($tableContext['sourceSql'])) {
67                $ctes[$tableName] = $this->quoter->quote($tableName) . " AS MATERIALIZED ({$tableContext['sourceSql']})";
68                continue;
69            }
70
71            $rows = $tableContext['rows'];
72            $columns = $tableContext['columns'];
73            /**
74             * @var array<string, ColumnDeclaration> $columnTypes
75             */
76            $columnTypes = $tableContext['columnTypes'];
77            $generatedExpressions = $tableContext['generatedExpressions'] ?? [];
78
79            if ($columns === [] && $rows !== []) {
80                $columns = array_keys($rows[0]);
81                foreach ($rows as $row) {
82                    foreach (array_keys($row) as $column) {
83                        if (!in_array($column, $columns, true)) {
84                            $columns[] = $column;
85                        }
86                    }
87                }
88            }
89
90            if ($columns === [] && $rows === []) {
91                continue;
92            }
93
94            $ctes[$tableName] = (new Cte\RowSourceRenderer($this->castRenderer, $this->generatedColumnProjector, $this->quoter, $this->valueRenderer))->generateCte(
95                $tableName,
96                $rows,
97                $columns,
98                $columnTypes,
99                $generatedExpressions,
100            );
101        }
102
103        return $this->cteComposer->compose($sql, $ctes);
104    }
105}
106