You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							211 lines
						
					
					
						
							6.4 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							211 lines
						
					
					
						
							6.4 KiB
						
					
					
				
								/**
							 | 
						|
								 * DailyNotificationDatabase.swift
							 | 
						|
								 * 
							 | 
						|
								 * iOS SQLite database management for daily notifications
							 | 
						|
								 * 
							 | 
						|
								 * @author Matthew Raymer
							 | 
						|
								 * @version 1.0.0
							 | 
						|
								 */
							 | 
						|
								
							 | 
						|
								import Foundation
							 | 
						|
								import SQLite3
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * SQLite database manager for daily notifications on iOS
							 | 
						|
								 * 
							 | 
						|
								 * This class manages the SQLite database with the three-table schema:
							 | 
						|
								 * - notif_contents: keep history, fast newest-first reads
							 | 
						|
								 * - notif_deliveries: track many deliveries per slot/time
							 | 
						|
								 * - notif_config: generic configuration KV
							 | 
						|
								 */
							 | 
						|
								class DailyNotificationDatabase {
							 | 
						|
								    
							 | 
						|
								    // MARK: - Constants
							 | 
						|
								    
							 | 
						|
								    private static let TAG = "DailyNotificationDatabase"
							 | 
						|
								    
							 | 
						|
								    // Table names
							 | 
						|
								    static let TABLE_NOTIF_CONTENTS = "notif_contents"
							 | 
						|
								    static let TABLE_NOTIF_DELIVERIES = "notif_deliveries"
							 | 
						|
								    static let TABLE_NOTIF_CONFIG = "notif_config"
							 | 
						|
								    
							 | 
						|
								    // Column names
							 | 
						|
								    static let COL_CONTENTS_ID = "id"
							 | 
						|
								    static let COL_CONTENTS_SLOT_ID = "slot_id"
							 | 
						|
								    static let COL_CONTENTS_PAYLOAD_JSON = "payload_json"
							 | 
						|
								    static let COL_CONTENTS_FETCHED_AT = "fetched_at"
							 | 
						|
								    static let COL_CONTENTS_ETAG = "etag"
							 | 
						|
								    
							 | 
						|
								    static let COL_DELIVERIES_ID = "id"
							 | 
						|
								    static let COL_DELIVERIES_SLOT_ID = "slot_id"
							 | 
						|
								    static let COL_DELIVERIES_FIRE_AT = "fire_at"
							 | 
						|
								    static let COL_DELIVERIES_DELIVERED_AT = "delivered_at"
							 | 
						|
								    static let COL_DELIVERIES_STATUS = "status"
							 | 
						|
								    static let COL_DELIVERIES_ERROR_CODE = "error_code"
							 | 
						|
								    static let COL_DELIVERIES_ERROR_MESSAGE = "error_message"
							 | 
						|
								    
							 | 
						|
								    static let COL_CONFIG_K = "k"
							 | 
						|
								    static let COL_CONFIG_V = "v"
							 | 
						|
								    
							 | 
						|
								    // Status values
							 | 
						|
								    static let STATUS_SCHEDULED = "scheduled"
							 | 
						|
								    static let STATUS_SHOWN = "shown"
							 | 
						|
								    static let STATUS_ERROR = "error"
							 | 
						|
								    static let STATUS_CANCELED = "canceled"
							 | 
						|
								    
							 | 
						|
								    // MARK: - Properties
							 | 
						|
								    
							 | 
						|
								    private var db: OpaquePointer?
							 | 
						|
								    private let path: String
							 | 
						|
								    
							 | 
						|
								    // MARK: - Initialization
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Initialize database with path
							 | 
						|
								     * 
							 | 
						|
								     * @param path Database file path
							 | 
						|
								     */
							 | 
						|
								    init(path: String) {
							 | 
						|
								        self.path = path
							 | 
						|
								        openDatabase()
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Open database connection
							 | 
						|
								     */
							 | 
						|
								    private func openDatabase() {
							 | 
						|
								        if sqlite3_open(path, &db) == SQLITE_OK {
							 | 
						|
								            print("\(Self.TAG): Database opened successfully at \(path)")
							 | 
						|
								            createTables()
							 | 
						|
								            configureDatabase()
							 | 
						|
								        } else {
							 | 
						|
								            print("\(Self.TAG): Error opening database: \(String(cString: sqlite3_errmsg(db)))")
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Create database tables
							 | 
						|
								     */
							 | 
						|
								    private func createTables() {
							 | 
						|
								        // Create notif_contents table
							 | 
						|
								        let createContentsTable = """
							 | 
						|
								            CREATE TABLE IF NOT EXISTS \(Self.TABLE_NOTIF_CONTENTS)(
							 | 
						|
								                \(Self.COL_CONTENTS_ID) INTEGER PRIMARY KEY AUTOINCREMENT,
							 | 
						|
								                \(Self.COL_CONTENTS_SLOT_ID) TEXT NOT NULL,
							 | 
						|
								                \(Self.COL_CONTENTS_PAYLOAD_JSON) TEXT NOT NULL,
							 | 
						|
								                \(Self.COL_CONTENTS_FETCHED_AT) INTEGER NOT NULL,
							 | 
						|
								                \(Self.COL_CONTENTS_ETAG) TEXT,
							 | 
						|
								                UNIQUE(\(Self.COL_CONTENTS_SLOT_ID), \(Self.COL_CONTENTS_FETCHED_AT))
							 | 
						|
								            );
							 | 
						|
								        """
							 | 
						|
								        
							 | 
						|
								        // Create notif_deliveries table
							 | 
						|
								        let createDeliveriesTable = """
							 | 
						|
								            CREATE TABLE IF NOT EXISTS \(Self.TABLE_NOTIF_DELIVERIES)(
							 | 
						|
								                \(Self.COL_DELIVERIES_ID) INTEGER PRIMARY KEY AUTOINCREMENT,
							 | 
						|
								                \(Self.COL_DELIVERIES_SLOT_ID) TEXT NOT NULL,
							 | 
						|
								                \(Self.COL_DELIVERIES_FIRE_AT) INTEGER NOT NULL,
							 | 
						|
								                \(Self.COL_DELIVERIES_DELIVERED_AT) INTEGER,
							 | 
						|
								                \(Self.COL_DELIVERIES_STATUS) TEXT NOT NULL DEFAULT '\(Self.STATUS_SCHEDULED)',
							 | 
						|
								                \(Self.COL_DELIVERIES_ERROR_CODE) TEXT,
							 | 
						|
								                \(Self.COL_DELIVERIES_ERROR_MESSAGE) TEXT
							 | 
						|
								            );
							 | 
						|
								        """
							 | 
						|
								        
							 | 
						|
								        // Create notif_config table
							 | 
						|
								        let createConfigTable = """
							 | 
						|
								            CREATE TABLE IF NOT EXISTS \(Self.TABLE_NOTIF_CONFIG)(
							 | 
						|
								                \(Self.COL_CONFIG_K) TEXT PRIMARY KEY,
							 | 
						|
								                \(Self.COL_CONFIG_V) TEXT NOT NULL
							 | 
						|
								            );
							 | 
						|
								        """
							 | 
						|
								        
							 | 
						|
								        // Create indexes
							 | 
						|
								        let createContentsIndex = """
							 | 
						|
								            CREATE INDEX IF NOT EXISTS notif_idx_contents_slot_time 
							 | 
						|
								            ON \(Self.TABLE_NOTIF_CONTENTS)(\(Self.COL_CONTENTS_SLOT_ID), \(Self.COL_CONTENTS_FETCHED_AT) DESC);
							 | 
						|
								        """
							 | 
						|
								        
							 | 
						|
								        // Execute table creation
							 | 
						|
								        executeSQL(createContentsTable)
							 | 
						|
								        executeSQL(createDeliveriesTable)
							 | 
						|
								        executeSQL(createConfigTable)
							 | 
						|
								        executeSQL(createContentsIndex)
							 | 
						|
								        
							 | 
						|
								        print("\(Self.TAG): Database tables created successfully")
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Configure database settings
							 | 
						|
								     */
							 | 
						|
								    private func configureDatabase() {
							 | 
						|
								        // Enable WAL mode
							 | 
						|
								        executeSQL("PRAGMA journal_mode=WAL")
							 | 
						|
								        
							 | 
						|
								        // Set synchronous mode
							 | 
						|
								        executeSQL("PRAGMA synchronous=NORMAL")
							 | 
						|
								        
							 | 
						|
								        // Set busy timeout
							 | 
						|
								        executeSQL("PRAGMA busy_timeout=5000")
							 | 
						|
								        
							 | 
						|
								        // Enable foreign keys
							 | 
						|
								        executeSQL("PRAGMA foreign_keys=ON")
							 | 
						|
								        
							 | 
						|
								        // Set user version
							 | 
						|
								        executeSQL("PRAGMA user_version=1")
							 | 
						|
								        
							 | 
						|
								        print("\(Self.TAG): Database configured successfully")
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Execute SQL statement
							 | 
						|
								     * 
							 | 
						|
								     * @param sql SQL statement to execute
							 | 
						|
								     */
							 | 
						|
								    private func executeSQL(_ sql: String) {
							 | 
						|
								        var statement: OpaquePointer?
							 | 
						|
								        
							 | 
						|
								        if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK {
							 | 
						|
								            if sqlite3_step(statement) == SQLITE_DONE {
							 | 
						|
								                print("\(Self.TAG): SQL executed successfully: \(sql)")
							 | 
						|
								            } else {
							 | 
						|
								                print("\(Self.TAG): SQL execution failed: \(String(cString: sqlite3_errmsg(db)))")
							 | 
						|
								            }
							 | 
						|
								        } else {
							 | 
						|
								            print("\(Self.TAG): SQL preparation failed: \(String(cString: sqlite3_errmsg(db)))")
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        sqlite3_finalize(statement)
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    // MARK: - Public Methods
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Close database connection
							 | 
						|
								     */
							 | 
						|
								    func close() {
							 | 
						|
								        if sqlite3_close(db) == SQLITE_OK {
							 | 
						|
								            print("\(Self.TAG): Database closed successfully")
							 | 
						|
								        } else {
							 | 
						|
								            print("\(Self.TAG): Error closing database: \(String(cString: sqlite3_errmsg(db)))")
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Get database path
							 | 
						|
								     * 
							 | 
						|
								     * @return Database file path
							 | 
						|
								     */
							 | 
						|
								    func getPath() -> String {
							 | 
						|
								        return path
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Check if database is open
							 | 
						|
								     * 
							 | 
						|
								     * @return true if database is open
							 | 
						|
								     */
							 | 
						|
								    func isOpen() -> Bool {
							 | 
						|
								        return db != nil
							 | 
						|
								    }
							 | 
						|
								}
							 | 
						|
								
							 |