Skip to content

Commit e2e1aac

Browse files
Merge pull request #437 from QueryaHub/issue/421-result-string-yield
perf(results): yielding row→string convert (no isolate double-copy) (#421)
2 parents 13417c8 + 8cd3786 commit e2e1aac

6 files changed

Lines changed: 87 additions & 32 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/// Converts SQL result cells to display strings without a second isolate copy.
2+
///
3+
/// Prefer this over [compute] for large matrices: shipping `List<List<Object?>>`
4+
/// across isolates often costs more than `toString()` itself and roughly
5+
/// doubles peak memory. Yielding every [yieldEvery] rows keeps the UI isolate
6+
/// responsive for 10k+ row caps.
7+
library;
8+
9+
const int kResultStringConvertYieldEvery = 250;
10+
11+
/// Maps null cells to `'NULL'` and others via [Object.toString].
12+
String resultCellToDisplayString(Object? value) =>
13+
value == null ? 'NULL' : value.toString();
14+
15+
/// Converts [rowValues] to string rows, yielding periodically.
16+
Future<List<List<String>>> convertResultRowsToStringsYielding(
17+
List<List<Object?>> rowValues, {
18+
int yieldEvery = kResultStringConvertYieldEvery,
19+
}) async {
20+
if (rowValues.isEmpty) return const [];
21+
22+
final out = <List<String>>[];
23+
for (var i = 0; i < rowValues.length; i++) {
24+
final row = rowValues[i];
25+
out.add([
26+
for (final value in row) resultCellToDisplayString(value),
27+
]);
28+
if (yieldEvery > 0 && (i + 1) % yieldEvery == 0) {
29+
await Future<void>.delayed(Duration.zero);
30+
}
31+
}
32+
return out;
33+
}

‎lib/features/mysql/mysql_sql_workspace.dart‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import 'dart:async';
22
import 'dart:io';
33

4-
import 'package:flutter/foundation.dart';
54
import 'package:flutter/material.dart' as material;
65
import 'package:flutter/services.dart' show LogicalKeyboardKey;
76
import 'package:file_selector/file_selector.dart';
87
import 'package:querya_desktop/core/actions/sql_editor_actions.dart';
98
import 'package:querya_desktop/core/actions/sql_editor_command_bridge.dart';
109
import 'package:querya_desktop/core/database/mysql_service.dart';
10+
import 'package:querya_desktop/core/database/result_row_string_convert.dart';
1111
import 'package:querya_desktop/core/layout/vertical_split_pane.dart';
1212
import 'package:querya_desktop/core/storage/app_settings.dart';
1313
import 'package:querya_desktop/core/storage/local_db.dart';
@@ -18,7 +18,6 @@ import 'package:querya_desktop/features/main_screen/query_editor_tab.dart';
1818
import 'package:querya_desktop/features/main_screen/results_tab.dart';
1919
import 'package:querya_desktop/features/main_screen/sql_editor_chrome.dart';
2020
import 'package:querya_desktop/features/main_screen/sql_query_history_dialog.dart';
21-
import 'package:querya_desktop/features/mysql/mysql_result_utils.dart';
2221
import 'package:querya_desktop/shared/widgets/widgets.dart';
2322

2423
/// Ad-hoc SQL editor + results for MySQL / MariaDB.
@@ -192,7 +191,8 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
192191
cols.add(c.name.isNotEmpty ? c.name : 'col_${cols.length}');
193192
}
194193

195-
final rawRows = <List<Object?>>[];
194+
// Convert while streaming — no Object? matrix + isolate double-copy (#421).
195+
final outRows = <List<String>>[];
196196
var n = 0;
197197
final cap = _resultMaxRows;
198198
var truncated = false;
@@ -201,17 +201,18 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
201201
truncated = true;
202202
break;
203203
}
204-
rawRows.add(
205-
List.generate(row.numOfColumns, (i) => row.colAt(i)),
204+
outRows.add(
205+
List.generate(
206+
row.numOfColumns,
207+
(i) => resultCellToDisplayString(row.colAt(i)),
208+
),
206209
);
207210
n++;
211+
if (n % kResultStringConvertYieldEvery == 0) {
212+
await Future<void>.delayed(Duration.zero);
213+
}
208214
}
209215

210-
final job = MysqlResultConvertJob(rowValues: rawRows);
211-
final outRows = rawRows.length > 500
212-
? await compute(convertMysqlResultRowsToStrings, job)
213-
: convertMysqlResultRowsToStrings(job);
214-
215216
int? affected;
216217
if (cols.isEmpty && outRows.isEmpty) {
217218
affected = _affectedInt(rs.affectedRows);

‎lib/features/postgresql/postgres_sql_workspace.dart‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import 'dart:async';
22
import 'dart:io';
33

4-
import 'package:flutter/foundation.dart' show compute;
54
import 'package:flutter/material.dart' as material;
65
import 'package:flutter/services.dart' show LogicalKeyboardKey;
76
import 'package:file_selector/file_selector.dart';
8-
import 'package:querya_desktop/features/postgresql/postgres_result_utils.dart';
97
import 'package:querya_desktop/core/actions/sql_editor_actions.dart';
108
import 'package:querya_desktop/core/actions/sql_editor_command_bridge.dart';
119
import 'package:postgres/postgres.dart' as pg;
1210
import 'package:querya_desktop/core/database/postgres_service.dart';
1311
import 'package:querya_desktop/core/database/postgres_sql.dart';
12+
import 'package:querya_desktop/core/database/result_row_string_convert.dart';
1413
import 'package:querya_desktop/core/layout/vertical_split_pane.dart';
1514
import 'package:querya_desktop/core/storage/app_settings.dart';
1615
import 'package:querya_desktop/core/storage/local_db.dart';
@@ -359,10 +358,8 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
359358
n++;
360359
}
361360

362-
final job = PostgresResultConvertJob(rowValues: rawRows);
363-
final outRows = rawRows.length > 500
364-
? await compute(convertPostgresResultRowsToStrings, job)
365-
: convertPostgresResultRowsToStrings(job);
361+
// Yielding convert avoids isolate double-copy of the matrix (#421).
362+
final outRows = await convertResultRowsToStringsYielding(rawRows);
366363

367364
setState(() {
368365
_columns = cols;

‎lib/features/postgresql/postgres_table_view.dart‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import 'package:flutter/foundation.dart' show compute;
21
import 'package:flutter/material.dart' as material;
32
import 'package:querya_desktop/core/database/postgres_connection.dart';
43
import 'package:querya_desktop/core/database/postgres_service.dart';
4+
import 'package:querya_desktop/core/database/result_row_string_convert.dart';
55
import 'package:querya_desktop/core/storage/local_db.dart';
66
import 'package:querya_desktop/features/postgresql/postgres_sql_editor_dialog.dart';
77
import 'package:querya_desktop/features/postgresql/postgres_table_privileges_dialog.dart';
@@ -187,11 +187,12 @@ class _PostgresTableViewState extends material.State<PostgresTableView> {
187187
(i) => result.schema.columns[i].columnName ?? 'col_$i',
188188
);
189189

190-
final rawRows = result.map((row) {
191-
return List<dynamic>.generate(row.length, (i) => row[i]);
192-
}).toList();
190+
final rawRows = <List<Object?>>[
191+
for (final row in result)
192+
List<Object?>.generate(row.length, (i) => row[i]),
193+
];
193194

194-
final stringRows = await compute(convertResultRowsToStrings, rawRows);
195+
final stringRows = await convertResultRowsToStringsYielding(rawRows);
195196

196197
if (!mounted) return;
197198
setState(() {
@@ -235,11 +236,12 @@ class _PostgresTableViewState extends material.State<PostgresTableView> {
235236
(i) => result.schema.columns[i].columnName ?? 'col_$i',
236237
);
237238

238-
final rawRows = result.map((row) {
239-
return List<dynamic>.generate(row.length, (i) => row[i]);
240-
}).toList();
239+
final rawRows = <List<Object?>>[
240+
for (final row in result)
241+
List<Object?>.generate(row.length, (i) => row[i]),
242+
];
241243

242-
final stringRows = await compute(convertResultRowsToStrings, rawRows);
244+
final stringRows = await convertResultRowsToStringsYielding(rawRows);
243245

244246
if (!mounted) return;
245247
setState(() {

‎lib/features/sqlite/sqlite_sql_workspace.dart‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import 'dart:async';
22
import 'dart:io';
3-
import 'package:flutter/foundation.dart' show compute;
43
import 'package:flutter/material.dart' as material;
54
import 'package:flutter/services.dart' show LogicalKeyboardKey;
65
import 'package:file_selector/file_selector.dart';
7-
import 'package:querya_desktop/features/sqlite/sqlite_result_utils.dart';
86
import 'package:querya_desktop/core/actions/sql_editor_actions.dart';
97
import 'package:querya_desktop/core/actions/sql_editor_command_bridge.dart';
10-
import 'package:querya_desktop/core/database/sql_limit.dart';
8+
import 'package:querya_desktop/core/database/result_row_string_convert.dart';
119
import 'package:querya_desktop/core/database/sqlite_service.dart';
10+
import 'package:querya_desktop/core/database/sql_limit.dart';
1211
import 'package:querya_desktop/core/layout/vertical_split_pane.dart';
1312
import 'package:querya_desktop/core/storage/app_settings.dart';
1413
import 'package:querya_desktop/core/storage/local_db.dart';
@@ -178,10 +177,8 @@ class _SqliteSqlWorkspaceState extends material.State<SqliteSqlWorkspace> {
178177
return cols.map((col) => row[col]).toList();
179178
}).toList();
180179

181-
final job = SqliteResultConvertJob(rowValues: rawRows);
182-
final outRows = rawRows.length > 500
183-
? await compute(convertSqliteResultRowsToStrings, job)
184-
: convertSqliteResultRowsToStrings(job);
180+
// Yielding convert avoids isolate double-copy of the matrix (#421).
181+
final outRows = await convertResultRowsToStringsYielding(rawRows);
185182

186183
setState(() {
187184
_columns = cols;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:querya_desktop/core/database/result_row_string_convert.dart';
3+
4+
void main() {
5+
group('convertResultRowsToStringsYielding', () {
6+
test('maps null to NULL and yields without isolate', () async {
7+
final rows = <List<Object?>>[
8+
[1, null, 'a'],
9+
[2, 'x', null],
10+
];
11+
final out = await convertResultRowsToStringsYielding(
12+
rows,
13+
yieldEvery: 1,
14+
);
15+
expect(out, [
16+
['1', 'NULL', 'a'],
17+
['2', 'x', 'NULL'],
18+
]);
19+
});
20+
21+
test('empty input returns empty', () async {
22+
expect(await convertResultRowsToStringsYielding(const []), isEmpty);
23+
});
24+
});
25+
}

0 commit comments

Comments
 (0)