packages/ztd-query-core/tests/Contract/CastRendererContractTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Contract;
6
7use PHPUnit\Framework\TestCase;
8use ZtdQuery\Platform\CastRenderer;
9use ZtdQuery\Schema\ColumnDeclaration;
10use ZtdQuery\Schema\ColumnTypeFamily;
11
12/**
13 * Abstract contract test for CastRenderer implementations.
14 *
15 * Enforces contracts defined in quality-standards.md Section 1.7 and properties P-CR-1 through P-CR-5.
16 */
17abstract class CastRendererContractTest extends TestCase
18{
19 /**
20 * Answers the renderer this dialect casts with.
21 *
22 * @return CastRenderer The renderer under test
23 */
24 abstract protected function createRenderer(): CastRenderer;
25
26 /**
27 * renderCast must return a non-empty string for every type family (P-CR-1).
28 */
29 public function testRenderCastReturnsNonEmptyString(): void
30 {
31 $renderer = $this->createRenderer();
32
33 foreach (ColumnTypeFamily::cases() as $family) {
34 $type = new ColumnDeclaration($family, $this->nativeTypeFor($family));
35 $result = $renderer->renderCast("'test'", $type);
36
37 self::assertNotEmpty(
38 $result,
39 sprintf('renderCast returned empty string for family %s', $family->value)
40 );
41 }
42 }
43
44 /**
45 * renderNullCast must return a non-empty string for every type family (P-CR-1).
46 */
47 public function testRenderNullCastReturnsNonEmptyString(): void
48 {
49 $renderer = $this->createRenderer();
50
51 foreach (ColumnTypeFamily::cases() as $family) {
52 $type = new ColumnDeclaration($family, $this->nativeTypeFor($family));
53 $result = $renderer->renderNullCast($type);
54
55 self::assertNotEmpty(
56 $result,
57 sprintf('renderNullCast returned empty string for family %s', $family->value)
58 );
59 }
60 }
61
62 /**
63 * renderNullCast output must contain the CAST keyword (P-CR-2).
64 */
65 public function testRenderNullCastContainsCastKeyword(): void
66 {
67 $renderer = $this->createRenderer();
68
69 foreach (ColumnTypeFamily::cases() as $family) {
70 $type = new ColumnDeclaration($family, $this->nativeTypeFor($family));
71 $result = $renderer->renderNullCast($type);
72
73 self::assertStringContainsString(
74 'CAST(',
75 strtoupper($result),
76 sprintf('renderNullCast output for family %s does not contain CAST(', $family->value)
77 );
78 }
79 }
80
81 /**
82 * renderNullCast output must contain NULL.
83 */
84 public function testRenderNullCastContainsNullKeyword(): void
85 {
86 $renderer = $this->createRenderer();
87
88 foreach (ColumnTypeFamily::cases() as $family) {
89 $type = new ColumnDeclaration($family, $this->nativeTypeFor($family));
90 $result = $renderer->renderNullCast($type);
91
92 self::assertStringContainsString(
93 'NULL',
94 strtoupper($result),
95 sprintf('renderNullCast output for family %s does not contain NULL', $family->value)
96 );
97 }
98 }
99
100 /**
101 * All ColumnTypeFamily cases must be handled without error (P-CR-5).
102 */
103 public function testAllColumnTypeFamiliesHandled(): void
104 {
105 $renderer = $this->createRenderer();
106
107 foreach (ColumnTypeFamily::cases() as $family) {
108 $type = new ColumnDeclaration($family, $this->nativeTypeFor($family));
109
110 $castResult = $renderer->renderCast("'value'", $type);
111 $nullResult = $renderer->renderNullCast($type);
112
113 self::assertNotEmpty(
114 $castResult,
115 sprintf('renderCast returned empty for family %s', $family->value)
116 );
117 self::assertNotEmpty(
118 $nullResult,
119 sprintf('renderNullCast returned empty for family %s', $family->value)
120 );
121 }
122 }
123
124 /**
125 * renderCast for INTEGER must produce exact CAST(expression AS ...) form (P-CR-3).
126 */
127 public function testRenderCastForIntegerProducesExactForm(): void
128 {
129 $renderer = $this->createRenderer();
130 $type = new ColumnDeclaration(ColumnTypeFamily::INTEGER, $this->nativeTypeFor(ColumnTypeFamily::INTEGER));
131
132 $result = $renderer->renderCast("'42'", $type);
133
134 self::assertMatchesRegularExpression(
135 '/^CAST\(\'42\'\s+AS\s+\S+\)$/i',
136 $result,
137 'renderCast for INTEGER must produce CAST(expression AS type) format'
138 );
139 }
140
141 /**
142 * renderNullCast must produce exact CAST(NULL AS type) form.
143 */
144 public function testRenderNullCastProducesExactForm(): void
145 {
146 $renderer = $this->createRenderer();
147 $type = new ColumnDeclaration(ColumnTypeFamily::INTEGER, $this->nativeTypeFor(ColumnTypeFamily::INTEGER));
148
149 $result = $renderer->renderNullCast($type);
150
151 self::assertMatchesRegularExpression(
152 '/^CAST\(NULL\s+AS\s+\S+\)$/i',
153 $result,
154 'renderNullCast must produce CAST(NULL AS type) format'
155 );
156 }
157
158 /**
159 * renderCast for STRING must include VARCHAR or TEXT or CHAR type.
160 */
161 public function testRenderCastForStringIncludesStringType(): void
162 {
163 $renderer = $this->createRenderer();
164 $type = new ColumnDeclaration(ColumnTypeFamily::STRING, $this->nativeTypeFor(ColumnTypeFamily::STRING));
165
166 $result = $renderer->renderCast("'hello'", $type);
167 $upper = strtoupper($result);
168
169 self::assertTrue(
170 str_contains($upper, 'VARCHAR') || str_contains($upper, 'TEXT') || str_contains($upper, 'CHAR'),
171 sprintf('renderCast for STRING must include VARCHAR, TEXT, or CHAR, got: %s', $result)
172 );
173 }
174
175 /**
176 * renderCast must be deterministic: same inputs produce identical outputs (P-CR-4).
177 */
178 public function testRenderCastIsDeterministic(): void
179 {
180 $renderer = $this->createRenderer();
181 $type = new ColumnDeclaration(ColumnTypeFamily::INTEGER, $this->nativeTypeFor(ColumnTypeFamily::INTEGER));
182
183 $result1 = $renderer->renderCast('42', $type);
184 $result2 = $renderer->renderCast('42', $type);
185
186 self::assertSame($result1, $result2);
187
188 $nullResult1 = $renderer->renderNullCast($type);
189 $nullResult2 = $renderer->renderNullCast($type);
190
191 self::assertSame($nullResult1, $nullResult2);
192 }
193
194 /**
195 * Provide a representative native type string for a given family.
196 * Subclasses may override this to provide platform-specific native types.
197 */
198 protected function nativeTypeFor(ColumnTypeFamily $family): string
199 {
200 return match ($family) {
201 ColumnTypeFamily::INTEGER => 'INTEGER',
202 ColumnTypeFamily::FLOAT => 'FLOAT',
203 ColumnTypeFamily::DOUBLE => 'DOUBLE',
204 ColumnTypeFamily::DECIMAL => 'DECIMAL(10,2)',
205 ColumnTypeFamily::STRING => 'VARCHAR(255)',
206 ColumnTypeFamily::TEXT => 'TEXT',
207 ColumnTypeFamily::BOOLEAN => 'BOOLEAN',
208 ColumnTypeFamily::DATE => 'DATE',
209 ColumnTypeFamily::TIME => 'TIME',
210 ColumnTypeFamily::DATETIME => 'DATETIME',
211 ColumnTypeFamily::TIMESTAMP => 'TIMESTAMP',
212 ColumnTypeFamily::BINARY => 'BLOB',
213 ColumnTypeFamily::JSON => 'JSON',
214 ColumnTypeFamily::UNKNOWN => 'CUSTOM_TYPE',
215 };
216 }
217}
218