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