Skip to content

Commit f2ded3c

Browse files
committed
Fix MySQL foreign key constraint errors by ensuring unsigned integer consistency
- Update user table migration to create unsigned id column for MySQL - Add unsigned attribute to token.user_id foreign key - Add foreign key before create() in GDPR Phase 3 migration - Make created_by nullable in GDPR Phase 4 data retention policies Fixes errno: 150 'Foreign key constraint is incorrectly formed' errors when migrating on empty MySQL database. All id columns are now unsigned on MySQL for consistency with newer Phinx defaults.
1 parent 526f607 commit f2ded3c

File tree

4 files changed

+19
-9
lines changed

4 files changed

+19
-9
lines changed

db/migrations/20160203130652_user.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class User extends AbstractMigration
1919
{
2020
public function change(): void
2121
{
22+
// Check if the database is MySQL to handle unsigned integers
23+
$databaseType = $this->getAdapter()->getOption('adapter');
24+
$unsigned = ($databaseType === 'mysql') ? ['signed' => false] : [];
25+
2226
// Migration for table users
2327
$table = $this->table('user');
2428
$table
@@ -37,7 +41,7 @@ public function change(): void
3741

3842
if ($this->adapter->getAdapterType() !== 'sqlite') {
3943
$table
40-
->changeColumn('id', 'integer', ['identity' => true])
44+
->changeColumn('id', 'integer', array_merge(['identity' => true], $unsigned))
4145
->save();
4246
}
4347
}

db/migrations/20221012185432_token.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,16 @@ final class Token extends AbstractMigration
3030
*/
3131
public function change(): void
3232
{
33+
// Check if the database is MySQL to handle unsigned integers
34+
$databaseType = $this->getAdapter()->getOption('adapter');
35+
$unsigned = ($databaseType === 'mysql') ? ['signed' => false] : [];
36+
3337
$table = $this->table('token');
3438
$table
3539
->addColumn('token', 'string', ['limit' => 64])
3640
->addColumn('start', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
3741
->addColumn('until', 'datetime', ['null' => true])
38-
->addColumn('user_id', 'integer', ['null' => false])
42+
->addColumn('user_id', 'integer', array_merge(['null' => false], $unsigned))
3943
->addForeignKey('user_id', 'user', 'id', ['constraint' => 'user_must_exist'])
4044
->create();
4145
}

db/migrations/20241020190000_gdpr_phase3_security_enhancements.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,17 @@ private function createTwoFactorAuthTables(): void
133133
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP'])
134134
->addColumn('last_used', 'timestamp', ['null' => true])
135135
->addColumn('recovery_codes_used', 'json', ['null' => true])
136-
->addIndex(['user_id'], ['unique' => true])
137-
->create();
136+
->addIndex(['user_id'], ['unique' => true]);
138137

139-
// Add foreign key constraint if not SQLite
138+
// Add foreign key constraint before create() if not SQLite
140139
if ($this->adapter->getAdapterType() !== 'sqlite') {
141140
$table->addForeignKey('user_id', 'user', 'id', [
142141
'delete' => 'CASCADE',
143142
'update' => 'NO_ACTION',
144-
])->save();
143+
]);
145144
}
145+
146+
$table->create();
146147
}
147148

148149
/**

db/migrations/20250120185000_gdpr_phase4_data_retention.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private function createDataRetentionPoliciesTable(): void
119119
$table->addColumn('legal_basis', 'string', ['limit' => 255, 'null' => true, 'comment' => 'Legal basis for retention period'])
120120
->addColumn('description', 'text', ['null' => true, 'comment' => 'Human-readable description of the policy'])
121121
->addColumn('enabled', 'boolean', ['default' => true, 'null' => false, 'comment' => 'Whether this policy is active'])
122-
->addColumn('created_by', 'integer', array_merge(['null' => false], $unsigned))
122+
->addColumn('created_by', 'integer', array_merge(['null' => true], $unsigned))
123123
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP'])
124124
->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP'])
125125
->addIndex(['policy_name'], ['unique' => true])
@@ -131,7 +131,7 @@ private function createDataRetentionPoliciesTable(): void
131131
// Add foreign key constraint if not SQLite
132132
if ($this->adapter->getAdapterType() !== 'sqlite') {
133133
$table->addForeignKey('created_by', 'user', 'id', [
134-
'delete' => 'RESTRICT',
134+
'delete' => 'SET_NULL',
135135
'update' => 'NO_ACTION',
136136
])->save();
137137
}
@@ -412,8 +412,9 @@ private function enhanceExistingTablesWithRetentionFields(): void
412412
private function insertDefaultRetentionPolicies(): void
413413
{
414414
// Get first admin user ID for created_by field
415+
// If no users exist yet, use NULL (created_by column allows null)
415416
$adminUser = $this->fetchRow('SELECT id FROM user WHERE enabled = 1 ORDER BY id LIMIT 1');
416-
$adminUserId = $adminUser['id'] ?? 1;
417+
$adminUserId = $adminUser['id'] ?? null;
417418

418419
$policies = [
419420
[

0 commit comments

Comments
 (0)