Files
2026-02-26 21:24:20 -05:00

188 lines
6.4 KiB
Swift

import Foundation
import SQLite3
// SQLITE_TRANSIENT tells SQLite to make its own copy of bound text data
private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
final class DatabaseManager {
static let shared = DatabaseManager()
private var db: OpaquePointer?
private init() {
openDatabase()
createTable()
}
deinit {
if db != nil {
sqlite3_close(db)
}
}
// MARK: - Database Setup
private func openDatabase() {
let fileURL = getDatabasePath()
if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
print("Error opening database: \(String(cString: sqlite3_errmsg(db)))")
db = nil
}
}
private func getDatabasePath() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0].appendingPathComponent("todos.db")
}
private func createTable() {
let sql = """
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
category TEXT DEFAULT 'Personal',
priority TEXT DEFAULT 'medium',
created_at INTEGER DEFAULT (strftime('%s','now'))
)
"""
var statement: OpaquePointer?
defer { sqlite3_finalize(statement) }
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK {
if sqlite3_step(statement) != SQLITE_DONE {
print("Error creating table: \(String(cString: sqlite3_errmsg(db)))")
}
} else {
print("Error preparing create table: \(String(cString: sqlite3_errmsg(db)))")
}
}
// MARK: - Helper
private func bindText(_ statement: OpaquePointer?, index: Int32, value: String) {
sqlite3_bind_text(statement, index, value, -1, SQLITE_TRANSIENT)
}
// MARK: - CRUD Operations
func loadTodos() -> [Todo] {
let sql = "SELECT id, title, completed, category, priority, created_at FROM todos ORDER BY created_at DESC"
var statement: OpaquePointer?
defer { sqlite3_finalize(statement) }
var todos: [Todo] = []
if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK {
while sqlite3_step(statement) == SQLITE_ROW {
let id = sqlite3_column_int64(statement, 0)
let title = String(cString: sqlite3_column_text(statement, 1))
let completed = sqlite3_column_int(statement, 2) == 1
let category = String(cString: sqlite3_column_text(statement, 3))
let priorityStr = String(cString: sqlite3_column_text(statement, 4))
let priority = Priority(rawValue: priorityStr) ?? .medium
let createdAt = sqlite3_column_int64(statement, 5)
let todo = Todo(
id: id,
title: title,
completed: completed,
category: category,
priority: priority,
createdAt: createdAt
)
todos.append(todo)
}
} else {
print("Error loading todos: \(String(cString: sqlite3_errmsg(db)))")
}
return todos
}
@discardableResult
func insertTodo(title: String, category: String, priority: Priority) -> Todo? {
let sql = "INSERT INTO todos (title, category, priority, completed) VALUES (?, ?, ?, 0)"
var statement: OpaquePointer?
defer { sqlite3_finalize(statement) }
guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
print("Error preparing insert: \(String(cString: sqlite3_errmsg(db)))")
return nil
}
bindText(statement, index: 1, value: title)
bindText(statement, index: 2, value: category)
bindText(statement, index: 3, value: priority.rawValue)
guard sqlite3_step(statement) == SQLITE_DONE else {
print("Error inserting todo: \(String(cString: sqlite3_errmsg(db)))")
return nil
}
let id = sqlite3_last_insert_rowid(db)
let createdAt = Int64(Date().timeIntervalSince1970)
return Todo(
id: id,
title: title,
completed: false,
category: category,
priority: priority,
createdAt: createdAt
)
}
func updateTodo(id: Int64, title: String, category: String, priority: Priority) {
let sql = "UPDATE todos SET title = ?, category = ?, priority = ? WHERE id = ?"
var statement: OpaquePointer?
defer { sqlite3_finalize(statement) }
guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
print("Error preparing update: \(String(cString: sqlite3_errmsg(db)))")
return
}
bindText(statement, index: 1, value: title)
bindText(statement, index: 2, value: category)
bindText(statement, index: 3, value: priority.rawValue)
sqlite3_bind_int64(statement, 4, id)
if sqlite3_step(statement) != SQLITE_DONE {
print("Error updating todo: \(String(cString: sqlite3_errmsg(db)))")
}
}
func toggleTodo(id: Int64, completed: Bool) {
let sql = "UPDATE todos SET completed = ? WHERE id = ?"
var statement: OpaquePointer?
defer { sqlite3_finalize(statement) }
guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
print("Error preparing toggle: \(String(cString: sqlite3_errmsg(db)))")
return
}
sqlite3_bind_int(statement, 1, completed ? 1 : 0)
sqlite3_bind_int64(statement, 2, id)
if sqlite3_step(statement) != SQLITE_DONE {
print("Error toggling todo: \(String(cString: sqlite3_errmsg(db)))")
}
}
func deleteTodo(id: Int64) {
let sql = "DELETE FROM todos WHERE id = ?"
var statement: OpaquePointer?
defer { sqlite3_finalize(statement) }
guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
print("Error preparing delete: \(String(cString: sqlite3_errmsg(db)))")
return
}
sqlite3_bind_int64(statement, 1, id)
if sqlite3_step(statement) != SQLITE_DONE {
print("Error deleting todo: \(String(cString: sqlite3_errmsg(db)))")
}
}
}