packages/ztd-query-core/tests/Contract/SchemaParserContractTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Contract;
6
7use PHPUnit\Framework\TestCase;
8use ZtdQuery\Platform\SchemaParser;
9use ZtdQuery\Schema\ColumnDeclaration;
10use ZtdQuery\Schema\TableDefinition;
11
12/**
13 * Abstract contract test for SchemaParser implementations.
14 *
15 * Enforces contracts defined in quality-standards.md Section 1.3 and properties P-SP-1 through P-SP-6.
16 */
17abstract class SchemaParserContractTest extends TestCase
18{
19    /**
20     * Answers the parser this dialect reads a declaration with.
21     *
22     * @return SchemaParser The parser under test
23     */
24    abstract protected function createParser(): SchemaParser;
25
26    /**
27     * A valid CREATE TABLE statement in the platform's dialect.
28     * Must define at least: columns, primary key, NOT NULL columns, column types, unique constraints.
29     */
30    abstract protected function validCreateTableSql(): string;
31
32    /**
33     * A SQL statement that is NOT a CREATE TABLE (e.g. SELECT, INSERT).
34     */
35    abstract protected function nonCreateTableSql(): string;
36
37    /**
38     * Valid CREATE TABLE must return a non-null TableDefinition (P-SP-5).
39     */
40    public function testValidCreateTableReturnsNonNull(): void
41    {
42        $parser = $this->createParser();
43        $result = $parser->parse($this->validCreateTableSql());
44
45        self::assertNotNull($result);
46    }
47
48    /**
49     * Non-CREATE TABLE SQL must return null (P-SP-6).
50     */
51    public function testNonCreateTableReturnsNull(): void
52    {
53        $parser = $this->createParser();
54        $result = $parser->parse($this->nonCreateTableSql());
55
56        self::assertNull($result);
57    }
58
59    /**
60     * primaryKeys must be a subset of columns (P-SP-1).
61     */
62    public function testPrimaryKeysSubsetOfColumns(): void
63    {
64        $parser = $this->createParser();
65        $definition = $parser->parse($this->validCreateTableSql());
66
67        self::assertNotNull($definition);
68
69        foreach ($definition->primaryKeys as $pk) {
70            self::assertContains(
71                $pk,
72                $definition->columns,
73                sprintf('Primary key "%s" is not in columns list', $pk)
74            );
75        }
76    }
77
78    /**
79     * notNullColumns must be a subset of columns (P-SP-3).
80     */
81    public function testNotNullSubsetOfColumns(): void
82    {
83        $parser = $this->createParser();
84        $definition = $parser->parse($this->validCreateTableSql());
85
86        self::assertNotNull($definition);
87
88        foreach ($definition->notNullColumns as $col) {
89            self::assertContains(
90                $col,
91                $definition->columns,
92                sprintf('NOT NULL column "%s" is not in columns list', $col)
93            );
94        }
95    }
96
97    /**
98     * Every key in columnTypes must exist in columns (P-SP-2).
99     */
100    public function testColumnTypesKeysSubsetOfColumns(): void
101    {
102        $parser = $this->createParser();
103        $definition = $parser->parse($this->validCreateTableSql());
104
105        self::assertNotNull($definition);
106
107        foreach (array_keys($definition->columnTypes) as $col) {
108            self::assertContains(
109                $col,
110                $definition->columns,
111                sprintf('Column type key "%s" is not in columns list', $col)
112            );
113        }
114    }
115
116    /**
117     * Every column list in uniqueConstraints must be a subset of columns (P-SP-4).
118     */
119    public function testUniqueConstraintColumnsSubsetOfColumns(): void
120    {
121        $parser = $this->createParser();
122        $definition = $parser->parse($this->validCreateTableSql());
123
124        self::assertNotNull($definition);
125
126        foreach ($definition->uniqueConstraints as $constraintName => $constraintColumns) {
127            foreach ($constraintColumns as $col) {
128                self::assertContains(
129                    $col,
130                    $definition->columns,
131                    sprintf(
132                        'Unique constraint "%s" column "%s" is not in columns list',
133                        $constraintName,
134                        $col
135                    )
136                );
137            }
138        }
139    }
140
141    /**
142     * Parsed TableDefinition must have non-empty columns.
143     */
144    public function testParsedDefinitionHasNonEmptyColumns(): void
145    {
146        $parser = $this->createParser();
147        $definition = $parser->parse($this->validCreateTableSql());
148
149        self::assertNotNull($definition);
150        self::assertNotEmpty($definition->columns);
151    }
152
153    /**
154     * Parsed columns must match expected column names in order.
155     */
156    public function testParsedColumnsMatchExpected(): void
157    {
158        $parser = $this->createParser();
159        $definition = $parser->parse($this->validCreateTableSql());
160
161        self::assertNotNull($definition);
162        self::assertSame(
163            $this->expectedColumns(),
164            $definition->columns,
165            'Parsed column names must match expected columns in order'
166        );
167    }
168
169    /**
170     * Parsed primary keys must match expected primary keys.
171     */
172    public function testParsedPrimaryKeysMatchExpected(): void
173    {
174        $parser = $this->createParser();
175        $definition = $parser->parse($this->validCreateTableSql());
176
177        self::assertNotNull($definition);
178        self::assertSame(
179            $this->expectedPrimaryKeys(),
180            $definition->primaryKeys,
181            'Parsed primary keys must match expected primary keys'
182        );
183    }
184
185    /**
186     * Parsed NOT NULL columns must include expected NOT NULL columns.
187     */
188    public function testParsedNotNullColumnsMatchExpected(): void
189    {
190        $parser = $this->createParser();
191        $definition = $parser->parse($this->validCreateTableSql());
192
193        self::assertNotNull($definition);
194
195        foreach ($this->expectedNotNullColumns() as $col) {
196            self::assertContains(
197                $col,
198                $definition->notNullColumns,
199                sprintf('Column "%s" should be NOT NULL', $col)
200            );
201        }
202    }
203
204    /**
205     * Column count must match exactly.
206     */
207    public function testColumnCountMatchesExpected(): void
208    {
209        $parser = $this->createParser();
210        $definition = $parser->parse($this->validCreateTableSql());
211
212        self::assertNotNull($definition);
213        self::assertCount(
214            count($this->expectedColumns()),
215            $definition->columns,
216            'Column count must match expected'
217        );
218    }
219
220    /**
221     * Return expected column names in order for the validCreateTableSql fixture.
222     *
223     * @return list<string>
224     */
225    protected function expectedColumns(): array
226    {
227        return ['id', 'name', 'email'];
228    }
229
230    /**
231     * Return expected primary key column names.
232     *
233     * @return list<string>
234     */
235    protected function expectedPrimaryKeys(): array
236    {
237        return ['id'];
238    }
239
240    /**
241     * Return columns that must be NOT NULL.
242     *
243     * @return list<string>
244     */
245    protected function expectedNotNullColumns(): array
246    {
247        return ['id', 'name'];
248    }
249
250    /**
251     * Malformed input must return null (does not throw).
252     */
253    public function testMalformedInputReturnsNull(): void
254    {
255        $parser = $this->createParser();
256        $result = $parser->parse('NOT VALID SQL AT ALL %%%');
257
258        self::assertNull($result);
259    }
260
261    /**
262     * Every key in typedColumns must exist in columns (structural invariant for ColumnDeclaration migration).
263     */
264    public function testTypedColumnsKeysSubsetOfColumns(): void
265    {
266        $parser = $this->createParser();
267        $definition = $parser->parse($this->validCreateTableSql());
268
269        self::assertNotNull($definition);
270
271        foreach (array_keys($definition->typedColumns) as $col) {
272            self::assertContains(
273                $col,
274                $definition->columns,
275                sprintf('Typed column key "%s" is not in columns list', $col)
276            );
277        }
278    }
279}
280