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

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit;
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\FixtureGenerator;
13use SqlFixture\FixtureProvider;
14use SqlFixture\Hydrator\ReflectionHydrator;
15use SqlFixture\Platform\MySql\MySqlSchemaParser;
16use SqlFixture\Platform\MySql\MySqlTypeMapper;
17use SqlFixture\Platform\PlatformFactory;
18use SqlFixture\Platform\PostgreSql\PostgreSqlTypeMapper;
19use SqlFixture\Platform\Sqlite\SqliteSchemaParser;
20use SqlFixture\Platform\Sqlite\SqliteTypeMapper;
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\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)]
159final class FixtureProviderTest extends TestCase
160{
161    #[Test]
162    public function testFixtureReturnsArray(): void
163    {
164        $faker = Factory::create();
165        $faker->seed(12345);
166        $data = (new FixtureProvider($faker))->fixture(
167            'CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255) NOT NULL)',
168        );
169
170        self::assertArrayHasKey('name', $data);
171        self::assertIsString($data['name']);
172    }
173
174    #[Test]
175    public function testFixtureWithOverrides(): void
176    {
177        $faker = Factory::create();
178        $faker->seed(12345);
179        $data = (new FixtureProvider($faker))->fixture(
180            'CREATE TABLE users (id INT, name VARCHAR(255))',
181            ['name' => 'Overridden'],
182        );
183
184        self::assertSame('Overridden', $data['name']);
185    }
186
187    #[Test]
188    public function testFixtureHydratesClass(): void
189    {
190        $target = new class (0, '') {
191            public function __construct(
192                public readonly int $id,
193                public readonly string $name,
194            ) {
195            }
196        };
197        $faker = Factory::create();
198        $faker->seed(12345);
199        $user = (new FixtureProvider($faker))->fixture(
200            'CREATE TABLE users (id INT, name VARCHAR(255))',
201            ['id' => 1, 'name' => 'Test User'],
202            $target::class,
203        );
204
205        self::assertSame(1, $user->id);
206        self::assertSame('Test User', $user->name);
207    }
208
209    #[Test]
210    public function testFixtureSkipsAutoIncrement(): void
211    {
212        $faker = Factory::create();
213        $faker->seed(12345);
214        $data = (new FixtureProvider($faker))->fixture(
215            'CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))',
216        );
217
218        self::assertArrayNotHasKey('id', $data);
219        self::assertArrayHasKey('name', $data);
220    }
221
222    #[Test]
223    public function testFixtureCanOverrideAutoIncrement(): void
224    {
225        $faker = Factory::create();
226        $faker->seed(12345);
227        $data = (new FixtureProvider($faker))->fixture(
228            'CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))',
229            ['id' => 42],
230        );
231
232        self::assertSame(42, $data['id']);
233    }
234
235    #[Test]
236    public function testFixtureWithAllNumericTypes(): void
237    {
238        $faker = Factory::create();
239        $faker->seed(12345);
240        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
241            CREATE TABLE numbers (
242                col_tinyint TINYINT NOT NULL,
243                col_smallint SMALLINT NOT NULL,
244                col_mediumint MEDIUMINT NOT NULL,
245                col_int INT NOT NULL,
246                col_bigint BIGINT NOT NULL,
247                col_float FLOAT NOT NULL,
248                col_double DOUBLE NOT NULL,
249                col_decimal DECIMAL(10,2) NOT NULL
250            )
251            SQL);
252
253        self::assertIsInt($data['col_tinyint']);
254        self::assertIsInt($data['col_smallint']);
255        self::assertIsInt($data['col_mediumint']);
256        self::assertIsInt($data['col_int']);
257        self::assertIsInt($data['col_bigint']);
258        self::assertIsFloat($data['col_float']);
259        self::assertIsFloat($data['col_double']);
260        self::assertIsFloat($data['col_decimal']);
261    }
262
263    #[Test]
264    public function testFixtureWithStringTypes(): void
265    {
266        $faker = Factory::create();
267        $faker->seed(12345);
268        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
269            CREATE TABLE strings (
270                col_char CHAR(10) NOT NULL,
271                col_varchar VARCHAR(100) NOT NULL,
272                col_text TEXT NOT NULL,
273                col_mediumtext MEDIUMTEXT NOT NULL
274            )
275            SQL);
276
277        self::assertIsString($data['col_char']);
278        self::assertSame(10, strlen($data['col_char']));
279        self::assertIsString($data['col_varchar']);
280        self::assertLessThanOrEqual(100, strlen($data['col_varchar']));
281        self::assertIsString($data['col_text']);
282        self::assertIsString($data['col_mediumtext']);
283    }
284
285    #[Test]
286    public function testFixtureWithDateTypes(): void
287    {
288        $faker = Factory::create();
289        $faker->seed(12345);
290        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
291            CREATE TABLE dates (
292                col_date DATE NOT NULL,
293                col_time TIME NOT NULL,
294                col_datetime DATETIME NOT NULL,
295                col_timestamp TIMESTAMP NOT NULL,
296                col_year YEAR NOT NULL
297            )
298            SQL);
299
300        self::assertIsString($data['col_date']);
301        self::assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}$/', $data['col_date']);
302        self::assertIsString($data['col_time']);
303        self::assertMatchesRegularExpression('/^\d{2}:\d{2}:\d{2}$/', $data['col_time']);
304        self::assertIsString($data['col_datetime']);
305        self::assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $data['col_datetime']);
306        self::assertIsInt($data['col_year']);
307    }
308
309    #[Test]
310    public function testFixtureWithEnum(): void
311    {
312        $faker = Factory::create();
313        $faker->seed(12345);
314        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
315            CREATE TABLE statuses (
316                status ENUM('active','inactive','pending') NOT NULL
317            )
318            SQL);
319
320        self::assertContains($data['status'], ['active', 'inactive', 'pending']);
321    }
322
323    #[Test]
324    public function testFixtureWithSet(): void
325    {
326        $faker = Factory::create();
327        $faker->seed(12345);
328        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
329            CREATE TABLE permissions (
330                perms SET('read','write','delete') NOT NULL
331            )
332            SQL);
333
334        self::assertIsString($data['perms']);
335        $parts = explode(',', $data['perms']);
336        array_walk($parts, static function (string $part): void {
337            self::assertContains($part, ['read', 'write', 'delete']);
338        });
339    }
340
341    #[Test]
342    public function testFixtureWithJson(): void
343    {
344        $faker = Factory::create();
345        $faker->seed(12345);
346        $data = (new FixtureProvider($faker))->fixture('CREATE TABLE jsons (data JSON NOT NULL)');
347
348        self::assertIsString($data['data']);
349        $decoded = json_decode($data['data'], true);
350        self::assertIsArray($decoded);
351    }
352
353    #[Test]
354    public function testFixtureWithSpatialTypes(): void
355    {
356        $faker = Factory::create();
357        $faker->seed(12345);
358        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
359            CREATE TABLE geo (
360                col_point POINT NOT NULL,
361                col_linestring LINESTRING NOT NULL,
362                col_polygon POLYGON NOT NULL
363            )
364            SQL);
365
366        self::assertIsString($data['col_point']);
367        self::assertStringStartsWith('POINT(', $data['col_point']);
368        self::assertIsString($data['col_linestring']);
369        self::assertStringStartsWith('LINESTRING(', $data['col_linestring']);
370        self::assertIsString($data['col_polygon']);
371        self::assertStringStartsWith('POLYGON((', $data['col_polygon']);
372    }
373
374    #[Test]
375    public function testFixtureResultIsReproducibleWithSeed(): void
376    {
377        $faker1 = Factory::create();
378        $faker1->seed(99999);
379        $data1 = (new FixtureProvider($faker1))->fixture('CREATE TABLE test (name VARCHAR(255))');
380        $faker2 = Factory::create();
381        $faker2->seed(99999);
382        $data2 = (new FixtureProvider($faker2))->fixture('CREATE TABLE test (name VARCHAR(255))');
383
384        self::assertSame($data1['name'], $data2['name']);
385    }
386
387    #[Test]
388    public function testFixtureCachesSchema(): void
389    {
390        $faker = Factory::create();
391        $faker->seed(12345);
392        $provider = new FixtureProvider($faker);
393        $sql = 'CREATE TABLE cache_test (id INT, name VARCHAR(255))';
394
395        $data1 = $provider->fixture($sql, ['name' => 'First']);
396        $data2 = $provider->fixture($sql, ['name' => 'Second']);
397
398        self::assertSame('First', $data1['name']);
399        self::assertSame('Second', $data2['name']);
400    }
401
402    #[Test]
403    public function testGetFixtureGenerator(): void
404    {
405        $faker = Factory::create();
406        $faker->seed(12345);
407        $generator = (new FixtureProvider($faker))->getFixtureGenerator();
408        self::assertSame(['id' => 7], $generator->generate(new TableSchema('sample', ['id' => new ColumnDefinition('id', 'INT')]), ['id' => 7]));
409    }
410
411    #[Test]
412    public function testGetDialectDefaultsToMysql(): void
413    {
414        $faker = Factory::create();
415        $faker->seed(12345);
416        self::assertSame('mysql', (new FixtureProvider($faker))->getDialect());
417    }
418
419    #[Test]
420    #[\PHPUnit\Framework\Attributes\DataProvider('providerNullableSeeds')]
421    public function testFixtureWithNullableColumns(int $seed): void
422    {
423        $faker = Factory::create();
424        $faker->seed($seed);
425        $provider = new FixtureProvider($faker);
426        $sql = 'CREATE TABLE test (id INT NOT NULL, name VARCHAR(255) DEFAULT NULL, notes TEXT)';
427
428        $data = $provider->fixture($sql);
429        self::assertArrayHasKey('id', $data);
430        self::assertIsInt($data['id']);
431    }
432
433    #[Test]
434    public function testFixtureWithBinaryColumns(): void
435    {
436        $faker = Factory::create();
437        $faker->seed(12345);
438        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
439            CREATE TABLE bins (
440                col_binary BINARY(16) NOT NULL,
441                col_varbinary VARBINARY(100) NOT NULL,
442                col_blob BLOB NOT NULL
443            )
444            SQL);
445
446        self::assertIsString($data['col_binary']);
447        self::assertSame(16, strlen($data['col_binary']));
448        self::assertIsString($data['col_varbinary']);
449        self::assertLessThanOrEqual(100, strlen($data['col_varbinary']));
450        self::assertIsString($data['col_blob']);
451    }
452
453    #[Test]
454    public function testFixtureWithBooleanType(): void
455    {
456        $faker = Factory::create();
457        $faker->seed(12345);
458        $data = (new FixtureProvider($faker))->fixture('CREATE TABLE test (active BOOLEAN NOT NULL)');
459        self::assertIsBool($data['active']);
460    }
461
462    #[Test]
463    public function testFixtureWithBitType(): void
464    {
465        $faker = Factory::create();
466        $faker->seed(12345);
467        $data = (new FixtureProvider($faker))->fixture('CREATE TABLE test (flags BIT(8) NOT NULL)');
468        self::assertIsInt($data['flags']);
469        self::assertGreaterThanOrEqual(0, $data['flags']);
470        self::assertLessThanOrEqual(255, $data['flags']);
471    }
472
473    #[Test]
474    public function testFixtureWithGeneratedColumnSkipped(): void
475    {
476        $faker = Factory::create();
477        $faker->seed(12345);
478        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
479            CREATE TABLE test (
480                a INT,
481                b INT,
482                c INT GENERATED ALWAYS AS (a + b) STORED
483            )
484            SQL);
485
486        self::assertArrayHasKey('a', $data);
487        self::assertArrayHasKey('b', $data);
488        self::assertArrayNotHasKey('c', $data);
489    }
490
491    #[Test]
492    public function testFixtureWithUnsignedTypes(): void
493    {
494        $faker = Factory::create();
495        $faker->seed(12345);
496        $data = (new FixtureProvider($faker))->fixture(<<<'SQL'
497            CREATE TABLE test (
498                col_uint INT UNSIGNED NOT NULL,
499                col_utinyint TINYINT UNSIGNED NOT NULL
500            )
501            SQL);
502
503        self::assertIsInt($data['col_uint']);
504        self::assertGreaterThanOrEqual(0, $data['col_uint']);
505        self::assertIsInt($data['col_utinyint']);
506        self::assertGreaterThanOrEqual(0, $data['col_utinyint']);
507        self::assertLessThanOrEqual(255, $data['col_utinyint']);
508    }
509
510    #[Test]
511    public function testFixtureWithDialectOverride(): void
512    {
513        $faker = Factory::create();
514        $faker->seed(12345);
515        $provider = new FixtureProvider($faker, dialect: 'mysql');
516
517        $data = $provider->fixture(
518            'CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)',
519            [],
520            null,
521            'sqlite',
522        );
523
524        self::assertArrayHasKey('name', $data);
525    }
526
527    #[Test]
528    public function testFixtureWithSqliteDialect(): void
529    {
530        $faker = Factory::create();
531        $faker->seed(12345);
532        $provider = new FixtureProvider($faker, dialect: 'sqlite');
533
534        self::assertSame('sqlite', $provider->getDialect());
535
536        $data = $provider->fixture(
537            'CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)',
538        );
539
540        self::assertArrayHasKey('name', $data);
541    }
542
543    #[Test]
544    public function testFixtureWithCustomTypeMapper(): void
545    {
546        $faker = Factory::create();
547        $faker->seed(12345);
548        $typeMapper = new MySqlTypeMapper();
549        $provider = new FixtureProvider($faker, typeMapper: $typeMapper);
550
551        $data = $provider->fixture('CREATE TABLE test (id INT, name VARCHAR(255))');
552
553        self::assertArrayHasKey('id', $data);
554    }
555
556    #[Test]
557    public function testFixtureWithCustomSchemaParser(): void
558    {
559        $faker = Factory::create();
560        $faker->seed(12345);
561        $parser = new MySqlSchemaParser();
562        $provider = new FixtureProvider($faker, schemaParser: $parser);
563
564        $data = $provider->fixture('CREATE TABLE test (id INT, name VARCHAR(255))');
565
566        self::assertArrayHasKey('id', $data);
567    }
568
569    #[Test]
570    public function testSchemaCacheUsesDialectInKey(): void
571    {
572        $faker = Factory::create();
573        $faker->seed(12345);
574        $provider = new FixtureProvider($faker, dialect: 'mysql');
575
576        $sql = 'CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT NOT NULL)';
577        $data1 = $provider->fixture($sql, [], null, 'sqlite');
578        $data2 = $provider->fixture($sql, [], null, 'sqlite');
579
580        self::assertArrayHasKey('name', $data1);
581        self::assertArrayHasKey('name', $data2);
582    }
583
584    #[Test]
585    public function testSchemaCacheDistinguishesDifferentDialects(): void
586    {
587        $faker = Factory::create();
588        $faker->seed(12345);
589        $provider = new FixtureProvider($faker, dialect: 'mysql');
590
591        $sql = 'CREATE TABLE test (id INTEGER NOT NULL, name TEXT NOT NULL)';
592
593        $faker->seed(12345);
594        $dataMysql = $provider->fixture($sql);
595
596        $faker->seed(12345);
597        $dataSqlite = $provider->fixture($sql, [], null, 'sqlite');
598
599        self::assertArrayHasKey('name', $dataMysql);
600        self::assertArrayHasKey('name', $dataSqlite);
601    }
602
603    #[Test]
604    public function testDefaultDialectIsUsedWhenNullDialectPassed(): void
605    {
606        $faker = Factory::create();
607        $faker->seed(12345);
608        $provider = new FixtureProvider($faker, dialect: 'sqlite');
609
610        $data = $provider->fixture(
611            'CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT NOT NULL)',
612            [],
613            null,
614            null,
615        );
616
617        self::assertArrayHasKey('name', $data);
618        self::assertSame('sqlite', $provider->getDialect());
619    }
620
621    #[Test]
622    public function testFixtureSameDialectUsesInternalParser(): void
623    {
624        $faker = Factory::create();
625        $faker->seed(12345);
626        $provider = new FixtureProvider($faker, dialect: 'mysql');
627
628        $data = $provider->fixture(
629            'CREATE TABLE test (id INT, name VARCHAR(255))',
630            [],
631            null,
632            'mysql',
633        );
634
635        self::assertArrayHasKey('name', $data);
636    }
637
638    #[Test]
639    public function testCacheDistinguishesDifferentSqlSameDialect(): void
640    {
641        $faker = Factory::create();
642        $faker->seed(12345);
643        $provider = new FixtureProvider($faker, dialect: 'mysql');
644
645        $data1 = $provider->fixture(
646            'CREATE TABLE test1 (id INT NOT NULL, name VARCHAR(255) NOT NULL)',
647        );
648        $data2 = $provider->fixture(
649            'CREATE TABLE test2 (age INT NOT NULL, email VARCHAR(100) NOT NULL)',
650        );
651
652        self::assertArrayHasKey('name', $data1);
653        self::assertArrayNotHasKey('email', $data1);
654        self::assertArrayHasKey('email', $data2);
655        self::assertArrayNotHasKey('name', $data2);
656    }
657
658    #[Test]
659    public function testCacheKeyIncludesDialect(): void
660    {
661        $faker = Factory::create();
662        $faker->seed(12345);
663        $provider = new FixtureProvider($faker, dialect: 'mysql');
664
665        $sql = 'CREATE TABLE test (id INTEGER NOT NULL, name TEXT NOT NULL)';
666
667        $dataMysql = $provider->fixture($sql);
668        $dataSqlite = $provider->fixture($sql, [], null, 'sqlite');
669
670        self::assertArrayHasKey('id', $dataMysql);
671        self::assertArrayHasKey('id', $dataSqlite);
672        self::assertArrayHasKey('name', $dataMysql);
673        self::assertArrayHasKey('name', $dataSqlite);
674    }
675
676    #[Test]
677    public function testDialectOverrideUsesOverrideNotDefault(): void
678    {
679        $faker = Factory::create();
680        $faker->seed(12345);
681        $provider = new FixtureProvider($faker, dialect: 'mysql');
682
683        $sql = 'CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)';
684
685        $data = $provider->fixture($sql, [], null, 'sqlite');
686
687        self::assertArrayNotHasKey('id', $data);
688        self::assertArrayHasKey('name', $data);
689    }
690
691    #[Test]
692    public function testCachedSchemaDistinguishesDialects(): void
693    {
694        $faker = Factory::create();
695        $faker->seed(12345);
696        $provider = new FixtureProvider($faker, dialect: 'mysql');
697
698        $sql = 'CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)';
699
700        $provider->fixture($sql, ['name' => 'first'], null, 'sqlite');
701
702        $data2 = $provider->fixture($sql, ['name' => 'second'], null, 'sqlite');
703
704        self::assertArrayNotHasKey('id', $data2);
705        self::assertSame('second', $data2['name']);
706    }
707
708    #[Test]
709    public function testCacheSeparatesDefaultAndOverrideDialect(): void
710    {
711        $faker = Factory::create();
712        $faker->seed(12345);
713        $provider = new FixtureProvider($faker, dialect: 'sqlite');
714
715        $sql = 'CREATE TABLE test (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL)';
716
717        $dataSqlite = $provider->fixture($sql);
718        self::assertArrayHasKey('id', $dataSqlite);
719
720        $dataMysql = $provider->fixture($sql, [], null, 'mysql');
721        self::assertArrayNotHasKey('id', $dataMysql);
722    }
723
724    #[Test]
725    public function testCustomTypeMapperIsPreserved(): void
726    {
727        $faker = Factory::create();
728        $faker->seed(12345);
729        $provider = new FixtureProvider($faker, typeMapper: new PostgreSqlTypeMapper());
730
731        $data = $provider->fixture('CREATE TABLE test (val YEAR NOT NULL)');
732
733        self::assertIsString($data['val']);
734    }
735
736    #[Test]
737    public function testCustomSchemaParserIsPreserved(): void
738    {
739        $faker = Factory::create();
740        $faker->seed(12345);
741        $parser = new SqliteSchemaParser();
742        $provider = new FixtureProvider($faker, schemaParser: $parser);
743
744        $data = $provider->fixture('CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)');
745
746        self::assertArrayNotHasKey('id', $data);
747        self::assertArrayHasKey('name', $data);
748    }
749
750    #[Test]
751    public function testRegisterSchemaReturnsTheParsedTable(): void
752    {
753        $faker = Factory::create();
754        $faker->seed(12345);
755        $provider = new FixtureProvider($faker);
756
757        $schema = $provider->registerSchema('CREATE TABLE test (id INT NOT NULL, name VARCHAR(255) NOT NULL)');
758
759        self::assertSame('test', $schema->tableName);
760        self::assertCount(2, $schema->columns);
761    }
762
763    #[Test]
764    public function testRegisterSchemaARegisteredSchemaBecomesAvailableToFixtures(): void
765    {
766        $faker = Factory::create();
767        $faker->seed(12345);
768        $provider = new FixtureProvider($faker);
769        $provider->registerSchema('CREATE TABLE orders (id INT AUTO_INCREMENT PRIMARY KEY, status VARCHAR(20) NOT NULL)');
770
771        self::assertTrue($provider->getSchemaResolver()->has('orders'));
772        self::assertSame('orders', $provider->getSchemaResolver()->resolve('orders')->tableName);
773    }
774
775    #[Test]
776    public function testGetSchemaResolverGeneratingAFixtureAlsoMakesItAvailableToFixtures(): void
777    {
778        $faker = Factory::create();
779        $faker->seed(12345);
780        $provider = new FixtureProvider($faker);
781        $provider->fixture('CREATE TABLE orders (id INT AUTO_INCREMENT PRIMARY KEY, status VARCHAR(20) NOT NULL)');
782
783        self::assertTrue($provider->getSchemaResolver()->has('orders'));
784    }
785    public function testFixturesGeneratesLinkedRowsFromRegisteredSchemas(): void
786    {
787        $provider = new FixtureProvider(Factory::create());
788        $provider->registerSchema('CREATE TABLE users (id INT PRIMARY KEY)');
789        $provider->registerSchema('CREATE TABLE posts (user_id INT)');
790        $fixtures = $provider->fixtures('users.id < posts.user_id', ['users' => ['id' => 9], 'posts' => 2]);
791        self::assertSame([['user_id' => 9], ['user_id' => 9]], $fixtures->rows('posts'));
792    }
793    /**
794     * @return list<array{int}>
795     */
796    public static function providerNullableSeeds(): array
797    {
798        return array_map(static fn (int $seed): array => [$seed], range(1, 20));
799    }
800}
801