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
39 changes: 22 additions & 17 deletions lib/features/mysql/mysql_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:querya_desktop/core/ui/querya_shell_status.dart';
import 'package:querya_desktop/features/mysql/mysql_sql_tx_guard.dart';
import 'package:querya_desktop/features/settings/preferences_dialog.dart';
import 'package:querya_desktop/features/settings/sql_statement_timeout_dropdown.dart';
import 'package:querya_desktop/features/workspace/sql_result_grid_schema.dart';
import 'package:querya_desktop/features/workspace/workspace.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';

Expand Down Expand Up @@ -327,6 +328,7 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
session.statusLine = null;
session.resultGridPrimaryKeys = const [];
session.resultGridColumnDataTypes = null;
session.resultGridColumnMeta = null;
});
QueryaShellStatus.instance.beginBusy(message: 'Running query…');
final sw = Stopwatch()..start();
Expand Down Expand Up @@ -393,22 +395,20 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
}

final target = SqlTableTargetExtractor.extract(userSql);
var pks = const <String>[];
Map<String, String>? types;
var gridSchema = SqlResultGridSchema.none;
final schemaName = target?.schema ?? _poolDatabaseKey();
if (target != null && cols.isNotEmpty && schemaName.isNotEmpty) {
try {
final meta = await conn.getTableSchema(
database: schemaName,
table: target.tableName,
);
pks = List<String>.from(meta.primaryKeys);
types = columnDataTypesFromSchema(meta);
} catch (_) {
pks = const [];
types = null;
}
gridSchema = SqlResultGridSchema.fromLoad(
await loadTableViewSchema(
() => conn.getTableSchema(
database: schemaName,
table: target.tableName,
),
),
);
}
final pks = gridSchema.primaryKeys;
final editHint = gridSchema.editHint(cols);
final canSave = sqlResultGridSaveEnabled(
sql: userSql,
resultColumns: cols,
Expand All @@ -421,7 +421,8 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
session.affectedRows = affected;
session.lastExecutedSql = userSql;
session.resultGridPrimaryKeys = canSave ? pks : const [];
session.resultGridColumnDataTypes = types;
session.resultGridColumnDataTypes = gridSchema.columnDataTypes;
session.resultGridColumnMeta = gridSchema.columnMeta;
session.stagingBuffer?.dispose();
session.stagingBuffer = canSave
? DataGridStagingBuffer(
Expand All @@ -435,9 +436,12 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
? 'OK. Rows affected: $affected.'
: 'Command completed.';
} else {
session.statusLine = truncated
? 'Showing first $cap row(s) (result capped).'
: '$n row(s).';
session.statusLine = withEditHint(
truncated
? 'Showing first $cap row(s) (result capped).'
: '$n row(s).',
editHint,
);
}
session.running = false;
});
Expand Down Expand Up @@ -512,6 +516,7 @@ class _MysqlSqlWorkspaceState extends material.State<MysqlSqlWorkspace> {
schema: schemaName,
primaryKeys: session.resultGridPrimaryKeys,
columnDataTypes: session.resultGridColumnDataTypes,
columnMeta: session.resultGridColumnMeta,
);
if (plan.isEmpty) {
setState(() => session.savingChanges = false);
Expand Down
39 changes: 22 additions & 17 deletions lib/features/postgresql/postgres_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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/settings/preferences_dialog.dart';
import 'package:querya_desktop/features/settings/sql_statement_timeout_dropdown.dart';
import 'package:querya_desktop/features/workspace/sql_result_grid_schema.dart';
import 'package:querya_desktop/features/workspace/workspace.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';

Expand Down Expand Up @@ -454,6 +455,7 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
session.statusLine = null;
session.resultGridPrimaryKeys = const [];
session.resultGridColumnDataTypes = null;
session.resultGridColumnMeta = null;
});
QueryaShellStatus.instance.beginBusy(message: 'Running query…');
final sw = Stopwatch()..start();
Expand Down Expand Up @@ -513,21 +515,19 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
);

final target = SqlTableTargetExtractor.extract(userSql);
var pks = const <String>[];
Map<String, String>? types;
var gridSchema = SqlResultGridSchema.none;
if (target != null && cols.isNotEmpty) {
try {
final meta = await conn.getTableSchema(
schema: target.schema ?? 'public',
table: target.tableName,
);
pks = List<String>.from(meta.primaryKeys);
types = columnDataTypesFromSchema(meta);
} catch (_) {
pks = const [];
types = null;
}
gridSchema = SqlResultGridSchema.fromLoad(
await loadTableViewSchema(
() => conn.getTableSchema(
schema: target.schema ?? 'public',
table: target.tableName,
),
),
);
}
final pks = gridSchema.primaryKeys;
final editHint = gridSchema.editHint(cols);
final canSave = sqlResultGridSaveEnabled(
sql: userSql,
resultColumns: cols,
Expand All @@ -540,7 +540,8 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
session.affectedRows = result.affectedRows;
session.lastExecutedSql = userSql;
session.resultGridPrimaryKeys = canSave ? pks : const [];
session.resultGridColumnDataTypes = types;
session.resultGridColumnDataTypes = gridSchema.columnDataTypes;
session.resultGridColumnMeta = gridSchema.columnMeta;
session.stagingBuffer?.dispose();
session.stagingBuffer = canSave
? DataGridStagingBuffer(
Expand All @@ -554,9 +555,12 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
'Command completed. Rows affected: ${result.affectedRows}.';
} else {
final truncated = result.length >= cap;
session.statusLine = truncated
? 'Showing first $cap row(s) (result capped).'
: '${result.length} row(s).';
session.statusLine = withEditHint(
truncated
? 'Showing first $cap row(s) (result capped).'
: '${result.length} row(s).',
editHint,
);
}
session.running = false;
});
Expand Down Expand Up @@ -635,6 +639,7 @@ class _PostgresSqlWorkspaceState extends material.State<PostgresSqlWorkspace> {
schema: target.schema ?? 'public',
primaryKeys: session.resultGridPrimaryKeys,
columnDataTypes: session.resultGridColumnDataTypes,
columnMeta: session.resultGridColumnMeta,
);
if (plan.isEmpty) {
setState(() => session.savingChanges = false);
Expand Down
38 changes: 28 additions & 10 deletions lib/features/sqlite/sqlite_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:querya_desktop/core/ui/querya_shell_status.dart';
import 'package:querya_desktop/features/sqlite/sqlite_result_utils.dart';
import 'package:querya_desktop/features/settings/preferences_dialog.dart';
import 'package:querya_desktop/features/settings/sql_statement_timeout_dropdown.dart';
import 'package:querya_desktop/features/workspace/sql_result_grid_schema.dart';
import 'package:querya_desktop/features/workspace/workspace.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';

Expand Down Expand Up @@ -319,6 +320,7 @@ class _SqliteSqlWorkspaceState extends material.State<SqliteSqlWorkspace> {
session.statusLine = null;
session.resultGridPrimaryKeys = const [];
session.resultGridColumnDataTypes = null;
session.resultGridColumnMeta = null;
});
QueryaShellStatus.instance.beginBusy(message: 'Running query…');
final sw = Stopwatch()..start();
Expand Down Expand Up @@ -368,18 +370,28 @@ class _SqliteSqlWorkspaceState extends material.State<SqliteSqlWorkspace> {
final outRows = await convertResultRowsToStringsAdaptive(rawRows);

final target = SqlTableTargetExtractor.extract(userSql);
var pks = const <String>[];
Map<String, String>? types;
var gridSchema = SqlResultGridSchema.none;
if (target != null && cols.isNotEmpty) {
// Views have no rowid, so they never fall back to it.
var isView = false;
try {
final meta = await conn.getTableSchema(table: target.tableName);
pks = List<String>.from(meta.primaryKeys);
types = columnDataTypesFromSchema(meta);
final kind = await conn.execute(
'SELECT type FROM sqlite_master WHERE name = ?',
[target.tableName],
);
isView = kind.isNotEmpty && kind.first['type'] == 'view';
} catch (_) {
pks = const [];
types = null;
// Unknown kind: treat as a table (the schema load surfaces real errors).
}
gridSchema = SqlResultGridSchema.fromLoad(
await loadTableViewSchema(
() => conn.getTableSchema(table: target.tableName),
),
sqliteImplicitRowid: !isView,
);
}
final pks = gridSchema.primaryKeys;
final editHint = gridSchema.editHint(cols);
final canSave = sqlResultGridSaveEnabled(
sql: userSql,
resultColumns: cols,
Expand All @@ -392,7 +404,8 @@ class _SqliteSqlWorkspaceState extends material.State<SqliteSqlWorkspace> {
session.affectedRows = null;
session.lastExecutedSql = userSql;
session.resultGridPrimaryKeys = canSave ? pks : const [];
session.resultGridColumnDataTypes = types;
session.resultGridColumnDataTypes = gridSchema.columnDataTypes;
session.resultGridColumnMeta = gridSchema.columnMeta;
session.stagingBuffer?.dispose();
session.stagingBuffer = canSave
? DataGridStagingBuffer(
Expand All @@ -404,9 +417,13 @@ class _SqliteSqlWorkspaceState extends material.State<SqliteSqlWorkspace> {
if (cols.isEmpty && outRows.isEmpty) {
session.statusLine = 'Command completed.';
} else if (truncated || (injectedLimit && results.length >= cap)) {
session.statusLine = 'Showing first $cap row(s) (result capped).';
session.statusLine = withEditHint(
'Showing first $cap row(s) (result capped).',
editHint,
);
} else {
session.statusLine = '${results.length} row(s).';
session.statusLine =
withEditHint('${results.length} row(s).', editHint);
}
session.running = false;
});
Expand Down Expand Up @@ -479,6 +496,7 @@ class _SqliteSqlWorkspaceState extends material.State<SqliteSqlWorkspace> {
schema: target.schema,
primaryKeys: session.resultGridPrimaryKeys,
columnDataTypes: session.resultGridColumnDataTypes,
columnMeta: session.resultGridColumnMeta,
);
if (plan.isEmpty) {
setState(() => session.savingChanges = false);
Expand Down
5 changes: 5 additions & 0 deletions lib/features/workspace/sql_query_tab_session.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/actions/sql_script_format.dart';
import 'package:querya_desktop/core/database/table_schema_meta.dart';
import 'package:querya_desktop/core/unsaved_work_registry.dart';
import 'package:querya_desktop/features/workspace/data_grid_staging_buffer.dart';

Expand Down Expand Up @@ -75,6 +76,10 @@ class SqlQueryTabSession {
/// Column types from [getTableSchema] for DML literals, if resolved.
Map<String, String>? resultGridColumnDataTypes;

/// Column flags (generated / default / nullable) so INSERT can omit columns
/// the database fills in, same as Table Browser.
Map<String, TableColumnMeta>? resultGridColumnMeta;

void formatSql() {
final next = formatSqlScript(controller.text);
controller.value = material.TextEditingValue(
Expand Down
87 changes: 87 additions & 0 deletions lib/features/workspace/sql_result_grid_schema.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import 'package:querya_desktop/core/database/table_schema_meta.dart';
import 'package:querya_desktop/features/sqlite/sqlite_table_utils.dart';
import 'package:querya_desktop/features/workspace/table_view_staging.dart';

/// Schema facts the SQL result grid needs to make a single-table SELECT
/// editable, mirroring what Table Browser derives from `getTableSchema`.
class SqlResultGridSchema {
const SqlResultGridSchema({
this.primaryKeys = const [],
this.columnDataTypes,
this.columnMeta,
this.schemaError,
this.needsRowidColumn = false,
});

/// No schema was looked up (not a simple single-table SELECT).
static const none = SqlResultGridSchema();

/// Builds the schema from a [loadTableViewSchema] outcome.
///
/// With [sqliteImplicitRowid], a table without a declared PRIMARY KEY is keyed
/// by its implicit `rowid` (as in Table Browser); Save then needs `rowid` to be
/// one of the SELECTed columns.
factory SqlResultGridSchema.fromLoad(
TableViewSchemaLoad loaded, {
bool sqliteImplicitRowid = false,
}) {
final schema = loaded.schema;
if (schema == null) {
return SqlResultGridSchema(schemaError: loaded.error);
}

var primaryKeys = List<String>.from(schema.primaryKeys);
final types = columnDataTypesFromSchema(schema);
final meta = columnMetaFromSchema(schema);

var needsRowid = false;
if (sqliteImplicitRowid) {
primaryKeys = sqliteTableBrowserPrimaryKeys(
declaredPrimaryKeys: primaryKeys,
isView: false,
);
needsRowid = sqliteBrowseNeedsRowidColumn(
primaryKeys: primaryKeys,
isView: false,
);
if (needsRowid && !meta.containsKey(kSqliteImplicitRowid)) {
types[kSqliteImplicitRowid] = sqliteImplicitRowidColumn.dataType;
meta[kSqliteImplicitRowid] = sqliteImplicitRowidColumn;
}
}

return SqlResultGridSchema(
primaryKeys: primaryKeys,
columnDataTypes: types,
columnMeta: meta,
needsRowidColumn: needsRowid,
);
}

final List<String> primaryKeys;
final Map<String, String>? columnDataTypes;
final Map<String, TableColumnMeta>? columnMeta;

/// `getTableSchema` failure, kept apart from a genuine "no primary key".
final Object? schemaError;

/// SQLite table keyed only by implicit `rowid`.
final bool needsRowidColumn;

/// Why Save is off, appended to the result status line. Null when nothing
/// needs saying (editable, or not a plain table SELECT).
String? editHint(List<String> resultColumns) {
final error = schemaError;
if (error != null) {
return 'Cannot edit: schema unavailable. $error. Run the query again to retry.';
}
if (needsRowidColumn && !resultColumns.contains(kSqliteImplicitRowid)) {
return 'To edit rows, include rowid in the SELECT (this table has no primary key).';
}
return null;
}
}

/// Appends [hint] to [status] as a second sentence.
String withEditHint(String status, String? hint) =>
hint == null ? status : '$status $hint';
Loading
Loading