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

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