packages/sql-fixture/tests/Unit/Fixture/PlanGeneratorTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Fixture;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Test;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use SqlFixture\Fixture\FixtureSet;
12use SqlFixture\Fixture\GenerationRun;
13use SqlFixture\Fixture\PlanGenerator;
14use SqlFixture\Fixture\PlanSchemaException;
15use SqlFixture\Fixture\PlanSchemaValidator;
16use SqlFixture\Fixture\RowSpec;
17use SqlFixture\Fixture\TableOverrides;
18use SqlFixture\FixtureGenerator;
19use SqlFixture\Plan\ColumnRef;
20use SqlFixture\Plan\FixturePlan;
21use SqlFixture\Plan\PlanParser;
22use SqlFixture\Plan\PlanPrinter;
23use SqlFixture\Plan\Relation;
24use SqlFixture\Plan\RelationKind;
25use SqlFixture\Plan\RelationSide;
26use SqlFixture\Platform\MySql\MySqlSchemaParser;
27use SqlFixture\Platform\MySql\MySqlTypeMapper;
28use SqlFixture\Schema\ColumnDefinition;
29use SqlFixture\Schema\SchemaNotFoundException;
30use SqlFixture\Schema\StaticSchemaResolver;
31use SqlFixture\Schema\TableSchema;
32
33#[CoversClass(PlanGenerator::class)]
34#[UsesClass(GenerationRun::class)]
35#[UsesClass(PlanSchemaValidator::class)]
36#[UsesClass(PlanSchemaException::class)]
37#[UsesClass(RowSpec::class)]
38#[UsesClass(FixtureSet::class)]
39#[UsesClass(TableOverrides::class)]
40#[UsesClass(FixtureGenerator::class)]
41#[UsesClass(FixturePlan::class)]
42#[UsesClass(PlanParser::class)]
43#[UsesClass(PlanPrinter::class)]
44#[UsesClass(Relation::class)]
45#[UsesClass(ColumnRef::class)]
46#[UsesClass(RelationKind::class)]
47#[UsesClass(RelationSide::class)]
48#[UsesClass(MySqlSchemaParser::class)]
49#[UsesClass(MySqlTypeMapper::class)]
50#[UsesClass(StaticSchemaResolver::class)]
51#[UsesClass(SchemaNotFoundException::class)]
52#[UsesClass(TableSchema::class)]
53#[UsesClass(ColumnDefinition::class)]
54#[CoversClass(\SqlFixture\Fixture\Generation\ConnectedTables::class)]
55#[CoversClass(\SqlFixture\Fixture\Generation\OverrideSpecs::class)]
56#[CoversClass(\SqlFixture\Fixture\Generation\RelationCounts::class)]
57#[CoversClass(\SqlFixture\Fixture\Generation\RelationProjection::class)]
58#[CoversClass(\SqlFixture\Fixture\Generation\RowMaterializer::class)]
59#[UsesClass(\SqlFixture\Fixture\OverrideRows::class)]
60#[UsesClass(\SqlFixture\Fixture\RowGeneration::class)]
61#[UsesClass(\SqlFixture\Hydrator\HydrationException::class)]
62#[UsesClass(\SqlFixture\Hydrator\HydratorInterface::class)]
63#[UsesClass(\SqlFixture\Hydrator\ReflectionHydrator::class)]
64#[UsesClass(\SqlFixture\Plan\PlanSyntaxException::class)]
65#[UsesClass(\SqlFixture\Schema\SchemaParseException::class)]
66#[UsesClass(\SqlFixture\Schema\SchemaParserInterface::class)]
67#[UsesClass(\SqlFixture\Schema\SchemaResolverInterface::class)]
68#[UsesClass(\SqlFixture\Schema\TableIdentifier::class)]
69#[UsesClass(\SqlFixture\TypeMapper\TypeMapperInterface::class)]
70#[UsesClass(\SqlFixture\Fixture\Validation\EndpointValidator::class)]
71#[UsesClass(\SqlFixture\Fixture\Validation\OverrideValidator::class)]
72#[UsesClass(\SqlFixture\Hydrator\Reflection\ConstructorHydration::class)]
73#[UsesClass(\SqlFixture\Hydrator\Reflection\PropertyHydration::class)]
74#[UsesClass(\SqlFixture\Hydrator\Reflection\PropertyNames::class)]
75#[UsesClass(\SqlFixture\Hydrator\Reflection\ValueConversion::class)]
76#[UsesClass(\SqlFixture\InvalidOverrideException::class)]
77#[UsesClass(\SqlFixture\Plan\Parsing\PlanStatements::class)]
78#[UsesClass(\SqlFixture\Plan\Parsing\RelationCursor::class)]
79#[UsesClass(\SqlFixture\Plan\Parsing\RelationReader::class)]
80#[UsesClass(\SqlFixture\Plan\PlanStructureException::class)]
81#[UsesClass(\SqlFixture\Plan\Printing\PlanTables::class)]
82#[UsesClass(\SqlFixture\Plan\Printing\RelationGroups::class)]
83#[UsesClass(\SqlFixture\Plan\Printing\StatementPrinter::class)]
84#[UsesClass(\SqlFixture\Plan\Validation\PlanValidation::class)]
85#[UsesClass(\SqlFixture\Plan\Validation\TableName::class)]
86#[UsesClass(\SqlFixture\Platform\MySql\Schema\ColumnParser::class)]
87#[UsesClass(\SqlFixture\Platform\MySql\Schema\DefaultExpression::class)]
88#[UsesClass(\SqlFixture\Platform\MySql\Schema\DefinitionIntegrity::class)]
89#[UsesClass(\SqlFixture\Platform\MySql\Schema\TableDefinition::class)]
90#[UsesClass(\SqlFixture\Platform\MySql\Schema\TypeParameters::class)]
91#[UsesClass(\SqlFixture\Platform\MySql\Value\ColumnGenerator::class)]
92#[UsesClass(\SqlFixture\Platform\MySql\Value\DecimalGenerator::class)]
93#[UsesClass(\SqlFixture\Platform\MySql\Value\GeometryGenerator::class)]
94#[UsesClass(\SqlFixture\Platform\MySql\Value\IntegerGenerator::class)]
95#[UsesClass(\SqlFixture\Platform\MySql\Value\NumericGenerator::class)]
96#[UsesClass(\SqlFixture\Platform\MySql\Value\SpatialGenerator::class)]
97#[UsesClass(\SqlFixture\Platform\MySql\Value\StringGenerator::class)]
98#[UsesClass(\SqlFixture\Platform\MySql\Value\TextGenerator::class)]
99#[UsesClass(\SqlFixture\Schema\TypeShape::class)]
100#[UsesClass(\SqlFixture\TypeMapper\ParagraphGenerator::class)]
101#[\PHPUnit\Framework\Attributes\Medium]
102#[UsesClass(\SqlFixture\Platform\MySql\Schema\TableDefinitionInput::class)]
103#[UsesClass(\SqlFixture\Hydrator\Exception\ClassNotFoundException::class)]
104#[UsesClass(\SqlFixture\Hydrator\Exception\MissingConstructorArgumentException::class)]
105#[UsesClass(\SqlFixture\Fixture\Exception\UnknownOverrideColumnException::class)]
106#[UsesClass(\SqlFixture\Fixture\Exception\NullOverrideException::class)]
107#[UsesClass(\SqlFixture\Fixture\Exception\GeneratedColumnOverrideException::class)]
108#[UsesClass(\SqlFixture\Fixture\Exception\GeneratedColumnReferenceException::class)]
109#[UsesClass(\SqlFixture\Fixture\Exception\MissingRelationValueException::class)]
110#[UsesClass(\SqlFixture\Fixture\Exception\UnknownPlanColumnException::class)]
111#[UsesClass(\SqlFixture\Plan\Exception\EmptyPlanException::class)]
112#[UsesClass(\SqlFixture\Plan\Exception\EmptyTableNameException::class)]
113#[UsesClass(\SqlFixture\Plan\Exception\MissingEndpointColumnsException::class)]
114#[UsesClass(\SqlFixture\Plan\Exception\InvalidTableNameException::class)]
115#[UsesClass(\SqlFixture\Plan\Exception\UnbalancedBracketsException::class)]
116#[UsesClass(\SqlFixture\Plan\Exception\UnexpectedPlanTokenException::class)]
117#[UsesClass(\SqlFixture\Plan\Exception\UnsupportedManyToManyException::class)]
118#[UsesClass(\SqlFixture\Plan\Exception\CompositeArityMismatchException::class)]
119#[UsesClass(\SqlFixture\Plan\Exception\DuplicateColumnBindingException::class)]
120#[UsesClass(\SqlFixture\Plan\Exception\CyclicDependencyException::class)]
121#[UsesClass(\SqlFixture\Plan\Exception\UnboundedSelfReferenceException::class)]
122#[UsesClass(\SqlFixture\Schema\Exception\InvalidSqlException::class)]
123#[UsesClass(\SqlFixture\Schema\Exception\ExpectedCreateTableException::class)]
124#[UsesClass(\SqlFixture\Schema\Exception\MissingColumnDefinitionsException::class)]
125#[UsesClass(\SqlFixture\TypeMapper\IntegerRange::class)]
126#[UsesClass(\SqlFixture\TypeMapper\IntegerWidth::class)]
127#[UsesClass(\SqlFixture\TypeMapper\DecimalRange::class)]
128#[UsesClass(\SqlFixture\Hydrator\Reflection\ConversionTarget::class)]
129final class PlanGeneratorTest extends TestCase
130{
131 #[Test]
132 public function testGenerateTheTableThePlanIsAboutGetsOneRow(): void
133 {
134 $schemas = new StaticSchemaResolver([
135 new TableSchema('order', [
136 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
137 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
138 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
139 'total' => new ColumnDefinition('total', 'INT', nullable: false),
140 ], ['id']),
141 new TableSchema('order_detail', [
142 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
143 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
144 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
145 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
146 ], ['id']),
147 ]);
148 $faker = \Faker\Factory::create();
149 $faker->seed(20260101);
150 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
151 $set = $generator->generate(
152 FixturePlan::from('order.id < order_detail.order_id')
153 );
154
155 self::assertArrayHasKey('id', (array) $set->row('order'));
156 }
157
158 #[Test]
159 public function testTheSubjectIsOneRowEvenWhenSomethingReferencesIt(): void
160 {
161 $schemas = new StaticSchemaResolver([
162 new TableSchema('customer', [
163 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
164 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
165 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
166 ], ['id']),
167 new TableSchema('order', [
168 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
169 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
170 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
171 'total' => new ColumnDefinition('total', 'INT', nullable: false),
172 ], ['id']),
173 new TableSchema('order_detail', [
174 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
175 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
176 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
177 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
178 ], ['id']),
179 ]);
180 $faker = \Faker\Factory::create();
181 $faker->seed(20260101);
182 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
183 $set = $generator->generate(
184 FixturePlan::from('order.id < order_detail.order_id, order.customer_id > customer.id')
185 );
186
187 self::assertArrayHasKey('id', (array) $set->row('order'));
188 self::assertArrayHasKey('id', (array) $set->row('customer'));
189 }
190
191 #[Test]
192 public function testChildRowsCarryTheParentKey(): void
193 {
194 $schemas = new StaticSchemaResolver([
195 new TableSchema('order', [
196 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
197 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
198 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
199 'total' => new ColumnDefinition('total', 'INT', nullable: false),
200 ], ['id']),
201 new TableSchema('order_detail', [
202 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
203 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
204 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
205 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
206 ], ['id']),
207 ]);
208 $faker = \Faker\Factory::create();
209 $faker->seed(20260101);
210 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
211 $set = $generator->generate(
212 FixturePlan::from('order.id < order_detail.order_id'),
213 ['order' => ['id' => 100], 'order_detail' => 3]
214 );
215
216 self::assertCount(3, $set->rows('order_detail'));
217 self::assertSame([100, 100, 100], array_column($set->rows('order_detail'), 'order_id'));
218 }
219
220 #[Test]
221 public function testAParentIsGeneratedAndLinkedTo(): void
222 {
223 $schemas = new StaticSchemaResolver([
224 new TableSchema('customer', [
225 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
226 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
227 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
228 ], ['id']),
229 new TableSchema('order', [
230 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
231 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
232 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
233 'total' => new ColumnDefinition('total', 'INT', nullable: false),
234 ], ['id']),
235 ]);
236 $faker = \Faker\Factory::create();
237 $faker->seed(20260101);
238 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
239 $set = $generator->generate(
240 FixturePlan::from('order.customer_id > customer.id')
241 );
242
243 self::assertSame($set->rows('customer')[0]['id'], $set->rows('order')[0]['customer_id']);
244 }
245
246 #[Test]
247 public function testACountOfZeroGeneratesNoRows(): void
248 {
249 $schemas = new StaticSchemaResolver([
250 new TableSchema('order', [
251 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
252 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
253 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
254 'total' => new ColumnDefinition('total', 'INT', nullable: false),
255 ], ['id']),
256 new TableSchema('order_detail', [
257 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
258 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
259 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
260 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
261 ], ['id']),
262 ]);
263 $faker = \Faker\Factory::create();
264 $faker->seed(20260101);
265 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
266 $set = $generator->generate(
267 FixturePlan::from('order.id < order_detail.order_id'),
268 ['order_detail' => []]
269 );
270
271 self::assertSame([], $set->rows('order_detail'));
272 }
273
274 #[Test]
275 public function testOneSetOfValuesAppliesToEveryGeneratedRow(): void
276 {
277 $schemas = new StaticSchemaResolver([
278 new TableSchema('order', [
279 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
280 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
281 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
282 'total' => new ColumnDefinition('total', 'INT', nullable: false),
283 ], ['id']),
284 new TableSchema('order_detail', [
285 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
286 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
287 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
288 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
289 ], ['id']),
290 ]);
291 $faker = \Faker\Factory::create();
292 $faker->seed(20260101);
293 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
294 $set = $generator->generate(
295 FixturePlan::from('order.id < order_detail.order_id'),
296 ['order_detail' => ['quantity' => 2]]
297 );
298
299 self::assertNotSame([], $set->rows('order_detail'));
300 $quantities = array_column($set->rows('order_detail'), 'quantity');
301
302 self::assertSame([2], array_values(array_unique($quantities, SORT_REGULAR)));
303 }
304
305 #[Test]
306 public function testAListGivesOneRowPerEntry(): void
307 {
308 $schemas = new StaticSchemaResolver([
309 new TableSchema('order', [
310 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
311 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
312 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
313 'total' => new ColumnDefinition('total', 'INT', nullable: false),
314 ], ['id']),
315 new TableSchema('order_detail', [
316 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
317 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
318 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
319 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
320 ], ['id']),
321 ]);
322 $faker = \Faker\Factory::create();
323 $faker->seed(20260101);
324 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
325 $set = $generator->generate(
326 FixturePlan::from('order.id < order_detail.order_id'),
327 ['order_detail' => [['quantity' => 1], ['quantity' => 2]]]
328 );
329
330 self::assertSame([1, 2], array_column($set->rows('order_detail'), 'quantity'));
331 }
332
333 #[Test]
334 public function testAnUnmentionedChildIsStillGenerated(): void
335 {
336 $schemas = new StaticSchemaResolver([
337 new TableSchema('order', [
338 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
339 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
340 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
341 'total' => new ColumnDefinition('total', 'INT', nullable: false),
342 ], ['id']),
343 new TableSchema('order_detail', [
344 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
345 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
346 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
347 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
348 ], ['id']),
349 ]);
350 $faker = \Faker\Factory::create();
351 $faker->seed(20260101);
352 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
353 $set = $generator->generate(
354 FixturePlan::from('order.id < order_detail.order_id')
355 );
356
357 self::assertGreaterThanOrEqual(1, count($set->rows('order_detail')));
358 }
359
360 #[Test]
361 public function testAnOptionalChildMayGenerateNoneAtAll(): void
362 {
363 $schemas = new StaticSchemaResolver([
364 new TableSchema('order', [
365 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
366 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
367 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
368 'total' => new ColumnDefinition('total', 'INT', nullable: false),
369 ], ['id']),
370 new TableSchema('order_detail', [
371 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
372 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
373 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
374 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
375 ], ['id']),
376 ]);
377 $faker = \Faker\Factory::create();
378 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
379 $counts = array_map(static function (int $seed) use ($faker, $generator): int {
380 $faker->seed($seed);
381 return count($generator->generate(FixturePlan::from('order.id <? order_detail.order_id'))->rows('order_detail'));
382 }, range(1, 40));
383
384 self::assertContains(0, $counts);
385 }
386
387 #[Test]
388 #[\PHPUnit\Framework\Attributes\DataProvider('providerGenerationSeeds')]
389 public function testARequiredChildNeverGeneratesNone(int $seed): void
390 {
391 $schemas = new StaticSchemaResolver([
392 new TableSchema('order', [
393 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
394 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
395 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
396 'total' => new ColumnDefinition('total', 'INT', nullable: false),
397 ], ['id']),
398 new TableSchema('order_detail', [
399 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
400 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
401 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
402 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
403 ], ['id']),
404 ]);
405 $faker = \Faker\Factory::create();
406 $faker->seed($seed);
407 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
408 $set = $generator->generate(FixturePlan::from('order.id < order_detail.order_id'));
409
410 self::assertGreaterThanOrEqual(1, count($set->rows('order_detail')));
411 }
412
413 #[Test]
414 public function testAOneToOneChildIsASingleRow(): void
415 {
416 $schemas = new StaticSchemaResolver([
417 new TableSchema('order', [
418 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
419 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
420 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
421 'total' => new ColumnDefinition('total', 'INT', nullable: false),
422 ], ['id']),
423 new TableSchema('order_shipping', [
424 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
425 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
426 'carrier' => new ColumnDefinition('carrier', 'VARCHAR', nullable: false, length: 50),
427 ], ['id']),
428 ]);
429 $faker = \Faker\Factory::create();
430 $faker->seed(20260101);
431 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
432 $set = $generator->generate(
433 FixturePlan::from('order.id - order_shipping.order_id'),
434 ['order' => ['id' => 5]]
435 );
436
437 self::assertSame(5, $set->rows('order_shipping')[0]['order_id']);
438 }
439
440 #[Test]
441 public function testAnOptionalOneToOneAskedForNoneIsNull(): void
442 {
443 $schemas = new StaticSchemaResolver([
444 new TableSchema('order', [
445 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
446 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
447 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
448 'total' => new ColumnDefinition('total', 'INT', nullable: false),
449 ], ['id']),
450 new TableSchema('order_shipping', [
451 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
452 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
453 'carrier' => new ColumnDefinition('carrier', 'VARCHAR', nullable: false, length: 50),
454 ], ['id']),
455 ]);
456 $faker = \Faker\Factory::create();
457 $faker->seed(20260101);
458 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
459 $set = $generator->generate(
460 FixturePlan::from('order.id -? order_shipping.order_id'),
461 ['order_shipping' => []]
462 );
463
464 self::assertNull($set->row('order_shipping'));
465 }
466
467 #[Test]
468 public function testEveryChildOfAListGetsItsOwnParent(): void
469 {
470 $schemas = new StaticSchemaResolver([
471 new TableSchema('order', [
472 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
473 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
474 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
475 'total' => new ColumnDefinition('total', 'INT', nullable: false),
476 ], ['id']),
477 new TableSchema('order_detail', [
478 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
479 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
480 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
481 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
482 ], ['id']),
483 new TableSchema('product', [
484 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
485 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
486 ], ['id']),
487 ]);
488 $faker = \Faker\Factory::create();
489 $faker->seed(20260101);
490 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
491 $set = $generator->generate(
492 FixturePlan::from('order.id < order_detail.order_id, order_detail.product_id > product.id'),
493 ['order_detail' => 3]
494 );
495
496 self::assertCount(3, $set->rows('product'));
497 self::assertSame(
498 array_column($set->rows('product'), 'id'),
499 array_column($set->rows('order_detail'), 'product_id')
500 );
501 }
502
503 #[Test]
504 public function testFixingTheLinkingColumnLeavesTheParentAlone(): void
505 {
506 $schemas = new StaticSchemaResolver([
507 new TableSchema('customer', [
508 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
509 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
510 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
511 ], ['id']),
512 new TableSchema('order', [
513 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
514 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
515 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
516 'total' => new ColumnDefinition('total', 'INT', nullable: false),
517 ], ['id']),
518 ]);
519 $faker = \Faker\Factory::create();
520 $faker->seed(20260101);
521 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
522 $set = $generator->generate(
523 FixturePlan::from('order.customer_id > customer.id'),
524 ['order' => ['customer_id' => 77]]
525 );
526
527 self::assertSame(77, $set->rows('order')[0]['customer_id']);
528 self::assertNull($set->row('customer'));
529 }
530
531 #[Test]
532 public function testTablesThatStandAloneAreGeneratedToo(): void
533 {
534 $schemas = new StaticSchemaResolver([
535 new TableSchema('order', [
536 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
537 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
538 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
539 'total' => new ColumnDefinition('total', 'INT', nullable: false),
540 ], ['id']),
541 new TableSchema('order_detail', [
542 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
543 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
544 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
545 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
546 ], ['id']),
547 new TableSchema('audit_log', [
548 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
549 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
550 'message' => new ColumnDefinition('message', 'VARCHAR', nullable: false, length: 255),
551 ], ['id']),
552 ]);
553 $faker = \Faker\Factory::create();
554 $faker->seed(20260101);
555 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
556 $set = $generator->generate(
557 FixturePlan::from('order.id < order_detail.order_id, audit_log')
558 );
559
560 self::assertArrayHasKey('message', (array) $set->row('audit_log'));
561 }
562
563 #[Test]
564 public function testEntriesComeBackInTheOrderThePlanNamesThem(): void
565 {
566 $schemas = new StaticSchemaResolver([
567 new TableSchema('customer', [
568 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
569 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
570 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
571 ], ['id']),
572 new TableSchema('order', [
573 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
574 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
575 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
576 'total' => new ColumnDefinition('total', 'INT', nullable: false),
577 ], ['id']),
578 new TableSchema('order_detail', [
579 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
580 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
581 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
582 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
583 ], ['id']),
584 ]);
585 $faker = \Faker\Factory::create();
586 $faker->seed(20260101);
587 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
588 $set = $generator->generate(
589 FixturePlan::from('order.id < order_detail.order_id, order.customer_id > customer.id'),
590 ['order' => ['id' => 1], 'order_detail' => 2]
591 );
592
593 [$order, $details, $customer] = $set;
594
595 self::assertSame(1, ((array) $order)['id']);
596 self::assertCount(2, (array) $details);
597 self::assertArrayHasKey('tier', (array) $customer);
598 }
599
600 #[Test]
601 public function testOverridesMayBeGivenAsTableOverrides(): void
602 {
603 $schemas = new StaticSchemaResolver([
604 new TableSchema('order', [
605 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
606 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
607 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
608 'total' => new ColumnDefinition('total', 'INT', nullable: false),
609 ], ['id']),
610 new TableSchema('order_detail', [
611 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
612 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
613 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
614 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
615 ], ['id']),
616 ]);
617 $faker = \Faker\Factory::create();
618 $faker->seed(20260101);
619 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
620 $set = $generator->generate(
621 FixturePlan::from('order.id < order_detail.order_id'),
622 ['order' => TableOverrides::of(['status' => 'paid'])]
623 );
624
625 self::assertSame('paid', $set->rows('order')[0]['status']);
626 }
627
628 #[Test]
629 public function testAnUnknownTableIsReported(): void
630 {
631 $schemas = new StaticSchemaResolver([
632 ]);
633 $faker = \Faker\Factory::create();
634 $faker->seed(20260101);
635 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
636 $this->expectException(SchemaNotFoundException::class);
637 $this->expectExceptionMessage('Schema not found for table: nope');
638
639 $generator->generate(FixturePlan::from('nope'));
640 }
641
642 #[Test]
643 public function testAPlanNamingAColumnTheTableLacksIsRejectedBeforeGenerating(): void
644 {
645 $schemas = new StaticSchemaResolver([
646 new TableSchema('order', [
647 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
648 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
649 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
650 'total' => new ColumnDefinition('total', 'INT', nullable: false),
651 ], ['id']),
652 new TableSchema('order_detail', [
653 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
654 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
655 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
656 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
657 ], ['id']),
658 ]);
659 $faker = \Faker\Factory::create();
660 $faker->seed(20260101);
661 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
662 $this->expectException(\SqlFixture\Fixture\Exception\UnknownPlanColumnException::class);
663 $this->expectExceptionMessage('order_detail has no column oder_id');
664
665 $generator->generate(FixturePlan::from('order.id < order_detail.oder_id'));
666 }
667
668 #[Test]
669 public function testTheSubjectHonoursACountItWasGiven(): void
670 {
671 $schemas = new StaticSchemaResolver([
672 new TableSchema('order', [
673 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
674 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
675 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
676 'total' => new ColumnDefinition('total', 'INT', nullable: false),
677 ], ['id']),
678 new TableSchema('order_detail', [
679 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
680 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
681 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
682 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
683 ], ['id']),
684 ]);
685 $faker = \Faker\Factory::create();
686 $faker->seed(20260101);
687 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
688 $set = $generator->generate(
689 FixturePlan::from('order.id < order_detail.order_id'),
690 ['order' => 2, 'order_detail' => 1]
691 );
692
693 self::assertCount(2, $set->rows('order'));
694 }
695
696 #[Test]
697 public function testARowCarriesBothItsInheritedKeyAndItsOwnParentKey(): void
698 {
699 $schemas = new StaticSchemaResolver([
700 new TableSchema('order', [
701 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
702 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
703 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
704 'total' => new ColumnDefinition('total', 'INT', nullable: false),
705 ], ['id']),
706 new TableSchema('order_detail', [
707 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
708 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
709 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
710 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
711 ], ['id']),
712 new TableSchema('product', [
713 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
714 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
715 ], ['id']),
716 ]);
717 $faker = \Faker\Factory::create();
718 $faker->seed(20260101);
719 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
720 $set = $generator->generate(
721 FixturePlan::from('order.id < order_detail.order_id, order_detail.product_id > product.id'),
722 ['order' => ['id' => 42], 'order_detail' => 1]
723 );
724
725 $detail = $set->rows('order_detail')[0];
726
727 self::assertSame(42, $detail['order_id']);
728 self::assertSame($set->rows('product')[0]['id'], $detail['product_id']);
729 }
730
731 #[Test]
732 public function testEveryOtherRelationOfAParentIsStillFollowed(): void
733 {
734 $schemas = new StaticSchemaResolver([
735 new TableSchema('customer', [
736 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
737 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
738 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
739 ], ['id']),
740 new TableSchema('order', [
741 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
742 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
743 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
744 'total' => new ColumnDefinition('total', 'INT', nullable: false),
745 ], ['id']),
746 new TableSchema('audit_log', [
747 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
748 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
749 'message' => new ColumnDefinition('message', 'VARCHAR', nullable: false, length: 255),
750 ], ['id']),
751 ]);
752 $faker = \Faker\Factory::create();
753 $faker->seed(20260101);
754 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
755 $set = $generator->generate(
756 FixturePlan::from('order.customer_id > customer.id, customer.id < audit_log.customer_id')
757 );
758
759 self::assertNotSame([], $set->rows('audit_log'));
760 }
761
762 #[Test]
763 public function testAnOptionalParentIsGeneratedWhenTheCallerAsksForIt(): void
764 {
765 $schemas = new StaticSchemaResolver([
766 new TableSchema('customer', [
767 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
768 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
769 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
770 ], ['id']),
771 new TableSchema('order', [
772 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
773 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
774 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
775 'total' => new ColumnDefinition('total', 'INT', nullable: false),
776 ], ['id']),
777 ]);
778 $faker = \Faker\Factory::create();
779 $faker->seed(20260101);
780 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
781 $set = $generator->generate(
782 FixturePlan::from('order.customer_id >? customer.id'),
783 ['customer' => ['tier' => 'gold']]
784 );
785
786 self::assertSame('gold', $set->rows('customer')[0]['tier']);
787 self::assertSame($set->rows('customer')[0]['id'], $set->rows('order')[0]['customer_id']);
788 }
789
790 #[Test]
791 public function testACompositeRelationCarriesEveryColumn(): void
792 {
793 $schemas = new StaticSchemaResolver([
794 new TableSchema('shop_order', [
795 'shop_id' => new ColumnDefinition('shop_id', 'INT', nullable: false, unsigned: true),
796 'no' => new ColumnDefinition('no', 'INT', nullable: false, unsigned: true),
797 ], ['shop_id', 'no']),
798 new TableSchema('shop_order_line', [
799 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
800 'shop_id' => new ColumnDefinition('shop_id', 'INT', nullable: false, unsigned: true),
801 'order_no' => new ColumnDefinition('order_no', 'INT', nullable: false, unsigned: true),
802 ], ['id']),
803 ]);
804 $faker = \Faker\Factory::create();
805 $faker->seed(20260101);
806 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
807 $set = $generator->generate(
808 FixturePlan::from('shop_order.(shop_id, no) < shop_order_line.(shop_id, order_no)'),
809 ['shop_order' => ['shop_id' => 7, 'no' => 9], 'shop_order_line' => 1]
810 );
811
812 $line = $set->rows('shop_order_line')[0];
813
814 self::assertSame(7, $line['shop_id']);
815 self::assertSame(9, $line['order_no']);
816 }
817
818 #[Test]
819 public function testAOneToOneChildIsGeneratedExactlyOnce(): void
820 {
821 $schemas = new StaticSchemaResolver([
822 new TableSchema('order', [
823 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
824 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
825 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
826 'total' => new ColumnDefinition('total', 'INT', nullable: false),
827 ], ['id']),
828 new TableSchema('order_shipping', [
829 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
830 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
831 'carrier' => new ColumnDefinition('carrier', 'VARCHAR', nullable: false, length: 50),
832 ], ['id']),
833 ]);
834 $faker = \Faker\Factory::create();
835 $faker->seed(20260101);
836 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
837 $set = $generator->generate(
838 FixturePlan::from('order.id - order_shipping.order_id')
839 );
840
841 self::assertCount(1, $set->rows('order_shipping'));
842 }
843
844 #[Test]
845 public function testKeysAreStoodInForEveryColumnARelationReads(): void
846 {
847 $schemas = new StaticSchemaResolver([
848 new TableSchema('twin', [
849 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
850 'other_id' => new ColumnDefinition('other_id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
851 ], ['id']),
852 new TableSchema('twin_child', [
853 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
854 'twin_id' => new ColumnDefinition('twin_id', 'INT', nullable: false, unsigned: true),
855 ], ['id']),
856 new TableSchema('twin_other', [
857 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
858 'twin_other_id' => new ColumnDefinition('twin_other_id', 'INT', nullable: false, unsigned: true),
859 ], ['id']),
860 ]);
861 $faker = \Faker\Factory::create();
862 $faker->seed(20260101);
863 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
864 $set = $generator->generate(
865 FixturePlan::from('twin.id < twin_child.twin_id, twin.other_id < twin_other.twin_other_id')
866 );
867
868 self::assertSame(1, $set->rows('twin')[0]['id']);
869 self::assertSame(1, $set->rows('twin')[0]['other_id']);
870 }
871
872 #[Test]
873 public function testARequiredParentIsGeneratedWhetherOrNotItWasAskedFor(): void
874 {
875 $schemas = new StaticSchemaResolver([
876 new TableSchema('customer', [
877 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
878 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
879 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
880 ], ['id']),
881 new TableSchema('order', [
882 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
883 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
884 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
885 'total' => new ColumnDefinition('total', 'INT', nullable: false),
886 ], ['id']),
887 ]);
888 $faker = \Faker\Factory::create();
889 $faker->seed(20260101);
890 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
891 $set = $generator->generate(FixturePlan::from('order.customer_id > customer.id'));
892
893 self::assertNotSame([], $set->rows('customer'));
894 }
895
896 #[Test]
897 public function testAnOptionalParentNobodyAskedForIsLeftOut(): void
898 {
899 $schemas = new StaticSchemaResolver([
900 new TableSchema('customer', [
901 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
902 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
903 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
904 ], ['id']),
905 new TableSchema('order', [
906 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
907 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
908 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
909 'total' => new ColumnDefinition('total', 'INT', nullable: false),
910 ], ['id']),
911 ]);
912 $faker = \Faker\Factory::create();
913 $faker->seed(20260101);
914 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
915 $set = $generator->generate(FixturePlan::from('order.customer_id >? customer.id'));
916
917 self::assertSame([], $set->rows('customer'));
918 }
919
920 #[Test]
921 public function testAnUnboundedChildCountCanExceedItsMinimum(): void
922 {
923 $schemas = new StaticSchemaResolver([
924 new TableSchema('order', [
925 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
926 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
927 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
928 'total' => new ColumnDefinition('total', 'INT', nullable: false),
929 ], ['id']),
930 new TableSchema('order_detail', [
931 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
932 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
933 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
934 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
935 ], ['id']),
936 ]);
937 $faker = \Faker\Factory::create();
938 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
939 $counts = array_map(static function (int $seed) use ($faker, $generator): int {
940 $faker->seed($seed);
941 return count($generator->generate(FixturePlan::from('order.id < order_detail.order_id'))->rows('order_detail'));
942 }, range(1, 40));
943
944 self::assertGreaterThan(1, max($counts));
945 }
946
947 #[Test]
948 #[\PHPUnit\Framework\Attributes\DataProvider('providerGenerationSeeds')]
949 public function testAOneToOneChildIsNeverGeneratedMoreThanOnce(int $seed): void
950 {
951 $schemas = new StaticSchemaResolver([
952 new TableSchema('order', [
953 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
954 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
955 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
956 'total' => new ColumnDefinition('total', 'INT', nullable: false),
957 ], ['id']),
958 new TableSchema('order_shipping', [
959 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
960 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
961 'carrier' => new ColumnDefinition('carrier', 'VARCHAR', nullable: false, length: 50),
962 ], ['id']),
963 ]);
964 $faker = \Faker\Factory::create();
965 $faker->seed($seed);
966 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
967 $set = $generator->generate(FixturePlan::from('order.id - order_shipping.order_id'));
968
969 self::assertCount(1, $set->rows('order_shipping'));
970 }
971
972 #[Test]
973 public function testATableReachableOnlyBackwardsAlongARelationIsStillPartOfTheWalk(): void
974 {
975 $schemas = new StaticSchemaResolver([
976 new TableSchema('order', [
977 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
978 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
979 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
980 'total' => new ColumnDefinition('total', 'INT', nullable: false),
981 ], ['id']),
982 new TableSchema('order_detail', [
983 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
984 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
985 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
986 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
987 ], ['id']),
988 new TableSchema('product', [
989 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
990 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
991 ], ['id']),
992 ]);
993 $faker = \Faker\Factory::create();
994 $faker->seed(20260101);
995 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
996 $set = $generator->generate(
997 FixturePlan::from('order.id < order_detail.order_id, product.id ?< order_detail.product_id')
998 );
999
1000 self::assertSame([], $set->rows('product'));
1001 }
1002 /**
1003 * @return list<array{int}>
1004 */
1005 public static function providerGenerationSeeds(): array
1006 {
1007 return array_map(static fn (int $seed): array => [$seed], range(1, 40));
1008 }
1009}
1010