Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions schemas/dbstructure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,8 @@ CREATE TABLE `UserGroup`
`id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`description` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
PRIMARY KEY (`id`),
UNIQUE KEY `uk_UserGroup_01` (`name`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
Expand Down Expand Up @@ -691,7 +692,8 @@ CREATE TABLE `UserProfile`
`id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`profile` blob NOT NULL,
PRIMARY KEY (`id`)
PRIMARY KEY (`id`),
UNIQUE KEY `uk_UserProfile_01` (`name`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
Expand Down
43 changes: 43 additions & 0 deletions tests/Unit/Infrastructure/Database/SchemaEnforcesIdentityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,49 @@ public function aLoginIsHeldUniqueOnItsOwn(): void
);
}

/**
* A name the application holds unique is unique in the database too.
*
* `UserProfile` and `UserGroup` each refuse a name that is already taken —
* `UPPER(:name) = UPPER(name)` in `checkDuplicatedOnAdd()` — and neither table had any unique
* index at all. That check is a SELECT followed by an INSERT, so two requests arriving
* together both find nothing and both insert, and there was nothing underneath to stop the
* second: two groups called Admins, and no way to tell which one a permission refers to.
*
* A plain unique index is the application's rule exactly, because `name` collates
* `utf8mb4_unicode_ci` — 'Admins' and 'ADMINS' collide, which is what `UPPER()` was asking
* for.
*
* @return array<string, array{string, string}>
*/
public static function uniqueNameProvider(): array
{
return [
'UserProfile' => ['UserProfile', 'name'],
'UserGroup' => ['UserGroup', 'name'],
'Category' => ['Category', 'hash'],
'Client' => ['Client', 'hash'],
'Tag' => ['Tag', 'hash'],
];
}

#[Test]
#[DataProvider('uniqueNameProvider')]
public function whatTheApplicationHoldsUniqueTheDatabaseHoldsUnique(string $table, string $column): void
{
self::assertMatchesRegularExpression(
sprintf('/UNIQUE KEY\s+`[^`]+`\s*\(`%s`\)/', preg_quote($column, '/')),
self::tableDefinition($table),
sprintf(
'%s.%s is refused as a duplicate by the repository, with a SELECT before the '
. 'INSERT that two concurrent requests both pass. Only a unique index stops the '
. 'second one.',
$table,
$column
)
);
}

/**
* Every table is utf8mb4, so text somebody actually types can be stored.
*
Expand Down