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)]
79#[UsesClass(\SqlFixture\Plan\Choice\ChoiceValidation::class)]
80#[UsesClass(\SqlFixture\Plan\Choice\PlanContents::class)]
81final class PlanSchemaValidatorTest extends TestCase
82{
83 #[Test]
84 public function testValidateAPlanMatchingTheSchemaPasses(): void
85 {
86 $schemas = new StaticSchemaResolver([
87 new TableSchema('order', [
88 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
89 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
90 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
91 'total' => new ColumnDefinition('total', 'INT', nullable: false),
92 ], ['id']),
93 new TableSchema('order_detail', [
94 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
95 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
96 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
97 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
98 ], ['id']),
99 ]);
100 $validator = new PlanSchemaValidator($schemas);
101
102 $validator->validate(FixturePlan::from('order.id < order_detail.order_id'));
103
104 $this->expectNotToPerformAssertions();
105 }
106
107 #[Test]
108 #[DataProvider('providerMismatchedPlans')]
109 public function aColumnTheTableDoesNotHaveIsRejected(string $plan, string $expected): void
110 {
111 $schemas = new StaticSchemaResolver([
112 new TableSchema('order', [
113 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
114 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
115 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
116 'total' => new ColumnDefinition('total', 'INT', nullable: false),
117 ], ['id']),
118 new TableSchema('order_detail', [
119 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
120 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
121 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
122 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
123 ], ['id']),
124 ]);
125 $validator = new PlanSchemaValidator($schemas);
126
127 $this->expectException(PlanSchemaException::class);
128 $this->expectExceptionMessage($expected);
129
130 $validator->validate(FixturePlan::from($plan));
131 }
132
133 /**
134 * @return array<string, array{string, string}>
135 */
136 public static function providerMismatchedPlans(): array
137 {
138 return [
139 'parent column misspelt' => [
140 'order.idd < order_detail.order_id',
141 'order has no column idd',
142 ],
143 'child column misspelt' => [
144 'order.id < order_detail.oder_id',
145 'order_detail has no column oder_id',
146 ],
147 'column belongs to another table' => [
148 'order.quantity < order_detail.order_id',
149 'order has no column quantity',
150 ],
151 'one of a composite key is wrong' => [
152 'order.(id, nope) < order_detail.(order_id, quantity)',
153 'order has no column nope',
154 ],
155 ];
156 }
157
158 #[Test]
159 public function testATableTheResolverDoesNotKnowIsRejected(): void
160 {
161 $schemas = new StaticSchemaResolver([
162 new TableSchema('order', [
163 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
164 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
165 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
166 'total' => new ColumnDefinition('total', 'INT', nullable: false),
167 ], ['id']),
168 new TableSchema('order_detail', [
169 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
170 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
171 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
172 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
173 ], ['id']),
174 ]);
175 $validator = new PlanSchemaValidator($schemas);
176
177 $this->expectException(SchemaNotFoundException::class);
178 $this->expectExceptionMessage('Schema not found for table: nope');
179
180 $validator->validate(FixturePlan::from('nope.id < order_detail.order_id'));
181 }
182
183 #[Test]
184 public function testATableNamedWithoutAnyRelationIsCheckedToo(): void
185 {
186 $schemas = new StaticSchemaResolver([
187 new TableSchema('order', [
188 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
189 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
190 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
191 'total' => new ColumnDefinition('total', 'INT', nullable: false),
192 ], ['id']),
193 new TableSchema('order_detail', [
194 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
195 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
196 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
197 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
198 ], ['id']),
199 ]);
200 $validator = new PlanSchemaValidator($schemas);
201
202 $this->expectException(SchemaNotFoundException::class);
203 $this->expectExceptionMessage('Schema not found for table: nope');
204
205 $validator->validate(FixturePlan::from('order.id < order_detail.order_id, nope'));
206 }
207
208 #[Test]
209 public function testAGeneratedColumnCannotBeLinked(): void
210 {
211 $resolver = new StaticSchemaResolver([
212 new TableSchema('order', [
213 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
214 'code' => new ColumnDefinition('code', 'VARCHAR', length: 10, generated: true),
215 ], ['id']),
216 new TableSchema('order_detail', [
217 'order_code' => new ColumnDefinition('order_code', 'VARCHAR', length: 10),
218 ]),
219 ]);
220
221 $this->expectException(\SqlFixture\Fixture\Exception\GeneratedColumnReferenceException::class);
222 $this->expectExceptionMessage('order.code is a generated column');
223
224 (new PlanSchemaValidator($resolver))->validate(FixturePlan::from('order.code < order_detail.order_code'));
225 }
226
227 #[Test]
228 public function testAGeneratedColumnCannotBeWrittenIntoEither(): void
229 {
230 $resolver = new StaticSchemaResolver([
231 new TableSchema('order', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)], ['id']),
232 new TableSchema('order_detail', [
233 'order_id' => new ColumnDefinition('order_id', 'INT', generated: true),
234 ]),
235 ]);
236
237 $this->expectException(\SqlFixture\Fixture\Exception\GeneratedColumnReferenceException::class);
238 $this->expectExceptionMessage('order_detail.order_id is a generated column');
239
240 (new PlanSchemaValidator($resolver))->validate(FixturePlan::from('order.id < order_detail.order_id'));
241 }
242}
243