From ad1850b110c0b660eb1b239150c49eaf2f64a493 Mon Sep 17 00:00:00 2001 From: Danny de Wit Date: Mon, 3 Aug 2026 04:50:47 +0200 Subject: [PATCH] fix: keep TableExtractor table names as strings (#1794) PHP coerces numeric-string array keys to ints, so collecting table names in $tables[$name] and returning array_keys() can yield ints. That breaks the declared list and throws a TypeError in Recorder::linkTable(string). Standard SQL like substring(x FROM 1 FOR 10) is enough to trigger it, since the numeric operand matches the FROM pattern. Closes #1793 Co-authored-by: Danny de Wit --- src/Plugins/Tia/TableExtractor.php | 4 ++-- tests/Unit/Plugins/Tia/TableExtractor.php | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Plugins/Tia/TableExtractor.php b/src/Plugins/Tia/TableExtractor.php index 06a41b91..8fe0ecd6 100644 --- a/src/Plugins/Tia/TableExtractor.php +++ b/src/Plugins/Tia/TableExtractor.php @@ -56,7 +56,7 @@ final class TableExtractor $tables[strtolower($name)] = true; } - $out = array_keys($tables); + $out = array_map(strval(...), array_keys($tables)); sort($out); return $out; @@ -112,7 +112,7 @@ final class TableExtractor } } - $out = array_keys($tables); + $out = array_map(strval(...), array_keys($tables)); sort($out); return $out; diff --git a/tests/Unit/Plugins/Tia/TableExtractor.php b/tests/Unit/Plugins/Tia/TableExtractor.php index b30c2a02..9ee59c68 100644 --- a/tests/Unit/Plugins/Tia/TableExtractor.php +++ b/tests/Unit/Plugins/Tia/TableExtractor.php @@ -47,6 +47,15 @@ describe('fromSql()', function (): void { ->and(TableExtractor::fromSql('select * from information_schema.tables'))->toBeEmpty(); }); + it('does not leak int keys for numeric identifiers', function (): void { + // `substring(x FROM 1 FOR 3)` is standard SQL, and the `1` matches the + // FROM pattern. Collecting names as array keys makes PHP coerce the + // numeric string to an int, which then violates the declared + // list and blows up Recorder::linkTable(string). + expect(TableExtractor::fromSql('select substring(name from 1 for 3) from users')) + ->each->toBeString(); + }); + it('returns nothing for non-DML statements', function (): void { expect(TableExtractor::fromSql('PRAGMA foreign_keys = ON'))->toBeEmpty() ->and(TableExtractor::fromSql(''))->toBeEmpty() @@ -89,6 +98,14 @@ describe('fromMigrationSource()', function (): void { ->toBe(['audits', 'events', 'sessions', 'settings', 'users']); }); + it('does not leak int keys for numeric table names', function (): void { + // A table named `123` is a legal quoted identifier. Collecting names as + // array keys makes PHP coerce it to an int, breaking the declared + // list, so it must survive as a string rather than be dropped. + expect(TableExtractor::fromMigrationSource("DB::table('123')->insert([]);")) + ->toBe(['123']); + }); + it('extracts tables from DB::table calls', function (): void { expect(TableExtractor::fromMigrationSource("DB::table('permissions')->insert([]);")) ->toBe(['permissions']);