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
6 changes: 4 additions & 2 deletions lib/core/database/mysql_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -239,20 +239,22 @@ class MysqlConnection {
Future<IResultSet> execute(
String sql, [
Map<String, dynamic>? params,
bool iterable = false,
]) async {
if (!isConnected || _conn == null) {
throw StateError('Not connected to MySQL');
}
return _conn!.execute(sql, params);
return _conn!.execute(sql, params, iterable);
}

/// Runs [execute] with an application-level [timeout] (driver limits still apply).
Future<IResultSet> executeWithTimeout(
String sql, {
Duration? timeout,
Map<String, dynamic>? params,
bool iterable = false,
}) async {
final f = execute(sql, params);
final f = execute(sql, params, iterable);
if (timeout == null) return f;
return f.timeout(timeout);
}
Expand Down
38 changes: 38 additions & 0 deletions lib/core/database/postgres_sql.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,41 @@ bool shouldSkipImplicitBegin(String sql) {

return false;
}

/// Injects a `LIMIT` clause to a read-only query (SELECT, WITH, VALUES)
/// if it does not already contain a `LIMIT` clause.
String injectSqlLimit(String sql, int limit) {
final cleanSql = stripLeadingWhitespaceAndLineComments(sql);
final upper = cleanSql.toUpperCase();

final isSelect = upper.startsWith('SELECT') ||
upper.startsWith('WITH') ||
upper.startsWith('VALUES');

if (!isSelect) {
return sql;
}

// Check if it already has a LIMIT clause
final hasLimit = RegExp(r'\bLIMIT\b', caseSensitive: false).hasMatch(sql);
if (hasLimit) {
return sql;
}

// Strip trailing whitespace and semicolons to build the body
var body = sql.trimRight();
var suffix = '';

while (true) {
if (body.isEmpty) break;
if (body.endsWith(';')) {
body = body.substring(0, body.length - 1).trimRight();
suffix = ';$suffix';
continue;
}
break;
}

return '$body\nLIMIT $limit$suffix';
}

16 changes: 9 additions & 7 deletions lib/features/mysql/mysql_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
}

final to = _statementTimeout();
final rs = await conn.executeWithTimeout(userSql, timeout: to);
final rs = await conn.executeWithTimeout(userSql, timeout: to, iterable: true);

if (!mounted) return;

Expand All @@ -159,8 +159,12 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
final rawRows = <List<Object?>>[];
var n = 0;
final cap = _resultMaxRows;
for (final row in rs.rows) {
if (n >= cap) break;
var truncated = false;
await for (final row in rs.rowsStream) {
if (n >= cap) {
truncated = true;
break;
}
rawRows.add(
List.generate(row.numOfColumns, (i) => row.colAt(i)),
);
Expand All @@ -186,11 +190,9 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
? 'OK. Rows affected: $affected.'
: 'Command completed.';
} else {
final total = rs.numOfRows;
final truncated = total > cap;
_statusLine = truncated
? 'Showing first $cap of $total row(s).'
: '$total row(s).';
? 'Showing first $cap row(s) (result capped).'
: '$n row(s).';
}
_running = false;
});
Expand Down
6 changes: 3 additions & 3 deletions lib/features/postgresql/postgres_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
Future<void> _execute() async {
final userSql = _sqlController.text.trim();
if (userSql.isEmpty) return;
var sql = userSql;
var sql = injectSqlLimit(userSql, _resultMaxRows);

setState(() {
_running = true;
Expand Down Expand Up @@ -323,9 +323,9 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
_statusLine =
'Command completed. Rows affected: ${result.affectedRows}.';
} else {
final truncated = result.length > cap;
final truncated = result.length >= cap;
_statusLine = truncated
? 'Showing first $cap of ${result.length} row(s).'
? 'Showing first $cap row(s) (result capped).'
: '${result.length} row(s).';
}
_running = false;
Expand Down
29 changes: 29 additions & 0 deletions test/core/database/postgres_sql_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,33 @@ void main() {
expect(shouldSkipImplicitBegin('UPDATE t SET x = 1'), isFalse);
});
});

group('injectSqlLimit', () {
test('appends LIMIT to select query without limit', () {
expect(injectSqlLimit('SELECT * FROM users', 5000), 'SELECT * FROM users\nLIMIT 5000');
});

test('handles trailing semicolons', () {
expect(injectSqlLimit('SELECT * FROM users;', 5000), 'SELECT * FROM users\nLIMIT 5000;');
expect(injectSqlLimit('SELECT * FROM users; ', 5000), 'SELECT * FROM users\nLIMIT 5000;');
expect(injectSqlLimit('SELECT * FROM users;;', 5000), 'SELECT * FROM users\nLIMIT 5000;;');
});

test('does not append LIMIT if LIMIT already exists', () {
expect(injectSqlLimit('SELECT * FROM users LIMIT 10', 5000), 'SELECT * FROM users LIMIT 10');
expect(injectSqlLimit('SELECT * FROM users limit 10;', 5000), 'SELECT * FROM users limit 10;');
});

test('does not modify non-select/non-read queries', () {
expect(injectSqlLimit('INSERT INTO users VALUES (1)', 5000), 'INSERT INTO users VALUES (1)');
expect(injectSqlLimit('UPDATE users SET x = 1', 5000), 'UPDATE users SET x = 1');
});

test('appends LIMIT to WITH query', () {
expect(
injectSqlLimit('WITH t AS (SELECT * FROM users) SELECT * FROM t;', 5000),
'WITH t AS (SELECT * FROM users) SELECT * FROM t\nLIMIT 5000;',
);
});
});
}
Loading