packages/sql-fixture/tests/Unit/Provider/FixtureProviderTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Provider;
6
7use Faker\Factory;
8use PHPUnit\Framework\Attributes\CoversClass;
9use PHPUnit\Framework\Attributes\Test;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use SqlFixture\Hydrator\ReflectionHydrator;
13use SqlFixture\Platform\MySql\MySqlSchemaParser;
14use SqlFixture\Platform\MySql\MySqlTypeMapper;
15use SqlFixture\Platform\PostgreSql\PostgreSqlTypeMapper;
16use SqlFixture\Platform\Sqlite\SqliteSchemaParser;
17use SqlFixture\Platform\Sqlite\SqliteTypeMapper;
18use SqlFixture\Provider\FixtureGenerator;
19use SqlFixture\Provider\FixtureProvider;
20use SqlFixture\Provider\PlatformFactory;
21use SqlFixture\Schema\ColumnDefinition;
22use SqlFixture\Schema\StaticSchemaResolver;
23use SqlFixture\Schema\TableSchema;
24
25#[CoversClass(FixtureProvider::class)]
26#[UsesClass(FixtureGenerator::class)]
27#[UsesClass(PlatformFactory::class)]
28#[UsesClass(MySqlSchemaParser::class)]
29#[UsesClass(MySqlTypeMapper::class)]
30#[UsesClass(ColumnDefinition::class)]
31#[UsesClass(TableSchema::class)]
32#[UsesClass(StaticSchemaResolver::class)]
33#[UsesClass(ReflectionHydrator::class)]
34#[UsesClass(PostgreSqlTypeMapper::class)]
35#[UsesClass(SqliteSchemaParser::class)]
36#[UsesClass(SqliteTypeMapper::class)]
37#[CoversClass(\SqlFixture\Provider\SqlSchemaProvider::class)]
38#[UsesClass(\SqlFixture\Fixture\FixtureSet::class)]
39#[UsesClass(\SqlFixture\Fixture\GenerationRun::class)]
40#[UsesClass(\SqlFixture\Fixture\OverrideRows::class)]
41#[UsesClass(\SqlFixture\Fixture\PlanGenerator::class)]
42#[UsesClass(\SqlFixture\Fixture\PlanSchemaException::class)]
43#[UsesClass(\SqlFixture\Fixture\PlanSchemaValidator::class)]
44#[UsesClass(\SqlFixture\Fixture\RowSpec::class)]
45#[UsesClass(\SqlFixture\Fixture\TableOverrides::class)]
46#[UsesClass(\SqlFixture\Hydrator\HydrationException::class)]
47#[UsesClass(\SqlFixture\Hydrator\HydratorInterface::class)]
48#[UsesClass(\SqlFixture\Plan\ColumnRef::class)]
49#[UsesClass(\SqlFixture\Plan\FixturePlan::class)]
50#[UsesClass(\SqlFixture\Plan\PlanParser::class)]
51#[UsesClass(\SqlFixture\Plan\PlanPrinter::class)]
52#[UsesClass(\SqlFixture\Plan\PlanSyntaxException::class)]
53#[UsesClass(\SqlFixture\Plan\Relation::class)]
54#[UsesClass(\SqlFixture\Plan\RelationKind::class)]
55#[UsesClass(\SqlFixture\Plan\RelationSide::class)]
56#[UsesClass(\SqlFixture\Platform\MySql\MySqlSchemaFetcher::class)]
57#[UsesClass(\SqlFixture\Platform\PostgreSql\PostgreSqlSchemaFetcher::class)]
58#[UsesClass(\SqlFixture\Platform\PostgreSql\PostgreSqlSchemaParser::class)]
59#[UsesClass(\SqlFixture\Platform\Sqlite\SqliteSchemaFetcher::class)]
60#[UsesClass(\SqlFixture\Schema\SchemaFetcherInterface::class)]
61#[UsesClass(\SqlFixture\Schema\SchemaNotFoundException::class)]
62#[UsesClass(\SqlFixture\Schema\SchemaParseException::class)]
63#[UsesClass(\SqlFixture\Schema\SchemaParserInterface::class)]
64#[UsesClass(\SqlFixture\Schema\SchemaResolverInterface::class)]
65#[UsesClass(\SqlFixture\Schema\TableIdentifier::class)]
66#[UsesClass(\SqlFixture\TypeMapper\TypeMapperInterface::class)]
67#[UsesClass(\SqlFixture\Fixture\Generation\ConnectedTables::class)]
68#[UsesClass(\SqlFixture\Fixture\Generation\OverrideSpecs::class)]
69#[UsesClass(\SqlFixture\Fixture\Generation\RelationCounts::class)]
70#[UsesClass(\SqlFixture\Fixture\Generation\RelationProjection::class)]
71#[UsesClass(\SqlFixture\Fixture\Generation\RowMaterializer::class)]
72#[UsesClass(\SqlFixture\Fixture\RowGeneration::class)]
73#[UsesClass(\SqlFixture\Fixture\Validation\EndpointValidator::class)]
74#[UsesClass(\SqlFixture\Fixture\Validation\OverrideValidator::class)]
75#[UsesClass(\SqlFixture\Hydrator\Reflection\ConstructorHydration::class)]
76#[UsesClass(\SqlFixture\Hydrator\Reflection\PropertyHydration::class)]
77#[UsesClass(\SqlFixture\Hydrator\Reflection\PropertyNames::class)]
78#[UsesClass(\SqlFixture\Hydrator\Reflection\ValueConversion::class)]
79#[UsesClass(\SqlFixture\Fixture\Exception\InvalidOverrideException::class)]
80#[UsesClass(\SqlFixture\Plan\Parsing\PlanStatements::class)]
81#[UsesClass(\SqlFixture\Plan\Parsing\RelationCursor::class)]
82#[UsesClass(\SqlFixture\Plan\Parsing\RelationReader::class)]
83#[UsesClass(\SqlFixture\Plan\PlanStructureException::class)]
84#[UsesClass(\SqlFixture\Plan\Printing\PlanTables::class)]
85#[UsesClass(\SqlFixture\Plan\Printing\RelationGroups::class)]
86#[UsesClass(\SqlFixture\Plan\Printing\StatementPrinter::class)]
87#[UsesClass(\SqlFixture\Plan\Validation\PlanValidation::class)]
88#[UsesClass(\SqlFixture\Plan\Validation\TableName::class)]
89#[UsesClass(\SqlFixture\Platform\MySql\Schema\ColumnParser::class)]
90#[UsesClass(\SqlFixture\Platform\MySql\Schema\CreateTableQuery::class)]
91#[UsesClass(\SqlFixture\Platform\MySql\Schema\DefaultExpression::class)]
92#[UsesClass(\SqlFixture\Platform\MySql\Schema\DefinitionIntegrity::class)]
93#[UsesClass(\SqlFixture\Platform\MySql\Schema\IdentifierQuoter::class)]
94#[UsesClass(\SqlFixture\Platform\MySql\Schema\TableDefinition::class)]
95#[UsesClass(\SqlFixture\Platform\MySql\Schema\TypeParameters::class)]
96#[UsesClass(\SqlFixture\Platform\MySql\Value\ColumnGenerator::class)]
97#[UsesClass(\SqlFixture\Platform\MySql\Value\DecimalGenerator::class)]
98#[UsesClass(\SqlFixture\Platform\MySql\Value\GeometryGenerator::class)]
99#[UsesClass(\SqlFixture\Platform\MySql\Value\IntegerGenerator::class)]
100#[UsesClass(\SqlFixture\Platform\MySql\Value\NumericGenerator::class)]
101#[UsesClass(\SqlFixture\Platform\MySql\Value\SpatialGenerator::class)]
102#[UsesClass(\SqlFixture\Platform\MySql\Value\StringGenerator::class)]
103#[UsesClass(\SqlFixture\Platform\MySql\Value\TextGenerator::class)]
104#[UsesClass(\SqlFixture\Platform\PostgreSql\Schema\CatalogColumn::class)]
105#[UsesClass(\SqlFixture\Platform\PostgreSql\Schema\CatalogDdl::class)]
106#[UsesClass(\SqlFixture\Platform\PostgreSql\Schema\CatalogQuery::class)]
107#[UsesClass(\SqlFixture\Platform\PostgreSql\Schema\CatalogSchema::class)]
108#[UsesClass(\SqlFixture\Platform\PostgreSql\Schema\ColumnParser::class)]
109#[UsesClass(\SqlFixture\Platform\PostgreSql\Schema\DefaultExpression::class)]
110#[UsesClass(\SqlFixture\Platform\PostgreSql\Schema\DefinitionList::class)]
111#[UsesClass(\SqlFixture\Platform\PostgreSql\Schema\TableSyntax::class)]
112#[UsesClass(\SqlFixture\Platform\PostgreSql\Schema\TypeDeclaration::class)]
113#[UsesClass(\SqlFixture\Platform\PostgreSql\Value\ColumnGenerator::class)]
114#[UsesClass(\SqlFixture\Platform\PostgreSql\Value\DecimalGenerator::class)]
115#[UsesClass(\SqlFixture\Platform\PostgreSql\Value\NumericGenerator::class)]
116#[UsesClass(\SqlFixture\Platform\PostgreSql\Value\StringGenerator::class)]
117#[UsesClass(\SqlFixture\Platform\PostgreSql\Value\StructuredGenerator::class)]
118#[UsesClass(\SqlFixture\Platform\PostgreSql\Value\TemporalGenerator::class)]
119#[UsesClass(\SqlFixture\Platform\Sqlite\Schema\ColumnParser::class)]
120#[UsesClass(\SqlFixture\Platform\Sqlite\Schema\CreateTableQuery::class)]
121#[UsesClass(\SqlFixture\Platform\Sqlite\Schema\DefaultExpression::class)]
122#[UsesClass(\SqlFixture\Platform\Sqlite\Schema\DefinitionList::class)]
123#[UsesClass(\SqlFixture\Platform\Sqlite\Schema\PragmaColumn::class)]
124#[UsesClass(\SqlFixture\Platform\Sqlite\Schema\PragmaSchema::class)]
125#[UsesClass(\SqlFixture\Platform\Sqlite\Schema\TableSyntax::class)]
126#[UsesClass(\SqlFixture\Platform\Sqlite\Schema\TypeDeclaration::class)]
127#[UsesClass(\SqlFixture\Platform\Sqlite\Value\ColumnGenerator::class)]
128#[UsesClass(\SqlFixture\Platform\Sqlite\Value\TypeAffinity::class)]
129#[UsesClass(\SqlFixture\Schema\DefinitionSegments::class)]
130#[UsesClass(\SqlFixture\Schema\TypeShape::class)]
131#[UsesClass(\SqlFixture\TypeMapper\ParagraphGenerator::class)]
132#[UsesClass(\SqlFixture\Platform\MySql\Schema\TableDefinitionInput::class)]
133#[UsesClass(\SqlFixture\Hydrator\Exception\ClassNotFoundException::class)]
134#[UsesClass(\SqlFixture\Hydrator\Exception\MissingConstructorArgumentException::class)]
135#[UsesClass(\SqlFixture\Fixture\Exception\UnknownOverrideColumnException::class)]
136#[UsesClass(\SqlFixture\Fixture\Exception\NullOverrideException::class)]
137#[UsesClass(\SqlFixture\Fixture\Exception\GeneratedColumnOverrideException::class)]
138#[UsesClass(\SqlFixture\Fixture\Exception\GeneratedColumnReferenceException::class)]
139#[UsesClass(\SqlFixture\Fixture\Exception\MissingRelationValueException::class)]
140#[UsesClass(\SqlFixture\Fixture\Exception\UnknownPlanColumnException::class)]
141#[UsesClass(\SqlFixture\Plan\Exception\EmptyPlanException::class)]
142#[UsesClass(\SqlFixture\Plan\Exception\EmptyTableNameException::class)]
143#[UsesClass(\SqlFixture\Plan\Exception\MissingEndpointColumnsException::class)]
144#[UsesClass(\SqlFixture\Plan\Exception\InvalidTableNameException::class)]
145#[UsesClass(\SqlFixture\Plan\Exception\UnbalancedBracketsException::class)]
146#[UsesClass(\SqlFixture\Plan\Exception\UnexpectedPlanTokenException::class)]
147#[UsesClass(\SqlFixture\Plan\Exception\UnsupportedManyToManyException::class)]
148#[UsesClass(\SqlFixture\Plan\Exception\CompositeArityMismatchException::class)]
149#[UsesClass(\SqlFixture\Plan\Exception\DuplicateColumnBindingException::class)]
150#[UsesClass(\SqlFixture\Plan\Exception\CyclicDependencyException::class)]
151#[UsesClass(\SqlFixture\Plan\Exception\UnboundedSelfReferenceException::class)]
152#[UsesClass(\SqlFixture\Schema\Exception\InvalidSqlException::class)]
153#[UsesClass(\SqlFixture\Schema\Exception\ExpectedCreateTableException::class)]
154#[UsesClass(\SqlFixture\Schema\Exception\MissingColumnDefinitionsException::class)]
155#[UsesClass(\SqlFixture\TypeMapper\IntegerRange::class)]
156#[UsesClass(\SqlFixture\TypeMapper\IntegerWidth::class)]
157#[UsesClass(\SqlFixture\TypeMapper\DecimalRange::class)]
158#[UsesClass(\SqlFixture\Hydrator\Reflection\ConversionTarget::class)]
159#[UsesClass(\SqlFixture\Fixture\Choice\InverseRelations::class)]
160#[UsesClass(\SqlFixture\Fixture\Choice\ResolvedRow::class)]
161#[UsesClass(\SqlFixture\Fixture\Choice\RowChoices::class)]
162#[UsesClass(\SqlFixture\Plan\Choice\ChoiceValidation::class)]
163#[UsesClass(\SqlFixture\Plan\Choice\PlanContents::class)]
164#[UsesClass(\SqlFixture\Fixture\Generation\RowBindings::class)]
165#[UsesClass(\SqlFixture\Fixture\Generation\RecursiveRelationException::class)]
166#[UsesClass(\SqlFixture\Fixture\Generation\RelationValueException::class)]
167#[UsesClass(\SqlFixture\Plan\Choice\ChoiceDefinitionException::class)]
168#[UsesClass(\SqlFixture\Plan\Choice\ChoiceLiteral::class)]
169#[UsesClass(\SqlFixture\Plan\Choice\ChoiceSyntax::class)]
170#[UsesClass(\SqlFixture\Plan\Choice\ChoiceCase::class)]
171#[UsesClass(\SqlFixture\Fixture\Choice\ChoiceBindings::class)]
172#[UsesClass(\SqlFixture\Fixture\Choice\ChoiceValueException::class)]
173#[UsesClass(\SqlFixture\Fixture\Choice\CaseSelection::class)]
174#[UsesClass(\SqlFixture\Plan\RelationChoice::class)]
175#[UsesClass(\SqlFixture\Fixture\RowGenerator::class)]
176#[UsesClass(\SqlFixture\Version\ServerVersion::class)]
177#[UsesClass(\SqlFixture\Version\Releases::class)]
178#[UsesClass(\SqlFixture\Version\ReleaseNumber::class)]
179#[UsesClass(\SqlFixture\Version\UnsupportedVersionException::class)]
180final class FixtureProviderTest extends TestCase
181{
182    #[Test]
183    public function testFixtureReturnsArray(): void
184    {
185        $faker = Factory::create();
186        $faker->seed(12345);
187        $data = (new FixtureProvider($faker))->fixture(
188            'CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255) NOT NULL)',
189        );
190
191        self::assertArrayHasKey('name', $data);
192        self::assertIsString($data['name']);
193    }
194
195    #[Test]
196    public function testFixtureWithOverrides(): void
197    {
198        $faker = Factory::create();
199        $faker->seed(12345);
200        $data = (new FixtureProvider($faker))->fixture(
201            'CREATE TABLE users (id INT, name VARCHAR(255))',
202            ['name' => 'Overridden'],
203        );
204
205        self::assertSame('Overridden', $data['name']);
206    }
207
208    #[Test]
209    public function testFixtureHydratesClass(): void
210    {
211        $target = new class (0, '') {
212            public function __construct(
213                public readonly int $id,
214                public readonly string $name,
215            ) {
216            }
217        };
218        $faker = Factory::create();
219        $faker->seed(12345);
220        $user = (new FixtureProvider($faker))->fixture(
221            'CREATE TABLE users (id INT, name VARCHAR(255))',
222            ['id' => 1, 'name' => 'Test User'],
223            $target::class,
224        );
225
226        self::assertSame(1, $user->id);
227        self::assertSame('Test User', $user->name);
228    }
229
230    #[Test]
231    public function testFixtureSkipsAutoIncrement(): void
232    {
233        $faker = Factory::create();
234        $faker->seed(12345);
235        $data = (new FixtureProvider($faker))->fixture(
236            'CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))',
237        );
238
239        self::assertArrayNotHasKey('id', $data);
240        self::assertArrayHasKey('name', $data);
241    }
242
243    #[Test]
244    public function testFixtureCanOverrideAutoIncrement(): void
245    {
246        $faker = Factory::create();
247        $faker->seed(12345);
248        $data = (new FixtureProvider($faker))->fixture(
249            'CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))',
250            ['id' => 42],
251        );
252
253        self::assertSame(42, $data['id']);
254    }
255
256    #[Test]
257    public function testFixtureWithAllNumericTypes(): void
258    {
259        $faker = Factory::create();
260        $faker->seed(12345);
261        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
262            CREATE TABLE numbers (
263                col_tinyint TINYINT NOT NULL,
264                col_smallint SMALLINT NOT NULL,
265                col_mediumint MEDIUMINT NOT NULL,
266                col_int INT NOT NULL,
267                col_bigint BIGINT NOT NULL,
268                col_float FLOAT NOT NULL,
269                col_double DOUBLE NOT NULL,
270                col_decimal DECIMAL(10,2) NOT NULL
271            )
272            SQL);
273
274        self::assertIsInt($data['col_tinyint']);
275        self::assertIsInt($data['col_smallint']);
276        self::assertIsInt($data['col_mediumint']);
277        self::assertIsInt($data['col_int']);
278        self::assertIsInt($data['col_bigint']);
279        self::assertIsFloat($data['col_float']);
280        self::assertIsFloat($data['col_double']);
281        self::assertIsFloat($data['col_decimal']);
282    }
283
284    #[Test]
285    public function testFixtureWithStringTypes(): void
286    {
287        $faker = Factory::create();
288        $faker->seed(12345);
289        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
290            CREATE TABLE strings (
291                col_char CHAR(10) NOT NULL,
292                col_varchar VARCHAR(100) NOT NULL,
293                col_text TEXT NOT NULL,
294                col_mediumtext MEDIUMTEXT NOT NULL
295            )
296            SQL);
297
298        self::assertIsString($data['col_char']);
299        self::assertSame(10, strlen($data['col_char']));
300        self::assertIsString($data['col_varchar']);
301        self::assertLessThanOrEqual(100, strlen($data['col_varchar']));
302        self::assertIsString($data['col_text']);
303        self::assertIsString($data['col_mediumtext']);
304    }
305
306    #[Test]
307    public function testFixtureWithDateTypes(): void
308    {
309        $faker = Factory::create();
310        $faker->seed(12345);
311        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
312            CREATE TABLE dates (
313                col_date DATE NOT NULL,
314                col_time TIME NOT NULL,
315                col_datetime DATETIME NOT NULL,
316                col_timestamp TIMESTAMP NOT NULL,
317                col_year YEAR NOT NULL
318            )
319            SQL);
320
321        self::assertIsString($data['col_date']);
322        self::assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}$/', $data['col_date']);
323        self::assertIsString($data['col_time']);
324        self::assertMatchesRegularExpression('/^\d{2}:\d{2}:\d{2}$/', $data['col_time']);
325        self::assertIsString($data['col_datetime']);
326        self::assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $data['col_datetime']);
327        self::assertIsInt($data['col_year']);
328    }
329
330    #[Test]
331    public function testFixtureWithEnum(): void
332    {
333        $faker = Factory::create();
334        $faker->seed(12345);
335        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
336            CREATE TABLE statuses (
337                status ENUM('active','inactive','pending') NOT NULL
338            )
339            SQL);
340
341        self::assertContains($data['status'], ['active', 'inactive', 'pending']);
342    }
343
344    #[Test]
345    public function testFixtureWithSet(): void
346    {
347        $faker = Factory::create();
348        $faker->seed(12345);
349        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
350            CREATE TABLE permissions (
351                perms SET('read','write','delete') NOT NULL
352            )
353            SQL);
354
355        self::assertIsString($data['perms']);
356        $parts = explode(',', $data['perms']);
357        array_walk($parts, static function (string $part): void {
358            self::assertContains($part, ['read', 'write', 'delete']);
359        });
360    }
361
362    #[Test]
363    public function testFixtureWithJson(): void
364    {
365        $faker = Factory::create();
366        $faker->seed(12345);
367        $data = (new FixtureProvider($faker))->fixture('CREATE TABLE jsons (data JSON NOT NULL)');
368
369        self::assertIsString($data['data']);
370        $decoded = json_decode($data['data'], true);
371        self::assertIsArray($decoded);
372    }
373
374    #[Test]
375    public function testFixtureWithSpatialTypes(): void
376    {
377        $faker = Factory::create();
378        $faker->seed(12345);
379        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
380            CREATE TABLE geo (
381                col_point POINT NOT NULL,
382                col_linestring LINESTRING NOT NULL,
383                col_polygon POLYGON NOT NULL
384            )
385            SQL);
386
387        self::assertIsString($data['col_point']);
388        self::assertStringStartsWith('POINT(', $data['col_point']);
389        self::assertIsString($data['col_linestring']);
390        self::assertStringStartsWith('LINESTRING(', $data['col_linestring']);
391        self::assertIsString($data['col_polygon']);
392        self::assertStringStartsWith('POLYGON((', $data['col_polygon']);
393    }
394
395    #[Test]
396    public function testFixtureResultIsReproducibleWithSeed(): void
397    {
398        $faker1 = Factory::create();
399        $faker1->seed(99999);
400        $data1 = (new FixtureProvider($faker1))->fixture('CREATE TABLE test (name VARCHAR(255))');
401        $faker2 = Factory::create();
402        $faker2->seed(99999);
403        $data2 = (new FixtureProvider($faker2))->fixture('CREATE TABLE test (name VARCHAR(255))');
404
405        self::assertSame($data1['name'], $data2['name']);
406    }
407
408    #[Test]
409    public function testFixtureCachesSchema(): void
410    {
411        $faker = Factory::create();
412        $faker->seed(12345);
413        $provider = new FixtureProvider($faker);
414        $sql = 'CREATE TABLE cache_test (id INT, name VARCHAR(255))';
415
416        $data1 = $provider->fixture($sql, ['name' => 'First']);
417        $data2 = $provider->fixture($sql, ['name' => 'Second']);
418
419        self::assertSame('First', $data1['name']);
420        self::assertSame('Second', $data2['name']);
421    }
422
423    #[Test]
424    public function testGetFixtureGenerator(): void
425    {
426        $faker = Factory::create();
427        $faker->seed(12345);
428        $generator = (new FixtureProvider($faker))->getFixtureGenerator();
429        self::assertSame(['id' => 7], $generator->generate(new TableSchema('sample', ['id' => new ColumnDefinition('id', 'INT')]), ['id' => 7]));
430    }
431
432    #[Test]
433    public function testGetDialectDefaultsToMysql(): void
434    {
435        $faker = Factory::create();
436        $faker->seed(12345);
437        self::assertSame('mysql', (new FixtureProvider($faker))->getDialect());
438    }
439
440    #[Test]
441    #[\PHPUnit\Framework\Attributes\DataProvider('providerNullableSeeds')]
442    public function testFixtureWithNullableColumns(int $seed): void
443    {
444        $faker = Factory::create();
445        $faker->seed($seed);
446        $provider = new FixtureProvider($faker);
447        $sql = 'CREATE TABLE test (id INT NOT NULL, name VARCHAR(255) DEFAULT NULL, notes TEXT)';
448
449        $data = $provider->fixture($sql);
450        self::assertArrayHasKey('id', $data);
451        self::assertIsInt($data['id']);
452    }
453
454    #[Test]
455    public function testFixtureWithBinaryColumns(): void
456    {
457        $faker = Factory::create();
458        $faker->seed(12345);
459        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
460            CREATE TABLE bins (
461                col_binary BINARY(16) NOT NULL,
462                col_varbinary VARBINARY(100) NOT NULL,
463                col_blob BLOB NOT NULL
464            )
465            SQL);
466
467        self::assertIsString($data['col_binary']);
468        self::assertSame(16, strlen($data['col_binary']));
469        self::assertIsString($data['col_varbinary']);
470        self::assertLessThanOrEqual(100, strlen($data['col_varbinary']));
471        self::assertIsString($data['col_blob']);
472    }
473
474    #[Test]
475    public function testFixtureWithBooleanType(): void
476    {
477        $faker = Factory::create();
478        $faker->seed(12345);
479        $data = (new FixtureProvider($faker))->fixture('CREATE TABLE test (active BOOLEAN NOT NULL)');
480        self::assertIsBool($data['active']);
481    }
482
483    #[Test]
484    public function testFixtureWithBitType(): void
485    {
486        $faker = Factory::create();
487        $faker->seed(12345);
488        $data = (new FixtureProvider($faker))->fixture('CREATE TABLE test (flags BIT(8) NOT NULL)');
489        self::assertIsInt($data['flags']);
490        self::assertGreaterThanOrEqual(0, $data['flags']);
491        self::assertLessThanOrEqual(255, $data['flags']);
492    }
493
494    #[Test]
495    public function testFixtureWithGeneratedColumnSkipped(): void
496    {
497        $faker = Factory::create();
498        $faker->seed(12345);
499        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
500            CREATE TABLE test (
501                a INT,
502                b INT,
503                c INT GENERATED ALWAYS AS (a + b) STORED
504            )
505            SQL);
506
507        self::assertArrayHasKey('a', $data);
508        self::assertArrayHasKey('b', $data);
509        self::assertArrayNotHasKey('c', $data);
510    }
511
512    #[Test]
513    public function testFixtureWithUnsignedTypes(): void
514    {
515        $faker = Factory::create();
516        $faker->seed(12345);
517        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
518            CREATE TABLE test (
519                col_uint INT UNSIGNED NOT NULL,
520                col_utinyint TINYINT UNSIGNED NOT NULL
521            )
522            SQL);
523
524        self::assertIsInt($data['col_uint']);
525        self::assertGreaterThanOrEqual(0, $data['col_uint']);
526        self::assertIsInt($data['col_utinyint']);
527        self::assertGreaterThanOrEqual(0, $data['col_utinyint']);
528        self::assertLessThanOrEqual(255, $data['col_utinyint']);
529    }
530
531    #[Test]
532    public function testFixtureWithDialectOverride(): void
533    {
534        $faker = Factory::create();
535        $faker->seed(12345);
536        $provider = new FixtureProvider($faker, dialect: 'mysql');
537
538        $data = $provider->fixture(
539            'CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)',
540            [],
541            null,
542            'sqlite',
543        );
544
545        self::assertArrayHasKey('name', $data);
546    }
547
548    #[Test]
549    public function testFixtureWithSqliteDialect(): void
550    {
551        $faker = Factory::create();
552        $faker->seed(12345);
553        $provider = new FixtureProvider($faker, dialect: 'sqlite');
554
555        self::assertSame('sqlite', $provider->getDialect());
556
557        $data = $provider->fixture(
558            'CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)',
559        );
560
561        self::assertArrayHasKey('name', $data);
562    }
563
564    #[Test]
565    public function testFixtureWithCustomTypeMapper(): void
566    {
567        $faker = Factory::create();
568        $faker->seed(12345);
569        $typeMapper = new MySqlTypeMapper();
570        $provider = new FixtureProvider($faker, typeMapper: $typeMapper);
571
572        $data = $provider->fixture('CREATE TABLE test (id INT, name VARCHAR(255))');
573
574        self::assertArrayHasKey('id', $data);
575    }
576
577    #[Test]
578    public function testFixtureWithCustomSchemaParser(): void
579    {
580        $faker = Factory::create();
581        $faker->seed(12345);
582        $parser = new MySqlSchemaParser();
583        $provider = new FixtureProvider($faker, schemaParser: $parser);
584
585        $data = $provider->fixture('CREATE TABLE test (id INT, name VARCHAR(255))');
586
587        self::assertArrayHasKey('id', $data);
588    }
589
590    #[Test]
591    public function testSchemaCacheUsesDialectInKey(): void
592    {
593        $faker = Factory::create();
594        $faker->seed(12345);
595        $provider = new FixtureProvider($faker, dialect: 'mysql');
596
597        $sql = 'CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT NOT NULL)';
598        $data1 = $provider->fixture($sql, [], null, 'sqlite');
599        $data2 = $provider->fixture($sql, [], null, 'sqlite');
600
601        self::assertArrayHasKey('name', $data1);
602        self::assertArrayHasKey('name', $data2);
603    }
604
605    #[Test]
606    public function testSchemaCacheDistinguishesDifferentDialects(): void
607    {
608        $faker = Factory::create();
609        $faker->seed(12345);
610        $provider = new FixtureProvider($faker, dialect: 'mysql');
611
612        $sql = 'CREATE TABLE test (id INTEGER NOT NULL, name TEXT NOT NULL)';
613
614        $faker->seed(12345);
615        $dataMysql = $provider->fixture($sql);
616
617        $faker->seed(12345);
618        $dataSqlite = $provider->fixture($sql, [], null, 'sqlite');
619
620        self::assertArrayHasKey('name', $dataMysql);
621        self::assertArrayHasKey('name', $dataSqlite);
622    }
623
624    #[Test]
625    public function testDefaultDialectIsUsedWhenNullDialectPassed(): void
626    {
627        $faker = Factory::create();
628        $faker->seed(12345);
629        $provider = new FixtureProvider($faker, dialect: 'sqlite');
630
631        $data = $provider->fixture(
632            'CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT NOT NULL)',
633            [],
634            null,
635            null,
636        );
637
638        self::assertArrayHasKey('name', $data);
639        self::assertSame('sqlite', $provider->getDialect());
640    }
641
642    #[Test]
643    public function testFixtureSameDialectUsesInternalParser(): void
644    {
645        $faker = Factory::create();
646        $faker->seed(12345);
647        $provider = new FixtureProvider($faker, dialect: 'mysql');
648
649        $data = $provider->fixture(
650            'CREATE TABLE test (id INT, name VARCHAR(255))',
651            [],
652            null,
653            'mysql',
654        );
655
656        self::assertArrayHasKey('name', $data);
657    }
658
659    #[Test]
660    public function testCacheDistinguishesDifferentSqlSameDialect(): void
661    {
662        $faker = Factory::create();
663        $faker->seed(12345);
664        $provider = new FixtureProvider($faker, dialect: 'mysql');
665
666        $data1 = $provider->fixture(
667            'CREATE TABLE test1 (id INT NOT NULL, name VARCHAR(255) NOT NULL)',
668        );
669        $data2 = $provider->fixture(
670            'CREATE TABLE test2 (age INT NOT NULL, email VARCHAR(100) NOT NULL)',
671        );
672
673        self::assertArrayHasKey('name', $data1);
674        self::assertArrayNotHasKey('email', $data1);
675        self::assertArrayHasKey('email', $data2);
676        self::assertArrayNotHasKey('name', $data2);
677    }
678
679    #[Test]
680    public function testCacheKeyIncludesDialect(): void
681    {
682        $faker = Factory::create();
683        $faker->seed(12345);
684        $provider = new FixtureProvider($faker, dialect: 'mysql');
685
686        $sql = 'CREATE TABLE test (id INTEGER NOT NULL, name TEXT NOT NULL)';
687
688        $dataMysql = $provider->fixture($sql);
689        $dataSqlite = $provider->fixture($sql, [], null, 'sqlite');
690
691        self::assertArrayHasKey('id', $dataMysql);
692        self::assertArrayHasKey('id', $dataSqlite);
693        self::assertArrayHasKey('name', $dataMysql);
694        self::assertArrayHasKey('name', $dataSqlite);
695    }
696
697    #[Test]
698    public function testDialectOverrideUsesOverrideNotDefault(): void
699    {
700        $faker = Factory::create();
701        $faker->seed(12345);
702        $provider = new FixtureProvider($faker, dialect: 'mysql');
703
704        $sql = 'CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)';
705
706        $data = $provider->fixture($sql, [], null, 'sqlite');
707
708        self::assertArrayNotHasKey('id', $data);
709        self::assertArrayHasKey('name', $data);
710    }
711
712    #[Test]
713    public function testCachedSchemaDistinguishesDialects(): void
714    {
715        $faker = Factory::create();
716        $faker->seed(12345);
717        $provider = new FixtureProvider($faker, dialect: 'mysql');
718
719        $sql = 'CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)';
720
721        $provider->fixture($sql, ['name' => 'first'], null, 'sqlite');
722
723        $data2 = $provider->fixture($sql, ['name' => 'second'], null, 'sqlite');
724
725        self::assertArrayNotHasKey('id', $data2);
726        self::assertSame('second', $data2['name']);
727    }
728
729    #[Test]
730    public function testCacheSeparatesDefaultAndOverrideDialect(): void
731    {
732        $faker = Factory::create();
733        $faker->seed(12345);
734        $provider = new FixtureProvider($faker, dialect: 'sqlite');
735
736        $sql = 'CREATE TABLE test (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL)';
737
738        $dataSqlite = $provider->fixture($sql);
739        self::assertArrayHasKey('id', $dataSqlite);
740
741        $dataMysql = $provider->fixture($sql, [], null, 'mysql');
742        self::assertArrayNotHasKey('id', $dataMysql);
743    }
744
745    #[Test]
746    public function testCustomTypeMapperIsPreserved(): void
747    {
748        $faker = Factory::create();
749        $faker->seed(12345);
750        $provider = new FixtureProvider($faker, typeMapper: new PostgreSqlTypeMapper());
751
752        $data = $provider->fixture('CREATE TABLE test (val YEAR NOT NULL)');
753
754        self::assertIsString($data['val']);
755    }
756
757    #[Test]
758    public function testCustomSchemaParserIsPreserved(): void
759    {
760        $faker = Factory::create();
761        $faker->seed(12345);
762        $parser = new SqliteSchemaParser();
763        $provider = new FixtureProvider($faker, schemaParser: $parser);
764
765        $data = $provider->fixture('CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
766
767        self::assertArrayNotHasKey('id', $data);
768        self::assertArrayHasKey('name', $data);
769    }
770
771    #[Test]
772    public function testRegisterSchemaReturnsTheParsedTable(): void
773    {
774        $faker = Factory::create();
775        $faker->seed(12345);
776        $provider = new FixtureProvider($faker);
777
778        $schema = $provider->registerSchema('CREATE TABLE test (id INT NOT NULL, name VARCHAR(255) NOT NULL)');
779
780        self::assertSame('test', $schema->tableName);
781        self::assertCount(2, $schema->columns);
782    }
783
784    #[Test]
785    public function testRegisterSchemaARegisteredSchemaBecomesAvailableToFixtures(): void
786    {
787        $faker = Factory::create();
788        $faker->seed(12345);
789        $provider = new FixtureProvider($faker);
790        $provider->registerSchema('CREATE TABLE orders (id INT AUTO_INCREMENT PRIMARY KEY, status VARCHAR(20) NOT NULL)');
791
792        self::assertTrue($provider->getSchemaResolver()->has('orders'));
793        self::assertSame('orders', $provider->getSchemaResolver()->resolve('orders')->tableName);
794    }
795
796    #[Test]
797    public function testGetSchemaResolverGeneratingAFixtureAlsoMakesItAvailableToFixtures(): void
798    {
799        $faker = Factory::create();
800        $faker->seed(12345);
801        $provider = new FixtureProvider($faker);
802        $provider->fixture('CREATE TABLE orders (id INT AUTO_INCREMENT PRIMARY KEY, status VARCHAR(20) NOT NULL)');
803
804        self::assertTrue($provider->getSchemaResolver()->has('orders'));
805    }
806    public function testFixturesGeneratesLinkedRowsFromRegisteredSchemas(): void
807    {
808        $provider = new FixtureProvider(Factory::create());
809        $provider->registerSchema('CREATE TABLE users (id INT PRIMARY KEY)');
810        $provider->registerSchema('CREATE TABLE posts (user_id INT)');
811        $fixtures = $provider->fixtures('users.id < posts.user_id', ['users' => ['id' => 9], 'posts' => 2]);
812        self::assertSame([['user_id' => 9], ['user_id' => 9]], $fixtures->rows('posts'));
813    }
814    /**
815     * @return list<array{int}>
816     */
817    public static function providerNullableSeeds(): array
818    {
819        return array_map(static fn (int $seed): array => [$seed], range(1, 20));
820    }
821
822    /**
823     * @return list<array{string}>
824     */
825    public static function providerChoiceDialects(): array
826    {
827        return [['mysql'], ['pgsql'], ['sqlite']];
828    }
829
830    #[\PHPUnit\Framework\Attributes\DataProvider('providerChoiceDialects')]
831    public function testFixturesSelectsRelationsForEveryDialect(string $dialect): void
832    {
833        $provider = new FixtureProvider(Factory::create(), dialect: $dialect);
834        $provider->registerSchema('CREATE TABLE comments (target_type VARCHAR(10), target_id INTEGER)');
835        $provider->registerSchema('CREATE TABLE posts (id INTEGER NOT NULL)');
836        $provider->registerSchema('CREATE TABLE videos (id INTEGER NOT NULL)');
837        $set = $provider->fixtures(
838            "choice comments.target_type { 'post' { comments.target_id > posts.id } 'video' { comments.target_id > videos.id } }",
839            ['comments' => [['target_type' => 'post'], ['target_type' => 'video']], 'posts' => ['id' => 11], 'videos' => ['id' => 22]],
840        );
841        self::assertSame([11, 22], array_column($set->rows('comments'), 'target_id'));
842        self::assertSame([['id' => 11]], $set->rows('posts'));
843        self::assertSame([['id' => 22]], $set->rows('videos'));
844    }
845
846    public function testGetVersionDefaultsToTheNewestShippedMysql(): void
847    {
848        $faker = Factory::create();
849
850        self::assertSame('mysql-8.4.7', (new FixtureProvider($faker))->getVersion());
851        self::assertSame('pg-17.2', (new FixtureProvider($faker, dialect: PlatformFactory::DRIVER_PGSQL))->getVersion());
852    }
853
854    public function testGetVersionKeepsTheSelectedRelease(): void
855    {
856        $provider = new FixtureProvider(Factory::create(), version: 'mysql-5.7.44');
857
858        self::assertSame('mysql-5.7.44', $provider->getVersion());
859        self::assertArrayHasKey('name', $provider->fixture('CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL)'));
860    }
861
862    public function testGetVersionRejectsATagOfAnotherDialect(): void
863    {
864        $this->expectException(\SqlFixture\Version\UnsupportedVersionException::class);
865        $this->expectExceptionMessage('Unsupported mysql version: pg-17.2');
866        new FixtureProvider(Factory::create(), version: 'pg-17.2');
867    }
868
869    public function testFixtureReadsOneStatementForAnotherRelease(): void
870    {
871        $provider = new FixtureProvider(Factory::create());
872
873        $row = $provider->fixture('CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL)', version: 'mysql-5.6.51');
874        self::assertArrayHasKey('name', $row);
875        self::assertSame('mysql-8.4.7', $provider->getVersion());
876
877        $this->expectException(\SqlFixture\Version\UnsupportedVersionException::class);
878        $provider->fixture('CREATE TABLE users (id INTEGER PRIMARY KEY)', dialect: PlatformFactory::DRIVER_SQLITE, version: 'mysql-8.4.7');
879    }
880}
881