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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions TablePro/Views/Editor/QueryEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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?()
}
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
81 changes: 81 additions & 0 deletions TableProUITests/QueryExecuteMenuUITests.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 3 additions & 1 deletion docs/features/sql-editor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...").
Expand Down
Loading