Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 13 additions & 33 deletions lib/features/postgresql/postgres_table_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> _splitPostgresStatements(String sql) {
final masked = _maskPostgresComments(maskSqlLiteralsForLimitScan(sql));
final masked = maskSqlLiteralsForLimitScan(sql);
final statements = <String>[];
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;
}
Expand All @@ -77,37 +88,6 @@ List<String> _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) {
Expand Down
30 changes: 30 additions & 0 deletions test/features/postgresql/postgres_table_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
Loading