Skip to content

Commit bebbefe

Browse files
authored
refactor: remove query history sync from iCloud Sync (#502)
1 parent f5afbe2 commit bebbefe

File tree

8 files changed

+9
-268
lines changed

8 files changed

+9
-268
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Confirmation dialogs for deep link queries, connection imports, and pre-connect scripts
1616
- JSON fields in Row Details sidebar now display in a scrollable monospaced text area
1717

18+
### Changed
19+
20+
- Removed query history sync from iCloud Sync (connections, groups, settings, and SSH profiles still sync)
21+
1822
### Fixed
1923

2024
- SQL editor not auto-focused on new tab and cursor missing after tab switch

TablePro/Core/Storage/QueryHistoryStorage.swift

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,7 @@ final class QueryHistoryStorage {
164164
execution_time REAL NOT NULL,
165165
row_count INTEGER NOT NULL,
166166
was_successful INTEGER NOT NULL,
167-
error_message TEXT,
168-
is_synced INTEGER DEFAULT 0
167+
error_message TEXT
169168
);
170169
"""
171170

@@ -206,7 +205,6 @@ final class QueryHistoryStorage {
206205

207206
// Execute all table creation statements
208207
execute(historyTable)
209-
migrateAddIsSyncedColumn()
210208
execute(ftsTable)
211209
execute(ftsInsertTrigger)
212210
execute(ftsDeleteTrigger)
@@ -550,80 +548,6 @@ final class QueryHistoryStorage {
550548
}
551549
}
552550

553-
// MARK: - Sync Support
554-
555-
/// Migration: add is_synced column if the table was created before sync support
556-
private func migrateAddIsSyncedColumn() {
557-
// Check if column already exists by querying table info
558-
let sql = "PRAGMA table_info(history);"
559-
var statement: OpaquePointer?
560-
guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else { return }
561-
defer { sqlite3_finalize(statement) }
562-
563-
var hasIsSynced = false
564-
while sqlite3_step(statement) == SQLITE_ROW {
565-
if let name = sqlite3_column_text(statement, 1).map({ String(cString: $0) }),
566-
name == "is_synced" {
567-
hasIsSynced = true
568-
break
569-
}
570-
}
571-
572-
if !hasIsSynced {
573-
execute("ALTER TABLE history ADD COLUMN is_synced INTEGER DEFAULT 0;")
574-
Self.logger.info("Migrated history table: added is_synced column")
575-
}
576-
}
577-
578-
/// Mark history entries as synced
579-
func markHistoryEntriesSynced(ids: [String]) async {
580-
guard !ids.isEmpty else { return }
581-
await performDatabaseWork { [weak self] in
582-
guard let self else { return }
583-
584-
let placeholders = ids.map { _ in "?" }.joined(separator: ", ")
585-
let sql = "UPDATE history SET is_synced = 1 WHERE id IN (\(placeholders));"
586-
587-
var statement: OpaquePointer?
588-
guard sqlite3_prepare_v2(self.db, sql, -1, &statement, nil) == SQLITE_OK else { return }
589-
defer { sqlite3_finalize(statement) }
590-
591-
let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
592-
for (index, id) in ids.enumerated() {
593-
sqlite3_bind_text(statement, Int32(index + 1), id, -1, SQLITE_TRANSIENT)
594-
}
595-
sqlite3_step(statement)
596-
}
597-
}
598-
599-
/// Fetch unsynced history entries
600-
func unsyncedHistoryEntries(limit: Int) async -> [QueryHistoryEntry] {
601-
await performDatabaseWork { [weak self] in
602-
guard let self else { return [] }
603-
604-
let sql = """
605-
SELECT id, query, connection_id, database_name, executed_at, execution_time, row_count, was_successful, error_message
606-
FROM history WHERE is_synced = 0 ORDER BY executed_at DESC LIMIT ?;
607-
"""
608-
609-
var statement: OpaquePointer?
610-
guard sqlite3_prepare_v2(self.db, sql, -1, &statement, nil) == SQLITE_OK else {
611-
return []
612-
}
613-
defer { sqlite3_finalize(statement) }
614-
615-
sqlite3_bind_int(statement, 1, Int32(limit))
616-
617-
var entries: [QueryHistoryEntry] = []
618-
while sqlite3_step(statement) == SQLITE_ROW {
619-
if let entry = self.parseHistoryEntry(from: statement) {
620-
entries.append(entry)
621-
}
622-
}
623-
return entries
624-
}
625-
}
626-
627551
// MARK: - Parsing Helpers
628552

629553
private func parseHistoryEntry(from statement: OpaquePointer?) -> QueryHistoryEntry? {

TablePro/Core/Sync/SyncCoordinator.swift

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -267,28 +267,6 @@ final class SyncCoordinator {
267267
collectDirtySSHProfiles(into: &recordsToSave, deletions: &recordIDsToDelete, zoneID: zoneID)
268268
}
269269

270-
// Collect unsynced query history
271-
if settings.syncQueryHistory {
272-
let limit = settings.historySyncLimit.limit ?? Int.max
273-
let unsyncedEntries = await QueryHistoryStorage.shared.unsyncedHistoryEntries(limit: limit)
274-
for entry in unsyncedEntries {
275-
recordsToSave.append(
276-
SyncRecordMapper.toCKRecord(
277-
entryId: entry.id.uuidString,
278-
query: entry.query,
279-
connectionId: entry.connectionId.uuidString,
280-
databaseName: entry.databaseName,
281-
executedAt: entry.executedAt,
282-
executionTime: entry.executionTime,
283-
rowCount: Int64(entry.rowCount),
284-
wasSuccessful: entry.wasSuccessful,
285-
errorMessage: entry.errorMessage,
286-
in: zoneID
287-
)
288-
)
289-
}
290-
}
291-
292270
// Collect dirty settings
293271
if settings.syncSettings {
294272
let dirtySettingsIds = changeTracker.dirtyRecords(for: .settings)
@@ -320,9 +298,6 @@ final class SyncCoordinator {
320298
if settings.syncSettings {
321299
changeTracker.clearAllDirty(.settings)
322300
}
323-
if settings.syncQueryHistory {
324-
changeTracker.clearAllDirty(.queryHistory)
325-
}
326301

327302
// Clear tombstones only for types that were actually pushed
328303
if settings.syncConnections {
@@ -348,19 +323,6 @@ final class SyncCoordinator {
348323
metadataStorage.removeTombstone(type: .settings, id: tombstone.id)
349324
}
350325
}
351-
if settings.syncQueryHistory {
352-
for tombstone in metadataStorage.tombstones(for: .queryHistory) {
353-
metadataStorage.removeTombstone(type: .queryHistory, id: tombstone.id)
354-
}
355-
356-
// Mark pushed history entries as synced in local storage
357-
let syncedIds = recordsToSave
358-
.filter { $0.recordType == SyncRecordType.queryHistory.rawValue }
359-
.compactMap { $0["entryId"] as? String }
360-
if !syncedIds.isEmpty {
361-
await QueryHistoryStorage.shared.markHistoryEntriesSynced(ids: syncedIds)
362-
}
363-
}
364326

365327
Self.logger.info("Push completed: \(recordsToSave.count) saved, \(recordIDsToDelete.count) deleted")
366328
} catch let error as CKError where error.code == .serverRecordChanged {
@@ -441,8 +403,6 @@ final class SyncCoordinator {
441403
applyRemoteSSHProfile(record)
442404
case SyncRecordType.settings.rawValue where settings.syncSettings:
443405
applyRemoteSettings(record)
444-
case SyncRecordType.queryHistory.rawValue where settings.syncQueryHistory:
445-
applyRemoteQueryHistory(record)
446406
default:
447407
break
448408
}
@@ -536,38 +496,6 @@ final class SyncCoordinator {
536496
applySettingsData(data, for: category)
537497
}
538498

539-
private func applyRemoteQueryHistory(_ record: CKRecord) {
540-
guard let entryIdString = record["entryId"] as? String,
541-
let entryId = UUID(uuidString: entryIdString),
542-
let query = record["query"] as? String,
543-
let executedAt = record["executedAt"] as? Date
544-
else { return }
545-
546-
let connectionId = (record["connectionId"] as? String).flatMap { UUID(uuidString: $0) } ?? UUID()
547-
let databaseName = record["databaseName"] as? String ?? ""
548-
let executionTime = record["executionTime"] as? Double ?? 0
549-
let rowCount = (record["rowCount"] as? Int64).map { Int($0) } ?? 0
550-
let wasSuccessful = (record["wasSuccessful"] as? Int64 ?? 1) != 0
551-
let errorMessage = record["errorMessage"] as? String
552-
553-
let entry = QueryHistoryEntry(
554-
id: entryId,
555-
query: query,
556-
connectionId: connectionId,
557-
databaseName: databaseName,
558-
executedAt: executedAt,
559-
executionTime: executionTime,
560-
rowCount: rowCount,
561-
wasSuccessful: wasSuccessful,
562-
errorMessage: errorMessage
563-
)
564-
565-
Task {
566-
_ = await QueryHistoryStorage.shared.addHistory(entry)
567-
await QueryHistoryStorage.shared.markHistoryEntriesSynced(ids: [entryIdString])
568-
}
569-
}
570-
571499
private func applyRemoteDeletion(_ recordID: CKRecord.ID) {
572500
let recordName = recordID.recordName
573501

@@ -714,7 +642,6 @@ final class SyncCoordinator {
714642
case SyncRecordType.group.rawValue: syncRecordType = .group
715643
case SyncRecordType.tag.rawValue: syncRecordType = .tag
716644
case SyncRecordType.settings.rawValue: syncRecordType = .settings
717-
case SyncRecordType.queryHistory.rawValue: syncRecordType = .queryHistory
718645
case SyncRecordType.sshProfile.rawValue: syncRecordType = .sshProfile
719646
default: continue
720647
}

TablePro/Core/Sync/SyncRecordMapper.swift

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ enum SyncRecordType: String, CaseIterable {
1515
case group = "ConnectionGroup"
1616
case tag = "ConnectionTag"
1717
case settings = "AppSettings"
18-
case queryHistory = "QueryHistory"
1918
case favorite = "SQLFavorite"
2019
case favoriteFolder = "SQLFavoriteFolder"
2120
case sshProfile = "SSHProfile"
@@ -30,9 +29,6 @@ struct SyncRecordMapper {
3029
/// Current schema version stamped on every record
3130
static let schemaVersion: Int64 = 1
3231

33-
/// Maximum query text length for CloudKit (10KB in UTF-8)
34-
static let maxQueryLength = 10_240
35-
3632
// MARK: - Record Name Helpers
3733

3834
static func recordID(type: SyncRecordType, id: String, in zone: CKRecordZone.ID) -> CKRecord.ID {
@@ -42,7 +38,6 @@ struct SyncRecordMapper {
4238
case .group: recordName = "Group_\(id)"
4339
case .tag: recordName = "Tag_\(id)"
4440
case .settings: recordName = "Settings_\(id)"
45-
case .queryHistory: recordName = "History_\(id)"
4641
case .favorite: recordName = "Favorite_\(id)"
4742
case .favoriteFolder: recordName = "FavoriteFolder_\(id)"
4843
case .sshProfile: recordName = "SSHProfile_\(id)"
@@ -275,58 +270,6 @@ struct SyncRecordMapper {
275270
record["settingsJson"] as? Data
276271
}
277272

278-
// MARK: - Query History
279-
280-
static func toCKRecord(
281-
entryId: String,
282-
query: String,
283-
connectionId: String?,
284-
databaseName: String?,
285-
executedAt: Date,
286-
executionTime: Double,
287-
rowCount: Int64,
288-
wasSuccessful: Bool,
289-
errorMessage: String?,
290-
in zone: CKRecordZone.ID
291-
) -> CKRecord {
292-
let recordID = recordID(type: .queryHistory, id: entryId, in: zone)
293-
let record = CKRecord(
294-
recordType: SyncRecordType.queryHistory.rawValue,
295-
recordID: recordID
296-
)
297-
298-
record["entryId"] = entryId as CKRecordValue
299-
// Cap query text at maxQueryLength bytes
300-
let cappedQuery: String
301-
let queryData = Data(query.utf8)
302-
if queryData.count > maxQueryLength {
303-
cappedQuery = String(
304-
data: queryData.prefix(maxQueryLength),
305-
encoding: .utf8
306-
) ?? String(query.prefix(maxQueryLength / 4))
307-
} else {
308-
cappedQuery = query
309-
}
310-
record["query"] = cappedQuery as CKRecordValue
311-
record["executedAt"] = executedAt as CKRecordValue
312-
record["executionTime"] = executionTime as CKRecordValue
313-
record["rowCount"] = rowCount as CKRecordValue
314-
record["wasSuccessful"] = Int64(wasSuccessful ? 1 : 0) as CKRecordValue
315-
record["schemaVersion"] = schemaVersion as CKRecordValue
316-
317-
if let connectionId {
318-
record["connectionId"] = connectionId as CKRecordValue
319-
}
320-
if let databaseName {
321-
record["databaseName"] = databaseName as CKRecordValue
322-
}
323-
if let errorMessage {
324-
record["errorMessage"] = errorMessage as CKRecordValue
325-
}
326-
327-
return record
328-
}
329-
330273
// MARK: - SSH Profile
331274

332275
static func toCKRecord(_ profile: SSHProfile, in zone: CKRecordZone.ID) -> CKRecord {

TablePro/Models/Settings/SyncSettings.swift

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ struct SyncSettings: Codable, Equatable {
1313
var syncConnections: Bool
1414
var syncGroupsAndTags: Bool
1515
var syncSettings: Bool
16-
var syncQueryHistory: Bool
17-
var historySyncLimit: HistorySyncLimit
1816
var syncPasswords: Bool
1917
var syncSSHProfiles: Bool
2018

@@ -23,17 +21,13 @@ struct SyncSettings: Codable, Equatable {
2321
syncConnections: Bool,
2422
syncGroupsAndTags: Bool,
2523
syncSettings: Bool,
26-
syncQueryHistory: Bool,
27-
historySyncLimit: HistorySyncLimit,
2824
syncPasswords: Bool = false,
2925
syncSSHProfiles: Bool = true
3026
) {
3127
self.enabled = enabled
3228
self.syncConnections = syncConnections
3329
self.syncGroupsAndTags = syncGroupsAndTags
3430
self.syncSettings = syncSettings
35-
self.syncQueryHistory = syncQueryHistory
36-
self.historySyncLimit = historySyncLimit
3731
self.syncPasswords = syncPasswords
3832
self.syncSSHProfiles = syncSSHProfiles
3933
}
@@ -44,8 +38,6 @@ struct SyncSettings: Codable, Equatable {
4438
syncConnections = try container.decode(Bool.self, forKey: .syncConnections)
4539
syncGroupsAndTags = try container.decode(Bool.self, forKey: .syncGroupsAndTags)
4640
syncSettings = try container.decode(Bool.self, forKey: .syncSettings)
47-
syncQueryHistory = try container.decode(Bool.self, forKey: .syncQueryHistory)
48-
historySyncLimit = try container.decode(HistorySyncLimit.self, forKey: .historySyncLimit)
4941
syncPasswords = try container.decodeIfPresent(Bool.self, forKey: .syncPasswords) ?? false
5042
syncSSHProfiles = try container.decodeIfPresent(Bool.self, forKey: .syncSSHProfiles) ?? true
5143
}
@@ -55,35 +47,7 @@ struct SyncSettings: Codable, Equatable {
5547
syncConnections: true,
5648
syncGroupsAndTags: true,
5749
syncSettings: true,
58-
syncQueryHistory: true,
59-
historySyncLimit: .entries500,
6050
syncPasswords: false,
6151
syncSSHProfiles: true
6252
)
6353
}
64-
65-
/// Maximum number of query history entries to sync
66-
enum HistorySyncLimit: String, Codable, CaseIterable {
67-
case entries100 = "100"
68-
case entries500 = "500"
69-
case entries1000 = "1000"
70-
case unlimited = "unlimited"
71-
72-
var displayName: String {
73-
switch self {
74-
case .entries100: return "100"
75-
case .entries500: return "500"
76-
case .entries1000: return "1,000"
77-
case .unlimited: return String(localized: "Unlimited")
78-
}
79-
}
80-
81-
var limit: Int? {
82-
switch self {
83-
case .entries100: return 100
84-
case .entries500: return 500
85-
case .entries1000: return 1_000
86-
case .unlimited: return nil
87-
}
88-
}
89-
}

TablePro/Views/Components/ConflictResolutionView.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,6 @@ struct ConflictResolutionView: View {
130130
if let color = record["color"] as? String {
131131
fieldRow(label: "Color", value: color)
132132
}
133-
case .queryHistory:
134-
if let query = record["query"] as? String {
135-
let nsQuery = query as NSString
136-
let preview = nsQuery.length > 80
137-
? nsQuery.substring(to: 80) + "..."
138-
: query
139-
fieldRow(label: "Query", value: preview)
140-
}
141133
case .favorite, .favoriteFolder:
142134
if let name = record["name"] as? String {
143135
fieldRow(label: String(localized: "Name"), value: name)

0 commit comments

Comments
 (0)