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 @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Format Query can now be undone with Cmd+Z; the formatting is applied as a single editor edit instead of clearing the undo history. (#1645)
- Sorting a query result no longer overwrites the SQL editor text or the contents of an opened `.sql` file; the sort runs as a separate query and the editor keeps what you wrote. (#1645)
- iCloud Sync between the iPhone and Mac apps: the iOS app now uses the Production CloudKit environment, so a development build no longer syncs into a separate database the Mac never reads.
- Exports no longer fail mid-table on servers that enforce a statement time limit; the export session disables the limit and restores it afterwards, the same way mysqldump does. (#1633)
- Refreshing a table now reloads its data even when the previous load is still running; before, the refresh was silently dropped and the grid kept stale rows. (#1637)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,11 @@ extension QueryExecutionCoordinator {

if isTruncated {
tab.pagination.hasMoreRows = true
tab.pagination.baseQueryForMore = sql
tab.pagination.isLoadingMore = false
} else {
tab.pagination.resetLoadMore()
}
tab.pagination.baseQueryForMore = sql

if tab.display.isResultsCollapsed {
tab.display.isResultsCollapsed = false
Expand Down
6 changes: 0 additions & 6 deletions TablePro/Core/Coordinators/RowEditingCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ final class RowEditingCoordinator {

parent.selectionState.indices = [result.rowIndex]
parent.tabManager.mutate(at: tabIndex) { $0.hasUserInteraction = true }
parent.querySortCache.removeValue(forKey: tabId)
parent.dataTabDelegate?.tableViewCoordinator?.applyDelta(result.delta)
parent.dataTabDelegate?.tableViewCoordinator?.beginEditing(displayRow: result.rowIndex, column: 0)
}
Expand Down Expand Up @@ -80,7 +79,6 @@ final class RowEditingCoordinator {
parent.tabManager.mutate(at: tabIndex) { $0.hasUserInteraction = true }

if !deleteResult.physicallyRemovedIndices.isEmpty {
parent.querySortCache.removeValue(forKey: tabId)
parent.dataTabDelegate?.tableViewCoordinator?.applyDelta(deleteResult.delta)
} else {
parent.dataTabDelegate?.tableViewCoordinator?.invalidateCachesForUndoRedo()
Expand Down Expand Up @@ -114,7 +112,6 @@ final class RowEditingCoordinator {

parent.selectionState.indices = [result.rowIndex]
parent.tabManager.mutate(at: tabIndex) { $0.hasUserInteraction = true }
parent.querySortCache.removeValue(forKey: tabId)
parent.dataTabDelegate?.tableViewCoordinator?.applyDelta(result.delta)
parent.dataTabDelegate?.tableViewCoordinator?.beginEditing(displayRow: result.rowIndex, column: 0)
}
Expand All @@ -138,7 +135,6 @@ final class RowEditingCoordinator {
}

parent.selectionState.indices = undoResult.adjustedSelection
parent.querySortCache.removeValue(forKey: tabId)
parent.dataTabDelegate?.tableViewCoordinator?.applyDelta(undoResult.delta)
}

Expand All @@ -159,7 +155,6 @@ final class RowEditingCoordinator {
}

parent.tabManager.mutate(at: tabIndex) { $0.hasUserInteraction = true }
parent.querySortCache.removeValue(forKey: tabId)
parent.dataTabDelegate?.tableViewCoordinator?.invalidateCachesForUndoRedo()
parent.dataTabDelegate?.tableViewCoordinator?.applyDelta(application.delta)
}
Expand Down Expand Up @@ -231,7 +226,6 @@ final class RowEditingCoordinator {
tab.selectedRowIndices = newIndices
tab.hasUserInteraction = true
}
parent.querySortCache.removeValue(forKey: tabId)
parent.dataTabDelegate?.tableViewCoordinator?.applyDelta(pasteResult.delta)
}

Expand Down
2 changes: 2 additions & 0 deletions TablePro/Models/Query/QueryTabState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ struct PaginationState: Equatable {
var isLoadingMore: Bool = false
var baseQueryForMore: String?
var baseQueryParameterValues: [String?]?
var sortExecutionOverride: String? // Derived ORDER BY query run for a grid sort; never written back to the editor

/// Default page size constant (used when no explicit value is provided)
/// Note: For new tabs, callers should pass AppSettingsManager.shared.dataGrid.defaultPageSize
Expand Down Expand Up @@ -217,6 +218,7 @@ struct PaginationState: Equatable {
isLoadingMore = false
baseQueryForMore = nil
baseQueryParameterValues = nil
sortExecutionOverride = nil
}

/// Update page size (limit)
Expand Down
5 changes: 5 additions & 0 deletions TablePro/Views/Editor/EditorEventRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ internal final class EditorEventRouter {
coordinator.findPrevious()
}

internal func performFormatSQLForKeyWindow() {
guard let (coordinator, _) = editor(for: NSApp.keyWindow) else { return }
coordinator.performFormatSQL()
}

/// Called by the SwiftUI "Clear Selection" menu when its Esc key equivalent fires.
/// Routes the keystroke to the active editor's Vim engine if it is in a non-normal
/// mode. Returns true when Vim consumed the escape — caller should suppress its
Expand Down
33 changes: 2 additions & 31 deletions TablePro/Views/Editor/QueryEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import TableProPluginKit

/// SQL query editor view with execute button
struct QueryEditorView: View {
private static let logger = Logger(subsystem: "com.TablePro", category: "QueryEditorView")


@Binding var queryText: String
@Binding var cursorPositions: [CursorPosition]
@Binding var parameters: [QueryParameter]
Expand Down Expand Up @@ -67,8 +64,7 @@ struct QueryEditorView: View {
onExecuteQuery: onExecuteQuery,
onAIExplain: onAIExplain,
onAIOptimize: onAIOptimize,
onSaveAsFavorite: onSaveAsFavorite,
onFormatSQL: formatQuery
onSaveAsFavorite: onSaveAsFavorite
)
.frame(minHeight: 100)
.clipped()
Expand Down Expand Up @@ -202,32 +198,7 @@ struct QueryEditorView: View {
}

private func formatQuery() {
// Get current database type
let dbType = databaseType ?? .mysql

// Create formatter service
let formatter = SQLFormatterService()
let options = SQLFormatterOptions.default

let cursorOffset = cursorPositions.first?.range.location ?? 0

do {
// Format SQL with cursor preservation
let result = try formatter.format(
queryText,
dialect: dbType,
cursorOffset: cursorOffset,
options: options
)

// Update text and cursor position
queryText = result.formattedSQL
if let newCursor = result.cursorOffset {
cursorPositions = [CursorPosition(range: NSRange(location: newCursor, length: 0))]
}
} catch {
Self.logger.error("SQL Formatting error: \(error.localizedDescription, privacy: .public)")
}
EditorEventRouter.shared.performFormatSQLForKeyWindow()
}
}

Expand Down
29 changes: 26 additions & 3 deletions TablePro/Views/Editor/SQLEditorCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ final class SQLEditorCoordinator: TextViewCoordinator, TextViewDelegate {
@ObservationIgnored var onAIExplain: ((String) -> Void)?
@ObservationIgnored var onAIOptimize: ((String) -> Void)?
@ObservationIgnored var onSaveAsFavorite: ((String) -> Void)?
@ObservationIgnored var onFormatSQL: (() -> Void)?
@ObservationIgnored var databaseType: DatabaseType?
@ObservationIgnored var tabID: UUID?
@ObservationIgnored var connectionId: UUID?
Expand Down Expand Up @@ -189,7 +188,6 @@ final class SQLEditorCoordinator: TextViewCoordinator, TextViewDelegate {
onAIExplain = nil
onAIOptimize = nil
onSaveAsFavorite = nil
onFormatSQL = nil
schemaProvider = nil
contextMenu = nil
vimEngine = nil
Expand Down Expand Up @@ -238,10 +236,35 @@ final class SQLEditorCoordinator: TextViewCoordinator, TextViewDelegate {
menu.onExplainWithAI = { [weak self] text in self?.onAIExplain?(text) }
menu.onOptimizeWithAI = { [weak self] text in self?.onAIOptimize?(text) }
menu.onSaveAsFavorite = { [weak self] text in self?.onSaveAsFavorite?(text) }
menu.onFormatSQL = { [weak self] in self?.onFormatSQL?() }
menu.onFormatSQL = { [weak self] in self?.performFormatSQL() }
contextMenu = menu
}

func performFormatSQL() {
guard let textView = controller?.textView else { return }
let dialect = databaseType ?? .mysql
let cursorLocation = textView.selectedRange().location
let cursorOffset = cursorLocation == NSNotFound ? 0 : cursorLocation
let formatter = SQLFormatterService()

do {
let result = try formatter.format(
textView.string,
dialect: dialect,
cursorOffset: cursorOffset,
options: .default
)
let fullRange = NSRange(location: 0, length: (textView.string as NSString).length)
textView.replaceCharacters(in: fullRange, with: result.formattedSQL)
if let newOffset = result.cursorOffset {
let clamped = min(newOffset, (result.formattedSQL as NSString).length)
controller?.setCursorPositions([CursorPosition(range: NSRange(location: clamped, length: 0))])
}
} catch {
Self.logger.error("SQL Formatting error: \(error.localizedDescription, privacy: .public)")
}
}

/// Called by EditorEventRouter when a right-click is detected in this editor's text view.
func showContextMenu(for event: NSEvent, in textView: TextView) {
if contextMenu == nil, let controller {
Expand Down
2 changes: 0 additions & 2 deletions TablePro/Views/Editor/SQLEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ struct SQLEditorView: View {
var onAIExplain: ((String) -> Void)?
var onAIOptimize: ((String) -> Void)?
var onSaveAsFavorite: ((String) -> Void)?
var onFormatSQL: (() -> Void)?

@State private var editorState = SourceEditorState()
@State private var completionAdapter: SQLCompletionAdapter?
Expand All @@ -46,7 +45,6 @@ struct SQLEditorView: View {
coordinator.onAIExplain = onAIExplain
coordinator.onAIOptimize = onAIOptimize
coordinator.onSaveAsFavorite = onSaveAsFavorite
coordinator.onFormatSQL = onFormatSQL
coordinator.schemaProvider = schemaProvider
coordinator.connectionAIPolicy = connectionAIPolicy
coordinator.databaseType = databaseType
Expand Down
65 changes: 2 additions & 63 deletions TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ struct MainEditorContentView: View {
}
.onChange(of: tabManager.tabStructureVersion) { _, _ in
let openTabIds = Set(tabManager.tabIds)
coordinator.cleanupSortCache(openTabIds: openTabIds)
coordinator.cleanupTabCaches(openTabIds: openTabIds)
erDiagramViewModels = erDiagramViewModels.filter { openTabIds.contains($0.key) }
serverDashboardViewModels = serverDashboardViewModels.filter { openTabIds.contains($0.key) }
}
Expand Down Expand Up @@ -573,7 +573,7 @@ struct MainEditorContentView: View {
showRowNumbers: AppSettingsManager.shared.dataGrid.showRowNumbers,
hiddenColumns: tab.columnLayout.hiddenColumns
),
sortedIDs: sortedIDsForTab(tab),
sortedIDs: nil,
displayFormats: displayFormats(for: tab),
delegate: dataTabDelegate,
selectedRowIndices: Binding(
Expand Down Expand Up @@ -656,67 +656,6 @@ struct MainEditorContentView: View {
return result
}

/// Returns the display order as a permutation of `RowID`, or nil when no sort applies.
/// For table tabs, sorting is handled server-side via SQL ORDER BY.
private func sortedIDsForTab(_ tab: QueryTab) -> [RowID]? {
if tab.tabType == .table {
return nil
}

guard tab.sortState.isSorting else {
return nil
}

let resolvedRows = resolvedTableRows(for: tab)
guard !resolvedRows.rows.isEmpty else {
return nil
}
let colTypes = resolvedRows.columnTypes

if let cached = coordinator.querySortCache[tab.id],
cached.columnIndex == (tab.sortState.columnIndex ?? -1),
cached.direction == tab.sortState.direction,
cached.schemaVersion == tab.schemaVersion
{
return cached.sortedIDs
}

if resolvedRows.rows.count > 1_000 {
return nil
}

let sortColumns = tab.sortState.columns
let storageRows = resolvedRows.rows
let sortedIndices = Array(storageRows.indices).sorted { idx1, idx2 in
let row1 = storageRows[idx1].values
let row2 = storageRows[idx2].values
for sortCol in sortColumns {
let val1 = sortCol.columnIndex < row1.count
? row1[sortCol.columnIndex].sortKey : ""
let val2 = sortCol.columnIndex < row2.count
? row2[sortCol.columnIndex].sortKey : ""
let colType = sortCol.columnIndex < colTypes.count
? colTypes[sortCol.columnIndex] : nil
let result = RowSortComparator.compare(val1, val2, columnType: colType)
if result == .orderedSame { continue }
return sortCol.direction == .ascending
? result == .orderedAscending
: result == .orderedDescending
}
return false
}
let sortedIDs = sortedIndices.map { storageRows[$0].id }

coordinator.querySortCache[tab.id] = QuerySortCacheEntry(
sortedIDs: sortedIDs,
columnIndex: tab.sortState.columnIndex ?? -1,
direction: tab.sortState.direction,
schemaVersion: tab.schemaVersion
)

return sortedIDs
}

private func sortStateBinding(for tab: QueryTab) -> Binding<SortState> {
Binding(
get: { tab.sortState },
Expand Down
18 changes: 1 addition & 17 deletions TablePro/Views/Main/MainContentCommandActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -758,23 +758,7 @@ final class MainContentCommandActions {
}

func formatQuery() {
guard let coordinator,
let (tab, tabIndex) = coordinator.tabManager.selectedTabAndIndex else { return }
let dbType = connection.type
let formatter = SQLFormatterService()
let options = SQLFormatterOptions.default

do {
let result = try formatter.format(
tab.content.query,
dialect: dbType,
cursorOffset: 0,
options: options
)
coordinator.tabManager.mutate(at: tabIndex) { $0.content.query = result.formattedSQL }
} catch {
Self.logger.error("SQL Formatting error: \(error.localizedDescription, privacy: .public)")
}
EditorEventRouter.shared.performFormatSQLForKeyWindow()
}

// MARK: - UI Operations (Group A — Called Directly)
Expand Down
Loading
Loading