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
40 changes: 32 additions & 8 deletions lib/core/database/compact_result_dataset.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,13 @@ class GenericCompactColumn implements CompactColumn {
GenericCompactColumn({
required this.values,
required this.length,
this.formatter,
});

final List<Object?> values;
@override
final int length;
final String Function(Object?)? formatter;

@override
CompactColumnKind get kind => CompactColumnKind.generic;
Expand All @@ -150,6 +152,10 @@ class GenericCompactColumn implements CompactColumn {
String stringValueAt(int index, [StringInternPool? pool]) {
final val = values[index];
if (val == null) return 'NULL';
if (formatter != null) {
final str = formatter!(val);
return pool != null ? pool.intern(str) : str;
}
return pool != null ? pool.internObject(val) : val.toString();
}
}
Expand Down Expand Up @@ -208,9 +214,13 @@ class CompactResultDataset {

/// Constructs a [CompactResultDataset] from raw rows, detecting column types
/// and packing numeric columns into [Int64List] / [Float64List].
///
/// An optional [cellFormatter] callback can be provided to customize display string
/// formatting for driver-specific types (e.g. blobs, timestamps, boolean tinyints).
static CompactResultDataset fromRawRows(
List<String> columnNames,
List<List<Object?>> rawRows, {
String Function(Object? value, int colIndex)? cellFormatter,
StringInternPool? pool,
}) {
final rowCount = rawRows.length;
Expand Down Expand Up @@ -239,14 +249,27 @@ class CompactResultDataset {
if (val == null) continue;
sampleCount++;

if (val is! int) {
allInt = false;
}
if (val is! num) {
allFloat = false;
}
if (val is! String) {
allString = false;
if (cellFormatter != null) {
final formatted = cellFormatter(val, col);
if (allInt && (val is! int || formatted != val.toString())) {
allInt = false;
}
if (allFloat && (val is! num || formatted != val.toString())) {
allFloat = false;
}
if (allString && (val is! String || formatted != val)) {
allString = false;
}
} else {
if (val is! int) {
allInt = false;
}
if (val is! num) {
allFloat = false;
}
if (val is! String) {
allString = false;
}
}

if (sampleCount >= 60 && !allInt && !allFloat && !allString) {
Expand Down Expand Up @@ -340,6 +363,7 @@ class CompactResultDataset {
compactColumns.add(GenericCompactColumn(
values: values,
length: rowCount,
formatter: cellFormatter != null ? (v) => cellFormatter(v, col) : null,
));
}
}
Expand Down
27 changes: 27 additions & 0 deletions lib/core/database/sql_query_runner.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:querya_desktop/core/database/compact_result_dataset.dart';

/// Read side of a SQL session: run a statement, explain it, release it.
///
Expand Down Expand Up @@ -32,12 +33,38 @@ class SqlExecutionResult {
this.statusMessage,
this.elapsed,
this.isTruncated = false,
this.dataset,
});

/// Creates a [SqlExecutionResult] backed by a [CompactResultDataset].
///
/// [rows] defaults to [dataset.asLazyRowList()], providing unformatted on-demand
/// access to cells for virtualized grid rendering while avoiding upfront
/// allocation of millions of string objects.
factory SqlExecutionResult.fromDataset({
required CompactResultDataset dataset,
int? affectedRows,
String? statusMessage,
Duration? elapsed,
bool isTruncated = false,
List<List<String>>? rows,
}) {
return SqlExecutionResult(
columns: dataset.columnNames,
rows: rows ?? dataset.asLazyRowList(),
affectedRows: affectedRows,
statusMessage: statusMessage,
elapsed: elapsed,
isTruncated: isTruncated,
dataset: dataset,
);
}

final List<String> columns;
final List<List<String>> rows;
final int? affectedRows;
final String? statusMessage;
final Duration? elapsed;
final bool isTruncated;
final CompactResultDataset? dataset;
}
45 changes: 20 additions & 25 deletions lib/features/mysql/mysql_sql_workspace.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/database/compact_result_dataset.dart';
import 'package:querya_desktop/core/database/mysql_result_cells.dart';
import 'package:querya_desktop/core/database/mysql_service.dart';
import 'package:querya_desktop/core/database/result_row_string_convert.dart';
Expand Down Expand Up @@ -107,34 +108,29 @@ class MysqlSqlExecutionDelegate extends BaseSqlExecutionDelegate {
},
);
final colList = rs.cols.toList();
final pool = StringInternPool();
final outRows = <List<String>>[];
final rawRows = <List<Object?>>[];
for (var r = 0; r < taken.items.length; r++) {
final row = taken.items[r];
outRows.add(
List.generate(
row.numOfColumns,
(i) {
final col = i < colList.length ? colList[i] : null;
return mysqlResultCellToDisplayString(
row.colAt(i),
column: col,
pool: pool,
);
},
),
);
if (kResultStringConvertYieldEvery > 0 &&
(r + 1) % kResultStringConvertYieldEvery == 0) {
await Future<void>.delayed(Duration.zero);
}
rawRows.add([
for (var i = 0; i < row.numOfColumns; i++) row.colAt(i),
]);
}

final dataset = CompactResultDataset.fromRawRows(
cols,
rawRows,
cellFormatter: (val, colIndex) {
final col = colIndex < colList.length ? colList[colIndex] : null;
return mysqlResultCellToDisplayString(val, column: col);
},
);

final truncated =
taken.truncated || (effectiveSql != sql && outRows.length >= cap);
final n = outRows.length;
taken.truncated || (effectiveSql != sql && dataset.rowCount >= cap);
final n = dataset.rowCount;

int? affected;
if (cols.isEmpty && outRows.isEmpty) {
if (cols.isEmpty && dataset.rowCount == 0) {
affected = _affectedInt(rs.affectedRows);
}

Expand All @@ -146,9 +142,8 @@ class MysqlSqlExecutionDelegate extends BaseSqlExecutionDelegate {
cap: cap,
);

return SqlExecutionResult(
columns: cols,
rows: outRows,
return SqlExecutionResult.fromDataset(
dataset: dataset,
affectedRows: affected,
statusMessage: statusMsg,
isTruncated: truncated,
Expand Down
26 changes: 14 additions & 12 deletions lib/features/postgresql/postgres_sql_workspace.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import 'dart:async';

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/database/compact_result_dataset.dart';
import 'package:querya_desktop/core/database/postgres_result_cells.dart';
import 'package:querya_desktop/core/database/postgres_service.dart';
import 'package:querya_desktop/core/database/postgres_sql.dart';
import 'package:querya_desktop/core/database/sql_table_target_extractor.dart';
import 'package:querya_desktop/core/database/table_mutation_engine.dart';
import 'package:querya_desktop/core/storage/app_settings.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/postgresql/postgres_object_kind.dart';
import 'package:querya_desktop/features/postgresql/postgres_result_utils.dart';
import 'package:querya_desktop/features/postgresql/postgres_table_utils.dart';
import 'package:querya_desktop/features/workspace/workspace.dart';
import 'package:querya_desktop/features/workspace/query_plan.dart';
Expand Down Expand Up @@ -129,28 +130,29 @@ class PostgresSqlExecutionDelegate extends BaseSqlExecutionDelegate {
n++;
}

final outRows = await convertPostgresResultRowsToStringsAdaptive(
PostgresResultConvertJob(
rowValues: rawRows,
columnTypeOids: [
for (final c in schema.columns) c.typeOid,
],
),
final dataset = CompactResultDataset.fromRawRows(
cols,
rawRows,
cellFormatter: (val, colIndex) {
final typeOid = colIndex < schema.columns.length
? schema.columns[colIndex].typeOid
: null;
return postgresResultCellToDisplayString(val, typeOid: typeOid);
},
);

final isTruncated = result.length >= cap;
final statusMsg = formatStatusMessage(
columnCount: cols.length,
rowCount: outRows.length,
rowCount: dataset.rowCount,
affectedRows: result.affectedRows,
isTruncated: isTruncated,
cap: cap,
successPrefix: 'Command completed.',
);

return SqlExecutionResult(
columns: cols,
rows: outRows,
return SqlExecutionResult.fromDataset(
dataset: dataset,
affectedRows: result.affectedRows,
statusMessage: statusMsg,
isTruncated: isTruncated,
Expand Down
14 changes: 8 additions & 6 deletions lib/features/sqlite/sqlite_sql_workspace.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/database/compact_result_dataset.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 @@ -112,8 +113,10 @@ class SqliteSqlExecutionDelegate extends BaseSqlExecutionDelegate {
count++;
}

final outRows = await convertSqliteResultRowsToStringsAdaptive(
SqliteResultConvertJob(rowValues: rawRows),
final dataset = CompactResultDataset.fromRawRows(
cols,
rawRows,
cellFormatter: (val, _) => sqliteResultCellToDisplayString(val),
);

final isTruncated = truncated || (injectedLimit && results.length >= cap);
Expand All @@ -124,11 +127,10 @@ class SqliteSqlExecutionDelegate extends BaseSqlExecutionDelegate {
cap: cap,
);

return SqlExecutionResult(
columns: cols,
rows: outRows,
return SqlExecutionResult.fromDataset(
dataset: dataset,
statusMessage: statusMsg,
isTruncated: truncated || (injectedLimit && results.length >= cap),
isTruncated: isTruncated,
);
}

Expand Down
71 changes: 71 additions & 0 deletions test/core/database/compact_result_dataset_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:typed_data';

import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/database/compact_result_dataset.dart';
import 'package:querya_desktop/core/database/sql_query_runner.dart';

void main() {
group('CompactResultDataset', () {
Expand Down Expand Up @@ -162,5 +163,75 @@ void main() {
expect(column.stringValueAt(2), '0.0');
expect(column.stringValueAt(3), '100.0');
});

test('fromRawRows with cellFormatter customizes generic column formatting and overrides primitives', () {
final columns = ['id', 'is_active', 'raw_bytes'];
final rows = <List<Object?>>[
[101, 1, [0xde, 0xad]],
[102, 0, [0xbe, 0xef]],
[103, null, null],
];

final dataset = CompactResultDataset.fromRawRows(
columns,
rows,
cellFormatter: (val, colIndex) {
if (colIndex == 1) {
return val == 1 ? 'true' : (val == 0 ? 'false' : 'NULL');
}
if (colIndex == 2 && val is List<int>) {
return '0x${val.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}';
}
return val.toString();
},
);

expect(dataset.columns[0].kind, CompactColumnKind.int64);
// is_active was 1 and 0, but cellFormatter returned 'true' / 'false' != '1' / '0',
// so it was correctly recognized as generic with formatter
expect(dataset.columns[1].kind, CompactColumnKind.generic);
expect(dataset.columns[2].kind, CompactColumnKind.generic);

expect(dataset.cellAt(0, 0), '101');
expect(dataset.cellAt(0, 1), 'true');
expect(dataset.cellAt(0, 2), '0xdead');

expect(dataset.cellAt(1, 0), '102');
expect(dataset.cellAt(1, 1), 'false');
expect(dataset.cellAt(1, 2), '0xbeef');

expect(dataset.cellAt(2, 0), '103');
expect(dataset.cellAt(2, 1), 'NULL');
expect(dataset.cellAt(2, 2), 'NULL');
});

test('SqlExecutionResult.fromDataset provides transparent lazy rows and backwards compatibility', () {
final columns = ['title', 'views'];
final rows = <List<Object?>>[
['First Post', 42],
['Second Post', 100],
];

final dataset = CompactResultDataset.fromRawRows(columns, rows);
final result = SqlExecutionResult.fromDataset(
dataset: dataset,
affectedRows: null,
statusMessage: '2 rows fetched',
isTruncated: false,
);

expect(result.dataset, same(dataset));
expect(result.columns, ['title', 'views']);
expect(result.statusMessage, '2 rows fetched');
expect(result.isTruncated, isFalse);

// rows is lazy and fully matches List<List<String>> contract
expect(result.rows.length, 2);
expect(result.rows[0], ['First Post', '42']);
expect(result.rows[1], ['Second Post', '100']);
expect(result.rows[0][0], 'First Post');
expect(result.rows[1][1], '100');
});
});
}

Loading