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