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