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