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
53 changes: 50 additions & 3 deletions lib/features/sqlite/sqlite_result_utils.dart
Original file line number Diff line number Diff line change
@@ -1,29 +1,72 @@
import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:querya_desktop/core/database/result_row_string_convert.dart';
import 'package:querya_desktop/core/database/table_mutation_engine.dart';

/// Serializable row batch for [convertSqliteResultRowsToStrings] in a worker isolate.
class SqliteResultConvertJob {
const SqliteResultConvertJob({
required this.rowValues,
this.columnDataTypes,
});

final List<List<Object?>> rowValues;
final List<String?>? columnDataTypes;
}

/// Converts SQLite result cell values to display strings off the UI thread.
List<List<String>> convertSqliteResultRowsToStrings(
SqliteResultConvertJob job) {
final types = job.columnDataTypes;
return [
for (final row in job.rowValues)
[for (final value in row) sqliteResultCellToDisplayString(value)],
[
for (var i = 0; i < row.length; i++)
sqliteResultCellToDisplayString(
row[i],
dataTypeName: types != null && i < types.length ? types[i] : null,
),
],
];
}

/// Typed conversion in a single pass: converts each cell to SQLite display string,
/// interns the resulting string (repeated values share one instance), and yields
/// to the event loop every [yieldEvery] rows.
Future<List<List<String>>> convertSqliteResultRowsToStringsAdaptive(
SqliteResultConvertJob job, {
int yieldEvery = kResultStringConvertYieldEvery,
StringInternPool? pool,
}) async {
final rowValues = job.rowValues;
if (rowValues.isEmpty) return const [];

final types = job.columnDataTypes;
final activePool = pool ?? StringInternPool();
final out = <List<String>>[];
for (var r = 0; r < rowValues.length; r++) {
final row = rowValues[r];
out.add([
for (var i = 0; i < row.length; i++)
activePool.intern(
sqliteResultCellToDisplayString(
row[i],
dataTypeName: types != null && i < types.length ? types[i] : null,
pool: activePool,
),
),
]);
if (yieldEvery > 0 && (r + 1) % yieldEvery == 0) {
await Future<void>.delayed(Duration.zero);
}
}
return out;
}

/// BLOB as `X'deadbeef'`; other values via [Object.toString]. Null → `NULL`.
String sqliteResultCellToDisplayString(
Object? value, {
String? dataTypeName,
StringInternPool? pool,
}) {
if (value == null) return 'NULL';
if (value is Uint8List) {
Expand All @@ -37,6 +80,10 @@ String sqliteResultCellToDisplayString(
TableMutationEngine.isBinaryType(dataTypeName)) {
return sqliteBlobHexLiteralFromCell(value);
}
if (pool != null) {
return pool.internObject(value);
}
if (value is bool) return value ? 'true' : 'false';
return value.toString();
}

Expand Down
17 changes: 10 additions & 7 deletions lib/features/sqlite/sqlite_sql_workspace.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dart:async';

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/database/result_row_string_convert.dart';
import 'package:querya_desktop/core/database/sql_limit.dart';
import 'package:querya_desktop/core/database/sql_table_target_extractor.dart';
import 'package:querya_desktop/core/database/sqlite_service.dart';
Expand Down Expand Up @@ -105,13 +104,17 @@ class SqliteSqlExecutionDelegate extends BaseSqlExecutionDelegate {
final limitCount = truncated ? cap : results.length;
final injectedLimit = effectiveSql != sql;

final rawRows = results.take(limitCount).map((row) {
return cols
.map((col) => sqliteResultCellToDisplayString(row[col]))
.toList();
}).toList();
final rawRows = <List<Object?>>[];
var count = 0;
for (final row in results) {
if (count >= limitCount) break;
rawRows.add([for (final col in cols) row[col]]);
count++;
}

final outRows = await convertResultRowsToStringsAdaptive(rawRows);
final outRows = await convertSqliteResultRowsToStringsAdaptive(
SqliteResultConvertJob(rowValues: rawRows),
);

final isTruncated = truncated || (injectedLimit && results.length >= cap);
final statusMsg = formatStatusMessage(
Expand Down
53 changes: 53 additions & 0 deletions test/features/sql_workspaces/result_conversion_perf_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,57 @@ void main() {
expect(ticks, 1, reason: 'a queued task ran while conversion was in flight');
});
});

group('convertSqliteResultRowsToStringsAdaptive (single typed pass)', () {
List<List<Object?>> sample(int n) => [
for (var i = 0; i < n; i++)
[
i,
i.isEven ? 'active' : 'custom_$i',
i % 3 == 0 ? Uint8List.fromList([0xca, 0xfe]) : null,
null,
],
];

test('matches synchronous convertSqliteResultRowsToStrings', () async {
final job = SqliteResultConvertJob(rowValues: sample(1200));
expect(
await convertSqliteResultRowsToStringsAdaptive(job),
convertSqliteResultRowsToStrings(job),
);
});

test('interns repeated values so they share one string instance', () async {
final out = await convertSqliteResultRowsToStringsAdaptive(
SqliteResultConvertJob(
rowValues: [
for (var i = 0; i < 6; i++) [i, 'status_${i % 2}_${'x' * 4}'],
],
),
);
expect(identical(out[0][1], out[2][1]), isTrue);
expect(identical(out[1][1], out[3][1]), isTrue);
});

test('empty input yields empty output', () async {
expect(
await convertSqliteResultRowsToStringsAdaptive(
const SqliteResultConvertJob(rowValues: []),
),
isEmpty,
);
});

test('formats blobs correctly as hex literals in adaptive mode', () async {
final out = await convertSqliteResultRowsToStringsAdaptive(
SqliteResultConvertJob(
rowValues: [
['ok', Uint8List.fromList([0xde, 0xad]), null],
],
),
);
expect(out.single, ['ok', "X'dead'", 'NULL']);
});
});
}

Loading