From 5d545b63af1efee4be9d4ecf35be3505c78557b6 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Thu, 15 Oct 2026 07:22:51 +0300 Subject: [PATCH] refactor(postgresql): drop the duplicate comment masking in the SELECT gate _splitPostgresStatements masked literals and comments with maskSqlLiteralsForLimitScan and then ran a second, hand-written comment masker over the result. The first pass already blanks comments, strings, quoted identifiers and dollar quotes, so the second one never changed anything. It is removed and the split reads code units. SqlStatementSplitter is not used here on purpose: it treats a backslash as a quote escape and a backtick as a quote (MySQL rules), so "SELECT 'a\\'; DELETE FROM t; --'" would be one statement for the gate while PostgreSQL runs the DELETE. Tests pin that and the nested block comments. Closes #1517 --- .../postgresql/postgres_table_utils.dart | 46 ++++++------------- .../postgresql/postgres_table_utils_test.dart | 30 ++++++++++++ 2 files changed, 43 insertions(+), 33 deletions(-) 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); });