diff --git a/lib/core/database/result_row_string_convert.dart b/lib/core/database/result_row_string_convert.dart new file mode 100644 index 00000000..16f9f254 --- /dev/null +++ b/lib/core/database/result_row_string_convert.dart @@ -0,0 +1,33 @@ +/// Converts SQL result cells to display strings without a second isolate copy. +/// +/// Prefer this over [compute] for large matrices: shipping `List>` +/// across isolates often costs more than `toString()` itself and roughly +/// doubles peak memory. Yielding every [yieldEvery] rows keeps the UI isolate +/// responsive for 10k+ row caps. +library; + +const int kResultStringConvertYieldEvery = 250; + +/// Maps null cells to `'NULL'` and others via [Object.toString]. +String resultCellToDisplayString(Object? value) => + value == null ? 'NULL' : value.toString(); + +/// Converts [rowValues] to string rows, yielding periodically. +Future>> convertResultRowsToStringsYielding( + List> rowValues, { + int yieldEvery = kResultStringConvertYieldEvery, +}) async { + if (rowValues.isEmpty) return const []; + + final out = >[]; + for (var i = 0; i < rowValues.length; i++) { + final row = rowValues[i]; + out.add([ + for (final value in row) resultCellToDisplayString(value), + ]); + if (yieldEvery > 0 && (i + 1) % yieldEvery == 0) { + await Future.delayed(Duration.zero); + } + } + return out; +} diff --git a/lib/features/mysql/mysql_sql_workspace.dart b/lib/features/mysql/mysql_sql_workspace.dart index 0a79a82b..955ff704 100644 --- a/lib/features/mysql/mysql_sql_workspace.dart +++ b/lib/features/mysql/mysql_sql_workspace.dart @@ -1,13 +1,13 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' as material; import 'package:flutter/services.dart' show LogicalKeyboardKey; import 'package:file_selector/file_selector.dart'; import 'package:querya_desktop/core/actions/sql_editor_actions.dart'; import 'package:querya_desktop/core/actions/sql_editor_command_bridge.dart'; import 'package:querya_desktop/core/database/mysql_service.dart'; +import 'package:querya_desktop/core/database/result_row_string_convert.dart'; import 'package:querya_desktop/core/layout/vertical_split_pane.dart'; import 'package:querya_desktop/core/storage/app_settings.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; @@ -18,7 +18,6 @@ import 'package:querya_desktop/features/main_screen/query_editor_tab.dart'; import 'package:querya_desktop/features/main_screen/results_tab.dart'; import 'package:querya_desktop/features/main_screen/sql_editor_chrome.dart'; import 'package:querya_desktop/features/main_screen/sql_query_history_dialog.dart'; -import 'package:querya_desktop/features/mysql/mysql_result_utils.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; /// Ad-hoc SQL editor + results for MySQL / MariaDB. @@ -192,7 +191,8 @@ class _MysqlSqlWorkspaceState extends material.State { cols.add(c.name.isNotEmpty ? c.name : 'col_${cols.length}'); } - final rawRows = >[]; + // Convert while streaming — no Object? matrix + isolate double-copy (#421). + final outRows = >[]; var n = 0; final cap = _resultMaxRows; var truncated = false; @@ -201,17 +201,18 @@ class _MysqlSqlWorkspaceState extends material.State { truncated = true; break; } - rawRows.add( - List.generate(row.numOfColumns, (i) => row.colAt(i)), + outRows.add( + List.generate( + row.numOfColumns, + (i) => resultCellToDisplayString(row.colAt(i)), + ), ); n++; + if (n % kResultStringConvertYieldEvery == 0) { + await Future.delayed(Duration.zero); + } } - final job = MysqlResultConvertJob(rowValues: rawRows); - final outRows = rawRows.length > 500 - ? await compute(convertMysqlResultRowsToStrings, job) - : convertMysqlResultRowsToStrings(job); - int? affected; if (cols.isEmpty && outRows.isEmpty) { affected = _affectedInt(rs.affectedRows); diff --git a/lib/features/postgresql/postgres_sql_workspace.dart b/lib/features/postgresql/postgres_sql_workspace.dart index 5e266483..464ae4df 100644 --- a/lib/features/postgresql/postgres_sql_workspace.dart +++ b/lib/features/postgresql/postgres_sql_workspace.dart @@ -1,16 +1,15 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart' show compute; import 'package:flutter/material.dart' as material; import 'package:flutter/services.dart' show LogicalKeyboardKey; import 'package:file_selector/file_selector.dart'; -import 'package:querya_desktop/features/postgresql/postgres_result_utils.dart'; import 'package:querya_desktop/core/actions/sql_editor_actions.dart'; import 'package:querya_desktop/core/actions/sql_editor_command_bridge.dart'; import 'package:postgres/postgres.dart' as pg; import 'package:querya_desktop/core/database/postgres_service.dart'; import 'package:querya_desktop/core/database/postgres_sql.dart'; +import 'package:querya_desktop/core/database/result_row_string_convert.dart'; import 'package:querya_desktop/core/layout/vertical_split_pane.dart'; import 'package:querya_desktop/core/storage/app_settings.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; @@ -359,10 +358,8 @@ class _PostgresSqlWorkspaceState extends material.State { n++; } - final job = PostgresResultConvertJob(rowValues: rawRows); - final outRows = rawRows.length > 500 - ? await compute(convertPostgresResultRowsToStrings, job) - : convertPostgresResultRowsToStrings(job); + // Yielding convert avoids isolate double-copy of the matrix (#421). + final outRows = await convertResultRowsToStringsYielding(rawRows); setState(() { _columns = cols; diff --git a/lib/features/postgresql/postgres_table_view.dart b/lib/features/postgresql/postgres_table_view.dart index b76e8658..05e6ea26 100644 --- a/lib/features/postgresql/postgres_table_view.dart +++ b/lib/features/postgresql/postgres_table_view.dart @@ -1,7 +1,7 @@ -import 'package:flutter/foundation.dart' show compute; import 'package:flutter/material.dart' as material; import 'package:querya_desktop/core/database/postgres_connection.dart'; import 'package:querya_desktop/core/database/postgres_service.dart'; +import 'package:querya_desktop/core/database/result_row_string_convert.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/features/postgresql/postgres_sql_editor_dialog.dart'; import 'package:querya_desktop/features/postgresql/postgres_table_privileges_dialog.dart'; @@ -187,11 +187,12 @@ class _PostgresTableViewState extends material.State { (i) => result.schema.columns[i].columnName ?? 'col_$i', ); - final rawRows = result.map((row) { - return List.generate(row.length, (i) => row[i]); - }).toList(); + final rawRows = >[ + for (final row in result) + List.generate(row.length, (i) => row[i]), + ]; - final stringRows = await compute(convertResultRowsToStrings, rawRows); + final stringRows = await convertResultRowsToStringsYielding(rawRows); if (!mounted) return; setState(() { @@ -235,11 +236,12 @@ class _PostgresTableViewState extends material.State { (i) => result.schema.columns[i].columnName ?? 'col_$i', ); - final rawRows = result.map((row) { - return List.generate(row.length, (i) => row[i]); - }).toList(); + final rawRows = >[ + for (final row in result) + List.generate(row.length, (i) => row[i]), + ]; - final stringRows = await compute(convertResultRowsToStrings, rawRows); + final stringRows = await convertResultRowsToStringsYielding(rawRows); if (!mounted) return; setState(() { diff --git a/lib/features/sqlite/sqlite_sql_workspace.dart b/lib/features/sqlite/sqlite_sql_workspace.dart index 64f034a1..8a98276f 100644 --- a/lib/features/sqlite/sqlite_sql_workspace.dart +++ b/lib/features/sqlite/sqlite_sql_workspace.dart @@ -1,14 +1,13 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart' show compute; import 'package:flutter/material.dart' as material; import 'package:flutter/services.dart' show LogicalKeyboardKey; import 'package:file_selector/file_selector.dart'; -import 'package:querya_desktop/features/sqlite/sqlite_result_utils.dart'; import 'package:querya_desktop/core/actions/sql_editor_actions.dart'; import 'package:querya_desktop/core/actions/sql_editor_command_bridge.dart'; -import 'package:querya_desktop/core/database/sql_limit.dart'; +import 'package:querya_desktop/core/database/result_row_string_convert.dart'; import 'package:querya_desktop/core/database/sqlite_service.dart'; +import 'package:querya_desktop/core/database/sql_limit.dart'; import 'package:querya_desktop/core/layout/vertical_split_pane.dart'; import 'package:querya_desktop/core/storage/app_settings.dart'; import 'package:querya_desktop/core/storage/local_db.dart'; @@ -178,10 +177,8 @@ class _SqliteSqlWorkspaceState extends material.State { return cols.map((col) => row[col]).toList(); }).toList(); - final job = SqliteResultConvertJob(rowValues: rawRows); - final outRows = rawRows.length > 500 - ? await compute(convertSqliteResultRowsToStrings, job) - : convertSqliteResultRowsToStrings(job); + // Yielding convert avoids isolate double-copy of the matrix (#421). + final outRows = await convertResultRowsToStringsYielding(rawRows); setState(() { _columns = cols; diff --git a/test/core/database/result_row_string_convert_test.dart b/test/core/database/result_row_string_convert_test.dart new file mode 100644 index 00000000..de451b1b --- /dev/null +++ b/test/core/database/result_row_string_convert_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/database/result_row_string_convert.dart'; + +void main() { + group('convertResultRowsToStringsYielding', () { + test('maps null to NULL and yields without isolate', () async { + final rows = >[ + [1, null, 'a'], + [2, 'x', null], + ]; + final out = await convertResultRowsToStringsYielding( + rows, + yieldEvery: 1, + ); + expect(out, [ + ['1', 'NULL', 'a'], + ['2', 'x', 'NULL'], + ]); + }); + + test('empty input returns empty', () async { + expect(await convertResultRowsToStringsYielding(const []), isEmpty); + }); + }); +}