/** * 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 } }