Skip to content
Open
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 @@ -48,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Find Next (Cmd+G) and Find Previous (Cmd+Shift+G) now work in the editor.
- Pagination buttons no longer fire their page shortcut twice.
- Running a PostgreSQL script with a `DO $$ ... $$` block or a dollar-quoted function body no longer fails with an unterminated dollar-quoted string error. (#1559)
- Toggling the right inspector in a narrow editor window now grows the window to fit, so the inspector no longer squeezes content or overflows.
- AWS IAM connections no longer ask for a password on connect or reconnect. IAM supplies the credentials, so the prompt was never needed. The same now holds for any auth mode that replaces the password, such as a Postgres password file.
- Oracle connection failures show the listener's actual reason (such as an unknown service name) instead of a generic "server closed the connection" message. (#483)

Expand Down
22 changes: 13 additions & 9 deletions TablePro/Core/Autocomplete/SQLContextAnalyzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,14 @@ final class SQLContextAnalyzer {

private static let tableRefRegexes: [NSRegularExpression] = {
let patterns = [
"(?i)\\bFROM\\s+[`\"']?([\\w.]+)[`\"']?" +
"(?i)\\bFROM\\s+([\\w.`\"']+)" +
"(?:\\s+(?:AS\\s+)?[`\"']?([\\w]+)[`\"']?)?",
"(?i)(?:LEFT|RIGHT|INNER|OUTER|CROSS|FULL)?\\s*(?:OUTER)?\\s*JOIN\\s+" +
"[`\"']?([\\w.]+)[`\"']?(?:\\s+(?:AS\\s+)?[`\"']?([\\w]+)[`\"']?)?",
"(?i)\\bUPDATE\\s+[`\"']?([\\w.]+)[`\"']?" +
"([\\w.`\"']+)(?:\\s+(?:AS\\s+)?[`\"']?([\\w]+)[`\"']?)?",
"(?i)\\bUPDATE\\s+([\\w.`\"']+)" +
"(?:\\s+(?:AS\\s+)?[`\"']?([\\w]+)[`\"']?)?",
"(?i)\\bINSERT\\s+INTO\\s+[`\"']?([\\w.]+)[`\"']?",
"(?i)\\bCREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+\\w+\\s+ON\\s+[`\"']?([\\w.]+)[`\"']?"
"(?i)\\bINSERT\\s+INTO\\s+([\\w.`\"']+)",
"(?i)\\bCREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+\\w+\\s+ON\\s+([\\w.`\"']+)"
]
return patterns.map { compileRegex($0) }
}()
Expand Down Expand Up @@ -768,13 +768,17 @@ final class SQLContextAnalyzer {
]

/// Strip schema prefix from a potentially schema-qualified name
private static let identifierQuotes = CharacterSet(charactersIn: "`\"'")

private static func stripSchemaPrefix(_ raw: String) -> String {
let ns = raw as NSString
let dotRange = ns.range(of: ".", options: .backwards)
guard dotRange.location != NSNotFound else { return raw }
guard dotRange.location != NSNotFound else {
return raw.trimmingCharacters(in: identifierQuotes)
}
let start = dotRange.location + 1
guard start < ns.length else { return raw }
return ns.substring(from: start)
guard start < ns.length else { return raw.trimmingCharacters(in: identifierQuotes) }
return ns.substring(from: start).trimmingCharacters(in: identifierQuotes)
}

/// Extract all table references (table names and aliases) from the query
Expand All @@ -798,7 +802,7 @@ final class SQLContextAnalyzer {
let segments = rawName.split(separator: ".")
let schema = segments.count >= 2
? String(segments[segments.count - 2])
.trimmingCharacters(in: CharacterSet(charactersIn: "`\""))
.trimmingCharacters(in: Self.identifierQuotes)
: nil

var alias: String?
Expand Down
6 changes: 3 additions & 3 deletions TablePro/Core/MCP/Protocol/Tools/ExecuteQueryTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ public struct ExecuteQueryTool: MCPToolImplementation {
throw MCPProtocolError.invalidParams(detail: "Query exceeds 100KB limit")
}

try await throwIfCancelled(context)
await context.progress.emit(progress: 0.0, total: 1.0, message: "Connecting")

let meta = try await ToolConnectionMetadata.resolve(connectionId: connectionId)

guard !QueryClassifier.isMultiStatement(query, databaseType: meta.databaseType) else {
Expand All @@ -85,9 +88,6 @@ public struct ExecuteQueryTool: MCPToolImplementation {
)
}

try await throwIfCancelled(context)
await context.progress.emit(progress: 0.0, total: 1.0, message: "Connecting")

if let database {
_ = try await services.connectionBridge.switchDatabase(
connectionId: connectionId,
Expand Down
82 changes: 16 additions & 66 deletions TablePro/Core/Services/Infrastructure/MainSplitViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
inspectorSplitItem.canCollapse = true
inspectorSplitItem.minimumThickness = 270
inspectorSplitItem.maximumThickness = 400
inspectorSplitItem.collapseBehavior = .preferResizingSplitViewWithFixedSiblings
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resize when restoring an already-visible inspector

This only helps when AppKit performs an uncollapse transition; it does not cover the startup path where inspectorPresented is restored and viewDidLoad sets inspectorSplitItem.isCollapsed = false before the window is visible. I checked TabWindowController: it restores and saves MainEditorWindow frames, so a previously saved narrow frame can reopen with the inspector already visible and no collapse animation to trigger this behavior, leaving the same squeezed/overflowing panes until the user toggles the inspector again. Please also handle the initially visible inspector after the window/frame is restored.

Useful? React with 👍 / 👎.

addSplitViewItem(inspectorSplitItem)

if currentSession?.driver == nil {
Expand All @@ -225,17 +226,21 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
inspectorSplitItem.isCollapsed = !inspectorPresented
}

override func splitViewDidResizeSubviews(_ notification: Notification) {
super.splitViewDidResizeSubviews(notification)
recomputeWindowMinSize()
}

private func materializeInspectorIfNeeded() {
guard !hasMaterializedInspector, let inspectorHosting else { return }
hasMaterializedInspector = true
inspectorHosting.rootView = AnyView(buildInspectorView())
}

private func setCollapsed(_ isCollapsed: Bool, for splitItem: NSSplitViewItem?) {
guard let splitItem, splitItem.isCollapsed != isCollapsed else { return }
if view.window?.isVisible == true {
splitItem.animator().isCollapsed = isCollapsed
} else {
splitItem.isCollapsed = isCollapsed
}
}

override func viewWillAppear() {
super.viewWillAppear()
guard let window = view.window else { return }
Expand All @@ -257,7 +262,6 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
}

installObservers()
recomputeWindowMinSize()
window.recalculateKeyViewLoop()
}

Expand Down Expand Up @@ -324,11 +328,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
sessionState = nil
currentSession = nil
sidebarContainer.updateSidebarState(nil, windowState: nil)
if view.window?.isVisible == true {
sidebarSplitItem.animator().isCollapsed = true
} else {
sidebarSplitItem.isCollapsed = true
}
setCollapsed(true, for: sidebarSplitItem)
}
return
}
Expand Down Expand Up @@ -356,11 +356,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
}

let collapseSidebar = newSession.driver == nil
if view.window?.isVisible == true {
sidebarSplitItem.animator().isCollapsed = collapseSidebar
} else {
sidebarSplitItem.isCollapsed = collapseSidebar
}
setCollapsed(collapseSidebar, for: sidebarSplitItem)
rebuildPanes()
}

Expand Down Expand Up @@ -526,15 +522,13 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi

func showInspector() {
materializeInspectorIfNeeded()
inspectorSplitItem?.animator().isCollapsed = false
setCollapsed(false, for: inspectorSplitItem)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the window minimum in sync when expanding panes

When the sidebar is visible, opening the inspector requires about 280 + 400 + 270 points plus dividers, but TabWindowController still leaves the window minSize at 720×480. collapseBehavior can grow the window for this uncollapse, but it does not raise the window's minimum size, so users can immediately drag the same window back down to 720pt with both panes visible and reproduce the squeezed/overflowing split view this change is meant to prevent. Please keep a dynamic content minimum while the extra panes are visible, without deriving it from the already-mutated minimum.

Useful? React with 👍 / 👎.

UserDefaults.standard.set(true, forKey: Self.inspectorPresentedKey)
recomputeWindowMinSize()
}

func hideInspector() {
inspectorSplitItem?.animator().isCollapsed = true
setCollapsed(true, for: inspectorSplitItem)
UserDefaults.standard.set(false, forKey: Self.inspectorPresentedKey)
recomputeWindowMinSize()
}

@objc override func toggleInspector(_ sender: Any?) {
Expand All @@ -560,58 +554,14 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi

if sidebarSplitItem?.isCollapsed == true {
sidebarState.selectedSidebarTab = tab
sidebarSplitItem?.animator().isCollapsed = false
setCollapsed(false, for: sidebarSplitItem)
} else if sidebarState.selectedSidebarTab == tab {
sidebarSplitItem?.animator().isCollapsed = true
setCollapsed(true, for: sidebarSplitItem)
} else {
sidebarState.selectedSidebarTab = tab
}
}

// MARK: - Dynamic Window Minimum Size

private static let baseWindowMinWidth: CGFloat = 720
private static let baseWindowMinHeight: CGFloat = 480

private func recomputeWindowMinSize() {
guard let window = view.window else { return }
let sidebarVisible = !(sidebarSplitItem?.isCollapsed ?? true)
let inspectorVisible = !(inspectorSplitItem?.isCollapsed ?? true)

let detailMin: CGFloat = detailSplitItem?.minimumThickness ?? 400
let sidebarMin: CGFloat = sidebarSplitItem?.minimumThickness ?? 280
let inspectorMin: CGFloat = inspectorSplitItem?.minimumThickness ?? 270
let dividerThickness = splitView.dividerThickness

var width: CGFloat = detailMin
if sidebarVisible {
width += sidebarMin + dividerThickness
}
if inspectorVisible {
width += inspectorMin + dividerThickness
}

let resolvedWidth = max(Self.baseWindowMinWidth, width)
let newMinSize = NSSize(width: resolvedWidth, height: Self.baseWindowMinHeight)

guard window.minSize != newMinSize else { return }
window.minSize = newMinSize

var frame = window.frame
var resized = false
if frame.size.width < resolvedWidth {
frame.size.width = resolvedWidth
resized = true
}
if frame.size.height < Self.baseWindowMinHeight {
frame.size.height = Self.baseWindowMinHeight
resized = true
}
if resized {
window.setFrame(frame, display: true, animate: false)
}
}

// MARK: - Constants

private static let inspectorPresentedKey = "com.TablePro.rightPanel.isPresented"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ enum PasswordSourceResolver {
stdoutPipe.fileHandleForReading.readabilityHandler = nil
stderrPipe.fileHandleForReading.readabilityHandler = nil

if let remainingStdout = try? stdoutPipe.fileHandleForReading.readToEnd(), !remainingStdout.isEmpty {
stdoutCollector.append(remainingStdout)
}
if let remainingStderr = try? stderrPipe.fileHandleForReading.readToEnd(), !remainingStderr.isEmpty {
stderrCollector.append(remainingStderr)
Comment on lines +133 to +137
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid blocking indefinitely when draining inherited pipes

If a password command exits after starting a background process that inherits stdout or stderr (for example a shell helper that daemonizes), process.waitUntilExit() returns and the timeout task is canceled, but these readToEnd() calls still wait for EOF on the inherited pipe. That can hang password resolution well past the 30-second timeout even though the command process has already exited; the final drain needs to be non-blocking or still covered by a timeout/closed descriptors.

Useful? React with 👍 / 👎.

}

if stdoutCollector.overflowed {
throw ResolutionError.outputTooLarge
}
Expand Down
Loading