From e3a8db2339697c0a14d291c127d2b51369513190 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Fri, 9 Oct 2026 12:24:36 +0300 Subject: [PATCH] fix(table): closing a table tab releases its lease without interrupting the shared session (#1215) Closing a table browser tab called cancel(interruptIfBusy: true), which interrupted the read-only and table-write sessions of the whole database. Those sessions are shared with the tree, other table tabs, the Relations view and MCP, so one close dropped their connection. cancel no longer closes a pooled session; a statement still running finishes or times out on its own, and the tab's dispose releases its lease. --- lib/features/mysql/mysql_table_view.dart | 18 ++++-------------- .../postgresql/postgres_table_view.dart | 18 ++++-------------- lib/features/sqlite/sqlite_table_view.dart | 16 ++++------------ lib/features/workspace/generic_table_view.dart | 3 ++- .../generic_table_view_relations_test.dart | 11 +++++++++++ 5 files changed, 25 insertions(+), 41 deletions(-) diff --git a/lib/features/mysql/mysql_table_view.dart b/lib/features/mysql/mysql_table_view.dart index 16a7b203..740a2662 100644 --- a/lib/features/mysql/mysql_table_view.dart +++ b/lib/features/mysql/mysql_table_view.dart @@ -233,21 +233,11 @@ class MysqlTableDataDelegate extends TableDataMutationDelegate { }); } + /// Pooled sessions are shared with every other view of this database, so + /// cancelling never closes them: a statement that is still running finishes + /// or times out on its own, and the lease is released by [dispose]. @override - void cancel({bool interruptIfBusy = false}) { - if (interruptIfBusy) { - MysqlService.instance.interrupt( - connectionRow, - database: database, - mode: MysqlSessionMode.readOnly, - ); - MysqlService.instance.interrupt( - connectionRow, - database: database, - mode: MysqlSessionMode.tableWrite, - ); - } - } + void cancel({bool interruptIfBusy = false}) {} @override void dispose() { diff --git a/lib/features/postgresql/postgres_table_view.dart b/lib/features/postgresql/postgres_table_view.dart index 5158aa13..c69db4a6 100644 --- a/lib/features/postgresql/postgres_table_view.dart +++ b/lib/features/postgresql/postgres_table_view.dart @@ -234,21 +234,11 @@ class PostgresTableDataDelegate extends TableDataMutationDelegate { }); } + /// Pooled sessions are shared with every other view of this database, so + /// cancelling never closes them: a statement that is still running finishes + /// or times out on its own, and the lease is released by [dispose]. @override - void cancel({bool interruptIfBusy = false}) { - if (interruptIfBusy) { - PostgresService.instance.interrupt( - connectionRow, - database: database, - mode: PgSessionMode.readOnly, - ); - PostgresService.instance.interrupt( - connectionRow, - database: database, - mode: PgSessionMode.tableWrite, - ); - } - } + void cancel({bool interruptIfBusy = false}) {} @override void dispose() { diff --git a/lib/features/sqlite/sqlite_table_view.dart b/lib/features/sqlite/sqlite_table_view.dart index 00f0b75f..1f1061b7 100644 --- a/lib/features/sqlite/sqlite_table_view.dart +++ b/lib/features/sqlite/sqlite_table_view.dart @@ -221,19 +221,11 @@ class SqliteTableDataDelegate extends TableDataMutationDelegate { }); } + /// Pooled sessions are shared with every other view of this database, so + /// cancelling never closes them: a statement that is still running finishes + /// or times out on its own, and the lease is released by [dispose]. @override - void cancel({bool interruptIfBusy = false}) { - if (interruptIfBusy) { - SqliteService.instance.interrupt( - connectionRow, - mode: SqliteSessionMode.readOnly, - ); - SqliteService.instance.interrupt( - connectionRow, - mode: SqliteSessionMode.tableWrite, - ); - } - } + void cancel({bool interruptIfBusy = false}) {} @override void dispose() { diff --git a/lib/features/workspace/generic_table_view.dart b/lib/features/workspace/generic_table_view.dart index dedd67a3..d93460b4 100644 --- a/lib/features/workspace/generic_table_view.dart +++ b/lib/features/workspace/generic_table_view.dart @@ -286,7 +286,8 @@ class GenericTableViewState extends material.State { void dispose() { TableViewCommandBridge.instance.unregister(owner: this); _resetStaging(); - widget.delegate.cancel(interruptIfBusy: true); + // Closing the tab releases its lease. It must not interrupt the shared + // session: other views of the database may be using it. widget.delegate.dispose(); super.dispose(); } diff --git a/test/features/workspace/generic_table_view_relations_test.dart b/test/features/workspace/generic_table_view_relations_test.dart index 2fca6023..bf65dbba 100644 --- a/test/features/workspace/generic_table_view_relations_test.dart +++ b/test/features/workspace/generic_table_view_relations_test.dart @@ -86,4 +86,15 @@ void main() { await t.pump(); expect(find.byType(ErdView), findsNothing); }); + + testWidgets('closing a table tab releases its delegate without cancelling it', + (t) async { + final delegate = FakeTableDataDelegate(); + await pumpGenericTableView(t, delegate); + await t.pumpWidget(const material.SizedBox()); + await t.pump(); + // The session is shared with other views: closing must not interrupt it. + expect(delegate.cancelCount, 0); + expect(delegate.disposeCount, 1); + }); }