diff --git a/lib/features/workspace/generic_sql_workspace.dart b/lib/features/workspace/generic_sql_workspace.dart index 751db788..07d99d1b 100644 --- a/lib/features/workspace/generic_sql_workspace.dart +++ b/lib/features/workspace/generic_sql_workspace.dart @@ -617,11 +617,6 @@ class GenericSqlWorkspaceState extends material.State { tableTitle: session.title, ); if (!safeToProceed) return; - if (session.stagingBuffer != null && session.stagingBuffer!.isDirty) { - session.stagingBuffer?.dispose(); - session.stagingBuffer = null; - } - // Checked once, over the text that will run. final confirmDestructive = await AppSettings.instance.getConfirmDestructiveOperations(); @@ -642,6 +637,12 @@ class GenericSqlWorkspaceState extends material.State { } } + // Only now: cancelling the destructive dialog above must keep the edits. + if (session.stagingBuffer != null && session.stagingBuffer!.isDirty) { + session.stagingBuffer?.dispose(); + session.stagingBuffer = null; + } + invalidatePane(session); setState(() { session.running = true; diff --git a/test/features/workspace/sql_workspace_execute_dirty_guard_test.dart b/test/features/workspace/sql_workspace_execute_dirty_guard_test.dart index ea717cb5..6f22b713 100644 --- a/test/features/workspace/sql_workspace_execute_dirty_guard_test.dart +++ b/test/features/workspace/sql_workspace_execute_dirty_guard_test.dart @@ -38,6 +38,19 @@ class _FakePathProvider extends PathProviderPlatform { Future getDownloadsPath() async => _root; } +/// Lets the real async work of a Run (the settings read in sqflite) finish: +/// `pumpAndSettle` alone runs on fake time and never waits for it. +Future _settleRealAsync(WidgetTester tester) async { + for (var i = 0; i < 10; i++) { + await tester.runAsync( + () => Future.delayed(const Duration(milliseconds: 50)), + ); + await tester.pump(); + } + // Long enough for the closing dialog to finish its exit transition. + await tester.pump(const Duration(seconds: 1)); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -143,8 +156,7 @@ void main() { expect(find.text('Unsaved changes in "Query 1"'), findsOneWidget); await tester.tap(find.text('Discard')); - await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 150))); - await tester.pumpAndSettle(); + await _settleRealAsync(tester); // Query was allowed to run, previous dirty buffer was discarded expect(find.text('Unsaved changes in "Query 1"'), findsNothing); @@ -156,4 +168,58 @@ void main() { await tester.pumpWidget(const SizedBox()); await tester.pumpAndSettle(); }); + + testWidgets( + 'cancelling the destructive-query dialog keeps the staged edits (#1771)', + (tester) async { + await tester.binding.setSurfaceSize(const material.Size(1200, 800)); + + final conn = ConnectionRow( + id: 501, + type: 'sqlite', + name: 'Test SQLite', + host: dbPath, + createdAt: '2026-09-24T00:00:00Z', + ); + + await tester.pumpWidget( + queryaThemeTestShell( + child: material.SizedBox.expand( + child: SqliteSqlWorkspace(connectionRow: conn), + ), + ), + ); + await tester.pumpAndSettle(); + + final dynamic dynamicState = tester.state(find.byType(SqliteSqlWorkspace)); + final dynamic activeSession = dynamicState.activeSession; + activeSession.controller.text = 'DELETE FROM users;'; + activeSession.columns = ['id', 'name']; + activeSession.rows = [['1', 'Alice']]; + final buffer = DataGridStagingBuffer( + columns: ['id', 'name'], + rows: [['1', 'Alice']], + ); + activeSession.stagingBuffer = buffer; + buffer.setCell(0, 1, 'Bob'); + + await tester.tap(find.widgetWithText(QueryaButton, 'Execute (F5)')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Discard')); + await _settleRealAsync(tester); + + expect(find.text('Destructive Operation Detected'), findsOneWidget); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + expect(find.text('Destructive Operation Detected'), findsNothing); + expect(buffer.isDirty, isTrue); + expect(activeSession.stagingBuffer, same(buffer)); + + material.FocusManager.instance.primaryFocus?.unfocus(); + await tester.pumpWidget(const SizedBox()); + // The connection pool arms a release timer when the workspace goes away. + await tester.pump(const Duration(seconds: 5)); + await tester.pumpAndSettle(); + }); }