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