diff --git a/CHANGELOG.md b/CHANGELOG.md index d816abf5d..a6b5d271c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/TablePro/Core/Autocomplete/SQLContextAnalyzer.swift b/TablePro/Core/Autocomplete/SQLContextAnalyzer.swift index 48b0f68e1..94a0da8b1 100644 --- a/TablePro/Core/Autocomplete/SQLContextAnalyzer.swift +++ b/TablePro/Core/Autocomplete/SQLContextAnalyzer.swift @@ -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) } }() @@ -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 @@ -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? diff --git a/TablePro/Core/MCP/Protocol/Tools/ExecuteQueryTool.swift b/TablePro/Core/MCP/Protocol/Tools/ExecuteQueryTool.swift index f1c01d9a2..93ceb6091 100644 --- a/TablePro/Core/MCP/Protocol/Tools/ExecuteQueryTool.swift +++ b/TablePro/Core/MCP/Protocol/Tools/ExecuteQueryTool.swift @@ -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 { @@ -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, diff --git a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift index cbb3d0638..7074329c0 100644 --- a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift +++ b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift @@ -212,6 +212,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi inspectorSplitItem.canCollapse = true inspectorSplitItem.minimumThickness = 270 inspectorSplitItem.maximumThickness = 400 + inspectorSplitItem.collapseBehavior = .preferResizingSplitViewWithFixedSiblings addSplitViewItem(inspectorSplitItem) if currentSession?.driver == nil { @@ -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 } @@ -257,7 +262,6 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi } installObservers() - recomputeWindowMinSize() window.recalculateKeyViewLoop() } @@ -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 } @@ -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() } @@ -526,15 +522,13 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi func showInspector() { materializeInspectorIfNeeded() - inspectorSplitItem?.animator().isCollapsed = false + setCollapsed(false, for: inspectorSplitItem) 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?) { @@ -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" diff --git a/TablePro/Core/Utilities/Connection/PasswordSourceResolver.swift b/TablePro/Core/Utilities/Connection/PasswordSourceResolver.swift index 45cb71a7c..4395dcd73 100644 --- a/TablePro/Core/Utilities/Connection/PasswordSourceResolver.swift +++ b/TablePro/Core/Utilities/Connection/PasswordSourceResolver.swift @@ -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) + } + if stdoutCollector.overflowed { throw ResolutionError.outputTooLarge }