packages/ztd-query-core/tests/Fake/FakeSqlTransformer.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Fake;
6
7use RuntimeException;
8use Stringable;
9use ZtdQuery\Rewrite\SqlTransformer;
10use ZtdQuery\Schema\ColumnDeclaration;
11use ZtdQuery\Schema\TableDefinition;
12
13/**
14 * Fake SqlTransformer that injects CTEs for shadow tables.
15 *
16 * Generates simplified CTE syntax using double-quoted identifiers
17 * and generic CAST expressions.
18 *
19 * @phpstan-import-type Row from TableDefinition
20 * @phpstan-import-type ShadowRows from SqlTransformer
21 */
22final class FakeSqlTransformer implements SqlTransformer
23{
24    private FakeCastRenderer $castRenderer;
25
26    private FakeIdentifierQuoter $quoter;
27
28    /**
29     * Binds the instance to what it will work from.
30     *
31     */
32    public function __construct()
33    {
34        $this->castRenderer = new FakeCastRenderer();
35        $this->quoter = new FakeIdentifierQuoter();
36    }
37
38    /**
39     * Transform.
40     *
41     * @param string $sql
42     * @return string
43     */
44    public function transform(string $sql, array $tables): string
45    {
46        if ($tables === []) {
47            return $sql;
48        }
49
50        $ctes = [];
51        foreach ($tables as $tableName => $tableData) {
52            if (isset($tableData['viewSql'])) {
53                $ctes[] = $this->quoter->quote($tableName) . ' AS (' . $tableData['viewSql'] . ')';
54                continue;
55            }
56
57            $ctes[] = $this->buildCte($tableName, $tableData);
58        }
59
60        return 'WITH ' . implode(', ', $ctes) . ' ' . $sql;
61    }
62
63    /**
64     * @param ShadowRows $tableData
65     *
66     * @throws RuntimeException When the fixture contains a value the fake renderer cannot encode.
67     */
68    public function buildCte(string $tableName, array $tableData): string
69    {
70        $quotedName = $this->quoter->quote($tableName);
71        $columns = $tableData['columns'];
72        $rows = $tableData['rows'];
73        $columnTypes = $tableData['columnTypes'];
74
75        if ($rows === []) {
76            $nullSelects = [];
77            foreach ($columns as $col) {
78                $type = $columnTypes[$col] ?? new ColumnDeclaration(\ZtdQuery\Schema\ColumnTypeFamily::TEXT, 'TEXT');
79                $nullSelects[] = $this->castRenderer->renderNullCast($type) . ' AS ' . $this->quoter->quote($col);
80            }
81
82            return $quotedName . ' AS (SELECT ' . implode(', ', $nullSelects) . ' WHERE FALSE)';
83        }
84
85        $rowSelects = [];
86        foreach (array_values($rows) as $i => $row) {
87            $colSelects = [];
88            foreach ($columns as $col) {
89                $value = $row[$col] ?? null;
90                $type = $columnTypes[$col] ?? new ColumnDeclaration(\ZtdQuery\Schema\ColumnTypeFamily::TEXT, 'TEXT');
91
92                if ($value === null) {
93                    $expr = $this->castRenderer->renderNullCast($type);
94                } else {
95                    if (is_string($value)) {
96                        $literal = "'" . str_replace("'", "''", $value) . "'";
97                    } elseif (is_bool($value)) {
98                        $literal = $value ? '1' : '0';
99                    } elseif (is_int($value) || is_float($value) || is_resource($value) || $value instanceof Stringable) {
100                        $literal = (string) $value;
101                    } else {
102                        throw new RuntimeException('The fake transformer requires a renderable SQL value.');
103                    }
104                    $expr = $this->castRenderer->renderCast($literal, $type);
105                }
106
107                if ($i === 0) {
108                    $colSelects[] = $expr . ' AS ' . $this->quoter->quote($col);
109                } else {
110                    $colSelects[] = $expr;
111                }
112            }
113            $rowSelects[] = 'SELECT ' . implode(', ', $colSelects);
114        }
115
116        return $quotedName . ' AS (' . implode(' UNION ALL ', $rowSelects) . ')';
117    }
118}
119