Lines 47.22% 85 / 180
Methods 60.00% 18 / 30
Classes 0.00% 0 / 1
Covered by tests of size
Name Lines Methods CRAP
 getVersion 100.00% 1 / 1 100.00% 1 / 1 1
 getDependencies 100.00% 1 / 1 100.00% 1 / 1 1
 getDescription 100.00% 1 / 1 100.00% 1 / 1 1
 up 21.21% 7 / 33 0.00% 0 / 1 17.23
 [phpMyFAQ\Setup\Migration\AbstractMigration] __construct 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] isReversible 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] down 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] table 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] isDbType 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] isMySql 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] isPostgreSql 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] isSqlite 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] isSqlServer 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] addColumn 62.50% 5 / 8 0.00% 0 / 1 4.84
 [phpMyFAQ\Setup\Migration\AbstractMigration] dropColumn 100.00% 2 / 2 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] dropColumns 100.00% 6 / 6 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] createIndex 33.33% 5 / 15 0.00% 0 / 1 8.74
 [phpMyFAQ\Setup\Migration\AbstractMigration] createUniqueIndex 28.57% 6 / 21 0.00% 0 / 1 6.28
 [phpMyFAQ\Setup\Migration\AbstractMigration] indexExists 40.62% 13 / 32 0.00% 0 / 1 10.23
 [phpMyFAQ\Setup\Migration\AbstractMigration] dropTable 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] dropTableIfExists 100.00% 1 / 1 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] updateLanguageCode 100.00% 11 / 11 100.00% 1 / 1 1
 [phpMyFAQ\Setup\Migration\AbstractMigration] integerType 66.66% 2 / 3 0.00% 0 / 1 3.33
 [phpMyFAQ\Setup\Migration\AbstractMigration] textType 66.66% 2 / 3 0.00% 0 / 1 3.33
 [phpMyFAQ\Setup\Migration\AbstractMigration] varcharType 0.00% 0 / 3 0.00% 0 / 1 12
 [phpMyFAQ\Setup\Migration\AbstractMigration] timestampType 40.00% 4 / 10 0.00% 0 / 1 21.82
 [phpMyFAQ\Setup\Migration\AbstractMigration] booleanType 0.00% 0 / 4 0.00% 0 / 1 20
 [phpMyFAQ\Setup\Migration\AbstractMigration] autoIncrementColumn 0.00% 0 / 6 0.00% 0 / 1 42
 [phpMyFAQ\Setup\Migration\AbstractMigration] getConfig 0.00% 0 / 1 0.00% 0 / 1 2
 [phpMyFAQ\Setup\Migration\AbstractMigration] getChecksum 100.00% 6 / 6 100.00% 1 / 1 1
25readonly class Migration323 extends AbstractMigration
26{
27    public function getVersion(): string
28    {
29        return '3.2.3';
30    }
31
32    public function getDependencies(): array
33    {
34        return ['3.2.0-RC'];
35    }
36
37    public function getDescription(): string
38    {
39        return 'Increase IP column size for IPv6 support';
40    }
41
42    public function up(OperationRecorder $recorder): void
43    {
44        if ($this->isMySql()) {
45            $recorder->addSql(
46                sprintf('ALTER TABLE %sfaquser CHANGE ip ip VARCHAR(64) NULL DEFAULT NULL', $this->tablePrefix),
47                'Increase faquser.ip column size (MySQL)',
48            );
49            return;
50        }
51
52        if ($this->isPostgreSql()) {
53            $recorder->addSql(
54                sprintf('ALTER TABLE %sfaquser ALTER COLUMN ip TYPE VARCHAR(64)', $this->tablePrefix),
55                'Increase faquser.ip column size (PostgreSQL)',
56            );
57            return;
58        }
59
60        if ($this->isSqlite()) {
61            // SQLite requires table rebuild
62            $recorder->addSql(sprintf(
63                'CREATE TABLE %sfaquser_new (
64                    user_id INTEGER NOT NULL,
65                    login VARCHAR(128) NOT NULL,
66                    session_id VARCHAR(150) NULL,
67                    session_timestamp INTEGER NULL,
68                    ip VARCHAR(64) NULL,
69                    account_status VARCHAR(50) NULL,
70                    last_login VARCHAR(14) NULL,
71                    auth_source VARCHAR(100) NULL,
72                    member_since VARCHAR(14) NULL,
73                    remember_me VARCHAR(150) NULL,
74                    success INTEGER NULL DEFAULT 1,
75                    is_superadmin INTEGER NULL DEFAULT 0,
76                    login_attempts INTEGER NULL DEFAULT 0,
77                    refresh_token TEXT NULL DEFAULT NULL,
78                    access_token TEXT NULL DEFAULT NULL,
79                    code_verifier VARCHAR(255) NULL DEFAULT NULL,
80                    jwt TEXT NULL DEFAULT NULL,
81                    PRIMARY KEY (user_id))',
82                $this->tablePrefix,
83            ), 'Create new faquser table with larger IP column (SQLite)');
84
85            $recorder->addSql(
86                sprintf('INSERT INTO %sfaquser_new SELECT * FROM %sfaquser', $this->tablePrefix, $this->tablePrefix),
87                'Copy data to new faquser table (SQLite)',
88            );
89
90            $recorder->addSql(sprintf('DROP TABLE %sfaquser', $this->tablePrefix), 'Drop old faquser table (SQLite)');
91
92            $recorder->addSql(
93                sprintf('ALTER TABLE %sfaquser_new RENAME TO %sfaquser', $this->tablePrefix, $this->tablePrefix),
94                'Rename new faquser table (SQLite)',
95            );
96            return;
97        }
98
99        if ($this->isSqlServer()) {
100            $recorder->addSql(
101                sprintf('ALTER TABLE %sfaquser ALTER COLUMN ip VARCHAR(64)', $this->tablePrefix),
102                'Increase faquser.ip column size (SQL Server)',
103            );
104        }
105    }
106}

Inherited from phpMyFAQ\Setup\Migration\AbstractMigration

32    public function __construct(
33        protected Configuration $configuration,
34    ) {
35        $this->tablePrefix = Database::getTablePrefix();
36        $this->dbType = Database::getType();
37    }
52    public function isReversible(): bool
53    {
54        return false;
55    }
60    public function down(OperationRecorder $recorder): void
61    {
62        // Override in subclass if migration is reversible
63    }
68    protected function table(string $name): string
69    {
70        return $this->tablePrefix . $name;
71    }
78    protected function isDbType(string|array $types): bool
79    {
80        $types = (array) $types;
81        return in_array($this->dbType, $types, strict: true);
82    }
87    protected function isMySql(): bool
88    {
89        return $this->isDbType(['mysqli', 'pdo_mysql']);
90    }
95    protected function isPostgreSql(): bool
96    {
97        return $this->isDbType(['pgsql', 'pdo_pgsql']);
98    }
103    protected function isSqlite(): bool
104    {
105        return $this->isDbType(['sqlite3', 'pdo_sqlite']);
106    }
111    protected function isSqlServer(): bool
112    {
113        return $this->isDbType(['sqlsrv', 'pdo_sqlsrv']);
114    }
120    protected function addColumn(string $table, string $column, string $type, ?string $default = null): string
121    {
122        $tableName = $this->table($table);
123        $defaultClause = '';
124        if ($default !== null) {
125            $defaultClause = " DEFAULT {$default}";
126        }
127
128        if ($this->isSqlite()) {
129            return sprintf('ALTER TABLE %s ADD COLUMN %s %s%s', $tableName, $column, $type, $defaultClause);
130        }
131
132        // MySQL, PostgreSQL, SQL Server
133        $columnKeyword = $this->isSqlServer() ? '' : 'COLUMN ';
134        return sprintf('ALTER TABLE %s ADD %s%s %s%s', $tableName, $columnKeyword, $column, $type, $defaultClause);
135    }
140    protected function dropColumn(string $table, string $column): string
141    {
142        $tableName = $this->table($table);
143        return sprintf('ALTER TABLE %s DROP COLUMN %s', $tableName, $column);
144    }
153    protected function dropColumns(string $table, array $columns): array
154    {
155        $tableName = $this->table($table);
156        return array_map(static fn(string $col): string => sprintf(
157            'ALTER TABLE %s DROP COLUMN %s',
158            $tableName,
159            $col,
160        ), $columns);
161    }
168    protected function createIndex(string $table, string $indexName, string|array $columns): string
169    {
170        $tableName = $this->table($table);
171        $columnList = is_array($columns) ? implode(', ', $columns) : $columns;
172
173        if ($this->isSqlServer()) {
174            return sprintf(
175                "IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = '%s' AND object_id = OBJECT_ID('%s')) "
176                . 'CREATE INDEX %s ON %s (%s)',
177                $indexName,
178                $tableName,
179                $indexName,
180                $tableName,
181                $columnList,
182            );
183        }
184
185        // MySQL/MariaDB don't support IF NOT EXISTS in older versions
186        // Use conditional execution or manual check instead
187        if ($this->isMySql()) {
188            return sprintf('CREATE INDEX %s ON %s (%s)', $indexName, $tableName, $columnList);
189        }
190
191        // PostgreSQL and SQLite support IF NOT EXISTS
192        return sprintf('CREATE INDEX IF NOT EXISTS %s ON %s (%s)', $indexName, $tableName, $columnList);
193    }
204    protected function createUniqueIndex(string $table, string $indexName, string|array $columns): string
205    {
206        $tableName = $this->table($table);
207        $columns = (array) $columns;
208        $columnList = implode(', ', $columns);
209
210        if ($this->isSqlServer()) {
211            $whereClause = implode(' AND ', array_map(static fn(string $col): string => sprintf(
212                '%s IS NOT NULL',
213                $col,
214            ), $columns));
215            return sprintf(
216                "IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = '%s' AND object_id = OBJECT_ID('%s')) "
217                . 'CREATE UNIQUE INDEX %s ON %s (%s) WHERE %s',
218                $indexName,
219                $tableName,
220                $indexName,
221                $tableName,
222                $columnList,
223                $whereClause,
224            );
225        }
226
227        if ($this->isMySql()) {
228            return sprintf('CREATE UNIQUE INDEX %s ON %s (%s)', $indexName, $tableName, $columnList);
229        }
230
231        return sprintf('CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (%s)', $indexName, $tableName, $columnList);
232    }
238    protected function indexExists(string $table, string $indexName): string
239    {
240        $db = $this->configuration->getDb();
241        $tableName = $this->table($table);
242        $escapedTableName = $db->escape($tableName);
243        $escapedIndexName = $db->escape($indexName);
244
245        if ($this->isMySql()) {
246            return sprintf(
247                'SELECT COUNT(*) as idx_count FROM information_schema.STATISTICS '
248                . "WHERE table_schema = DATABASE() AND table_name = '%s' AND index_name = '%s'",
249                $escapedTableName,
250                $escapedIndexName,
251            );
252        }
253
254        if ($this->isPostgreSql()) {
255            return sprintf(
256                'SELECT COUNT(*) as idx_count FROM pg_indexes '
257                . "WHERE schemaname = 'public' AND tablename = '%s' AND indexname = '%s'",
258                $escapedTableName,
259                $escapedIndexName,
260            );
261        }
262
263        if ($this->isSqlite()) {
264            return sprintf(
265                'SELECT COUNT(*) as idx_count FROM sqlite_master '
266                . "WHERE type = 'index' AND name = '%s' AND tbl_name = '%s'",
267                $escapedIndexName,
268                $escapedTableName,
269            );
270        }
271
272        if ($this->isSqlServer()) {
273            return sprintf(
274                'SELECT COUNT(*) as idx_count FROM sys.indexes ' . "WHERE name = '%s' AND object_id = OBJECT_ID('%s')",
275                $escapedIndexName,
276                $escapedTableName,
277            );
278        }
279
280        throw new \RuntimeException("Unsupported database type for index existence check: {$this->dbType}");
281    }
286    protected function dropTable(string $table): string
287    {
288        return sprintf('DROP TABLE %s', $this->table($table));
289    }
294    protected function dropTableIfExists(string $table): string
295    {
296        return sprintf('DROP TABLE IF EXISTS %s', $this->table($table));
297    }
302    protected function updateLanguageCode(string $table, string $column, string $oldCode, string $newCode): string
303    {
304        $db = $this->configuration->getDb();
305        $escapedOldCode = $db->escape($oldCode);
306        $escapedNewCode = $db->escape($newCode);
307
308        return sprintf(
309            "UPDATE %s SET %s='%s' WHERE %s='%s'",
310            $this->table($table),
311            $column,
312            $escapedNewCode,
313            $column,
314            $escapedOldCode,
315        );
316    }
321    protected function integerType(): string
322    {
323        return match ($this->dbType) {
324            'mysqli', 'pdo_mysql' => 'INT',
325            default => 'INTEGER',
326        };
327    }
332    protected function textType(): string
333    {
334        return match ($this->dbType) {
335            'sqlsrv', 'pdo_sqlsrv' => 'NVARCHAR(MAX)',
336            default => 'TEXT',
337        };
338    }
343    protected function varcharType(int $length): string
344    {
345        return match ($this->dbType) {
346            'sqlsrv', 'pdo_sqlsrv' => "NVARCHAR({$length})",
347            default => "VARCHAR({$length})",
348        };
349    }
354    protected function timestampType(bool $withDefault = true): string
355    {
356        $type = match ($this->dbType) {
357            'mysqli', 'pdo_mysql' => 'TIMESTAMP',
358            'sqlsrv', 'pdo_sqlsrv' => 'DATETIME',
359            default => 'TIMESTAMP',
360        };
361
362        if (!$withDefault) {
363            return $type;
364        }
365
366        return match ($this->dbType) {
367            'sqlsrv', 'pdo_sqlsrv' => $type . ' NOT NULL DEFAULT GETDATE()',
368            'sqlite3', 'pdo_sqlite' => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
369            default => $type . ' NOT NULL DEFAULT CURRENT_TIMESTAMP',
370        };
371    }
376    protected function booleanType(): string
377    {
378        return match ($this->dbType) {
379            'mysqli', 'pdo_mysql' => 'TINYINT(1)',
380            'sqlsrv', 'pdo_sqlsrv' => 'TINYINT',
381            default => 'INTEGER',
382        };
383    }
388    protected function autoIncrementColumn(string $columnName = 'id'): string
389    {
390        return match ($this->dbType) {
391            'mysqli', 'pdo_mysql' => "{$columnName} INT NOT NULL AUTO_INCREMENT",
392            'pgsql', 'pdo_pgsql' => "{$columnName} SERIAL NOT NULL",
393            'sqlite3', 'pdo_sqlite' => "{$columnName} INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT",
394            'sqlsrv', 'pdo_sqlsrv' => "{$columnName} INT IDENTITY(1,1) NOT NULL",
395            default => "{$columnName} INTEGER NOT NULL",
396        };
397    }
402    protected function getConfig(string $key): mixed
403    {
404        return $this->configuration->get(item: $key);
405    }
410    public function getChecksum(): string
411    {
412        $data = [
413            'version' => $this->getVersion(),
414            'description' => $this->getDescription(),
415            'class' => static::class,
416        ];
417        return hash('sha256', (string) json_encode($data));
418    }