@@ -164,7 +164,8 @@ 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
167+ error_message TEXT,
168+ is_synced INTEGER DEFAULT 0
168169 );
169170 """
170171
@@ -205,6 +206,7 @@ final class QueryHistoryStorage {
205206
206207 // Execute all table creation statements
207208 execute ( historyTable)
209+ migrateAddIsSyncedColumn ( )
208210 execute ( ftsTable)
209211 execute ( ftsInsertTrigger)
210212 execute ( ftsDeleteTrigger)
@@ -548,6 +550,80 @@ final class QueryHistoryStorage {
548550 }
549551 }
550552
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+
551627 // MARK: - Parsing Helpers
552628
553629 private func parseHistoryEntry( from statement: OpaquePointer ? ) -> QueryHistoryEntry ? {
0 commit comments