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