packages/sql-fixture/tests/Unit/Fixture/PlanSchemaValidatorTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Fixture;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\Test;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use SqlFixture\Fixture\PlanSchemaException;
13use SqlFixture\Fixture\PlanSchemaValidator;
14use SqlFixture\Plan\ColumnRef;
15use SqlFixture\Plan\FixturePlan;
16use SqlFixture\Plan\PlanParser;
17use SqlFixture\Plan\Relation;
18use SqlFixture\Plan\RelationKind;
19use SqlFixture\Plan\RelationSide;
20use SqlFixture\Platform\MySql\MySqlSchemaParser;
21use SqlFixture\Schema\ColumnDefinition;
22use SqlFixture\Schema\SchemaNotFoundException;
23use SqlFixture\Schema\StaticSchemaResolver;
24use SqlFixture\Schema\TableSchema;
25
26#[CoversClass(PlanSchemaValidator::class)]
27#[UsesClass(PlanSchemaException::class)]
28#[UsesClass(FixturePlan::class)]
29#[UsesClass(PlanParser::class)]
30#[UsesClass(Relation::class)]
31#[UsesClass(ColumnRef::class)]
32#[UsesClass(RelationKind::class)]
33#[UsesClass(RelationSide::class)]
34#[UsesClass(MySqlSchemaParser::class)]
35#[UsesClass(StaticSchemaResolver::class)]
36#[UsesClass(SchemaNotFoundException::class)]
37#[UsesClass(TableSchema::class)]
38#[UsesClass(ColumnDefinition::class)]
39#[CoversClass(\SqlFixture\Fixture\Validation\EndpointValidator::class)]
40#[UsesClass(\SqlFixture\Plan\PlanPrinter::class)]
41#[UsesClass(\SqlFixture\Plan\PlanSyntaxException::class)]
42#[UsesClass(\SqlFixture\Schema\SchemaParseException::class)]
43#[UsesClass(\SqlFixture\Schema\SchemaParserInterface::class)]
44#[UsesClass(\SqlFixture\Schema\SchemaResolverInterface::class)]
45#[UsesClass(\SqlFixture\Schema\TableIdentifier::class)]
46#[UsesClass(\SqlFixture\Plan\Parsing\PlanStatements::class)]
47#[UsesClass(\SqlFixture\Plan\Parsing\RelationCursor::class)]
48#[UsesClass(\SqlFixture\Plan\Parsing\RelationReader::class)]
49#[UsesClass(\SqlFixture\Plan\PlanStructureException::class)]
50#[UsesClass(\SqlFixture\Plan\Printing\PlanTables::class)]
51#[UsesClass(\SqlFixture\Plan\Printing\RelationGroups::class)]
52#[UsesClass(\SqlFixture\Plan\Printing\StatementPrinter::class)]
53#[UsesClass(\SqlFixture\Plan\Validation\PlanValidation::class)]
54#[UsesClass(\SqlFixture\Plan\Validation\TableName::class)]
55#[UsesClass(\SqlFixture\Platform\MySql\Schema\ColumnParser::class)]
56#[UsesClass(\SqlFixture\Platform\MySql\Schema\DefaultExpression::class)]
57#[UsesClass(\SqlFixture\Platform\MySql\Schema\DefinitionIntegrity::class)]
58#[UsesClass(\SqlFixture\Platform\MySql\Schema\TableDefinition::class)]
59#[UsesClass(\SqlFixture\Platform\MySql\Schema\TypeParameters::class)]
60#[UsesClass(\SqlFixture\Schema\TypeShape::class)]
61#[UsesClass(\SqlFixture\Platform\MySql\Schema\TableDefinitionInput::class)]
62#[UsesClass(\SqlFixture\Fixture\Exception\GeneratedColumnReferenceException::class)]
63#[UsesClass(\SqlFixture\Fixture\Exception\MissingRelationValueException::class)]
64#[UsesClass(\SqlFixture\Fixture\Exception\UnknownPlanColumnException::class)]
65#[UsesClass(\SqlFixture\Plan\Exception\EmptyPlanException::class)]
66#[UsesClass(\SqlFixture\Plan\Exception\EmptyTableNameException::class)]
67#[UsesClass(\SqlFixture\Plan\Exception\MissingEndpointColumnsException::class)]
68#[UsesClass(\SqlFixture\Plan\Exception\InvalidTableNameException::class)]
69#[UsesClass(\SqlFixture\Plan\Exception\UnbalancedBracketsException::class)]
70#[UsesClass(\SqlFixture\Plan\Exception\UnexpectedPlanTokenException::class)]
71#[UsesClass(\SqlFixture\Plan\Exception\UnsupportedManyToManyException::class)]
72#[UsesClass(\SqlFixture\Plan\Exception\CompositeArityMismatchException::class)]
73#[UsesClass(\SqlFixture\Plan\Exception\DuplicateColumnBindingException::class)]
74#[UsesClass(\SqlFixture\Plan\Exception\CyclicDependencyException::class)]
75#[UsesClass(\SqlFixture\Plan\Exception\UnboundedSelfReferenceException::class)]
76#[UsesClass(\SqlFixture\Schema\Exception\InvalidSqlException::class)]
77#[UsesClass(\SqlFixture\Schema\Exception\ExpectedCreateTableException::class)]
78#[UsesClass(\SqlFixture\Schema\Exception\MissingColumnDefinitionsException::class)]
79final class PlanSchemaValidatorTest extends TestCase
80{
81 #[Test]
82 public function testValidateAPlanMatchingTheSchemaPasses(): void
83 {
84 $schemas = new StaticSchemaResolver([
85 new TableSchema('order', [
86 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
87 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
88 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
89 'total' => new ColumnDefinition('total', 'INT', nullable: false),
90 ], ['id']),
91 new TableSchema('order_detail', [
92 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
93 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
94 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
95 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
96 ], ['id']),
97 ]);
98 $validator = new PlanSchemaValidator($schemas);
99
100 $validator->validate(FixturePlan::from('order.id < order_detail.order_id'));
101
102 $this->expectNotToPerformAssertions();
103 }
104
105 #[Test]
106 #[DataProvider('providerMismatchedPlans')]
107 public function aColumnTheTableDoesNotHaveIsRejected(string $plan, string $expected): void
108 {
109 $schemas = new StaticSchemaResolver([
110 new TableSchema('order', [
111 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
112 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
113 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
114 'total' => new ColumnDefinition('total', 'INT', nullable: false),
115 ], ['id']),
116 new TableSchema('order_detail', [
117 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
118 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
119 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
120 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
121 ], ['id']),
122 ]);
123 $validator = new PlanSchemaValidator($schemas);
124
125 $this->expectException(PlanSchemaException::class);
126 $this->expectExceptionMessage($expected);
127
128 $validator->validate(FixturePlan::from($plan));
129 }
130
131 /**
132 * @return array<string, array{string, string}>
133 */
134 public static function providerMismatchedPlans(): array
135 {
136 return [
137 'parent column misspelt' => [
138 'order.idd < order_detail.order_id',
139 'order has no column idd',
140 ],
141 'child column misspelt' => [
142 'order.id < order_detail.oder_id',
143 'order_detail has no column oder_id',
144 ],
145 'column belongs to another table' => [
146 'order.quantity < order_detail.order_id',
147 'order has no column quantity',
148 ],
149 'one of a composite key is wrong' => [
150 'order.(id, nope) < order_detail.(order_id, quantity)',
151 'order has no column nope',
152 ],
153 ];
154 }
155
156 #[Test]
157 public function testATableTheResolverDoesNotKnowIsRejected(): void
158 {
159 $schemas = new StaticSchemaResolver([
160 new TableSchema('order', [
161 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
162 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
163 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
164 'total' => new ColumnDefinition('total', 'INT', nullable: false),
165 ], ['id']),
166 new TableSchema('order_detail', [
167 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
168 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
169 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
170 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
171 ], ['id']),
172 ]);
173 $validator = new PlanSchemaValidator($schemas);
174
175 $this->expectException(SchemaNotFoundException::class);
176 $this->expectExceptionMessage('Schema not found for table: nope');
177
178 $validator->validate(FixturePlan::from('nope.id < order_detail.order_id'));
179 }
180
181 #[Test]
182 public function testATableNamedWithoutAnyRelationIsCheckedToo(): void
183 {
184 $schemas = new StaticSchemaResolver([
185 new TableSchema('order', [
186 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
187 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
188 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
189 'total' => new ColumnDefinition('total', 'INT', nullable: false),
190 ], ['id']),
191 new TableSchema('order_detail', [
192 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
193 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
194 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
195 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
196 ], ['id']),
197 ]);
198 $validator = new PlanSchemaValidator($schemas);
199
200 $this->expectException(SchemaNotFoundException::class);
201 $this->expectExceptionMessage('Schema not found for table: nope');
202
203 $validator->validate(FixturePlan::from('order.id < order_detail.order_id, nope'));
204 }
205
206 #[Test]
207 public function testAGeneratedColumnCannotBeLinked(): void
208 {
209 $resolver = new StaticSchemaResolver([
210 new TableSchema('order', [
211 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
212 'code' => new ColumnDefinition('code', 'VARCHAR', length: 10, generated: true),
213 ], ['id']),
214 new TableSchema('order_detail', [
215 'order_code' => new ColumnDefinition('order_code', 'VARCHAR', length: 10),
216 ]),
217 ]);
218
219 $this->expectException(\SqlFixture\Fixture\Exception\GeneratedColumnReferenceException::class);
220 $this->expectExceptionMessage('order.code is a generated column');
221
222 (new PlanSchemaValidator($resolver))->validate(FixturePlan::from('order.code < order_detail.order_code'));
223 }
224
225 #[Test]
226 public function testAGeneratedColumnCannotBeWrittenIntoEither(): void
227 {
228 $resolver = new StaticSchemaResolver([
229 new TableSchema('order', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)], ['id']),
230 new TableSchema('order_detail', [
231 'order_id' => new ColumnDefinition('order_id', 'INT', generated: true),
232 ]),
233 ]);
234
235 $this->expectException(\SqlFixture\Fixture\Exception\GeneratedColumnReferenceException::class);
236 $this->expectExceptionMessage('order_detail.order_id is a generated column');
237
238 (new PlanSchemaValidator($resolver))->validate(FixturePlan::from('order.id < order_detail.order_id'));
239 }
240}
241