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
36 changes: 29 additions & 7 deletions lib/core/database/result_row_string_convert.dart
Original file line number Diff line number Diff line change
@@ -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<List<Object?>>`
/// 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<List<String>> convertResultRowsToStringsSync(List<List<Object?>> 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<List<String>> convertResultRowsToStringsCompute(List<List<Object?>> rowValues) =>
convertResultRowsToStringsSync(rowValues);

/// Converts [rowValues] to string rows, yielding periodically.
Future<List<List<String>>> convertResultRowsToStringsYielding(
List<List<Object?>> rowValues, {
Expand All @@ -31,3 +39,17 @@ Future<List<List<String>>> convertResultRowsToStringsYielding(
}
return out;
}

/// Converts [rowValues] adaptively: offloads to a background isolate via [compute]
/// if row count >= [computeThreshold], otherwise yields on the main isolate.
Future<List<List<String>>> convertResultRowsToStringsAdaptive(
List<List<Object?>> 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);
}
4 changes: 2 additions & 2 deletions lib/features/postgresql/postgres_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,8 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
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;
Expand Down
4 changes: 2 additions & 2 deletions lib/features/postgresql/postgres_table_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class _PostgresTableViewState extends material.State<PostgresTableView> {
List<Object?>.generate(row.length, (i) => row[i]),
];

final stringRows = await convertResultRowsToStringsYielding(rawRows);
final stringRows = await convertResultRowsToStringsAdaptive(rawRows);

if (!mounted) return;
setState(() {
Expand Down Expand Up @@ -257,7 +257,7 @@ class _PostgresTableViewState extends material.State<PostgresTableView> {
List<Object?>.generate(row.length, (i) => row[i]),
];

final stringRows = await convertResultRowsToStringsYielding(rawRows);
final stringRows = await convertResultRowsToStringsAdaptive(rawRows);

if (!mounted) return;
setState(() {
Expand Down
4 changes: 2 additions & 2 deletions lib/features/sqlite/sqlite_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ class _SqliteSqlWorkspaceState extends material.State<SqliteSqlWorkspace> {
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;
Expand Down
60 changes: 47 additions & 13 deletions test/core/database/result_row_string_convert_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <List<Object?>>[
[1, null, 'a'],
[2, 'x', null],
];
group('result_row_string_convert', () {
final sampleRows = <List<Object?>>[
[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<List<Object?>>.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']);
});
});
}

Loading