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: 0 additions & 1 deletion TablePro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,6 @@
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SWIFT_COMPILATION_MODE = wholemodule;
/* NOTE: ONLY_ACTIVE_ARCH is intentionally not set to YES for Release to support multi-architecture builds. Building Release without the custom build script will attempt to build for all supported architectures by default. */
};
name = Release;
};
Expand Down
69 changes: 63 additions & 6 deletions TablePro/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// Configure windows after app launch
configureWelcomeWindow()

// Close any restored main windows (no active connection on fresh launch)
// macOS may restore window state from previous session
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
for window in NSApp.windows where window.identifier?.rawValue.contains("main") == true {
window.close()
}
// Check startup behavior setting
let settings = AppSettingsStorage.shared.loadGeneral()
let shouldReopenLast = settings.startupBehavior == .reopenLast

if shouldReopenLast, let lastConnectionId = AppSettingsStorage.shared.loadLastConnectionId() {
// Try to auto-reconnect to last session
attemptAutoReconnect(connectionId: lastConnectionId)
} else {
// Normal startup: close any restored main windows
closeRestoredMainWindows()
}

// Observe for new windows being created
Expand All @@ -50,6 +54,59 @@ class AppDelegate: NSObject, NSApplicationDelegate {
)
}

/// Attempt to auto-reconnect to the last used connection
private func attemptAutoReconnect(connectionId: UUID) {
// Load connections and find the one we want
let connections = ConnectionStorage.shared.loadConnections()
guard let connection = connections.first(where: { $0.id == connectionId }) else {
// Connection was deleted, fall back to welcome window
AppSettingsStorage.shared.saveLastConnectionId(nil)
closeRestoredMainWindows()
openWelcomeWindow()
return
}

// Open main window first, then attempt connection
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }

// Open main window via notification FIRST (before closing welcome window)
// The OpenWindowHandler in welcome window will process this
NotificationCenter.default.post(name: .openMainWindow, object: nil)

// Connect in background and handle result
Task { @MainActor in
do {
try await DatabaseManager.shared.connectToSession(connection)

// Connection successful - close welcome window
for window in NSApp.windows where self.isWelcomeWindow(window) {
window.close()
}
} catch {
// Log the error for debugging
print("[AppDelegate] Auto-reconnect failed for '\(connection.name)': \(error.localizedDescription)")

// Connection failed - close main window and show welcome
for window in NSApp.windows where self.isMainWindow(window) {
window.close()
}

self.openWelcomeWindow()
}
}
}
}

/// Close any macOS-restored main windows
private func closeRestoredMainWindows() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
for window in NSApp.windows where window.identifier?.rawValue.contains("main") == true {
window.close()
}
}
}

@objc
private func windowWillClose(_ notification: Notification) {
guard let window = notification.object as? NSWindow else { return }
Expand Down
41 changes: 39 additions & 2 deletions TablePro/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ struct ContentView: View {
@State private var showDeleteConfirmation = false
@State private var showUnsavedChangesAlert = false
@State private var pendingCloseSessionId: UUID?
@State private var showDisconnectConfirmation = false
@State private var pendingDisconnectSessionId: UUID?
@State private var hasLoaded = false
@State private var escapeKeyMonitor: Any?
@State private var isInspectorPresented = false // Right sidebar (inspector) visibility
Expand Down Expand Up @@ -76,6 +78,35 @@ struct ContentView: View {
} message: {
Text("This connection has unsaved changes. Are you sure you want to close it?")
}
.alert(
"Disconnect",
isPresented: $showDisconnectConfirmation
) {
Button("Cancel", role: .cancel) {
pendingDisconnectSessionId = nil
}
Button("Disconnect", role: .destructive) {
if let sessionId = pendingDisconnectSessionId {
Task {
await dbManager.disconnectSession(sessionId)
}
}
pendingDisconnectSessionId = nil
}
Button("Don't Ask Again") {
// Disable future confirmations
AppSettingsManager.shared.general.confirmBeforeDisconnecting = false
// Then disconnect
if let sessionId = pendingDisconnectSessionId {
Task {
await dbManager.disconnectSession(sessionId)
}
}
pendingDisconnectSessionId = nil
}
} message: {
Text("Are you sure you want to disconnect from this database?")
}
.onAppear {
loadConnections()
setupEscapeKeyMonitor()
Expand All @@ -88,8 +119,14 @@ struct ContentView: View {
}
.onReceive(NotificationCenter.default.publisher(for: .deselectConnection)) { _ in
if let sessionId = dbManager.currentSessionId {
Task {
await dbManager.disconnectSession(sessionId)
// Check if confirmation is required
if AppSettingsManager.shared.general.confirmBeforeDisconnecting {
pendingDisconnectSessionId = sessionId
showDisconnectConfirmation = true
} else {
Task {
await dbManager.disconnectSession(sessionId)
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions TablePro/Core/Database/DatabaseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ final class DatabaseManager: ObservableObject {
activeSessions[connection.id]?.selectedTabId = tabState.selectedTabId
}

// Save as last connection for "Reopen Last Session" feature
AppSettingsStorage.shared.saveLastConnectionId(connection.id)

// Post notification for reliable delivery
NotificationCenter.default.post(name: .databaseDidConnect, object: nil)
} catch {
Expand Down
12 changes: 11 additions & 1 deletion TablePro/Core/Database/LibPQConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct LibPQError: Error, LocalizedError {
/// Result from a PostgreSQL query execution
struct LibPQQueryResult {
let columns: [String]
let columnOids: [UInt32] // NEW: PostgreSQL Oid for each column
let rows: [[String?]]
let affectedRows: Int
let commandTag: String?
Expand Down Expand Up @@ -211,6 +212,7 @@ final class LibPQConnection: @unchecked Sendable {
PQclear(result)
return LibPQQueryResult(
columns: [],
columnOids: [],
rows: [],
affectedRows: affected,
commandTag: cmdTag
Expand All @@ -237,17 +239,24 @@ final class LibPQConnection: @unchecked Sendable {
let numFields = Int(PQnfields(result))
let numRows = Int(PQntuples(result))

// Fetch column names
// Fetch column names and types
var columns: [String] = []
var columnOids: [UInt32] = []
columns.reserveCapacity(numFields)
columnOids.reserveCapacity(numFields)

for i in 0..<numFields {
// Extract column name
if let namePtr = PQfname(result, Int32(i)) {
let cStr = String(cString: namePtr)
columns.append(String(cStr.unicodeScalars.map { Character($0) }))
} else {
columns.append("column_\(i)")
}

// Extract column type Oid (NEW)
let oid = PQftype(result, Int32(i))
columnOids.append(UInt32(oid))
}

// Fetch all rows
Expand Down Expand Up @@ -287,6 +296,7 @@ final class LibPQConnection: @unchecked Sendable {

return LibPQQueryResult(
columns: columns,
columnOids: columnOids,
rows: rows,
affectedRows: numRows,
commandTag: getCommandTag(from: result)
Expand Down
8 changes: 8 additions & 0 deletions TablePro/Core/Database/MariaDBConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct MariaDBError: Error, LocalizedError {
/// Result from a MySQL query execution
struct MariaDBQueryResult {
let columns: [String]
let columnTypes: [UInt32] // NEW: MySQL field type for each column
let rows: [[String?]]
let affectedRows: UInt64
let insertId: UInt64
Expand Down Expand Up @@ -301,6 +302,7 @@ final class MariaDBConnection: @unchecked Sendable {
let insertId = mysql_insert_id(mysql)
return MariaDBQueryResult(
columns: [],
columnTypes: [],
rows: [],
affectedRows: affected,
insertId: insertId
Expand All @@ -314,18 +316,23 @@ final class MariaDBConnection: @unchecked Sendable {
// Fetch column metadata
let numFields = Int(mysql_num_fields(resultPtr))
var columns: [String] = []
var columnTypes: [UInt32] = [] // NEW: Store column types
columns.reserveCapacity(numFields)
columnTypes.reserveCapacity(numFields)

if let fields = mysql_fetch_fields(resultPtr) {
for i in 0..<numFields {
let field = fields[i]
// Extract column name
if let namePtr = field.name {
// Create completely independent copy of column name
let cStr = String(cString: namePtr)
columns.append(String(cStr.unicodeScalars.map { Character($0) }))
} else {
columns.append("column_\(i)")
}
// Extract column type (NEW)
columnTypes.append(field.type.rawValue)
}
}

Expand Down Expand Up @@ -375,6 +382,7 @@ final class MariaDBConnection: @unchecked Sendable {

return MariaDBQueryResult(
columns: columns,
columnTypes: columnTypes,
rows: rows,
affectedRows: UInt64(rows.count),
insertId: 0
Expand Down
9 changes: 9 additions & 0 deletions TablePro/Core/Database/MySQLDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ final class MySQLDriver: DatabaseDriver {
let columns = try await fetchColumnNames(for: tableName)
return QueryResult(
columns: columns,
columnTypes: Array(repeating: .text, count: columns.count), // Default to text for empty results
rows: [],
rowsAffected: Int(result.affectedRows),
executionTime: Date().timeIntervalSince(startTime),
Expand All @@ -108,8 +109,16 @@ final class MySQLDriver: DatabaseDriver {
}
}

// Convert MySQL column types to ColumnType enum
let columnTypes = result.columnTypes.enumerated().map { index, mysqlType in
// Also check field length for boolean detection (TINYINT(1))
// Note: We don't have length info here, so we use just the type
ColumnType(fromMySQLType: mysqlType)
}

return QueryResult(
columns: result.columns,
columnTypes: columnTypes,
rows: result.rows,
rowsAffected: Int(result.affectedRows),
executionTime: Date().timeIntervalSince(startTime),
Expand Down
6 changes: 6 additions & 0 deletions TablePro/Core/Database/PostgreSQLDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,15 @@ final class PostgreSQLDriver: DatabaseDriver {

do {
let result = try await pqConn.executeQuery(query)

// Convert PostgreSQL Oids to ColumnType enum
let columnTypes = result.columnOids.map { oid in
ColumnType(fromPostgreSQLOid: oid)
}

return QueryResult(
columns: result.columns,
columnTypes: columnTypes,
rows: result.rows,
rowsAffected: result.affectedRows,
executionTime: Date().timeIntervalSince(startTime),
Expand Down
14 changes: 14 additions & 0 deletions TablePro/Core/Database/SQLiteDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,26 @@ final class SQLiteDriver: DatabaseDriver {
// Get column info
let columnCount = sqlite3_column_count(statement)
var columns: [String] = []
var columnTypes: [ColumnType] = []

for i in 0..<columnCount {
// Extract column name
if let name = sqlite3_column_name(statement, i) {
columns.append(String(cString: name))
} else {
columns.append("column_\(i)")
}

// Extract column type from declared type
// sqlite3_column_decltype returns the declared type (e.g., "INTEGER", "TEXT", "DATETIME")
let declaredType: String? = {
if let typePtr = sqlite3_column_decltype(statement, i) {
return String(cString: typePtr)
}
return nil
}()

columnTypes.append(ColumnType(fromSQLiteType: declaredType))
}

// Execute and fetch rows
Expand Down Expand Up @@ -126,6 +139,7 @@ final class SQLiteDriver: DatabaseDriver {

return QueryResult(
columns: columns,
columnTypes: columnTypes,
rows: rows,
rowsAffected: rowsAffected,
executionTime: executionTime,
Expand Down
Loading
Loading