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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Execute All Statements shortcut (Cmd+Shift+Enter) to run all statements in the editor (#770)
- Structure tab: search, sort, count badges, PK column, Copy As (CSV/JSON/SQL), destructive change confirmation
- Structure tab: DDL view with tree-sitter highlighting, line numbers, and "Open in Editor"
- Structure tab: charset/collation (MySQL), index prefix length, partial indexes (PostgreSQL), cross-schema FK
Expand All @@ -26,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Selection highlight not covering the last line on Cmd+A (#770)
- AI chat freeze when large queries or results are included in the system prompt (#774)
- AI chat panel not updating when switching database connections
- Schema restored on reconnect for PostgreSQL, Redshift, and BigQuery (#777)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ extension TextSelectionManager {
} else if let maxFragmentRect = layoutManager.rectForOffset(intersectionRange.max) {
maxRect = maxFragmentRect
} else {
continue
maxRect = CGRect(
x: minRect.maxX,
y: minRect.origin.y,
width: 0,
height: minRect.height
)
}

fillRects.append(CGRect(
Expand Down
6 changes: 6 additions & 0 deletions TablePro/TableProApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@ struct AppMenuCommands: Commands {
.keyboardShortcut(.return, modifiers: .command)
.disabled(!(actions?.isConnected ?? false) || !(actions?.hasQueryText ?? false))

Button(String(localized: "Execute All Statements")) {
actions?.runAllStatements()
}
.keyboardShortcut(.return, modifiers: [.command, .shift])
.disabled(!(actions?.isConnected ?? false) || !(actions?.hasQueryText ?? false))

Button("Explain Query") {
actions?.explainQuery()
}
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Editor/SQLEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ struct SQLEditorView: View {
indentOption: .spaces(count: ThemeEngine.shared.tabWidth)
),
layout: .init(
contentInsets: NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
contentInsets: NSEdgeInsets(top: 0, left: 0, bottom: 8, right: 0)
),
peripherals: .init(
showGutter: ThemeEngine.shared.showLineNumbers,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//
// MainContentCoordinator+ExecuteAll.swift
// TablePro
//
// Execute All Statements and safe mode dispatch logic shared
// between runQuery() and runAllStatements().
//

import AppKit
import Foundation

extension MainContentCoordinator {
func runAllStatements() {
guard let index = tabManager.selectedTabIndex else { return }
guard !tabManager.tabs[index].isExecuting else { return }
guard tabManager.tabs[index].tabType == .query else { return }

let fullQuery = tabManager.tabs[index].query
guard !fullQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }

let statements = SQLStatementScanner.allStatements(in: fullQuery)
guard !statements.isEmpty else { return }

dispatchStatements(statements, tabIndex: index)
}

internal func dispatchStatements(_ statements: [String], tabIndex index: Int) {
let level = safeModeLevel

if level == .readOnly {
let writeStatements = statements.filter { isWriteQuery($0) }
if !writeStatements.isEmpty {
tabManager.tabs[index].errorMessage =
String(localized: "Cannot execute write queries: connection is read-only")
return
}
}

if level == .silent {
if statements.count == 1 {
Task { @MainActor in
let window = NSApp.keyWindow
guard await confirmDangerousQueryIfNeeded(statements[0], window: window) else { return }
executeQueryInternal(statements[0])
}
} else {
Task { @MainActor in
let window = NSApp.keyWindow
let dangerousStatements = statements.filter { isDangerousQuery($0) }
if !dangerousStatements.isEmpty {
guard await confirmDangerousQueries(dangerousStatements, window: window) else { return }
}
executeMultipleStatements(statements)
}
}
} else if level.requiresConfirmation {
guard !isShowingSafeModePrompt else { return }
isShowingSafeModePrompt = true
Task { @MainActor in
defer { isShowingSafeModePrompt = false }
let window = NSApp.keyWindow
let combinedSQL = statements.joined(separator: "\n")
let hasWrite = statements.contains { isWriteQuery($0) }
let permission = await SafeModeGuard.checkPermission(
level: level,
isWriteOperation: hasWrite,
sql: combinedSQL,
operationDescription: String(localized: "Execute Query"),
window: window,
databaseType: connection.type
)
switch permission {
case .allowed:
if statements.count == 1 {
executeQueryInternal(statements[0])
} else {
executeMultipleStatements(statements)
}
case .blocked(let reason):
if index < tabManager.tabs.count {
tabManager.tabs[index].errorMessage = reason
}
}
}
} else {
if statements.count == 1 {
executeQueryInternal(statements[0])
} else {
executeMultipleStatements(statements)
}
}
}
}
4 changes: 4 additions & 0 deletions TablePro/Views/Main/MainContentCommandActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,10 @@ final class MainContentCommandActions {
coordinator?.runQuery()
}

func runAllStatements() {
coordinator?.runAllStatements()
}

func cancelCurrentQuery() {
coordinator?.cancelCurrentQuery()
}
Expand Down
70 changes: 3 additions & 67 deletions TablePro/Views/Main/MainContentCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ final class MainContentCoordinator {
@ObservationIgnored internal var isShowingConfirmAlert = false

/// Guards against duplicate safe mode confirmation prompts
@ObservationIgnored private var isShowingSafeModePrompt = false
@ObservationIgnored internal var isShowingSafeModePrompt = false

/// Continuation for callers that need to await the result of a fire-and-forget save
/// (e.g. save-then-close). Set before calling `saveChanges`, resumed by `executeCommitStatements`.
Expand Down Expand Up @@ -746,71 +746,7 @@ final class MainContentCoordinator {
let statements = SQLStatementScanner.allStatements(in: sql)
guard !statements.isEmpty else { return }

// Safe mode enforcement for query execution
let level = safeModeLevel

if level == .readOnly {
let writeStatements = statements.filter { isWriteQuery($0) }
if !writeStatements.isEmpty {
tabManager.tabs[index].errorMessage =
"Cannot execute write queries: connection is read-only"
return
}
}

if level == .silent {
if statements.count == 1 {
Task { @MainActor in
let window = NSApp.keyWindow
guard await confirmDangerousQueryIfNeeded(statements[0], window: window) else { return }
executeQueryInternal(statements[0])
}
} else {
Task { @MainActor in
let window = NSApp.keyWindow
let dangerousStatements = statements.filter { isDangerousQuery($0) }
if !dangerousStatements.isEmpty {
guard await confirmDangerousQueries(dangerousStatements, window: window) else { return }
}
executeMultipleStatements(statements)
}
}
} else if level.requiresConfirmation {
guard !isShowingSafeModePrompt else { return }
isShowingSafeModePrompt = true
Task { @MainActor in
defer { isShowingSafeModePrompt = false }
let window = NSApp.keyWindow
let combinedSQL = statements.joined(separator: "\n")
let hasWrite = statements.contains { isWriteQuery($0) }
let permission = await SafeModeGuard.checkPermission(
level: level,
isWriteOperation: hasWrite,
sql: combinedSQL,
operationDescription: String(localized: "Execute Query"),
window: window,
databaseType: connection.type
)
switch permission {
case .allowed:
if statements.count == 1 {
executeQueryInternal(statements[0])
} else {
executeMultipleStatements(statements)
}
case .blocked(let reason):
if index < tabManager.tabs.count {
tabManager.tabs[index].errorMessage = reason
}
}
}
} else {
if statements.count == 1 {
executeQueryInternal(statements[0])
} else {
executeMultipleStatements(statements)
}
}
dispatchStatements(statements, tabIndex: index)
}

/// Execute table tab query directly.
Expand Down Expand Up @@ -990,7 +926,7 @@ final class MainContentCoordinator {
}

/// Internal query execution (called after any confirmations)
private func executeQueryInternal(
internal func executeQueryInternal(
_ sql: String
) {
guard let index = tabManager.selectedTabIndex else { return }
Expand Down
1 change: 1 addition & 0 deletions docs/features/keyboard-shortcuts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ TablePro is keyboard-driven. Most actions have shortcuts, and most menu shortcut
| Action | Shortcut | Description |
|--------|----------|-------------|
| Execute query | `Cmd+Enter` | Run query at cursor, or all selected statements sequentially |
| Execute all statements | `Cmd+Shift+Enter` | Run all statements in the editor sequentially |
| Cancel query | `Cmd+.` | Stop the currently running query |
| Explain query | `Option+Cmd+E` | Show execution plan for query at cursor |
| Format SQL | `Cmd+Shift+L` | Format SQL query |
Expand Down
Loading