From 86bd09b9e1ec1d098a47a27aab166bfdae2556d3 Mon Sep 17 00:00:00 2001 From: Mariamawit Berta Date: Tue, 7 Jul 2026 11:24:08 -0700 Subject: [PATCH] Fix foreign key parsing broken by MariaDB system versioning When a MariaDB table has system versioning enabled, SHOW CREATE TABLE includes constructs the sql-parser library does not handle: - GENERATED ALWAYS AS ROW START/END columns - PERIOD FOR SYSTEM_TIME clause These cause getForeigners() to return an empty array, making the Relation View appear empty for system-versioned tables. Fix: Pre-process the SHOW CREATE TABLE string to strip these MariaDB-specific constructs before passing to the parser. Add PHPUnit tests covering versioned and non-versioned tables. Fixes: #20095 Signed-off-by: Mariamawit Berta --- libraries/classes/ConfigStorage/Relation.php | 14 ++ test/classes/ConfigStorage/RelationTest.php | 140 +++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/libraries/classes/ConfigStorage/Relation.php b/libraries/classes/ConfigStorage/Relation.php index f5b25a811408..889d7d5300ed 100644 --- a/libraries/classes/ConfigStorage/Relation.php +++ b/libraries/classes/ConfigStorage/Relation.php @@ -445,6 +445,20 @@ public function getForeigners($db, $table, $column = '', $source = 'both') $tableObj = new Table($table, $db); $show_create_table = $tableObj->showCreate(); if ($show_create_table !== '') { + // Strip MariaDB system versioning clauses that confuse the SQL parser. + // PERIOD FOR SYSTEM_TIME and ROW START/END columns are not relevant + // for foreign key detection and cause parsing failures. + // See: https://github.com/phpmyadmin/phpmyadmin/issues/20095 + $show_create_table = (string) preg_replace( + '/^\s*`?\w+`?\s+\w+(?:\(\d+\))?\s+GENERATED ALWAYS AS ROW (?:START|END)[^\n]*,?\n?/im', + '', + $show_create_table + ); + $show_create_table = (string) preg_replace( + '/^\s*PERIOD\s+FOR\s+SYSTEM_TIME\s*\([^)]*\),?\n?/im', + '', + $show_create_table + ); $parser = new Parser($show_create_table); $stmt = $parser->statements[0]; $foreign['foreign_keys_data'] = []; diff --git a/test/classes/ConfigStorage/RelationTest.php b/test/classes/ConfigStorage/RelationTest.php index d3e786604add..8eb3cb6f7800 100644 --- a/test/classes/ConfigStorage/RelationTest.php +++ b/test/classes/ConfigStorage/RelationTest.php @@ -2244,4 +2244,144 @@ public function testRenameTableEscaping(): void $this->assertAllQueriesConsumed(); } + + /** + * Test that getForeigners correctly parses foreign keys from a system-versioned table. + * System-versioned tables include PERIOD FOR SYSTEM_TIME and GENERATED ALWAYS AS ROW + * START/END columns in SHOW CREATE TABLE output, which previously broke the SQL parser. + * + * @see https://github.com/phpmyadmin/phpmyadmin/issues/20095 + */ + public function testGetForeignersWithSystemVersionedTable(): void + { + $showCreateTable = "CREATE TABLE `orders` (\n" + . " `id` int(11) NOT NULL,\n" + . " `customer_id` int(11) DEFAULT NULL,\n" + . " `ts` timestamp(6) GENERATED ALWAYS AS ROW START,\n" + . " `te` timestamp(6) GENERATED ALWAYS AS ROW END,\n" + . " PRIMARY KEY (`id`,`te`),\n" + . " KEY `fk_customer` (`customer_id`),\n" + . " PERIOD FOR SYSTEM_TIME (`ts`, `te`),\n" + . " CONSTRAINT `fk_customer` FOREIGN KEY (`customer_id`)" + . " REFERENCES `customers` (`id`)\n" + . ') ENGINE=InnoDB WITH SYSTEM VERSIONING'; + + // Strip GENERATED ALWAYS AS ROW START/END columns + $processed = (string) preg_replace( + '/^\s*`?\w+`?\s+\w+(?:\(\d+\))?\s+GENERATED ALWAYS AS ROW (?:START|END)[^\n]*,?\n?/im', + '', + $showCreateTable + ); + // Strip PERIOD FOR SYSTEM_TIME clause + $processed = (string) preg_replace( + '/^\s*PERIOD\s+FOR\s+SYSTEM_TIME\s*\([^)]*\),?\n?/im', + '', + $processed + ); + + // After stripping, the parser should find the foreign key + $parser = new \PhpMyAdmin\SqlParser\Parser($processed); + $stmt = $parser->statements[0]; + self::assertInstanceOf(\PhpMyAdmin\SqlParser\Statements\CreateStatement::class, $stmt); + + $foreignKeys = $stmt->getForeignKeys(); + self::assertCount(1, $foreignKeys); + self::assertSame('fk_customer', $foreignKeys[0]->constraint); + self::assertSame(['customer_id'], $foreignKeys[0]->indexList); + self::assertSame('customers', $foreignKeys[0]->refTableName); + } + + /** + * Test that PERIOD FOR SYSTEM_TIME clause is stripped before parsing. + * + * @see https://github.com/phpmyadmin/phpmyadmin/issues/20095 + */ + public function testStripPeriodForSystemTime(): void + { + $showCreateTable = "CREATE TABLE `orders` (\n" + . " `id` int(11) NOT NULL,\n" + . " `customer_id` int(11) DEFAULT NULL,\n" + . " PERIOD FOR SYSTEM_TIME (`ts`, `te`),\n" + . " CONSTRAINT `fk_customer` FOREIGN KEY (`customer_id`)" + . " REFERENCES `customers` (`id`)\n" + . ') ENGINE=InnoDB'; + + $processed = (string) preg_replace( + '/^\s*PERIOD\s+FOR\s+SYSTEM_TIME\s*\([^)]*\),?\n?/im', + '', + $showCreateTable + ); + + self::assertStringNotContainsString('PERIOD FOR SYSTEM_TIME', $processed); + + $parser = new \PhpMyAdmin\SqlParser\Parser($processed); + $stmt = $parser->statements[0]; + $foreignKeys = $stmt->getForeignKeys(); + self::assertCount(1, $foreignKeys); + self::assertSame('fk_customer', $foreignKeys[0]->constraint); + } + + /** + * Test that GENERATED ALWAYS AS ROW START/END columns are stripped before parsing. + * + * @see https://github.com/phpmyadmin/phpmyadmin/issues/20095 + */ + public function testStripGeneratedAlwaysAsRowStartEnd(): void + { + $showCreateTable = "CREATE TABLE `orders` (\n" + . " `id` int(11) NOT NULL,\n" + . " `ts` timestamp(6) GENERATED ALWAYS AS ROW START,\n" + . " `te` timestamp(6) GENERATED ALWAYS AS ROW END,\n" + . " CONSTRAINT `fk_customer` FOREIGN KEY (`customer_id`)" + . " REFERENCES `customers` (`id`)\n" + . ') ENGINE=InnoDB'; + + $processed = (string) preg_replace( + '/^\s*`?\w+`?\s+\w+(?:\(\d+\))?\s+GENERATED ALWAYS AS ROW (?:START|END)[^\n]*,?\n?/im', + '', + $showCreateTable + ); + + self::assertStringNotContainsString('ROW START', $processed); + self::assertStringNotContainsString('ROW END', $processed); + } + + /** + * Test that non-versioned tables are unaffected by the system versioning fix. + * + * @see https://github.com/phpmyadmin/phpmyadmin/issues/20095 + */ + public function testGetForeignersWithNonVersionedTable(): void + { + $showCreateTable = "CREATE TABLE `orders` (\n" + . " `id` int(11) NOT NULL,\n" + . " `customer_id` int(11) DEFAULT NULL,\n" + . " PRIMARY KEY (`id`),\n" + . " CONSTRAINT `fk_customer` FOREIGN KEY (`customer_id`)" + . " REFERENCES `customers` (`id`)\n" + . ') ENGINE=InnoDB'; + + // Apply the same pre-processing — should not alter a non-versioned table + $processed = (string) preg_replace( + '/^\s*`?\w+`?\s+\w+(?:\(\d+\))?\s+GENERATED ALWAYS AS ROW (?:START|END)[^\n]*,?\n?/im', + '', + $showCreateTable + ); + $processed = (string) preg_replace( + '/^\s*PERIOD\s+FOR\s+SYSTEM_TIME\s*\([^)]*\),?\n?/im', + '', + $processed + ); + + // String should be unchanged + self::assertSame($showCreateTable, $processed); + + // Foreign key should still be found + $parser = new \PhpMyAdmin\SqlParser\Parser($processed); + $stmt = $parser->statements[0]; + $foreignKeys = $stmt->getForeignKeys(); + self::assertCount(1, $foreignKeys); + self::assertSame('fk_customer', $foreignKeys[0]->constraint); + } } +