diff --git a/CHANGELOG.md b/CHANGELOG.md index 900b034fe..c38e84e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - An administrator can set a minimum Safe Mode level for every connection through a macOS configuration profile, so a managed Mac cannot be dropped below it. A connection set stricter keeps its own level, since the policy is a floor rather than a ceiling. The control shows as managed instead of editable. - Plugins signed by other developers can be installed. TablePro used to refuse any plugin bundle it had not signed itself, so the only way to publish a driver was through the TablePro repository. A bundle signed with a Developer ID and notarized by Apple now installs after you agree to trust that developer by name, and the prompt says plainly that a database plugin runs as part of TablePro and can read the credentials of every connection you open. Trust is recorded per developer rather than per plugin, so their updates install without asking again, and you can withdraw it. Unsigned and ad-hoc signed bundles are still refused. - `Cmd+F` on a table tab opens a find bar over the results. Type a term and the matching cell is highlighted and scrolled to; `Return` and `Cmd+G` step forward, `Cmd+Shift+G` steps back, `Escape` clears the term and then closes the bar. Matching ignores case and accents and runs over the text as displayed, skipping binary and spatial columns. The counter always says what it searched, reading "3 of 12 on this page" while rows remain unfetched and "3 of 12" once everything is loaded, so a result is never mistaken for an answer about the whole table. When nothing matches on the page and more rows exist, Search All Rows turns the term into a server-side filter. +- The Execute button's menu in the SQL editor offers Execute All Statements, alongside the `Cmd+Shift+Enter` shortcut and the Query menu. Running every statement in a tab no longer means selecting the whole tab first. (#2230) ### Changed diff --git a/TablePro/Views/Editor/QueryEditorView.swift b/TablePro/Views/Editor/QueryEditorView.swift index ae4eaf4bd..1c2f29983 100644 --- a/TablePro/Views/Editor/QueryEditorView.swift +++ b/TablePro/Views/Editor/QueryEditorView.swift @@ -18,6 +18,7 @@ struct QueryEditorView: View { @Binding var isParameterPanelVisible: Bool var onExecute: () -> Void var onExecuteWithoutLimit: (() -> Void)? + var onExecuteAllStatements: (() -> Void)? var schemaProvider: SQLSchemaProvider? var databaseType: DatabaseType? var connectionId: UUID? @@ -140,6 +141,13 @@ struct QueryEditorView: View { explainButton(hasQueryText: hasQueryText) Menu { + Button(String(localized: "Execute All Statements")) { + onExecuteAllStatements?() + } + .optionalKeyboardShortcut( + AppSettingsManager.shared.keyboard.keyboardShortcut(for: .executeAllStatements) + ) + Button(String(localized: "Execute Without Limit")) { onExecuteWithoutLimit?() } @@ -160,6 +168,7 @@ struct QueryEditorView: View { .fixedSize() .help(shortcutHint(String(localized: "Execute"), for: .executeQuery)) .optionalKeyboardShortcut(AppSettingsManager.shared.keyboard.keyboardShortcut(for: .executeQuery)) + .accessibilityIdentifier("query-execute-menu") } .padding(.horizontal, 12) .padding(.vertical, 8) diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index b1b015334..1a6177784 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -402,6 +402,7 @@ struct MainEditorContentView: View { isParameterPanelVisible: parameterVisibilityBinding(for: tab), onExecute: { coordinator.runQuery() }, onExecuteWithoutLimit: { coordinator.runQuery(bypassRowLimit: true) }, + onExecuteAllStatements: { coordinator.runAllStatements() }, schemaProvider: SchemaProviderRegistry.shared.getOrCreate(for: coordinator.connection.id), databaseType: coordinator.connection.type, connectionId: coordinator.connection.id, diff --git a/TableProUITests/QueryExecuteMenuUITests.swift b/TableProUITests/QueryExecuteMenuUITests.swift new file mode 100644 index 000000000..162be295c --- /dev/null +++ b/TableProUITests/QueryExecuteMenuUITests.swift @@ -0,0 +1,81 @@ +// +// QueryExecuteMenuUITests.swift +// TableProUITests +// +// Covers #2230: running every statement in the tab must be reachable from the editor's own +// Execute button. It shipped as a menu-bar command and a chord only, so users selected the +// whole tab by hand instead. +// + +import XCTest + +final class QueryExecuteMenuUITests: UITestCase { + func testExecuteMenuOffersExecuteAllStatements() throws { + let app = try launchWithSampleDatabase() + + app.typeKey("t", modifierFlags: .command) + let queryEditor = editorTextView(in: app) + XCTAssertTrue(queryEditor.waitForExistence(timeout: 10)) + queryEditor.click() + app.typeText("SELECT 1;\nSELECT 2;") + + let menu = openExecuteMenu(in: app) + XCTAssertTrue( + menu.menuItems["Execute All Statements"].waitForExistence(timeout: 5), + "The Execute button's menu must offer Execute All Statements" + ) + XCTAssertTrue( + menu.menuItems["Execute Without Limit"].exists, + "Execute Without Limit must survive alongside the new item" + ) + app.typeKey(.escape, modifierFlags: []) + } + + /// Asserting the item exists would still pass if the menu were wired to the wrong command, or + /// to nothing at all: the callback is optional, so dropping it at the call site is not even a + /// compile error. Running it and counting the results is what covers the wiring. + func testExecuteAllStatementsRunsEveryStatementInTheTab() throws { + let app = try launchWithSampleDatabase() + + app.typeKey("t", modifierFlags: .command) + let queryEditor = editorTextView(in: app) + XCTAssertTrue(queryEditor.waitForExistence(timeout: 10)) + queryEditor.click() + app.typeText("SELECT 1;\nSELECT 2;") + + openExecuteMenu(in: app).menuItems["Execute All Statements"].click() + + let resultTabs = app.windows.firstMatch.buttons.matching(identifier: "result-tab") + XCTAssertTrue( + waitForPredicate(timeout: 30) { resultTabs.count == 2 }, + "Both statements must run, one result tab each, without selecting anything first" + ) + } + + /// The control is a split button: its leading half runs the query and only its trailing + /// chevron opens the menu, so a plain `click()` would execute instead of opening. + /// + /// The opened menu is scoped to the window rather than matched by identifier, because the CI + /// runner's macOS build exposes a just-opened menu without one (see `ResultTabPinUITests`). + private func openExecuteMenu(in app: XCUIApplication) -> XCUIElement { + let window = app.windows.firstMatch + let executeMenu = window.descendants(matching: .any) + .matching(identifier: "query-execute-menu") + .firstMatch + XCTAssertTrue( + waitUntilHittable(executeMenu, timeout: 10), + "The editor toolbar must expose the Execute split button" + ) + executeMenu.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)).click() + return window.menus.firstMatch + } + + private func editorTextView(in app: XCUIApplication) -> XCUIElement { + let window = app.windows.firstMatch + let identified = window.textViews.matching(identifier: "sql-editor-textview").firstMatch + if identified.exists { + return identified + } + return window.textViews.firstMatch + } +} diff --git a/docs/features/sql-editor.mdx b/docs/features/sql-editor.mdx index 8f3c013c3..5cbb31a32 100644 --- a/docs/features/sql-editor.mdx +++ b/docs/features/sql-editor.mdx @@ -35,7 +35,9 @@ SELECT * FROM users LIMIT 10; SELECT COUNT(*) FROM orders; ``` -Select text and press `Cmd+Enter` to run only the selection. Multiple statements in a selection run sequentially: +To run the whole tab, choose **Execute All Statements** from the Execute button's menu, press `Cmd+Shift+Enter`, or use **Query > Execute All Statements**. Nothing needs to be selected first. + +Select text and press `Cmd+Enter` to run only the selection. Multiple statements, whether from a selection or from Execute All Statements, run sequentially: - On databases with transaction support, the batch runs in a transaction: if a statement fails, execution stops and all changes roll back. Databases without transactions run each statement as-is, with no rollback. - The error names the failing statement ("Statement 3/5 failed: ...").