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

### Fixed

- The New/Edit Favorite and linked file metadata dialogs no longer open with a large empty area and fields crushed against the right edge. They now use the standard macOS grouped form layout: the query editor spans the full dialog width, the cursor marker hint sits under the editor, keyword validation shows under the keyword field, and the Global option is a checkbox that explains its scope. (#1809)
- Tables and views outside the connection's default schema now open and save correctly when reached through the Quick Switcher, a restored tab, or the MCP table-open tool. These paths used to send unqualified queries, which failed with "Invalid object name" on SQL Server or silently targeted the wrong table. (#1774)
- Quick Switcher opens again on macOS Sequoia. Since 0.51.0 the panel came up invisible on macOS 15, and the toolbar button and keyboard shortcut appeared to do nothing. (#1806)
- Quick Switcher keyboard navigation (Ctrl-J/K and arrow shortcuts) no longer goes dead after the switcher has been opened and closed repeatedly. (#1806)
Expand Down
112 changes: 112 additions & 0 deletions TablePro/Core/Storage/SQLFavoriteEditValidation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// SQLFavoriteEditValidation.swift
// TablePro
//

import Foundation
import Observation

internal enum SQLFavoriteKeywordValidation: Equatable {
case valid
case error(String)
case warning(String)

var blocksSave: Bool {
if case .error = self { return true }
return false
}

var isWarning: Bool {
if case .warning = self { return true }
return false
}

var displayText: String? {
switch self {
case .valid:
return nil
case let .error(text), let .warning(text):
return text
}
}
}

internal enum SQLFavoriteKeywordValidator {
static let reservedSQLKeywords: Set<String> = [
"select", "from", "where", "insert", "update", "delete",
"create", "drop", "alter", "join", "on", "and", "or",
"not", "in", "like", "between", "order", "group", "having",
"limit", "set", "values", "into", "as", "is", "null",
"true", "false", "case", "when", "then", "else", "end"
]

static func requiresAvailabilityCheck(_ trimmedKeyword: String) -> Bool {
!trimmedKeyword.isEmpty && !trimmedKeyword.contains(" ")
}

static func classify(trimmedKeyword: String, isAvailable: Bool) -> SQLFavoriteKeywordValidation {
guard !trimmedKeyword.isEmpty else { return .valid }
guard !trimmedKeyword.contains(" ") else {
return .error(String(localized: "Keyword cannot contain spaces"))
}
guard isAvailable else {
return .error(String(localized: "This keyword is already in use"))
}
guard !reservedSQLKeywords.contains(trimmedKeyword.lowercased()) else {
return .warning(String(
format: String(localized: "Shadows the SQL keyword '%@'"),
trimmedKeyword.uppercased()
))
}
return .valid
}
}

@MainActor
@Observable
internal final class SQLFavoriteKeywordField {
var keyword = ""
private(set) var validation: SQLFavoriteKeywordValidation = .valid

private var validationId = 0
private let availabilityCheck: (String, UUID?, UUID?) async -> Bool

init(
availabilityCheck: @escaping (String, UUID?, UUID?) async -> Bool = { keyword, connectionId, excludingFavoriteId in
await SQLFavoriteManager.shared.isKeywordAvailable(
keyword,
connectionId: connectionId,
excludingFavoriteId: excludingFavoriteId
)
}
) {
self.availabilityCheck = availabilityCheck
}

var trimmedKeyword: String {
keyword.trimmingCharacters(in: .whitespaces)
}

func validate(connectionId: UUID?, excludingFavoriteId: UUID?) async {
validationId += 1
let currentId = validationId
let trimmed = trimmedKeyword
guard SQLFavoriteKeywordValidator.requiresAvailabilityCheck(trimmed) else {
validation = SQLFavoriteKeywordValidator.classify(trimmedKeyword: trimmed, isAvailable: true)
return
}
let available = await availabilityCheck(trimmed, connectionId, excludingFavoriteId)
guard currentId == validationId else { return }
validation = SQLFavoriteKeywordValidator.classify(trimmedKeyword: trimmed, isAvailable: available)
}
}

internal enum SQLFavoriteEditValidation {
static func canSave(
isNameBlank: Bool,
isQueryBlank: Bool = false,
keywordValidation: SQLFavoriteKeywordValidation
) -> Bool {
!isNameBlank && !isQueryBlank && !keywordValidation.blocksSave
}
}
Loading
Loading