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
12 changes: 12 additions & 0 deletions Plugins/SQLiteDriverPlugin/SQLitePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,18 @@ final class SQLitePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
var supportsSchemas: Bool { false }
var supportsTransactions: Bool { true }

var capabilities: PluginCapabilities {
[
.parameterizedQueries,
.transactions,
.alterTableDDL,
.foreignKeyToggle,
.truncateTable,
.cancelQuery,
.batchExecute,
]
}

func quoteIdentifier(_ name: String) -> String {
let escaped = name.replacingOccurrences(of: "`", with: "``")
return "`\(escaped)`"
Expand Down
22 changes: 22 additions & 0 deletions Plugins/TableProPluginKit/PluginCapabilities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

public struct PluginCapabilities: OptionSet, Sendable {
public let rawValue: UInt32

public init(rawValue: UInt32) {
self.rawValue = rawValue
}

public static let materializedViews = PluginCapabilities(rawValue: 1 << 0)
public static let foreignTables = PluginCapabilities(rawValue: 1 << 1)
public static let storedProcedures = PluginCapabilities(rawValue: 1 << 2)
public static let userFunctions = PluginCapabilities(rawValue: 1 << 3)
public static let alterTableDDL = PluginCapabilities(rawValue: 1 << 4)
public static let foreignKeyToggle = PluginCapabilities(rawValue: 1 << 5)
public static let truncateTable = PluginCapabilities(rawValue: 1 << 6)
public static let multiSchema = PluginCapabilities(rawValue: 1 << 7)
public static let parameterizedQueries = PluginCapabilities(rawValue: 1 << 8)
public static let cancelQuery = PluginCapabilities(rawValue: 1 << 9)
public static let batchExecute = PluginCapabilities(rawValue: 1 << 10)
public static let transactions = PluginCapabilities(rawValue: 1 << 11)
}
4 changes: 4 additions & 0 deletions Plugins/TableProPluginKit/PluginDatabaseDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public struct PluginRowChange: Sendable {
}

public protocol PluginDatabaseDriver: AnyObject, Sendable {
var capabilities: PluginCapabilities { get }

// Connection
func connect() async throws
func disconnect()
Expand Down Expand Up @@ -141,6 +143,8 @@ public protocol PluginDatabaseDriver: AnyObject, Sendable {
}

public extension PluginDatabaseDriver {
var capabilities: PluginCapabilities { [] }

var supportsSchemas: Bool { false }

func fetchSchemas() async throws -> [String] { [] }
Expand Down
20 changes: 20 additions & 0 deletions Plugins/TableProPluginKit/PluginProcedureFunctionSupport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

public protocol PluginProcedureFunctionSupport {
func fetchProcedures(schema: String?) async throws -> [PluginRoutineInfo]
func fetchFunctions(schema: String?) async throws -> [PluginRoutineInfo]
func fetchProcedureDDL(name: String, schema: String?) async throws -> String
func fetchFunctionDDL(name: String, schema: String?) async throws -> String
}

public struct PluginRoutineInfo: Codable, Sendable {
public let name: String
public let returnType: String?
public let language: String?

public init(name: String, returnType: String? = nil, language: String? = nil) {
self.name = name
self.returnType = returnType
self.language = language
}
}
91 changes: 25 additions & 66 deletions TablePro/Core/Autocomplete/SQLContextAnalyzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
//

import Foundation
import os

private let regexLogger = Logger(subsystem: "com.TablePro", category: "SQLContextAnalyzer.Regex")

private func compileRegex(_ pattern: String, options: NSRegularExpression.Options = []) -> NSRegularExpression {
if let regex = try? NSRegularExpression(pattern: pattern, options: options) {
return regex
}
regexLogger.fault("Failed to compile static regex pattern: \(pattern, privacy: .public)")
return NSRegularExpression()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use a valid fallback regular expression

This fallback calls NSRegularExpression() with no arguments, but Foundation does not provide a parameterless initializer for NSRegularExpression in Swift, so the target will fail to compile whenever this file is built. Use a valid pattern such as (?!) for the fallback instead of constructing an empty regex.

Useful? React with 👍 / 👎.

}

/// Type of SQL clause the cursor is in
enum SQLClauseType {
Expand Down Expand Up @@ -188,80 +199,28 @@ final class SQLContextAnalyzer {
// SELECT is most general
("\\bSELECT\\s+[^;]*$", .select),
]
return patterns.compactMap { pattern, clause in
guard let regex = try? NSRegularExpression(
pattern: pattern, options: .caseInsensitive
) else {
assertionFailure("Invalid SQL clause regex pattern: \(pattern)")
return nil
}
return (regex, clause)
return patterns.map { pattern, clause in
(compileRegex(pattern, options: .caseInsensitive), clause)
}
}()

/// Pre-compiled regex for removing strings and comments
private static let singleQuoteStringRegex: NSRegularExpression = {
if let regex = try? NSRegularExpression(pattern: "'[^']*'") {
return regex
}
assertionFailure("Failed to compile singleQuoteStringRegex - invalid pattern")
return try! NSRegularExpression(pattern: "(?!)")
}()
private static let singleQuoteStringRegex = compileRegex("'[^']*'")

private static let doubleQuoteStringRegex: NSRegularExpression = {
if let regex = try? NSRegularExpression(pattern: "\"[^\"]*\"") {
return regex
}
assertionFailure("Failed to compile doubleQuoteStringRegex - invalid pattern")
return try! NSRegularExpression(pattern: "(?!)")
}()
private static let doubleQuoteStringRegex = compileRegex("\"[^\"]*\"")

private static let blockCommentRegex: NSRegularExpression = {
if let regex = try? NSRegularExpression(pattern: "/\\*[\\s\\S]*?\\*/") {
return regex
}
assertionFailure("Failed to compile blockCommentRegex - invalid pattern")
return try! NSRegularExpression(pattern: "(?!)")
}()
private static let blockCommentRegex = compileRegex("/\\*[\\s\\S]*?\\*/")

private static let lineCommentRegex: NSRegularExpression = {
if let regex = try? NSRegularExpression(pattern: "--[^\n]*") {
return regex
}
assertionFailure("Failed to compile lineCommentRegex - invalid pattern")
return try! NSRegularExpression(pattern: "(?!)")
}()
private static let lineCommentRegex = compileRegex("--[^\n]*")

/// Combined regex for removing strings and comments in a single pass (SVC-13)
private static let stringsAndCommentsRegex: NSRegularExpression = {
// Alternation: single-quoted strings | double-quoted strings | block comments | line comments
let pattern = #"'[^']*'|"[^"]*"|/\*[\s\S]*?\*/|--[^\n]*"#
if let regex = try? NSRegularExpression(pattern: pattern) {
return regex
}
assertionFailure("Failed to compile stringsAndCommentsRegex - invalid pattern")
return try! NSRegularExpression(pattern: "(?!)")
}()
private static let stringsAndCommentsRegex = compileRegex(
#"'[^']*'|"[^"]*"|/\*[\s\S]*?\*/|--[^\n]*"#
)

private static let cteFirstRegex: NSRegularExpression = {
if let regex = try? NSRegularExpression(
pattern: "(?i)\\bWITH\\s+(?:RECURSIVE\\s+)?([\\w]+)\\s+AS\\s*\\("
) {
return regex
}
assertionFailure("Failed to compile cteFirstRegex")
return try! NSRegularExpression(pattern: "(?!)")
}()
private static let cteFirstRegex = compileRegex(
"(?i)\\bWITH\\s+(?:RECURSIVE\\s+)?([\\w]+)\\s+AS\\s*\\("
)

private static let cteCommaRegex: NSRegularExpression = {
if let regex = try? NSRegularExpression(
pattern: "(?i),\\s*([\\w]+)\\s+AS\\s*\\("
) {
return regex
}
assertionFailure("Failed to compile cteCommaRegex")
return try! NSRegularExpression(pattern: "(?!)")
}()
private static let cteCommaRegex = compileRegex("(?i),\\s*([\\w]+)\\s+AS\\s*\\(")

private static let tableRefRegexes: [NSRegularExpression] = {
let patterns = [
Expand All @@ -274,7 +233,7 @@ final class SQLContextAnalyzer {
"(?i)\\bINSERT\\s+INTO\\s+[`\"']?([\\w.]+)[`\"']?",
"(?i)\\bCREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+\\w+\\s+ON\\s+[`\"']?([\\w.]+)[`\"']?"
]
return patterns.compactMap { try? NSRegularExpression(pattern: $0) }
return patterns.map { compileRegex($0) }
}()

// MARK: - UTF-16 Helpers
Expand Down
9 changes: 7 additions & 2 deletions TablePro/Core/Database/DatabaseManager+Sessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import AppKit
import Combine
import Foundation
import os
import TableProPluginKit
Expand Down Expand Up @@ -378,13 +379,17 @@ extension DatabaseManager {
internal func setSession(_ session: ConnectionSession, for connectionId: UUID) {
activeSessions[connectionId] = session
connectionStatusVersions[connectionId, default: 0] &+= 1
NotificationCenter.default.post(name: .connectionStatusDidChange, object: connectionId)
AppEvents.shared.connectionStatusChanged.send(
ConnectionStatusChange(connectionId: connectionId, status: session.status)
)
}

internal func removeSessionEntry(for connectionId: UUID) {
activeSessions.removeValue(forKey: connectionId)
connectionStatusVersions.removeValue(forKey: connectionId)
NotificationCenter.default.post(name: .connectionStatusDidChange, object: connectionId)
AppEvents.shared.connectionStatusChanged.send(
ConnectionStatusChange(connectionId: connectionId, status: .disconnected)
)
}

#if DEBUG
Expand Down
23 changes: 23 additions & 0 deletions TablePro/Core/Events/AppEvents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// AppEvents.swift
// TablePro
//

import Combine
import Foundation

@MainActor
final class AppEvents {
static let shared = AppEvents()

let themeChanged = PassthroughSubject<Void, Never>()

let connectionStatusChanged = PassthroughSubject<ConnectionStatusChange, Never>()

private init() {}
}

struct ConnectionStatusChange: Sendable {
let connectionId: UUID
let status: ConnectionStatus
}
46 changes: 46 additions & 0 deletions TablePro/Core/Services/AppServices.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// AppServices.swift
// TablePro
//

import SwiftUI

@MainActor
struct AppServices {
let appEvents: AppEvents
let appSettings: AppSettingsManager
let connectionStorage: ConnectionStorage
let databaseManager: DatabaseManager
let pluginManager: PluginManager
let schemaService: SchemaService
let queryHistoryStorage: QueryHistoryStorage
let sqlFavoriteManager: SQLFavoriteManager
let aiChatStorage: AIChatStorage
let syncTracker: SyncChangeTracker
let themeEngine: ThemeEngine

static let live = AppServices(
appEvents: .shared,
appSettings: .shared,
connectionStorage: .shared,
databaseManager: .shared,
pluginManager: .shared,
schemaService: .shared,
queryHistoryStorage: .shared,
sqlFavoriteManager: .shared,
aiChatStorage: .shared,
syncTracker: .shared,
themeEngine: .shared
)
}

private struct AppServicesEnvironmentKey: EnvironmentKey {
@MainActor static var defaultValue: AppServices { .live }
}

extension EnvironmentValues {
var appServices: AppServices {
get { self[AppServicesEnvironmentKey.self] }
set { self[AppServicesEnvironmentKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ extension Notification.Name {
// MARK: - Connections

static let connectionUpdated = Notification.Name("connectionUpdated")
static let connectionStatusDidChange = Notification.Name("connectionStatusDidChange")
static let databaseDidConnect = Notification.Name("databaseDidConnect")
static let exportConnections = Notification.Name("exportConnections")
static let importConnections = Notification.Name("importConnections")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//

import AppKit
import Combine
import os
import SwiftUI

Expand Down Expand Up @@ -45,7 +46,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi

// MARK: - Observers

private var connectionStatusObserver: NSObjectProtocol?
private var connectionStatusCancellable: AnyCancellable?

// MARK: - Init

Expand Down Expand Up @@ -196,24 +197,17 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
// MARK: - Observers

private func installObservers() {
guard connectionStatusObserver == nil else { return }
connectionStatusObserver = NotificationCenter.default.addObserver(
forName: .connectionStatusDidChange,
object: nil,
queue: .main
) { [weak self] _ in
MainActor.assumeIsolated {
guard connectionStatusCancellable == nil else { return }
connectionStatusCancellable = AppEvents.shared.connectionStatusChanged
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.handleConnectionStatusChange()
}
}
handleConnectionStatusChange()
}

private func removeObservers() {
if let observer = connectionStatusObserver {
NotificationCenter.default.removeObserver(observer)
connectionStatusObserver = nil
}
connectionStatusCancellable = nil
}

// MARK: - Toolbar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ extension Notification.Name {
/// Observers should reload fonts via ThemeEngine.shared.reloadFontCaches().
static let accessibilityTextSizeDidChange = Notification.Name("accessibilityTextSizeDidChange")

/// Posted when the active theme changes (colors, fonts, or entire theme switch).
/// Used by AppKit components that cannot observe @Observable directly.
static let themeDidChange = Notification.Name("themeDidChange")

/// Posted when terminal settings change (font, theme, cursor, etc.)
/// Used by terminal views to live-update configuration.
static let terminalSettingsDidChange = Notification.Name("terminalSettingsDidChange")
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Storage/SSHProfileStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ final class SSHProfileStorage {
}

func deleteProfile(_ profile: SSHProfile) {
SyncChangeTracker.shared.markDeleted(.sshProfile, id: profile.id.uuidString)
var profiles = loadProfiles()
guard !lastLoadFailed else { return }
profiles.removeAll { $0.id == profile.id }
saveProfiles(profiles)
SyncChangeTracker.shared.markDeleted(.sshProfile, id: profile.id.uuidString)

deleteSSHPassword(for: profile.id)
deleteKeyPassphrase(for: profile.id)
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Storage/TagStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ final class TagStorage {
/// Delete a custom tag (presets cannot be deleted)
func deleteTag(_ tag: ConnectionTag) {
guard !tag.isPreset else { return }
SyncChangeTracker.shared.markDeleted(.tag, id: tag.id.uuidString)
var tags = loadTags()
tags.removeAll { $0.id == tag.id }
saveTags(tags)
SyncChangeTracker.shared.markDeleted(.tag, id: tag.id.uuidString)
}

/// Get tag by ID
Expand Down
Loading
Loading