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); + } } +