Skip to content

Commit b7221ba

Browse files
authored
Merge pull request #19 from datlechin/feature/settings-and-editor-improvements
Settings Validation, Advanced Editor Features & Bug Fixes
2 parents 9024d78 + 6fccdc0 commit b7221ba

38 files changed

Lines changed: 2017 additions & 101 deletions

TablePro.xcodeproj/project.pbxproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,6 @@
240240
MTL_ENABLE_DEBUG_INFO = NO;
241241
MTL_FAST_MATH = YES;
242242
SWIFT_COMPILATION_MODE = wholemodule;
243-
/* 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. */
244243
};
245244
name = Release;
246245
};

TablePro/AppDelegate.swift

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2525
// Configure windows after app launch
2626
configureWelcomeWindow()
2727

28-
// Close any restored main windows (no active connection on fresh launch)
29-
// macOS may restore window state from previous session
30-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
31-
for window in NSApp.windows where window.identifier?.rawValue.contains("main") == true {
32-
window.close()
33-
}
28+
// Check startup behavior setting
29+
let settings = AppSettingsStorage.shared.loadGeneral()
30+
let shouldReopenLast = settings.startupBehavior == .reopenLast
31+
32+
if shouldReopenLast, let lastConnectionId = AppSettingsStorage.shared.loadLastConnectionId() {
33+
// Try to auto-reconnect to last session
34+
attemptAutoReconnect(connectionId: lastConnectionId)
35+
} else {
36+
// Normal startup: close any restored main windows
37+
closeRestoredMainWindows()
3438
}
3539

3640
// Observe for new windows being created
@@ -50,6 +54,59 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5054
)
5155
}
5256

57+
/// Attempt to auto-reconnect to the last used connection
58+
private func attemptAutoReconnect(connectionId: UUID) {
59+
// Load connections and find the one we want
60+
let connections = ConnectionStorage.shared.loadConnections()
61+
guard let connection = connections.first(where: { $0.id == connectionId }) else {
62+
// Connection was deleted, fall back to welcome window
63+
AppSettingsStorage.shared.saveLastConnectionId(nil)
64+
closeRestoredMainWindows()
65+
openWelcomeWindow()
66+
return
67+
}
68+
69+
// Open main window first, then attempt connection
70+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
71+
guard let self = self else { return }
72+
73+
// Open main window via notification FIRST (before closing welcome window)
74+
// The OpenWindowHandler in welcome window will process this
75+
NotificationCenter.default.post(name: .openMainWindow, object: nil)
76+
77+
// Connect in background and handle result
78+
Task { @MainActor in
79+
do {
80+
try await DatabaseManager.shared.connectToSession(connection)
81+
82+
// Connection successful - close welcome window
83+
for window in NSApp.windows where self.isWelcomeWindow(window) {
84+
window.close()
85+
}
86+
} catch {
87+
// Log the error for debugging
88+
print("[AppDelegate] Auto-reconnect failed for '\(connection.name)': \(error.localizedDescription)")
89+
90+
// Connection failed - close main window and show welcome
91+
for window in NSApp.windows where self.isMainWindow(window) {
92+
window.close()
93+
}
94+
95+
self.openWelcomeWindow()
96+
}
97+
}
98+
}
99+
}
100+
101+
/// Close any macOS-restored main windows
102+
private func closeRestoredMainWindows() {
103+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
104+
for window in NSApp.windows where window.identifier?.rawValue.contains("main") == true {
105+
window.close()
106+
}
107+
}
108+
}
109+
53110
@objc
54111
private func windowWillClose(_ notification: Notification) {
55112
guard let window = notification.object as? NSWindow else { return }

TablePro/ContentView.swift

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ struct ContentView: View {
2121
@State private var showDeleteConfirmation = false
2222
@State private var showUnsavedChangesAlert = false
2323
@State private var pendingCloseSessionId: UUID?
24+
@State private var showDisconnectConfirmation = false
25+
@State private var pendingDisconnectSessionId: UUID?
2426
@State private var hasLoaded = false
2527
@State private var escapeKeyMonitor: Any?
2628
@State private var isInspectorPresented = false // Right sidebar (inspector) visibility
@@ -76,6 +78,35 @@ struct ContentView: View {
7678
} message: {
7779
Text("This connection has unsaved changes. Are you sure you want to close it?")
7880
}
81+
.alert(
82+
"Disconnect",
83+
isPresented: $showDisconnectConfirmation
84+
) {
85+
Button("Cancel", role: .cancel) {
86+
pendingDisconnectSessionId = nil
87+
}
88+
Button("Disconnect", role: .destructive) {
89+
if let sessionId = pendingDisconnectSessionId {
90+
Task {
91+
await dbManager.disconnectSession(sessionId)
92+
}
93+
}
94+
pendingDisconnectSessionId = nil
95+
}
96+
Button("Don't Ask Again") {
97+
// Disable future confirmations
98+
AppSettingsManager.shared.general.confirmBeforeDisconnecting = false
99+
// Then disconnect
100+
if let sessionId = pendingDisconnectSessionId {
101+
Task {
102+
await dbManager.disconnectSession(sessionId)
103+
}
104+
}
105+
pendingDisconnectSessionId = nil
106+
}
107+
} message: {
108+
Text("Are you sure you want to disconnect from this database?")
109+
}
79110
.onAppear {
80111
loadConnections()
81112
setupEscapeKeyMonitor()
@@ -88,8 +119,14 @@ struct ContentView: View {
88119
}
89120
.onReceive(NotificationCenter.default.publisher(for: .deselectConnection)) { _ in
90121
if let sessionId = dbManager.currentSessionId {
91-
Task {
92-
await dbManager.disconnectSession(sessionId)
122+
// Check if confirmation is required
123+
if AppSettingsManager.shared.general.confirmBeforeDisconnecting {
124+
pendingDisconnectSessionId = sessionId
125+
showDisconnectConfirmation = true
126+
} else {
127+
Task {
128+
await dbManager.disconnectSession(sessionId)
129+
}
93130
}
94131
}
95132
}

TablePro/Core/Database/DatabaseManager.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ final class DatabaseManager: ObservableObject {
116116
activeSessions[connection.id]?.selectedTabId = tabState.selectedTabId
117117
}
118118

119+
// Save as last connection for "Reopen Last Session" feature
120+
AppSettingsStorage.shared.saveLastConnectionId(connection.id)
121+
119122
// Post notification for reliable delivery
120123
NotificationCenter.default.post(name: .databaseDidConnect, object: nil)
121124
} catch {

TablePro/Core/Database/LibPQConnection.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ struct LibPQError: Error, LocalizedError {
3939
/// Result from a PostgreSQL query execution
4040
struct LibPQQueryResult {
4141
let columns: [String]
42+
let columnOids: [UInt32] // NEW: PostgreSQL Oid for each column
4243
let rows: [[String?]]
4344
let affectedRows: Int
4445
let commandTag: String?
@@ -211,6 +212,7 @@ final class LibPQConnection: @unchecked Sendable {
211212
PQclear(result)
212213
return LibPQQueryResult(
213214
columns: [],
215+
columnOids: [],
214216
rows: [],
215217
affectedRows: affected,
216218
commandTag: cmdTag
@@ -237,17 +239,24 @@ final class LibPQConnection: @unchecked Sendable {
237239
let numFields = Int(PQnfields(result))
238240
let numRows = Int(PQntuples(result))
239241

240-
// Fetch column names
242+
// Fetch column names and types
241243
var columns: [String] = []
244+
var columnOids: [UInt32] = []
242245
columns.reserveCapacity(numFields)
246+
columnOids.reserveCapacity(numFields)
243247

244248
for i in 0..<numFields {
249+
// Extract column name
245250
if let namePtr = PQfname(result, Int32(i)) {
246251
let cStr = String(cString: namePtr)
247252
columns.append(String(cStr.unicodeScalars.map { Character($0) }))
248253
} else {
249254
columns.append("column_\(i)")
250255
}
256+
257+
// Extract column type Oid (NEW)
258+
let oid = PQftype(result, Int32(i))
259+
columnOids.append(UInt32(oid))
251260
}
252261

253262
// Fetch all rows
@@ -287,6 +296,7 @@ final class LibPQConnection: @unchecked Sendable {
287296

288297
return LibPQQueryResult(
289298
columns: columns,
299+
columnOids: columnOids,
290300
rows: rows,
291301
affectedRows: numRows,
292302
commandTag: getCommandTag(from: result)

TablePro/Core/Database/MariaDBConnection.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ struct MariaDBError: Error, LocalizedError {
3737
/// Result from a MySQL query execution
3838
struct MariaDBQueryResult {
3939
let columns: [String]
40+
let columnTypes: [UInt32] // NEW: MySQL field type for each column
4041
let rows: [[String?]]
4142
let affectedRows: UInt64
4243
let insertId: UInt64
@@ -301,6 +302,7 @@ final class MariaDBConnection: @unchecked Sendable {
301302
let insertId = mysql_insert_id(mysql)
302303
return MariaDBQueryResult(
303304
columns: [],
305+
columnTypes: [],
304306
rows: [],
305307
affectedRows: affected,
306308
insertId: insertId
@@ -314,18 +316,23 @@ final class MariaDBConnection: @unchecked Sendable {
314316
// Fetch column metadata
315317
let numFields = Int(mysql_num_fields(resultPtr))
316318
var columns: [String] = []
319+
var columnTypes: [UInt32] = [] // NEW: Store column types
317320
columns.reserveCapacity(numFields)
321+
columnTypes.reserveCapacity(numFields)
318322

319323
if let fields = mysql_fetch_fields(resultPtr) {
320324
for i in 0..<numFields {
321325
let field = fields[i]
326+
// Extract column name
322327
if let namePtr = field.name {
323328
// Create completely independent copy of column name
324329
let cStr = String(cString: namePtr)
325330
columns.append(String(cStr.unicodeScalars.map { Character($0) }))
326331
} else {
327332
columns.append("column_\(i)")
328333
}
334+
// Extract column type (NEW)
335+
columnTypes.append(field.type.rawValue)
329336
}
330337
}
331338

@@ -375,6 +382,7 @@ final class MariaDBConnection: @unchecked Sendable {
375382

376383
return MariaDBQueryResult(
377384
columns: columns,
385+
columnTypes: columnTypes,
378386
rows: rows,
379387
affectedRows: UInt64(rows.count),
380388
insertId: 0

TablePro/Core/Database/MySQLDriver.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ final class MySQLDriver: DatabaseDriver {
100100
let columns = try await fetchColumnNames(for: tableName)
101101
return QueryResult(
102102
columns: columns,
103+
columnTypes: Array(repeating: .text, count: columns.count), // Default to text for empty results
103104
rows: [],
104105
rowsAffected: Int(result.affectedRows),
105106
executionTime: Date().timeIntervalSince(startTime),
@@ -108,8 +109,16 @@ final class MySQLDriver: DatabaseDriver {
108109
}
109110
}
110111

112+
// Convert MySQL column types to ColumnType enum
113+
let columnTypes = result.columnTypes.enumerated().map { index, mysqlType in
114+
// Also check field length for boolean detection (TINYINT(1))
115+
// Note: We don't have length info here, so we use just the type
116+
ColumnType(fromMySQLType: mysqlType)
117+
}
118+
111119
return QueryResult(
112120
columns: result.columns,
121+
columnTypes: columnTypes,
113122
rows: result.rows,
114123
rowsAffected: Int(result.affectedRows),
115124
executionTime: Date().timeIntervalSince(startTime),

TablePro/Core/Database/PostgreSQLDriver.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,15 @@ final class PostgreSQLDriver: DatabaseDriver {
7272

7373
do {
7474
let result = try await pqConn.executeQuery(query)
75+
76+
// Convert PostgreSQL Oids to ColumnType enum
77+
let columnTypes = result.columnOids.map { oid in
78+
ColumnType(fromPostgreSQLOid: oid)
79+
}
7580

7681
return QueryResult(
7782
columns: result.columns,
83+
columnTypes: columnTypes,
7884
rows: result.rows,
7985
rowsAffected: result.affectedRows,
8086
executionTime: Date().timeIntervalSince(startTime),

TablePro/Core/Database/SQLiteDriver.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,26 @@ final class SQLiteDriver: DatabaseDriver {
8888
// Get column info
8989
let columnCount = sqlite3_column_count(statement)
9090
var columns: [String] = []
91+
var columnTypes: [ColumnType] = []
9192

9293
for i in 0..<columnCount {
94+
// Extract column name
9395
if let name = sqlite3_column_name(statement, i) {
9496
columns.append(String(cString: name))
9597
} else {
9698
columns.append("column_\(i)")
9799
}
100+
101+
// Extract column type from declared type
102+
// sqlite3_column_decltype returns the declared type (e.g., "INTEGER", "TEXT", "DATETIME")
103+
let declaredType: String? = {
104+
if let typePtr = sqlite3_column_decltype(statement, i) {
105+
return String(cString: typePtr)
106+
}
107+
return nil
108+
}()
109+
110+
columnTypes.append(ColumnType(fromSQLiteType: declaredType))
98111
}
99112

100113
// Execute and fetch rows
@@ -126,6 +139,7 @@ final class SQLiteDriver: DatabaseDriver {
126139

127140
return QueryResult(
128141
columns: columns,
142+
columnTypes: columnTypes,
129143
rows: rows,
130144
rowsAffected: rowsAffected,
131145
executionTime: executionTime,

0 commit comments

Comments
 (0)