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\Plan\ColumnRef;
19use SqlFixture\Plan\FixturePlan;
20use SqlFixture\Plan\PlanParser;
21use SqlFixture\Plan\PlanPrinter;
22use SqlFixture\Plan\Relation;
23use SqlFixture\Plan\RelationKind;
24use SqlFixture\Plan\RelationSide;
25use SqlFixture\Platform\MySql\MySqlSchemaParser;
26use SqlFixture\Platform\MySql\MySqlTypeMapper;
27use SqlFixture\Provider\FixtureGenerator;
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#[CoversClass(PlanSchemaValidator::class)]
36#[UsesClass(PlanSchemaException::class)]
37#[UsesClass(RowSpec::class)]
38#[UsesClass(FixtureSet::class)]
39#[UsesClass(TableOverrides::class)]
40#[UsesClass(FixtureGenerator::class)]
41#[CoversClass(FixturePlan::class)]
42#[CoversClass(PlanParser::class)]
43#[CoversClass(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\Fixture\Exception\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)]
129#[CoversClass(\SqlFixture\Fixture\Choice\CaseSelection::class)]
130#[CoversClass(\SqlFixture\Fixture\Choice\ChoiceBindings::class)]
131#[CoversClass(\SqlFixture\Fixture\Choice\InverseRelations::class)]
132#[CoversClass(\SqlFixture\Fixture\Choice\ResolvedRow::class)]
133#[CoversClass(\SqlFixture\Fixture\Choice\RowChoices::class)]
134#[UsesClass(\SqlFixture\Plan\Choice\ChoiceCase::class)]
135#[UsesClass(\SqlFixture\Plan\Choice\ChoiceDefinitionException::class)]
136#[UsesClass(\SqlFixture\Plan\Choice\ChoiceLiteral::class)]
137#[UsesClass(\SqlFixture\Plan\Choice\ChoiceSyntax::class)]
138#[UsesClass(\SqlFixture\Plan\Choice\ChoiceValidation::class)]
139#[UsesClass(\SqlFixture\Plan\Choice\PlanContents::class)]
140#[UsesClass(\SqlFixture\Plan\RelationChoice::class)]
141#[CoversClass(\SqlFixture\Fixture\Generation\RowBindings::class)]
142#[UsesClass(\SqlFixture\Fixture\Generation\RelationValueException::class)]
143#[UsesClass(\SqlFixture\Fixture\Generation\RecursiveRelationException::class)]
144#[UsesClass(\SqlFixture\Fixture\RowGenerator::class)]
145final class PlanGeneratorTest extends TestCase
146{
147 #[Test]
148 public function testGenerateTheTableThePlanIsAboutGetsOneRow(): void
149 {
150 $schemas = new StaticSchemaResolver([
151 new TableSchema('order', [
152 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
153 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
154 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
155 'total' => new ColumnDefinition('total', 'INT', nullable: false),
156 ], ['id']),
157 new TableSchema('order_detail', [
158 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
159 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
160 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
161 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
162 ], ['id']),
163 ]);
164 $faker = \Faker\Factory::create();
165 $faker->seed(20260101);
166 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
167 $set = $generator->generate(
168 FixturePlan::from('order.id < order_detail.order_id')
169 );
170
171 self::assertArrayHasKey('id', (array) $set->row('order'));
172 }
173
174 #[Test]
175 public function testTheSubjectIsOneRowEvenWhenSomethingReferencesIt(): void
176 {
177 $schemas = new StaticSchemaResolver([
178 new TableSchema('customer', [
179 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
180 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
181 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
182 ], ['id']),
183 new TableSchema('order', [
184 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
185 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
186 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
187 'total' => new ColumnDefinition('total', 'INT', nullable: false),
188 ], ['id']),
189 new TableSchema('order_detail', [
190 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
191 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
192 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
193 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
194 ], ['id']),
195 ]);
196 $faker = \Faker\Factory::create();
197 $faker->seed(20260101);
198 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
199 $set = $generator->generate(
200 FixturePlan::from('order.id < order_detail.order_id, order.customer_id > customer.id')
201 );
202
203 self::assertArrayHasKey('id', (array) $set->row('order'));
204 self::assertArrayHasKey('id', (array) $set->row('customer'));
205 }
206
207 #[Test]
208 public function testChildRowsCarryTheParentKey(): void
209 {
210 $schemas = new StaticSchemaResolver([
211 new TableSchema('order', [
212 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
213 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
214 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
215 'total' => new ColumnDefinition('total', 'INT', nullable: false),
216 ], ['id']),
217 new TableSchema('order_detail', [
218 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
219 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
220 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
221 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
222 ], ['id']),
223 ]);
224 $faker = \Faker\Factory::create();
225 $faker->seed(20260101);
226 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
227 $set = $generator->generate(
228 FixturePlan::from('order.id < order_detail.order_id'),
229 ['order' => ['id' => 100], 'order_detail' => 3]
230 );
231
232 self::assertCount(3, $set->rows('order_detail'));
233 self::assertSame([100, 100, 100], array_column($set->rows('order_detail'), 'order_id'));
234 }
235
236 #[Test]
237 public function testAParentIsGeneratedAndLinkedTo(): void
238 {
239 $schemas = new StaticSchemaResolver([
240 new TableSchema('customer', [
241 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
242 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
243 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
244 ], ['id']),
245 new TableSchema('order', [
246 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
247 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
248 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
249 'total' => new ColumnDefinition('total', 'INT', nullable: false),
250 ], ['id']),
251 ]);
252 $faker = \Faker\Factory::create();
253 $faker->seed(20260101);
254 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
255 $set = $generator->generate(
256 FixturePlan::from('order.customer_id > customer.id')
257 );
258
259 self::assertSame($set->rows('customer')[0]['id'], $set->rows('order')[0]['customer_id']);
260 }
261
262 #[Test]
263 public function testACountOfZeroGeneratesNoRows(): void
264 {
265 $schemas = new StaticSchemaResolver([
266 new TableSchema('order', [
267 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
268 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
269 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
270 'total' => new ColumnDefinition('total', 'INT', nullable: false),
271 ], ['id']),
272 new TableSchema('order_detail', [
273 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
274 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
275 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
276 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
277 ], ['id']),
278 ]);
279 $faker = \Faker\Factory::create();
280 $faker->seed(20260101);
281 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
282 $set = $generator->generate(
283 FixturePlan::from('order.id < order_detail.order_id'),
284 ['order_detail' => []]
285 );
286
287 self::assertSame([], $set->rows('order_detail'));
288 }
289
290 #[Test]
291 public function testOneSetOfValuesAppliesToEveryGeneratedRow(): void
292 {
293 $schemas = new StaticSchemaResolver([
294 new TableSchema('order', [
295 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
296 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
297 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
298 'total' => new ColumnDefinition('total', 'INT', nullable: false),
299 ], ['id']),
300 new TableSchema('order_detail', [
301 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
302 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
303 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
304 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
305 ], ['id']),
306 ]);
307 $faker = \Faker\Factory::create();
308 $faker->seed(20260101);
309 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
310 $set = $generator->generate(
311 FixturePlan::from('order.id < order_detail.order_id'),
312 ['order_detail' => ['quantity' => 2]]
313 );
314
315 self::assertNotSame([], $set->rows('order_detail'));
316 $quantities = array_column($set->rows('order_detail'), 'quantity');
317
318 self::assertSame([2], array_values(array_unique($quantities, SORT_REGULAR)));
319 }
320
321 #[Test]
322 public function testAListGivesOneRowPerEntry(): void
323 {
324 $schemas = new StaticSchemaResolver([
325 new TableSchema('order', [
326 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
327 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
328 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
329 'total' => new ColumnDefinition('total', 'INT', nullable: false),
330 ], ['id']),
331 new TableSchema('order_detail', [
332 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
333 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
334 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
335 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
336 ], ['id']),
337 ]);
338 $faker = \Faker\Factory::create();
339 $faker->seed(20260101);
340 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
341 $set = $generator->generate(
342 FixturePlan::from('order.id < order_detail.order_id'),
343 ['order_detail' => [['quantity' => 1], ['quantity' => 2]]]
344 );
345
346 self::assertSame([1, 2], array_column($set->rows('order_detail'), 'quantity'));
347 }
348
349 #[Test]
350 public function testAnUnmentionedChildIsStillGenerated(): void
351 {
352 $schemas = new StaticSchemaResolver([
353 new TableSchema('order', [
354 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
355 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
356 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
357 'total' => new ColumnDefinition('total', 'INT', nullable: false),
358 ], ['id']),
359 new TableSchema('order_detail', [
360 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
361 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
362 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
363 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
364 ], ['id']),
365 ]);
366 $faker = \Faker\Factory::create();
367 $faker->seed(20260101);
368 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
369 $set = $generator->generate(
370 FixturePlan::from('order.id < order_detail.order_id')
371 );
372
373 self::assertGreaterThanOrEqual(1, count($set->rows('order_detail')));
374 }
375
376 #[Test]
377 public function testAnOptionalChildMayGenerateNoneAtAll(): void
378 {
379 $schemas = new StaticSchemaResolver([
380 new TableSchema('order', [
381 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
382 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
383 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
384 'total' => new ColumnDefinition('total', 'INT', nullable: false),
385 ], ['id']),
386 new TableSchema('order_detail', [
387 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
388 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
389 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
390 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
391 ], ['id']),
392 ]);
393 $faker = \Faker\Factory::create();
394 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
395 $counts = array_map(static function (int $seed) use ($faker, $generator): int {
396 $faker->seed($seed);
397 return count($generator->generate(FixturePlan::from('order.id <? order_detail.order_id'))->rows('order_detail'));
398 }, range(1, 40));
399
400 self::assertContains(0, $counts);
401 }
402
403 #[Test]
404 #[\PHPUnit\Framework\Attributes\DataProvider('providerGenerationSeeds')]
405 public function testARequiredChildNeverGeneratesNone(int $seed): void
406 {
407 $schemas = new StaticSchemaResolver([
408 new TableSchema('order', [
409 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
410 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
411 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
412 'total' => new ColumnDefinition('total', 'INT', nullable: false),
413 ], ['id']),
414 new TableSchema('order_detail', [
415 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
416 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
417 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
418 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
419 ], ['id']),
420 ]);
421 $faker = \Faker\Factory::create();
422 $faker->seed($seed);
423 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
424 $set = $generator->generate(FixturePlan::from('order.id < order_detail.order_id'));
425
426 self::assertGreaterThanOrEqual(1, count($set->rows('order_detail')));
427 }
428
429 #[Test]
430 public function testAOneToOneChildIsASingleRow(): void
431 {
432 $schemas = new StaticSchemaResolver([
433 new TableSchema('order', [
434 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
435 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
436 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
437 'total' => new ColumnDefinition('total', 'INT', nullable: false),
438 ], ['id']),
439 new TableSchema('order_shipping', [
440 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
441 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
442 'carrier' => new ColumnDefinition('carrier', 'VARCHAR', nullable: false, length: 50),
443 ], ['id']),
444 ]);
445 $faker = \Faker\Factory::create();
446 $faker->seed(20260101);
447 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
448 $set = $generator->generate(
449 FixturePlan::from('order.id - order_shipping.order_id'),
450 ['order' => ['id' => 5]]
451 );
452
453 self::assertSame(5, $set->rows('order_shipping')[0]['order_id']);
454 }
455
456 #[Test]
457 public function testAnOptionalOneToOneAskedForNoneIsNull(): void
458 {
459 $schemas = new StaticSchemaResolver([
460 new TableSchema('order', [
461 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
462 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
463 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
464 'total' => new ColumnDefinition('total', 'INT', nullable: false),
465 ], ['id']),
466 new TableSchema('order_shipping', [
467 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
468 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
469 'carrier' => new ColumnDefinition('carrier', 'VARCHAR', nullable: false, length: 50),
470 ], ['id']),
471 ]);
472 $faker = \Faker\Factory::create();
473 $faker->seed(20260101);
474 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
475 $set = $generator->generate(
476 FixturePlan::from('order.id -? order_shipping.order_id'),
477 ['order_shipping' => []]
478 );
479
480 self::assertNull($set->row('order_shipping'));
481 }
482
483 #[Test]
484 public function testEveryChildOfAListGetsItsOwnParent(): void
485 {
486 $schemas = new StaticSchemaResolver([
487 new TableSchema('order', [
488 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
489 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
490 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
491 'total' => new ColumnDefinition('total', 'INT', nullable: false),
492 ], ['id']),
493 new TableSchema('order_detail', [
494 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
495 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
496 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
497 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
498 ], ['id']),
499 new TableSchema('product', [
500 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
501 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
502 ], ['id']),
503 ]);
504 $faker = \Faker\Factory::create();
505 $faker->seed(20260101);
506 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
507 $set = $generator->generate(
508 FixturePlan::from('order.id < order_detail.order_id, order_detail.product_id > product.id'),
509 ['order_detail' => 3]
510 );
511
512 self::assertCount(3, $set->rows('product'));
513 self::assertSame(
514 array_column($set->rows('product'), 'id'),
515 array_column($set->rows('order_detail'), 'product_id')
516 );
517 }
518
519 #[Test]
520 public function testFixingTheLinkingColumnLeavesTheParentAlone(): void
521 {
522 $schemas = new StaticSchemaResolver([
523 new TableSchema('customer', [
524 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
525 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
526 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
527 ], ['id']),
528 new TableSchema('order', [
529 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
530 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
531 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
532 'total' => new ColumnDefinition('total', 'INT', nullable: false),
533 ], ['id']),
534 ]);
535 $faker = \Faker\Factory::create();
536 $faker->seed(20260101);
537 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
538 $set = $generator->generate(
539 FixturePlan::from('order.customer_id > customer.id'),
540 ['order' => ['customer_id' => 77]]
541 );
542
543 self::assertSame(77, $set->rows('order')[0]['customer_id']);
544 self::assertNull($set->row('customer'));
545 }
546
547 #[Test]
548 public function testTablesThatStandAloneAreGeneratedToo(): void
549 {
550 $schemas = new StaticSchemaResolver([
551 new TableSchema('order', [
552 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
553 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
554 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
555 'total' => new ColumnDefinition('total', 'INT', nullable: false),
556 ], ['id']),
557 new TableSchema('order_detail', [
558 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
559 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
560 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
561 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
562 ], ['id']),
563 new TableSchema('audit_log', [
564 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
565 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
566 'message' => new ColumnDefinition('message', 'VARCHAR', nullable: false, length: 255),
567 ], ['id']),
568 ]);
569 $faker = \Faker\Factory::create();
570 $faker->seed(20260101);
571 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
572 $set = $generator->generate(
573 FixturePlan::from('order.id < order_detail.order_id, audit_log')
574 );
575
576 self::assertArrayHasKey('message', (array) $set->row('audit_log'));
577 }
578
579 #[Test]
580 public function testEntriesComeBackInTheOrderThePlanNamesThem(): void
581 {
582 $schemas = new StaticSchemaResolver([
583 new TableSchema('customer', [
584 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
585 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
586 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
587 ], ['id']),
588 new TableSchema('order', [
589 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
590 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
591 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
592 'total' => new ColumnDefinition('total', 'INT', nullable: false),
593 ], ['id']),
594 new TableSchema('order_detail', [
595 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
596 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
597 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
598 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
599 ], ['id']),
600 ]);
601 $faker = \Faker\Factory::create();
602 $faker->seed(20260101);
603 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
604 $set = $generator->generate(
605 FixturePlan::from('order.id < order_detail.order_id, order.customer_id > customer.id'),
606 ['order' => ['id' => 1], 'order_detail' => 2]
607 );
608
609 [$order, $details, $customer] = $set;
610
611 self::assertSame(1, ((array) $order)['id']);
612 self::assertCount(2, (array) $details);
613 self::assertArrayHasKey('tier', (array) $customer);
614 }
615
616 #[Test]
617 public function testOverridesMayBeGivenAsTableOverrides(): void
618 {
619 $schemas = new StaticSchemaResolver([
620 new TableSchema('order', [
621 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
622 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
623 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
624 'total' => new ColumnDefinition('total', 'INT', nullable: false),
625 ], ['id']),
626 new TableSchema('order_detail', [
627 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
628 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
629 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
630 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
631 ], ['id']),
632 ]);
633 $faker = \Faker\Factory::create();
634 $faker->seed(20260101);
635 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
636 $set = $generator->generate(
637 FixturePlan::from('order.id < order_detail.order_id'),
638 ['order' => TableOverrides::of(['status' => 'paid'])]
639 );
640
641 self::assertSame('paid', $set->rows('order')[0]['status']);
642 }
643
644 #[Test]
645 public function testAnUnknownTableIsReported(): void
646 {
647 $schemas = new StaticSchemaResolver([
648 ]);
649 $faker = \Faker\Factory::create();
650 $faker->seed(20260101);
651 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
652 $this->expectException(SchemaNotFoundException::class);
653 $this->expectExceptionMessage('Schema not found for table: nope');
654
655 $generator->generate(FixturePlan::from('nope'));
656 }
657
658 #[Test]
659 public function testAPlanNamingAColumnTheTableLacksIsRejectedBeforeGenerating(): void
660 {
661 $schemas = new StaticSchemaResolver([
662 new TableSchema('order', [
663 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
664 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
665 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
666 'total' => new ColumnDefinition('total', 'INT', nullable: false),
667 ], ['id']),
668 new TableSchema('order_detail', [
669 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
670 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
671 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
672 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
673 ], ['id']),
674 ]);
675 $faker = \Faker\Factory::create();
676 $faker->seed(20260101);
677 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
678 $this->expectException(\SqlFixture\Fixture\Exception\UnknownPlanColumnException::class);
679 $this->expectExceptionMessage('order_detail has no column oder_id');
680
681 $generator->generate(FixturePlan::from('order.id < order_detail.oder_id'));
682 }
683
684 #[Test]
685 public function testTheSubjectHonoursACountItWasGiven(): void
686 {
687 $schemas = new StaticSchemaResolver([
688 new TableSchema('order', [
689 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
690 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
691 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
692 'total' => new ColumnDefinition('total', 'INT', nullable: false),
693 ], ['id']),
694 new TableSchema('order_detail', [
695 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
696 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
697 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
698 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
699 ], ['id']),
700 ]);
701 $faker = \Faker\Factory::create();
702 $faker->seed(20260101);
703 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
704 $set = $generator->generate(
705 FixturePlan::from('order.id < order_detail.order_id'),
706 ['order' => 2, 'order_detail' => 1]
707 );
708
709 self::assertCount(2, $set->rows('order'));
710 }
711
712 #[Test]
713 public function testARowCarriesBothItsInheritedKeyAndItsOwnParentKey(): void
714 {
715 $schemas = new StaticSchemaResolver([
716 new TableSchema('order', [
717 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
718 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
719 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
720 'total' => new ColumnDefinition('total', 'INT', nullable: false),
721 ], ['id']),
722 new TableSchema('order_detail', [
723 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
724 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
725 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
726 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
727 ], ['id']),
728 new TableSchema('product', [
729 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
730 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
731 ], ['id']),
732 ]);
733 $faker = \Faker\Factory::create();
734 $faker->seed(20260101);
735 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
736 $set = $generator->generate(
737 FixturePlan::from('order.id < order_detail.order_id, order_detail.product_id > product.id'),
738 ['order' => ['id' => 42], 'order_detail' => 1]
739 );
740
741 $detail = $set->rows('order_detail')[0];
742
743 self::assertSame(42, $detail['order_id']);
744 self::assertSame($set->rows('product')[0]['id'], $detail['product_id']);
745 }
746
747 #[Test]
748 public function testEveryOtherRelationOfAParentIsStillFollowed(): void
749 {
750 $schemas = new StaticSchemaResolver([
751 new TableSchema('customer', [
752 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
753 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
754 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
755 ], ['id']),
756 new TableSchema('order', [
757 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
758 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
759 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
760 'total' => new ColumnDefinition('total', 'INT', nullable: false),
761 ], ['id']),
762 new TableSchema('audit_log', [
763 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
764 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
765 'message' => new ColumnDefinition('message', 'VARCHAR', nullable: false, length: 255),
766 ], ['id']),
767 ]);
768 $faker = \Faker\Factory::create();
769 $faker->seed(20260101);
770 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
771 $set = $generator->generate(
772 FixturePlan::from('order.customer_id > customer.id, customer.id < audit_log.customer_id')
773 );
774
775 self::assertNotSame([], $set->rows('audit_log'));
776 }
777
778 #[Test]
779 public function testAnOptionalParentIsGeneratedWhenTheCallerAsksForIt(): void
780 {
781 $schemas = new StaticSchemaResolver([
782 new TableSchema('customer', [
783 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
784 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
785 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
786 ], ['id']),
787 new TableSchema('order', [
788 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
789 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
790 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
791 'total' => new ColumnDefinition('total', 'INT', nullable: false),
792 ], ['id']),
793 ]);
794 $faker = \Faker\Factory::create();
795 $faker->seed(20260101);
796 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
797 $set = $generator->generate(
798 FixturePlan::from('order.customer_id >? customer.id'),
799 ['customer' => ['tier' => 'gold']]
800 );
801
802 self::assertSame('gold', $set->rows('customer')[0]['tier']);
803 self::assertSame($set->rows('customer')[0]['id'], $set->rows('order')[0]['customer_id']);
804 }
805
806 #[Test]
807 public function testACompositeRelationCarriesEveryColumn(): void
808 {
809 $schemas = new StaticSchemaResolver([
810 new TableSchema('shop_order', [
811 'shop_id' => new ColumnDefinition('shop_id', 'INT', nullable: false, unsigned: true),
812 'no' => new ColumnDefinition('no', 'INT', nullable: false, unsigned: true),
813 ], ['shop_id', 'no']),
814 new TableSchema('shop_order_line', [
815 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
816 'shop_id' => new ColumnDefinition('shop_id', 'INT', nullable: false, unsigned: true),
817 'order_no' => new ColumnDefinition('order_no', 'INT', nullable: false, unsigned: true),
818 ], ['id']),
819 ]);
820 $faker = \Faker\Factory::create();
821 $faker->seed(20260101);
822 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
823 $set = $generator->generate(
824 FixturePlan::from('shop_order.(shop_id, no) < shop_order_line.(shop_id, order_no)'),
825 ['shop_order' => ['shop_id' => 7, 'no' => 9], 'shop_order_line' => 1]
826 );
827
828 $line = $set->rows('shop_order_line')[0];
829
830 self::assertSame(7, $line['shop_id']);
831 self::assertSame(9, $line['order_no']);
832 }
833
834 #[Test]
835 public function testAOneToOneChildIsGeneratedExactlyOnce(): void
836 {
837 $schemas = new StaticSchemaResolver([
838 new TableSchema('order', [
839 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
840 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
841 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
842 'total' => new ColumnDefinition('total', 'INT', nullable: false),
843 ], ['id']),
844 new TableSchema('order_shipping', [
845 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
846 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
847 'carrier' => new ColumnDefinition('carrier', 'VARCHAR', nullable: false, length: 50),
848 ], ['id']),
849 ]);
850 $faker = \Faker\Factory::create();
851 $faker->seed(20260101);
852 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
853 $set = $generator->generate(
854 FixturePlan::from('order.id - order_shipping.order_id')
855 );
856
857 self::assertCount(1, $set->rows('order_shipping'));
858 }
859
860 #[Test]
861 public function testKeysAreStoodInForEveryColumnARelationReads(): void
862 {
863 $schemas = new StaticSchemaResolver([
864 new TableSchema('twin', [
865 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
866 'other_id' => new ColumnDefinition('other_id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
867 ], ['id']),
868 new TableSchema('twin_child', [
869 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
870 'twin_id' => new ColumnDefinition('twin_id', 'INT', nullable: false, unsigned: true),
871 ], ['id']),
872 new TableSchema('twin_other', [
873 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
874 'twin_other_id' => new ColumnDefinition('twin_other_id', 'INT', nullable: false, unsigned: true),
875 ], ['id']),
876 ]);
877 $faker = \Faker\Factory::create();
878 $faker->seed(20260101);
879 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
880 $set = $generator->generate(
881 FixturePlan::from('twin.id < twin_child.twin_id, twin.other_id < twin_other.twin_other_id')
882 );
883
884 self::assertSame(1, $set->rows('twin')[0]['id']);
885 self::assertSame(1, $set->rows('twin')[0]['other_id']);
886 }
887
888 #[Test]
889 public function testARequiredParentIsGeneratedWhetherOrNotItWasAskedFor(): void
890 {
891 $schemas = new StaticSchemaResolver([
892 new TableSchema('customer', [
893 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
894 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
895 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
896 ], ['id']),
897 new TableSchema('order', [
898 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
899 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
900 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
901 'total' => new ColumnDefinition('total', 'INT', nullable: false),
902 ], ['id']),
903 ]);
904 $faker = \Faker\Factory::create();
905 $faker->seed(20260101);
906 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
907 $set = $generator->generate(FixturePlan::from('order.customer_id > customer.id'));
908
909 self::assertNotSame([], $set->rows('customer'));
910 }
911
912 #[Test]
913 public function testAnOptionalParentNobodyAskedForIsLeftOut(): void
914 {
915 $schemas = new StaticSchemaResolver([
916 new TableSchema('customer', [
917 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
918 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
919 'tier' => new ColumnDefinition('tier', 'ENUM', nullable: false, enumValues: ['gold', 'silver']),
920 ], ['id']),
921 new TableSchema('order', [
922 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
923 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
924 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
925 'total' => new ColumnDefinition('total', 'INT', nullable: false),
926 ], ['id']),
927 ]);
928 $faker = \Faker\Factory::create();
929 $faker->seed(20260101);
930 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
931 $set = $generator->generate(FixturePlan::from('order.customer_id >? customer.id'));
932
933 self::assertSame([], $set->rows('customer'));
934 }
935
936 #[Test]
937 public function testAnUnboundedChildCountCanExceedItsMinimum(): void
938 {
939 $schemas = new StaticSchemaResolver([
940 new TableSchema('order', [
941 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
942 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
943 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
944 'total' => new ColumnDefinition('total', 'INT', nullable: false),
945 ], ['id']),
946 new TableSchema('order_detail', [
947 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
948 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
949 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
950 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
951 ], ['id']),
952 ]);
953 $faker = \Faker\Factory::create();
954 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
955 $counts = array_map(static function (int $seed) use ($faker, $generator): int {
956 $faker->seed($seed);
957 return count($generator->generate(FixturePlan::from('order.id < order_detail.order_id'))->rows('order_detail'));
958 }, range(1, 40));
959
960 self::assertGreaterThan(1, max($counts));
961 }
962
963 #[Test]
964 #[\PHPUnit\Framework\Attributes\DataProvider('providerGenerationSeeds')]
965 public function testAOneToOneChildIsNeverGeneratedMoreThanOnce(int $seed): void
966 {
967 $schemas = new StaticSchemaResolver([
968 new TableSchema('order', [
969 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
970 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
971 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
972 'total' => new ColumnDefinition('total', 'INT', nullable: false),
973 ], ['id']),
974 new TableSchema('order_shipping', [
975 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
976 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
977 'carrier' => new ColumnDefinition('carrier', 'VARCHAR', nullable: false, length: 50),
978 ], ['id']),
979 ]);
980 $faker = \Faker\Factory::create();
981 $faker->seed($seed);
982 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
983 $set = $generator->generate(FixturePlan::from('order.id - order_shipping.order_id'));
984
985 self::assertCount(1, $set->rows('order_shipping'));
986 }
987
988 #[Test]
989 public function testATableReachableOnlyBackwardsAlongARelationIsStillPartOfTheWalk(): void
990 {
991 $schemas = new StaticSchemaResolver([
992 new TableSchema('order', [
993 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
994 'customer_id' => new ColumnDefinition('customer_id', 'INT', nullable: false, unsigned: true),
995 'status' => new ColumnDefinition('status', 'VARCHAR', nullable: false, length: 20),
996 'total' => new ColumnDefinition('total', 'INT', nullable: false),
997 ], ['id']),
998 new TableSchema('order_detail', [
999 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
1000 'order_id' => new ColumnDefinition('order_id', 'INT', nullable: false, unsigned: true),
1001 'product_id' => new ColumnDefinition('product_id', 'INT', nullable: false, unsigned: true),
1002 'quantity' => new ColumnDefinition('quantity', 'INT', nullable: false),
1003 ], ['id']),
1004 new TableSchema('product', [
1005 'id' => new ColumnDefinition('id', 'INT', nullable: false, unsigned: true, autoIncrement: true),
1006 'name' => new ColumnDefinition('name', 'VARCHAR', nullable: false, length: 255),
1007 ], ['id']),
1008 ]);
1009 $faker = \Faker\Factory::create();
1010 $faker->seed(20260101);
1011 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1012 $set = $generator->generate(
1013 FixturePlan::from('order.id < order_detail.order_id, product.id ?< order_detail.product_id')
1014 );
1015
1016 self::assertSame([], $set->rows('product'));
1017 }
1018 /**
1019 * @return list<array{int}>
1020 */
1021 public static function providerGenerationSeeds(): array
1022 {
1023 return array_map(static fn (int $seed): array => [$seed], range(1, 40));
1024 }
1025
1026 public function testGenerateMixedChoiceRows(): void
1027 {
1028
1029 $schemas = new StaticSchemaResolver([
1030 new TableSchema('comments', [
1031 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1032 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1033 'target_id' => new ColumnDefinition('target_id', 'INT'),
1034 'tenant' => new ColumnDefinition('tenant', 'INT'),
1035 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1036 ]),
1037 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1038 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1039 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1040 ]);
1041 $faker = \Faker\Factory::create();
1042 $faker->seed(42);
1043 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1044
1045 $plan = FixturePlan::from("choice comments.kind { 'post' { comments.target_id > posts.id } 'video' { comments.target_id > videos.id } 'none' {} }");
1046 $set = $generator->generate($plan, ['comments' => [['kind' => 'post'], ['kind' => 'video'], ['kind' => 'post'], ['kind' => 'none']]]);
1047 self::assertSame(['post', 'video', 'post', 'none'], array_column($set->rows('comments'), 'kind'));
1048 self::assertSame([1, 1, 2, null], array_column($set->rows('comments'), 'target_id'));
1049 self::assertSame([1, 2], array_column($set->rows('posts'), 'id'));
1050 self::assertSame([1], array_column($set->rows('videos'), 'id'));
1051 self::assertSame(['comments', 'posts', 'videos'], $set->tables());
1052
1053 }
1054
1055 public function testGenerateChildTablesSelectedPerPayment(): void
1056 {
1057
1058 $schemas = new StaticSchemaResolver([
1059 new TableSchema('payments', [
1060 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1061 'method' => new ColumnDefinition('method', 'VARCHAR', length: 20),
1062 ]),
1063 new TableSchema('cards', ['payment_id' => new ColumnDefinition('payment_id', 'INT')]),
1064 new TableSchema('banks', ['payment_id' => new ColumnDefinition('payment_id', 'INT')]),
1065 new TableSchema('receipts', ['payment_id' => new ColumnDefinition('payment_id', 'INT')]),
1066 ]);
1067 $faker = \Faker\Factory::create();
1068 $faker->seed(42);
1069 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1070
1071 $plan = FixturePlan::from("choice payments.method { 'card' { payments.id - cards.payment_id } 'bank' { payments.id - banks.payment_id } }");
1072 $set = $generator->generate($plan, ['payments' => [['method' => 'card'], ['method' => 'bank'], ['method' => 'card']]]);
1073 self::assertSame([1, 3], array_column($set->rows('cards'), 'payment_id'));
1074 self::assertSame([2], array_column($set->rows('banks'), 'payment_id'));
1075
1076 }
1077
1078 public function testGenerateAllRelationsInTheSelectedBranch(): void
1079 {
1080
1081 $schemas = new StaticSchemaResolver([
1082 new TableSchema('payments', [
1083 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1084 'method' => new ColumnDefinition('method', 'VARCHAR', length: 20),
1085 ]),
1086 new TableSchema('cards', ['payment_id' => new ColumnDefinition('payment_id', 'INT')]),
1087 new TableSchema('banks', ['payment_id' => new ColumnDefinition('payment_id', 'INT')]),
1088 new TableSchema('receipts', ['payment_id' => new ColumnDefinition('payment_id', 'INT')]),
1089 ]);
1090 $faker = \Faker\Factory::create();
1091 $faker->seed(42);
1092 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1093
1094 $plan = FixturePlan::from("choice payments.method { 'card' { payments.id - cards.payment_id, payments.id < receipts.payment_id } 'bank' { payments.id - banks.payment_id } }");
1095 $set = $generator->generate($plan, ['payments' => ['id' => 8, 'method' => 'card'], 'receipts' => 3]);
1096 self::assertSame([8], array_column($set->rows('cards'), 'payment_id'));
1097 self::assertSame([8, 8, 8], array_column($set->rows('receipts'), 'payment_id'));
1098 self::assertSame([], $set->rows('banks'));
1099
1100 }
1101
1102 public function testGenerateInversePolymorphicChildren(): void
1103 {
1104
1105 $schemas = new StaticSchemaResolver([
1106 new TableSchema('comments', [
1107 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1108 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1109 'target_id' => new ColumnDefinition('target_id', 'INT'),
1110 'tenant' => new ColumnDefinition('tenant', 'INT'),
1111 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1112 ]),
1113 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1114 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1115 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1116 ]);
1117 $faker = \Faker\Factory::create();
1118 $faker->seed(42);
1119 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1120
1121 $plan = FixturePlan::from("posts, choice comments.kind { 'post' { comments.target_id > posts.id } 'video' { comments.target_id > videos.id } }");
1122 $set = $generator->generate($plan, ['posts' => ['id' => 9], 'comments' => 2]);
1123 self::assertSame(['post', 'post'], array_column($set->rows('comments'), 'kind'));
1124 self::assertSame([9, 9], array_column($set->rows('comments'), 'target_id'));
1125 self::assertSame([], $set->rows('videos'));
1126
1127 }
1128
1129 public function testGenerateInverseSubtypeParent(): void
1130 {
1131
1132 $schemas = new StaticSchemaResolver([
1133 new TableSchema('payments', [
1134 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1135 'method' => new ColumnDefinition('method', 'VARCHAR', length: 20),
1136 ]),
1137 new TableSchema('cards', ['payment_id' => new ColumnDefinition('payment_id', 'INT')]),
1138 new TableSchema('banks', ['payment_id' => new ColumnDefinition('payment_id', 'INT')]),
1139 new TableSchema('receipts', ['payment_id' => new ColumnDefinition('payment_id', 'INT')]),
1140 ]);
1141 $faker = \Faker\Factory::create();
1142 $faker->seed(42);
1143 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1144
1145 $plan = FixturePlan::from("cards, choice payments.method { 'card' { payments.id - cards.payment_id } 'bank' { payments.id - banks.payment_id } }");
1146 $set = $generator->generate($plan);
1147 self::assertSame(['card'], array_column($set->rows('payments'), 'method'));
1148 self::assertSame(array_column($set->rows('payments'), 'id'), array_column($set->rows('cards'), 'payment_id'));
1149 self::assertSame([], $set->rows('banks'));
1150
1151 }
1152
1153 public function testGeneratePolymorphicJunctionRows(): void
1154 {
1155
1156 $schemas = new StaticSchemaResolver([
1157 new TableSchema('comments', [
1158 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1159 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1160 'target_id' => new ColumnDefinition('target_id', 'INT'),
1161 'tenant' => new ColumnDefinition('tenant', 'INT'),
1162 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1163 ]),
1164 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1165 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1166 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1167 ]);
1168 $faker = \Faker\Factory::create();
1169 $faker->seed(42);
1170 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1171
1172 $plan = FixturePlan::from("tags.id < comments.tag_id, choice comments.kind { 'post' { comments.target_id > posts.id } 'video' { comments.target_id > videos.id } }");
1173 $set = $generator->generate($plan, ['tags' => ['id' => 12], 'comments' => [['kind' => 'post'], ['kind' => 'video']]]);
1174 self::assertSame([12, 12], array_column($set->rows('comments'), 'tag_id'));
1175 self::assertCount(1, $set->rows('tags'));
1176 self::assertCount(1, $set->rows('posts'));
1177 self::assertCount(1, $set->rows('videos'));
1178
1179 }
1180
1181 public function testGenerateCompositeChoiceKeys(): void
1182 {
1183
1184 $schemas = new StaticSchemaResolver([
1185 new TableSchema('comments', [
1186 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1187 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1188 'target_id' => new ColumnDefinition('target_id', 'INT'),
1189 'tenant' => new ColumnDefinition('tenant', 'INT'),
1190 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1191 ]),
1192 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1193 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1194 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1195 ]);
1196 $faker = \Faker\Factory::create();
1197 $faker->seed(42);
1198 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1199
1200 $plan = FixturePlan::from("choice comments.kind { 'post' { comments.(tenant, target_id) > posts.(tenant, id) } 'video' { comments.(tenant, target_id) > videos.(tenant, id) } }");
1201 $set = $generator->generate($plan, ['comments' => [['kind' => 'post'], ['kind' => 'video']], 'posts' => ['tenant' => 7], 'videos' => ['tenant' => 8]]);
1202 self::assertSame([7, 8], array_column($set->rows('comments'), 'tenant'));
1203 self::assertSame([1, 1], array_column($set->rows('comments'), 'target_id'));
1204
1205 }
1206
1207 public function testGenerateExplicitReferencesWithoutInventingParents(): void
1208 {
1209
1210 $schemas = new StaticSchemaResolver([
1211 new TableSchema('comments', [
1212 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1213 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1214 'target_id' => new ColumnDefinition('target_id', 'INT'),
1215 'tenant' => new ColumnDefinition('tenant', 'INT'),
1216 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1217 ]),
1218 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1219 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1220 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1221 ]);
1222 $faker = \Faker\Factory::create();
1223 $faker->seed(42);
1224 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1225
1226 $plan = FixturePlan::from("choice comments.kind { 'post' { comments.target_id > posts.id } 'video' { comments.target_id > videos.id } }");
1227 $set = $generator->generate($plan, ['comments' => ['kind' => 'video', 'target_id' => 77]]);
1228 self::assertSame([77], array_column($set->rows('comments'), 'target_id'));
1229 self::assertSame([], $set->rows('posts'));
1230 self::assertSame([], $set->rows('videos'));
1231
1232 }
1233
1234 public function testGenerateFallbackAndNullBranches(): void
1235 {
1236
1237 $schemas = new StaticSchemaResolver([
1238 new TableSchema('comments', [
1239 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1240 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1241 'target_id' => new ColumnDefinition('target_id', 'INT'),
1242 'tenant' => new ColumnDefinition('tenant', 'INT'),
1243 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1244 ]),
1245 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1246 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1247 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1248 ]);
1249 $faker = \Faker\Factory::create();
1250 $faker->seed(42);
1251 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1252
1253 $plan = FixturePlan::from("choice comments.kind { 'post' { comments.target_id > posts.id } null {} otherwise {} }");
1254 $set = $generator->generate($plan, ['comments' => [['kind' => null], ['kind' => 'external']]]);
1255 self::assertSame([null, 'external'], array_column($set->rows('comments'), 'kind'));
1256 self::assertSame([null, null], array_column($set->rows('comments'), 'target_id'));
1257 self::assertSame([], $set->rows('posts'));
1258
1259 }
1260
1261 public function testGenerateOptionalParentsWithNullReferences(): void
1262 {
1263
1264 $schemas = new StaticSchemaResolver([
1265 new TableSchema('comments', [
1266 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1267 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1268 'target_id' => new ColumnDefinition('target_id', 'INT'),
1269 'tenant' => new ColumnDefinition('tenant', 'INT'),
1270 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1271 ]),
1272 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1273 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1274 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1275 ]);
1276 $faker = \Faker\Factory::create();
1277 $faker->seed(42);
1278 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1279
1280 $plan = FixturePlan::from("choice comments.kind { 'post' { comments.target_id >? posts.id } }");
1281 $set = $generator->generate($plan, ['comments' => ['kind' => 'post']]);
1282 self::assertSame([null], array_column($set->rows('comments'), 'target_id'));
1283 self::assertSame([], $set->rows('posts'));
1284
1285 }
1286
1287 public function testGenerateInverseEquivalentCasesOncePerChild(): void
1288 {
1289
1290 $schemas = new StaticSchemaResolver([
1291 new TableSchema('comments', [
1292 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1293 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1294 'target_id' => new ColumnDefinition('target_id', 'INT'),
1295 'tenant' => new ColumnDefinition('tenant', 'INT'),
1296 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1297 ]),
1298 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1299 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1300 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1301 ]);
1302 $faker = \Faker\Factory::create();
1303 $faker->seed(42);
1304 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1305
1306 $plan = FixturePlan::from("posts, choice comments.kind { 'review' { comments.target_id > posts.id } 'comment' { comments.target_id > posts.id } }");
1307 $set = $generator->generate($plan, ['comments' => [['kind' => 'review'], ['kind' => 'comment']]]);
1308 self::assertSame(['review', 'comment'], array_column($set->rows('comments'), 'kind'));
1309 self::assertCount(1, $set->rows('posts'));
1310 self::assertSame([1, 1], array_column($set->rows('comments'), 'target_id'));
1311
1312 }
1313
1314 public function testGenerateUnknownDiscriminatorReportsItsColumn(): void
1315 {
1316
1317 $schemas = new StaticSchemaResolver([
1318 new TableSchema('comments', [
1319 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1320 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1321 'target_id' => new ColumnDefinition('target_id', 'INT'),
1322 'tenant' => new ColumnDefinition('tenant', 'INT'),
1323 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1324 ]),
1325 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1326 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1327 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1328 ]);
1329 $faker = \Faker\Factory::create();
1330 $faker->seed(42);
1331 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1332
1333 $plan = FixturePlan::from("choice comments.kind { 'post' { comments.target_id > posts.id } }");
1334 $this->expectException(\SqlFixture\Fixture\Choice\ChoiceValueException::class);
1335 $this->expectExceptionMessage('comments.kind');
1336 $generator->generate($plan, ['comments' => ['kind' => 'unknown']]);
1337
1338 }
1339
1340 public function testGenerateConflictingInverseDiscriminatorIsRejected(): void
1341 {
1342
1343 $schemas = new StaticSchemaResolver([
1344 new TableSchema('comments', [
1345 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1346 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1347 'target_id' => new ColumnDefinition('target_id', 'INT'),
1348 'tenant' => new ColumnDefinition('tenant', 'INT'),
1349 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1350 ]),
1351 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1352 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1353 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1354 ]);
1355 $faker = \Faker\Factory::create();
1356 $faker->seed(42);
1357 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1358
1359 $plan = FixturePlan::from("posts, choice comments.kind { 'post' { comments.target_id > posts.id } 'video' { comments.target_id > videos.id } }");
1360 $this->expectException(\SqlFixture\Fixture\Choice\ChoiceValueException::class);
1361 $generator->generate($plan, ['comments' => ['kind' => 'video']]);
1362
1363 }
1364
1365 public function testGenerateUnknownDiscriminatorColumnsAreRejected(): void
1366 {
1367
1368 $schemas = new StaticSchemaResolver([
1369 new TableSchema('comments', [
1370 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1371 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1372 'target_id' => new ColumnDefinition('target_id', 'INT'),
1373 'tenant' => new ColumnDefinition('tenant', 'INT'),
1374 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1375 ]),
1376 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1377 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1378 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1379 ]);
1380 $faker = \Faker\Factory::create();
1381 $faker->seed(42);
1382 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1383
1384 $plan = FixturePlan::from("choice comments.missing { 'post' { comments.target_id > posts.id } }");
1385 $this->expectException(\SqlFixture\Fixture\Exception\UnknownPlanColumnException::class);
1386 $generator->generate($plan);
1387
1388 }
1389
1390 public function testGeneratePartialCompositeOverridesAreCarriedToTheParent(): void
1391 {
1392
1393 $schemas = new StaticSchemaResolver([
1394 new TableSchema('comments', [
1395 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1396 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1397 'target_id' => new ColumnDefinition('target_id', 'INT'),
1398 'tenant' => new ColumnDefinition('tenant', 'INT'),
1399 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1400 ]),
1401 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1402 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1403 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1404 ]);
1405 $faker = \Faker\Factory::create();
1406 $faker->seed(42);
1407 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1408
1409 $plan = FixturePlan::from("choice comments.kind { 'post' { comments.(tenant, target_id) > posts.(tenant, id) } }");
1410 $set = $generator->generate($plan, ['comments' => ['kind' => 'post', 'tenant' => 42]]);
1411 self::assertSame([42], array_column($set->rows('comments'), 'tenant'));
1412 self::assertSame([42], array_column($set->rows('posts'), 'tenant'));
1413 self::assertSame([1], array_column($set->rows('comments'), 'target_id'));
1414
1415 }
1416
1417 public function testGenerateConflictingParentKeysAreRejected(): void
1418 {
1419
1420 $schemas = new StaticSchemaResolver([
1421 new TableSchema('comments', [
1422 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1423 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1424 'target_id' => new ColumnDefinition('target_id', 'INT'),
1425 'tenant' => new ColumnDefinition('tenant', 'INT'),
1426 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1427 ]),
1428 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1429 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1430 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1431 ]);
1432 $faker = \Faker\Factory::create();
1433 $faker->seed(42);
1434 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1435
1436 $plan = FixturePlan::from("choice comments.kind { 'post' { comments.(tenant, target_id) > posts.(tenant, id) } }");
1437 $this->expectException(\SqlFixture\Fixture\Generation\RelationValueException::class);
1438 $this->expectExceptionMessage('posts.tenant');
1439 $generator->generate($plan, ['comments' => ['kind' => 'post', 'tenant' => 42], 'posts' => ['tenant' => 99]]);
1440
1441 }
1442
1443 public function testGenerateConflictingChildKeysAreRejected(): void
1444 {
1445
1446 $schemas = new StaticSchemaResolver([
1447 new TableSchema('comments', [
1448 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1449 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1450 'target_id' => new ColumnDefinition('target_id', 'INT'),
1451 'tenant' => new ColumnDefinition('tenant', 'INT'),
1452 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1453 ]),
1454 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1455 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1456 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1457 ]);
1458 $faker = \Faker\Factory::create();
1459 $faker->seed(42);
1460 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1461
1462 $plan = FixturePlan::from("posts, choice comments.kind { 'post' { comments.target_id > posts.id } }");
1463 $this->expectException(\SqlFixture\Fixture\Generation\RelationValueException::class);
1464 $generator->generate($plan, ['posts' => ['id' => 42], 'comments' => ['target_id' => 99]]);
1465
1466 }
1467
1468 public function testGenerateRepeatedRolesFailWithoutUnboundedRecursion(): void
1469 {
1470
1471 $schemas = new StaticSchemaResolver([
1472 new TableSchema('comments', [
1473 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1474 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1475 'target_id' => new ColumnDefinition('target_id', 'INT'),
1476 'tenant' => new ColumnDefinition('tenant', 'INT'),
1477 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1478 ]),
1479 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1480 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1481 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1482 ]);
1483 $faker = \Faker\Factory::create();
1484 $faker->seed(42);
1485 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1486
1487 $plan = FixturePlan::from("choice comments.kind { 'post' { comments.target_id > posts.id, comments.tag_id > posts.tenant } }");
1488 $this->expectException(\SqlFixture\Fixture\Generation\RecursiveRelationException::class);
1489 $generator->generate($plan, ['comments' => ['kind' => 'post']]);
1490
1491 }
1492
1493 public function testGenerateSeededCaseSelectionIsReproducible(): void
1494 {
1495 /**
1496 * Destructors of earlier Faker instances reset the shared random seed.
1497 * Collect them before starting the seeded generation below.
1498 */
1499 gc_collect_cycles();
1500
1501 $schemas = new StaticSchemaResolver([
1502 new TableSchema('comments', [
1503 'id' => new ColumnDefinition('id', 'INT', autoIncrement: true),
1504 'kind' => new ColumnDefinition('kind', 'VARCHAR', length: 20),
1505 'target_id' => new ColumnDefinition('target_id', 'INT'),
1506 'tenant' => new ColumnDefinition('tenant', 'INT'),
1507 'tag_id' => new ColumnDefinition('tag_id', 'INT'),
1508 ]),
1509 new TableSchema('posts', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1510 new TableSchema('videos', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true), 'tenant' => new ColumnDefinition('tenant', 'INT')]),
1511 new TableSchema('tags', ['id' => new ColumnDefinition('id', 'INT', autoIncrement: true)]),
1512 ]);
1513 $faker = \Faker\Factory::create();
1514 $faker->seed(42);
1515 $generator = new PlanGenerator($schemas, new FixtureGenerator($faker), $faker);
1516
1517 $plan = FixturePlan::from("choice comments.kind { 'post' { comments.target_id > posts.id } 'video' { comments.target_id > videos.id } }");
1518 $first = $generator->generate($plan, ['comments' => 12]);
1519 $faker->seed(42);
1520 $second = $generator->generate($plan, ['comments' => 12]);
1521 self::assertSame($first->rows('comments'), $second->rows('comments'));
1522 self::assertContains('post', array_column($first->rows('comments'), 'kind'));
1523 self::assertContains('video', array_column($first->rows('comments'), 'kind'));
1524
1525 }
1526}
1527