diff --git a/lib/features/postgresql/postgres_table_utils.dart b/lib/features/postgresql/postgres_table_utils.dart index a91d252e..0d2d11b3 100644 --- a/lib/features/postgresql/postgres_table_utils.dart +++ b/lib/features/postgresql/postgres_table_utils.dart @@ -63,12 +63,23 @@ bool isAllowedPostgresSelectQuery(String sql) { return _postgresStatementIsReadOnlySelect(statements.first); } +/// Splits [sql] on the semicolons that PostgreSQL itself would treat as +/// statement ends. +/// +/// This is the gate that keeps writes out of the Table Browser, so it follows +/// PostgreSQL's lexical rules and not the editor's script splitter +/// (`SqlStatementSplitter`): there a backslash escapes a quote (MySQL style) +/// and a backtick quotes, so `SELECT 'a\'; DELETE FROM t; --'` would look like +/// one statement; PostgreSQL (standard strings) ends the literal at the second +/// quote and runs the DELETE. Block comments nest here, as in PostgreSQL. +/// [maskSqlLiteralsForLimitScan] already blanks comments, strings, quoted +/// identifiers and dollar quotes, so one masked copy is enough (#1517). List _splitPostgresStatements(String sql) { - final masked = _maskPostgresComments(maskSqlLiteralsForLimitScan(sql)); + final masked = maskSqlLiteralsForLimitScan(sql); final statements = []; var start = 0; for (var i = 0; i < masked.length; i++) { - if (masked[i] == ';') { + if (masked.codeUnitAt(i) == 0x3B /* ; */) { statements.add(sql.substring(start, i)); start = i + 1; } @@ -77,37 +88,6 @@ List _splitPostgresStatements(String sql) { return statements; } -String _maskPostgresComments(String sql) { - final out = StringBuffer(); - var i = 0; - while (i < sql.length) { - if (i + 1 < sql.length && sql[i] == '-' && sql[i + 1] == '-') { - while (i < sql.length && sql[i] != '\n') { - out.write(' '); - i++; - } - continue; - } - if (i + 1 < sql.length && sql[i] == '/' && sql[i + 1] == '*') { - out.write(' '); - i += 2; - while (i < sql.length) { - if (i + 1 < sql.length && sql[i] == '*' && sql[i + 1] == '/') { - out.write(' '); - i += 2; - break; - } - out.write(' '); - i++; - } - continue; - } - out.write(sql[i]); - i++; - } - return out.toString(); -} - bool _postgresFragmentIsOnlyComments(String sql) { var s = sql.trim(); while (s.isNotEmpty) { diff --git a/test/features/postgresql/postgres_table_utils_test.dart b/test/features/postgresql/postgres_table_utils_test.dart index db70c9b9..62d5e1bf 100644 --- a/test/features/postgresql/postgres_table_utils_test.dart +++ b/test/features/postgresql/postgres_table_utils_test.dart @@ -76,6 +76,36 @@ void main() { ); }); + test('splits like PostgreSQL, not like the script splitter (#1517)', () { + // Standard strings: a backslash is an ordinary character, so the literal + // ends at the second quote and the DELETE is a second statement. + expect( + isAllowedPostgresSelectQuery("SELECT 'a\\'; DELETE FROM t; --'"), + isFalse, + ); + // A backtick is an operator character in PostgreSQL, not a quote. + expect( + isAllowedPostgresSelectQuery('SELECT 1 ` 2; DELETE FROM t; SELECT 1 ` 3'), + isFalse, + ); + // Block comments nest: the whole tail is one comment. + expect( + isAllowedPostgresSelectQuery( + 'SELECT 1 /* a /* b */ ; DELETE FROM t; */', + ), + isTrue, + ); + // Semicolons in quoted identifiers and dollar quotes do not split. + expect( + isAllowedPostgresSelectQuery('SELECT "a;b" FROM t'), + isTrue, + ); + expect( + isAllowedPostgresSelectQuery(r'SELECT $$ x; y $$'), + isTrue, + ); + }); + test('rejects empty', () { expect(isAllowedPostgresSelectQuery(''), isFalse); });