diff --git a/lib/core/database/result_row_string_convert.dart b/lib/core/database/result_row_string_convert.dart index 16f9f25..f77ef8b 100644 --- a/lib/core/database/result_row_string_convert.dart +++ b/lib/core/database/result_row_string_convert.dart @@ -1,17 +1,25 @@ -/// 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; +import 'package:flutter/foundation.dart'; const int kResultStringConvertYieldEvery = 250; +const int kResultStringConvertComputeThreshold = 1000; /// Maps null cells to `'NULL'` and others via [Object.toString]. String resultCellToDisplayString(Object? value) => value == null ? 'NULL' : value.toString(); +/// Converts [rowValues] to string rows synchronously. +List> convertResultRowsToStringsSync(List> rowValues) { + if (rowValues.isEmpty) return const []; + return [ + for (final row in rowValues) + [for (final value in row) resultCellToDisplayString(value)], + ]; +} + +/// Top-level function suitable for [compute] offloading. +List> convertResultRowsToStringsCompute(List> rowValues) => + convertResultRowsToStringsSync(rowValues); + /// Converts [rowValues] to string rows, yielding periodically. Future>> convertResultRowsToStringsYielding( List> rowValues, { @@ -31,3 +39,17 @@ Future>> convertResultRowsToStringsYielding( } return out; } + +/// Converts [rowValues] adaptively: offloads to a background isolate via [compute] +/// if row count >= [computeThreshold], otherwise yields on the main isolate. +Future>> convertResultRowsToStringsAdaptive( + List> rowValues, { + int computeThreshold = kResultStringConvertComputeThreshold, + int yieldEvery = kResultStringConvertYieldEvery, +}) async { + if (rowValues.isEmpty) return const []; + if (rowValues.length >= computeThreshold) { + return compute(convertResultRowsToStringsCompute, rowValues); + } + return convertResultRowsToStringsYielding(rowValues, yieldEvery: yieldEvery); +} diff --git a/lib/features/postgresql/postgres_sql_workspace.dart b/lib/features/postgresql/postgres_sql_workspace.dart index 0cd85be..f511f59 100644 --- a/lib/features/postgresql/postgres_sql_workspace.dart +++ b/lib/features/postgresql/postgres_sql_workspace.dart @@ -358,8 +358,8 @@ class _PostgresSqlWorkspaceState extends material.State { n++; } - // Yielding convert avoids isolate double-copy of the matrix (#421). - final outRows = await convertResultRowsToStringsYielding(rawRows); + // Adaptive convert offloads to background compute for large row sets (#522). + final outRows = await convertResultRowsToStringsAdaptive(rawRows); setState(() { _columns = cols; diff --git a/lib/features/postgresql/postgres_table_view.dart b/lib/features/postgresql/postgres_table_view.dart index 636aa0c..5a85e20 100644 --- a/lib/features/postgresql/postgres_table_view.dart +++ b/lib/features/postgresql/postgres_table_view.dart @@ -200,7 +200,7 @@ class _PostgresTableViewState extends material.State { List.generate(row.length, (i) => row[i]), ]; - final stringRows = await convertResultRowsToStringsYielding(rawRows); + final stringRows = await convertResultRowsToStringsAdaptive(rawRows); if (!mounted) return; setState(() { @@ -257,7 +257,7 @@ class _PostgresTableViewState extends material.State { List.generate(row.length, (i) => row[i]), ]; - final stringRows = await convertResultRowsToStringsYielding(rawRows); + final stringRows = await convertResultRowsToStringsAdaptive(rawRows); if (!mounted) return; setState(() { diff --git a/lib/features/sqlite/sqlite_sql_workspace.dart b/lib/features/sqlite/sqlite_sql_workspace.dart index 5a47861..0ecab18 100644 --- a/lib/features/sqlite/sqlite_sql_workspace.dart +++ b/lib/features/sqlite/sqlite_sql_workspace.dart @@ -177,8 +177,8 @@ class _SqliteSqlWorkspaceState extends material.State { return cols.map((col) => row[col]).toList(); }).toList(); - // Yielding convert avoids isolate double-copy of the matrix (#421). - final outRows = await convertResultRowsToStringsYielding(rawRows); + // Adaptive convert offloads to background compute for large row sets (#522). + final outRows = await convertResultRowsToStringsAdaptive(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 index de451b1..44d4253 100644 --- a/test/core/database/result_row_string_convert_test.dart +++ b/test/core/database/result_row_string_convert_test.dart @@ -2,24 +2,58 @@ 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], - ]; + group('result_row_string_convert', () { + final sampleRows = >[ + [1, null, 'a'], + [2, 'x', null], + ]; + + final expectedOutput = [ + ['1', 'NULL', 'a'], + ['2', 'x', 'NULL'], + ]; + + test('convertResultRowsToStringsSync maps rows correctly', () { + expect(convertResultRowsToStringsSync(sampleRows), expectedOutput); + expect(convertResultRowsToStringsSync(const []), isEmpty); + }); + + test('convertResultRowsToStringsCompute maps rows correctly', () { + expect(convertResultRowsToStringsCompute(sampleRows), expectedOutput); + expect(convertResultRowsToStringsCompute(const []), isEmpty); + }); + + test('convertResultRowsToStringsYielding maps null to NULL and yields', () async { final out = await convertResultRowsToStringsYielding( - rows, + sampleRows, yieldEvery: 1, ); - expect(out, [ - ['1', 'NULL', 'a'], - ['2', 'x', 'NULL'], - ]); + expect(out, expectedOutput); + expect(await convertResultRowsToStringsYielding(const []), isEmpty); + }); + + test('convertResultRowsToStringsAdaptive handles small payload via yielding', () async { + final out = await convertResultRowsToStringsAdaptive( + sampleRows, + computeThreshold: 100, + ); + expect(out, expectedOutput); + expect(await convertResultRowsToStringsAdaptive(const []), isEmpty); }); - test('empty input returns empty', () async { - expect(await convertResultRowsToStringsYielding(const []), isEmpty); + test('convertResultRowsToStringsAdaptive handles large payload via compute', () async { + final largeRows = List>.generate( + 10, + (i) => [i, null, 'val_$i'], + ); + final out = await convertResultRowsToStringsAdaptive( + largeRows, + computeThreshold: 5, + ); + expect(out.length, 10); + expect(out[0], ['0', 'NULL', 'val_0']); + expect(out[9], ['9', 'NULL', 'val_9']); }); }); } +