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

### Added

- Per-connection "Local only" option to exclude individual connections from iCloud sync
- Filter operator picker shows SQL symbols alongside names for quick visual recognition
- SQL autocomplete now suggests column names before a FROM clause is written, using all cached schema columns as fallback
- Eager column cache warming after schema load for faster autocomplete
Expand Down
4 changes: 3 additions & 1 deletion TablePro/Core/Services/Export/ConnectionExportService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ enum ConnectionExportService {
aiPolicy: aiPolicy,
additionalFields: additionalFields,
redisDatabase: connection.redisDatabase,
startupCommands: connection.startupCommands
startupCommands: connection.startupCommands,
localOnly: connection.localOnly ? true : nil
)

exportableConnections.append(exportable)
Expand Down Expand Up @@ -679,6 +680,7 @@ enum ConnectionExportService {
aiPolicy: exportable.aiPolicy.flatMap { AIConnectionPolicy(rawValue: $0) },
redisDatabase: exportable.redisDatabase,
startupCommands: exportable.startupCommands,
localOnly: exportable.localOnly ?? false,
additionalFields: exportable.additionalFields
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ struct DBeaverImporter: ForeignAppImporter {
aiPolicy: nil,
additionalFields: nil,
redisDatabase: nil,
startupCommands: nil
startupCommands: nil,
localOnly: nil
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ struct SequelAceImporter: ForeignAppImporter {
aiPolicy: nil,
additionalFields: nil,
redisDatabase: nil,
startupCommands: nil
startupCommands: nil,
localOnly: nil
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ struct TablePlusImporter: ForeignAppImporter {
aiPolicy: nil,
additionalFields: nil,
redisDatabase: nil,
startupCommands: nil
startupCommands: nil,
localOnly: nil
)
}

Expand Down
29 changes: 24 additions & 5 deletions TablePro/Core/Storage/ConnectionStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ final class ConnectionStorage {
var connections = loadConnections()
connections.append(connection)
saveConnections(connections)
SyncChangeTracker.shared.markDirty(.connection, id: connection.id.uuidString)
if !connection.localOnly {
SyncChangeTracker.shared.markDirty(.connection, id: connection.id.uuidString)
}

if let password = password, !password.isEmpty {
savePassword(password, for: connection.id)
Expand All @@ -125,7 +127,9 @@ final class ConnectionStorage {
if let index = connections.firstIndex(where: { $0.id == connection.id }) {
connections[index] = connection
saveConnections(connections)
SyncChangeTracker.shared.markDirty(.connection, id: connection.id.uuidString)
if !connection.localOnly {
SyncChangeTracker.shared.markDirty(.connection, id: connection.id.uuidString)
}

if let password = password {
if password.isEmpty {
Expand All @@ -139,7 +143,9 @@ final class ConnectionStorage {

/// Delete a connection
func deleteConnection(_ connection: DatabaseConnection) {
SyncChangeTracker.shared.markDeleted(.connection, id: connection.id.uuidString)
if !connection.localOnly {
SyncChangeTracker.shared.markDeleted(.connection, id: connection.id.uuidString)
}
var connections = loadConnections()
connections.removeAll { $0.id == connection.id }
saveConnections(connections)
Expand All @@ -157,7 +163,7 @@ final class ConnectionStorage {

/// Batch-delete multiple connections and clean up their Keychain entries
func deleteConnections(_ connectionsToDelete: [DatabaseConnection]) {
for conn in connectionsToDelete {
for conn in connectionsToDelete where !conn.localOnly {
SyncChangeTracker.shared.markDeleted(.connection, id: conn.id.uuidString)
}
let idsToDelete = Set(connectionsToDelete.map(\.id))
Expand Down Expand Up @@ -202,14 +208,17 @@ final class ConnectionStorage {
redisDatabase: connection.redisDatabase,
startupCommands: connection.startupCommands,
sortOrder: connection.sortOrder,
localOnly: connection.localOnly,
additionalFields: connection.additionalFields.isEmpty ? nil : connection.additionalFields
)

// Save the duplicate connection
var connections = loadConnections()
connections.append(duplicate)
saveConnections(connections)
SyncChangeTracker.shared.markDirty(.connection, id: duplicate.id.uuidString)
if !duplicate.localOnly {
SyncChangeTracker.shared.markDirty(.connection, id: duplicate.id.uuidString)
}

// Copy all passwords from source to duplicate (skip DB password in prompt mode)
if !connection.promptForPassword, let password = loadPassword(for: connection.id) {
Expand Down Expand Up @@ -433,6 +442,9 @@ private struct StoredConnection: Codable {
// Sort order for sync
let sortOrder: Int

// Local-only (excluded from iCloud sync)
let localOnly: Bool

// TOTP configuration
let totpMode: String
let totpAlgorithm: String
Expand Down Expand Up @@ -508,6 +520,9 @@ private struct StoredConnection: Codable {
// Sort order
self.sortOrder = connection.sortOrder

// Local-only
self.localOnly = connection.localOnly

// SSH tunnel mode (v2 format preserving jump hosts, profiles, etc.)
self.sshTunnelModeJson = try? JSONEncoder().encode(connection.sshTunnelMode)

Expand All @@ -529,6 +544,7 @@ private struct StoredConnection: Codable {
case mssqlSchema, oracleServiceName, startupCommands, sortOrder
case sshTunnelModeJson
case additionalFields
case localOnly
}

func encode(to encoder: Encoder) throws {
Expand Down Expand Up @@ -567,6 +583,7 @@ private struct StoredConnection: Codable {
try container.encode(sortOrder, forKey: .sortOrder)
try container.encodeIfPresent(sshTunnelModeJson, forKey: .sshTunnelModeJson)
try container.encodeIfPresent(additionalFields, forKey: .additionalFields)
try container.encode(localOnly, forKey: .localOnly)
}

// Custom decoder to handle migration from old format
Expand Down Expand Up @@ -631,6 +648,7 @@ private struct StoredConnection: Codable {
sortOrder = try container.decodeIfPresent(Int.self, forKey: .sortOrder) ?? 0
sshTunnelModeJson = try container.decodeIfPresent(Data.self, forKey: .sshTunnelModeJson)
additionalFields = try container.decodeIfPresent([String: String].self, forKey: .additionalFields)
localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly) ?? false
}

func toConnection() -> DatabaseConnection {
Expand Down Expand Up @@ -716,6 +734,7 @@ private struct StoredConnection: Codable {
redisDatabase: redisDatabase,
startupCommands: startupCommands,
sortOrder: sortOrder,
localOnly: localOnly,
additionalFields: mergedFields
)
}
Expand Down
Loading
Loading