2022-07-03 06:52:41 +00:00
|
|
|
//
|
|
|
|
// DatabaseController.swift
|
|
|
|
// MastoSearch
|
|
|
|
//
|
|
|
|
// Created by Shadowfacts on 12/10/21.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import FMDB
|
|
|
|
import OSLog
|
|
|
|
import Combine
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public class DatabaseController {
|
2022-07-03 06:52:41 +00:00
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public static let shared = DatabaseController()
|
2022-07-03 06:52:41 +00:00
|
|
|
|
|
|
|
private let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DatabaseController")
|
|
|
|
static let dateFormat = Date.ISO8601FormatStyle(includingFractionalSeconds: true)
|
|
|
|
|
|
|
|
private let applicationSupport: URL
|
|
|
|
private let databaseURL: URL
|
|
|
|
|
|
|
|
private var queue: FMDatabaseQueue!
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public private(set) var isInitialized = false
|
|
|
|
public let onInitialize = PassthroughSubject<(), Never>()
|
2022-07-03 06:52:41 +00:00
|
|
|
|
|
|
|
private init() {
|
|
|
|
// this dir will be inside the application sandbox container
|
|
|
|
applicationSupport = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
|
|
|
databaseURL = applicationSupport.appendingPathComponent("statuses").appendingPathExtension("sqlite")
|
|
|
|
}
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public func initialize() {
|
2022-07-03 06:52:41 +00:00
|
|
|
if !FileManager.default.fileExists(atPath: databaseURL.absoluteString) {
|
|
|
|
FileManager.default.createFile(atPath: databaseURL.absoluteString, contents: nil, attributes: nil)
|
|
|
|
}
|
|
|
|
queue = FMDatabaseQueue(path: databaseURL.absoluteString)
|
|
|
|
queue.inDatabase { db in
|
|
|
|
let success = db.executeStatements("""
|
|
|
|
CREATE TABLE IF NOT EXISTS statuses (
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
api_id TEXT,
|
|
|
|
url TEXT,
|
|
|
|
summary TEXT,
|
|
|
|
status_content TEXT NOT NULL,
|
|
|
|
published TEXT NOT NULL
|
|
|
|
);
|
|
|
|
|
|
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS statuses_fts USING fts5(
|
|
|
|
summary,
|
|
|
|
status_content,
|
|
|
|
url UNINDEXED,
|
|
|
|
published UNINDEXED,
|
|
|
|
api_id UNINDEXED,
|
|
|
|
content = 'statuses',
|
|
|
|
content_rowid = 'id',
|
|
|
|
tokenize = "porter unicode61 tokenchars '@-_'"
|
|
|
|
);
|
|
|
|
|
|
|
|
CREATE TRIGGER IF NOT EXISTS statuses_ai AFTER INSERT ON statuses BEGIN
|
|
|
|
INSERT INTO statuses_fts(rowid, summary, status_content) VALUES (new.id, new.summary, new.status_content);
|
|
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS statuses_ad AFTER DELETE ON statuses BEGIN
|
|
|
|
INSERT INTO statuses_fts(statuses_fts, rowid, summary, status_content) VALUES('delete', old.id, old.summary, old.status_content);
|
|
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS statuses_au AFTER UPDATE ON statuses BEGIN
|
|
|
|
INSERT INTO statuses_fts(statuses_fts, rowid, summary, status_content) VALUES('delete', old.id, old.summary, new.status_content);
|
|
|
|
INSERT INTO statuses_fts(rowid, summary, status_content) VALUES (new.id, new.summary, new.status_content);
|
|
|
|
END;
|
|
|
|
""")
|
|
|
|
guard success else {
|
|
|
|
fatalError("failed to create schema: \(db.lastError())")
|
|
|
|
}
|
|
|
|
log.info("Setup schema")
|
|
|
|
|
|
|
|
}
|
|
|
|
isInitialized = true
|
|
|
|
// this is safe, FMDatabaseQueue calls are blocking
|
|
|
|
onInitialize.send()
|
|
|
|
}
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public func close() {
|
2022-07-03 06:52:41 +00:00
|
|
|
// db.close()
|
|
|
|
// log.info("Closed database")
|
|
|
|
}
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public func addStatuses<S: Sequence>(_ statuses: S) where S.Element == Status {
|
2022-07-03 06:52:41 +00:00
|
|
|
queue.inTransaction { db, rollback in
|
|
|
|
var i = 0
|
|
|
|
for status in statuses {
|
|
|
|
do {
|
|
|
|
let summary: AnyObject
|
|
|
|
if let s = status.summary {
|
|
|
|
summary = s as NSString
|
|
|
|
} else {
|
|
|
|
summary = NSNull()
|
|
|
|
}
|
|
|
|
try db.executeUpdate("INSERT INTO statuses (api_id, url, summary, status_content, published) VALUES (?, ?, ?, ?, ?);", values: [
|
|
|
|
status.id as NSString,
|
|
|
|
status.url as NSString,
|
|
|
|
summary,
|
|
|
|
status.content as NSString,
|
|
|
|
DatabaseController.dateFormat.format(status.published) as NSString
|
|
|
|
])
|
|
|
|
} catch {
|
|
|
|
log.error("failed to insert status: \(error.localizedDescription, privacy: .public)")
|
|
|
|
rollback.pointee = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
i += 1
|
|
|
|
if i % 100 == 0 {
|
|
|
|
log.debug("Imported \(i, privacy: .public) statuses...")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log.info("Finished import of \(i, privacy: .public) statuses")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public func getStatuses(sortDescriptor: NSSortDescriptor?, completion: @escaping (StatusSequence) -> Void) {
|
2022-07-03 06:52:41 +00:00
|
|
|
queue.inDatabase { db in
|
|
|
|
let sortKey = sortDescriptor?.key ?? "published"
|
|
|
|
let asc = sortDescriptor?.ascending == true ? "ASC" : "DESC"
|
|
|
|
let results = try! db.executeQuery("SELECT * FROM statuses ORDER BY \(sortKey) \(asc)", values: nil)
|
|
|
|
let sequence = StatusSequence(results: results)
|
|
|
|
completion(sequence)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public func getStatuses(query: String, sortDescriptor: NSSortDescriptor?, completion: @escaping (StatusSequence) -> Void) {
|
2022-07-03 06:52:41 +00:00
|
|
|
queue.inDatabase { db in
|
|
|
|
let sortKey = sortDescriptor?.key ?? "rank"
|
|
|
|
let asc = sortDescriptor?.ascending == false ? "DESC" : "ASC"
|
|
|
|
let results = try! db.executeQuery("SELECT * FROM statuses_fts WHERE statuses_fts match ? ORDER BY \(sortKey) \(asc)", values: [query as NSString])
|
|
|
|
let sequence = StatusSequence(results: results)
|
|
|
|
completion(sequence)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public func getNewestStatus(completion: @escaping (Status?) -> Void) {
|
2022-07-03 06:52:41 +00:00
|
|
|
queue.inDatabase { db in
|
|
|
|
let results = try! db.executeQuery("SELECT * FROM statuses ORDER BY published DESC LIMIT 1", values: nil)
|
|
|
|
completion(StatusSequence(results: results).makeIterator().next())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public func countStatuses() -> Int {
|
2022-07-03 06:52:41 +00:00
|
|
|
var res: Int!
|
|
|
|
queue.inDatabase { db in
|
|
|
|
let results = try! db.executeQuery("SELECT COUNT(*) AS count FROM statuses", values: nil)
|
|
|
|
results.next()
|
|
|
|
res = Int(results.int(forColumn: "count"))
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public struct StatusSequence: Sequence {
|
|
|
|
public typealias Element = Status
|
2022-07-03 06:52:41 +00:00
|
|
|
|
|
|
|
let results: FMResultSet
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public func makeIterator() -> Iterator {
|
2022-07-03 06:52:41 +00:00
|
|
|
return Iterator(results: results)
|
|
|
|
}
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public class Iterator: IteratorProtocol {
|
|
|
|
public typealias Element = Status
|
2022-07-03 06:52:41 +00:00
|
|
|
|
|
|
|
let results: FMResultSet
|
|
|
|
|
|
|
|
init(results: FMResultSet) {
|
|
|
|
self.results = results
|
|
|
|
}
|
|
|
|
|
|
|
|
deinit {
|
|
|
|
results.close()
|
|
|
|
}
|
|
|
|
|
2022-07-03 18:58:59 +00:00
|
|
|
public func next() -> Status? {
|
2022-07-03 06:52:41 +00:00
|
|
|
if results.next() {
|
|
|
|
return Status(
|
|
|
|
id: results.string(forColumn: "api_id")!,
|
|
|
|
url: results.string(forColumn: "url")!,
|
|
|
|
summary: results.string(forColumn: "summary"),
|
|
|
|
content: results.string(forColumn: "status_content")!,
|
|
|
|
published: try! DatabaseController.dateFormat.parse(results.string(forColumn: "published")!)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|