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