refactor: consolidate schema detection
This commit is contained in:
parent
f9258472c8
commit
5b5c8bcc50
@ -2,84 +2,50 @@ import Foundation
|
||||
import SQLite
|
||||
|
||||
extension MessageStore {
|
||||
static func detectThreadOriginatorGUIDColumn(connection: Connection) -> Bool {
|
||||
static func tableColumns(connection: Connection, table: String) -> Set<String> {
|
||||
do {
|
||||
let rows = try connection.prepare("PRAGMA table_info(message)")
|
||||
let rows = try connection.prepare("PRAGMA table_info(\(table))")
|
||||
var columns = Set<String>()
|
||||
for row in rows {
|
||||
if let name = row[1] as? String,
|
||||
name.caseInsensitiveCompare("thread_originator_guid") == .orderedSame
|
||||
{
|
||||
return true
|
||||
if let name = row[1] as? String {
|
||||
columns.insert(name.lowercased())
|
||||
}
|
||||
}
|
||||
return columns
|
||||
} catch {
|
||||
return false
|
||||
return []
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func reactionColumnsPresent(in columns: Set<String>) -> Bool {
|
||||
return columns.contains("guid")
|
||||
&& columns.contains("associated_message_guid")
|
||||
&& columns.contains("associated_message_type")
|
||||
}
|
||||
|
||||
static func detectReactionColumns(connection: Connection) -> Bool {
|
||||
let columns = tableColumns(connection: connection, table: "message")
|
||||
return reactionColumnsPresent(in: columns)
|
||||
}
|
||||
|
||||
static func detectThreadOriginatorGUIDColumn(connection: Connection) -> Bool {
|
||||
return tableColumns(connection: connection, table: "message").contains("thread_originator_guid")
|
||||
}
|
||||
|
||||
static func detectAttributedBody(connection: Connection) -> Bool {
|
||||
do {
|
||||
let rows = try connection.prepare("PRAGMA table_info(message)")
|
||||
for row in rows {
|
||||
if let name = row[1] as? String,
|
||||
name.caseInsensitiveCompare("attributedBody") == .orderedSame
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
return tableColumns(connection: connection, table: "message").contains("attributedbody")
|
||||
}
|
||||
|
||||
static func detectDestinationCallerID(connection: Connection) -> Bool {
|
||||
do {
|
||||
let rows = try connection.prepare("PRAGMA table_info(message)")
|
||||
for row in rows {
|
||||
if let name = row[1] as? String,
|
||||
name.caseInsensitiveCompare("destination_caller_id") == .orderedSame
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
return tableColumns(connection: connection, table: "message").contains("destination_caller_id")
|
||||
}
|
||||
|
||||
static func detectAudioMessageColumn(connection: Connection) -> Bool {
|
||||
do {
|
||||
let rows = try connection.prepare("PRAGMA table_info(message)")
|
||||
for row in rows {
|
||||
if let name = row[1] as? String,
|
||||
name.caseInsensitiveCompare("is_audio_message") == .orderedSame
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
return tableColumns(connection: connection, table: "message").contains("is_audio_message")
|
||||
}
|
||||
|
||||
static func detectAttachmentUserInfo(connection: Connection) -> Bool {
|
||||
do {
|
||||
let rows = try connection.prepare("PRAGMA table_info(attachment)")
|
||||
for row in rows {
|
||||
if let name = row[1] as? String,
|
||||
name.caseInsensitiveCompare("user_info") == .orderedSame
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
return tableColumns(connection: connection, table: "attachment").contains("user_info")
|
||||
}
|
||||
|
||||
static func enhance(error: Error, path: String) -> Error {
|
||||
|
||||
@ -60,24 +60,40 @@ extension MessageStore {
|
||||
return try withConnection { db in
|
||||
var messages: [Message] = []
|
||||
for row in try db.prepare(sql, bindings) {
|
||||
let rowID = int64Value(row[0]) ?? 0
|
||||
let handleID = int64Value(row[1])
|
||||
var sender = stringValue(row[2])
|
||||
let text = stringValue(row[3])
|
||||
let date = appleDate(from: int64Value(row[4]))
|
||||
let isFromMe = boolValue(row[5])
|
||||
let service = stringValue(row[6])
|
||||
let isAudioMessage = boolValue(row[7])
|
||||
let destinationCallerID = stringValue(row[8])
|
||||
let colRowID = 0
|
||||
let colHandleID = 1
|
||||
let colSender = 2
|
||||
let colText = 3
|
||||
let colDate = 4
|
||||
let colIsFromMe = 5
|
||||
let colService = 6
|
||||
let colIsAudioMessage = 7
|
||||
let colDestinationCallerID = 8
|
||||
let colGUID = 9
|
||||
let colAssociatedGUID = 10
|
||||
let colAssociatedType = 11
|
||||
let colAttachments = 12
|
||||
let colBody = 13
|
||||
let colThreadOriginatorGUID = 14
|
||||
|
||||
let rowID = int64Value(row[colRowID]) ?? 0
|
||||
let handleID = int64Value(row[colHandleID])
|
||||
var sender = stringValue(row[colSender])
|
||||
let text = stringValue(row[colText])
|
||||
let date = appleDate(from: int64Value(row[colDate]))
|
||||
let isFromMe = boolValue(row[colIsFromMe])
|
||||
let service = stringValue(row[colService])
|
||||
let isAudioMessage = boolValue(row[colIsAudioMessage])
|
||||
let destinationCallerID = stringValue(row[colDestinationCallerID])
|
||||
if sender.isEmpty && !destinationCallerID.isEmpty {
|
||||
sender = destinationCallerID
|
||||
}
|
||||
let guid = stringValue(row[9])
|
||||
let associatedGuid = stringValue(row[10])
|
||||
let associatedType = intValue(row[11])
|
||||
let attachments = intValue(row[12]) ?? 0
|
||||
let body = dataValue(row[13])
|
||||
let threadOriginatorGUID = stringValue(row[14])
|
||||
let guid = stringValue(row[colGUID])
|
||||
let associatedGuid = stringValue(row[colAssociatedGUID])
|
||||
let associatedType = intValue(row[colAssociatedType])
|
||||
let attachments = intValue(row[colAttachments]) ?? 0
|
||||
let body = dataValue(row[colBody])
|
||||
let threadOriginatorGUID = stringValue(row[colThreadOriginatorGUID])
|
||||
var resolvedText = text.isEmpty ? TypedStreamParser.parseAttributedBody(body) : text
|
||||
if isAudioMessage, let transcription = try audioTranscription(for: rowID) {
|
||||
resolvedText = transcription
|
||||
@ -142,25 +158,42 @@ extension MessageStore {
|
||||
return try withConnection { db in
|
||||
var messages: [Message] = []
|
||||
for row in try db.prepare(sql, bindings) {
|
||||
let rowID = int64Value(row[0]) ?? 0
|
||||
let resolvedChatID = int64Value(row[1]) ?? chatID ?? 0
|
||||
let handleID = int64Value(row[2])
|
||||
var sender = stringValue(row[3])
|
||||
let text = stringValue(row[4])
|
||||
let date = appleDate(from: int64Value(row[5]))
|
||||
let isFromMe = boolValue(row[6])
|
||||
let service = stringValue(row[7])
|
||||
let isAudioMessage = boolValue(row[8])
|
||||
let destinationCallerID = stringValue(row[9])
|
||||
let colRowID = 0
|
||||
let colChatID = 1
|
||||
let colHandleID = 2
|
||||
let colSender = 3
|
||||
let colText = 4
|
||||
let colDate = 5
|
||||
let colIsFromMe = 6
|
||||
let colService = 7
|
||||
let colIsAudioMessage = 8
|
||||
let colDestinationCallerID = 9
|
||||
let colGUID = 10
|
||||
let colAssociatedGUID = 11
|
||||
let colAssociatedType = 12
|
||||
let colAttachments = 13
|
||||
let colBody = 14
|
||||
let colThreadOriginatorGUID = 15
|
||||
|
||||
let rowID = int64Value(row[colRowID]) ?? 0
|
||||
let resolvedChatID = int64Value(row[colChatID]) ?? chatID ?? 0
|
||||
let handleID = int64Value(row[colHandleID])
|
||||
var sender = stringValue(row[colSender])
|
||||
let text = stringValue(row[colText])
|
||||
let date = appleDate(from: int64Value(row[colDate]))
|
||||
let isFromMe = boolValue(row[colIsFromMe])
|
||||
let service = stringValue(row[colService])
|
||||
let isAudioMessage = boolValue(row[colIsAudioMessage])
|
||||
let destinationCallerID = stringValue(row[colDestinationCallerID])
|
||||
if sender.isEmpty && !destinationCallerID.isEmpty {
|
||||
sender = destinationCallerID
|
||||
}
|
||||
let guid = stringValue(row[10])
|
||||
let associatedGuid = stringValue(row[11])
|
||||
let associatedType = intValue(row[12])
|
||||
let attachments = intValue(row[13]) ?? 0
|
||||
let body = dataValue(row[14])
|
||||
let threadOriginatorGUID = stringValue(row[15])
|
||||
let guid = stringValue(row[colGUID])
|
||||
let associatedGuid = stringValue(row[colAssociatedGUID])
|
||||
let associatedType = intValue(row[colAssociatedType])
|
||||
let attachments = intValue(row[colAttachments]) ?? 0
|
||||
let body = dataValue(row[colBody])
|
||||
let threadOriginatorGUID = stringValue(row[colThreadOriginatorGUID])
|
||||
var resolvedText = text.isEmpty ? TypedStreamParser.parseAttributedBody(body) : text
|
||||
if isAudioMessage, let transcription = try audioTranscription(for: rowID) {
|
||||
resolvedText = transcription
|
||||
|
||||
@ -31,20 +31,17 @@ public final class MessageStore: @unchecked Sendable {
|
||||
let location = Connection.Location.uri(uri, parameters: [.mode(.readOnly)])
|
||||
self.connection = try Connection(location, readonly: true)
|
||||
self.connection.busyTimeout = 5
|
||||
self.hasAttributedBody = MessageStore.detectAttributedBody(connection: self.connection)
|
||||
self.hasReactionColumns = MessageStore.detectReactionColumns(connection: self.connection)
|
||||
self.hasThreadOriginatorGUIDColumn = MessageStore.detectThreadOriginatorGUIDColumn(
|
||||
connection: self.connection
|
||||
)
|
||||
self.hasDestinationCallerID = MessageStore.detectDestinationCallerID(
|
||||
connection: self.connection
|
||||
)
|
||||
self.hasAudioMessageColumn = MessageStore.detectAudioMessageColumn(
|
||||
connection: self.connection
|
||||
)
|
||||
self.hasAttachmentUserInfo = MessageStore.detectAttachmentUserInfo(
|
||||
connection: self.connection
|
||||
let messageColumns = MessageStore.tableColumns(connection: self.connection, table: "message")
|
||||
let attachmentColumns = MessageStore.tableColumns(
|
||||
connection: self.connection,
|
||||
table: "attachment"
|
||||
)
|
||||
self.hasAttributedBody = messageColumns.contains("attributedbody")
|
||||
self.hasReactionColumns = MessageStore.reactionColumnsPresent(in: messageColumns)
|
||||
self.hasThreadOriginatorGUIDColumn = messageColumns.contains("thread_originator_guid")
|
||||
self.hasDestinationCallerID = messageColumns.contains("destination_caller_id")
|
||||
self.hasAudioMessageColumn = messageColumns.contains("is_audio_message")
|
||||
self.hasAttachmentUserInfo = attachmentColumns.contains("user_info")
|
||||
} catch {
|
||||
throw MessageStore.enhance(error: error, path: normalized)
|
||||
}
|
||||
@ -65,37 +62,37 @@ public final class MessageStore: @unchecked Sendable {
|
||||
self.queue.setSpecific(key: queueKey, value: ())
|
||||
self.connection = connection
|
||||
self.connection.busyTimeout = 5
|
||||
let messageColumns = MessageStore.tableColumns(connection: connection, table: "message")
|
||||
let attachmentColumns = MessageStore.tableColumns(connection: connection, table: "attachment")
|
||||
if let hasAttributedBody {
|
||||
self.hasAttributedBody = hasAttributedBody
|
||||
} else {
|
||||
self.hasAttributedBody = MessageStore.detectAttributedBody(connection: connection)
|
||||
self.hasAttributedBody = messageColumns.contains("attributedbody")
|
||||
}
|
||||
if let hasReactionColumns {
|
||||
self.hasReactionColumns = hasReactionColumns
|
||||
} else {
|
||||
self.hasReactionColumns = MessageStore.detectReactionColumns(connection: connection)
|
||||
self.hasReactionColumns = MessageStore.reactionColumnsPresent(in: messageColumns)
|
||||
}
|
||||
if let hasThreadOriginatorGUIDColumn {
|
||||
self.hasThreadOriginatorGUIDColumn = hasThreadOriginatorGUIDColumn
|
||||
} else {
|
||||
self.hasThreadOriginatorGUIDColumn = MessageStore.detectThreadOriginatorGUIDColumn(
|
||||
connection: connection
|
||||
)
|
||||
self.hasThreadOriginatorGUIDColumn = messageColumns.contains("thread_originator_guid")
|
||||
}
|
||||
if let hasDestinationCallerID {
|
||||
self.hasDestinationCallerID = hasDestinationCallerID
|
||||
} else {
|
||||
self.hasDestinationCallerID = MessageStore.detectDestinationCallerID(connection: connection)
|
||||
self.hasDestinationCallerID = messageColumns.contains("destination_caller_id")
|
||||
}
|
||||
if let hasAudioMessageColumn {
|
||||
self.hasAudioMessageColumn = hasAudioMessageColumn
|
||||
} else {
|
||||
self.hasAudioMessageColumn = MessageStore.detectAudioMessageColumn(connection: connection)
|
||||
self.hasAudioMessageColumn = messageColumns.contains("is_audio_message")
|
||||
}
|
||||
if let hasAttachmentUserInfo {
|
||||
self.hasAttachmentUserInfo = hasAttachmentUserInfo
|
||||
} else {
|
||||
self.hasAttachmentUserInfo = MessageStore.detectAttachmentUserInfo(connection: connection)
|
||||
self.hasAttachmentUserInfo = attachmentColumns.contains("user_info")
|
||||
}
|
||||
}
|
||||
|
||||
@ -379,23 +376,6 @@ extension MessageStore {
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func detectReactionColumns(connection: Connection) -> Bool {
|
||||
do {
|
||||
let rows = try connection.prepare("PRAGMA table_info(message)")
|
||||
var columns = Set<String>()
|
||||
for row in rows {
|
||||
if let name = row[1] as? String {
|
||||
columns.insert(name.lowercased())
|
||||
}
|
||||
}
|
||||
return columns.contains("guid")
|
||||
&& columns.contains("associated_message_guid")
|
||||
&& columns.contains("associated_message_type")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private struct ReactionKey: Hashable {
|
||||
let sender: String
|
||||
let isFromMe: Bool
|
||||
|
||||
@ -4,6 +4,32 @@ import Testing
|
||||
|
||||
@testable import IMsgCore
|
||||
|
||||
private func makeInMemoryMessageDB(includeThreadOriginatorGUID: Bool = false) throws -> Connection {
|
||||
let db = try Connection(.inMemory)
|
||||
let threadOriginatorColumn = includeThreadOriginatorGUID ? "thread_originator_guid TEXT," : ""
|
||||
try db.execute(
|
||||
"""
|
||||
CREATE TABLE message (
|
||||
ROWID INTEGER PRIMARY KEY,
|
||||
handle_id INTEGER,
|
||||
text TEXT,
|
||||
guid TEXT,
|
||||
associated_message_guid TEXT,
|
||||
associated_message_type INTEGER,
|
||||
\(threadOriginatorColumn)
|
||||
date INTEGER,
|
||||
is_from_me INTEGER,
|
||||
service TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
try db.execute("CREATE TABLE handle (ROWID INTEGER PRIMARY KEY, id TEXT);")
|
||||
try db.execute("CREATE TABLE chat_message_join (chat_id INTEGER, message_id INTEGER);")
|
||||
try db.execute(
|
||||
"CREATE TABLE message_attachment_join (message_id INTEGER, attachment_id INTEGER);")
|
||||
return db
|
||||
}
|
||||
|
||||
@Test
|
||||
func listChatsReturnsChat() throws {
|
||||
let store = try TestDatabase.makeStore()
|
||||
@ -113,26 +139,7 @@ func messagesAfterReturnsMessages() throws {
|
||||
|
||||
@Test
|
||||
func messagesAfterExcludesReactionRows() throws {
|
||||
let db = try Connection(.inMemory)
|
||||
try db.execute(
|
||||
"""
|
||||
CREATE TABLE message (
|
||||
ROWID INTEGER PRIMARY KEY,
|
||||
handle_id INTEGER,
|
||||
text TEXT,
|
||||
guid TEXT,
|
||||
associated_message_guid TEXT,
|
||||
associated_message_type INTEGER,
|
||||
date INTEGER,
|
||||
is_from_me INTEGER,
|
||||
service TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
try db.execute("CREATE TABLE handle (ROWID INTEGER PRIMARY KEY, id TEXT);")
|
||||
try db.execute("CREATE TABLE chat_message_join (chat_id INTEGER, message_id INTEGER);")
|
||||
try db.execute(
|
||||
"CREATE TABLE message_attachment_join (message_id INTEGER, attachment_id INTEGER);")
|
||||
let db = try makeInMemoryMessageDB()
|
||||
|
||||
let now = Date()
|
||||
try db.run("INSERT INTO handle(ROWID, id) VALUES (1, '+123')")
|
||||
@ -172,26 +179,7 @@ func messagesAfterExcludesReactionRows() throws {
|
||||
|
||||
@Test
|
||||
func messagesExcludeReactionRows() throws {
|
||||
let db = try Connection(.inMemory)
|
||||
try db.execute(
|
||||
"""
|
||||
CREATE TABLE message (
|
||||
ROWID INTEGER PRIMARY KEY,
|
||||
handle_id INTEGER,
|
||||
text TEXT,
|
||||
guid TEXT,
|
||||
associated_message_guid TEXT,
|
||||
associated_message_type INTEGER,
|
||||
date INTEGER,
|
||||
is_from_me INTEGER,
|
||||
service TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
try db.execute("CREATE TABLE handle (ROWID INTEGER PRIMARY KEY, id TEXT);")
|
||||
try db.execute("CREATE TABLE chat_message_join (chat_id INTEGER, message_id INTEGER);")
|
||||
try db.execute(
|
||||
"CREATE TABLE message_attachment_join (message_id INTEGER, attachment_id INTEGER);")
|
||||
let db = try makeInMemoryMessageDB()
|
||||
|
||||
let now = Date()
|
||||
try db.run("INSERT INTO handle(ROWID, id) VALUES (1, '+123')")
|
||||
@ -220,26 +208,7 @@ func messagesExcludeReactionRows() throws {
|
||||
|
||||
@Test
|
||||
func messagesExposeReplyToGuid() throws {
|
||||
let db = try Connection(.inMemory)
|
||||
try db.execute(
|
||||
"""
|
||||
CREATE TABLE message (
|
||||
ROWID INTEGER PRIMARY KEY,
|
||||
handle_id INTEGER,
|
||||
text TEXT,
|
||||
guid TEXT,
|
||||
associated_message_guid TEXT,
|
||||
associated_message_type INTEGER,
|
||||
date INTEGER,
|
||||
is_from_me INTEGER,
|
||||
service TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
try db.execute("CREATE TABLE handle (ROWID INTEGER PRIMARY KEY, id TEXT);")
|
||||
try db.execute("CREATE TABLE chat_message_join (chat_id INTEGER, message_id INTEGER);")
|
||||
try db.execute(
|
||||
"CREATE TABLE message_attachment_join (message_id INTEGER, attachment_id INTEGER);")
|
||||
let db = try makeInMemoryMessageDB()
|
||||
|
||||
let now = Date()
|
||||
try db.run("INSERT INTO handle(ROWID, id) VALUES (1, '+123')")
|
||||
@ -269,26 +238,7 @@ func messagesExposeReplyToGuid() throws {
|
||||
|
||||
@Test
|
||||
func messagesReplyToGuidHandlesNoPrefix() throws {
|
||||
let db = try Connection(.inMemory)
|
||||
try db.execute(
|
||||
"""
|
||||
CREATE TABLE message (
|
||||
ROWID INTEGER PRIMARY KEY,
|
||||
handle_id INTEGER,
|
||||
text TEXT,
|
||||
guid TEXT,
|
||||
associated_message_guid TEXT,
|
||||
associated_message_type INTEGER,
|
||||
date INTEGER,
|
||||
is_from_me INTEGER,
|
||||
service TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
try db.execute("CREATE TABLE handle (ROWID INTEGER PRIMARY KEY, id TEXT);")
|
||||
try db.execute("CREATE TABLE chat_message_join (chat_id INTEGER, message_id INTEGER);")
|
||||
try db.execute(
|
||||
"CREATE TABLE message_attachment_join (message_id INTEGER, attachment_id INTEGER);")
|
||||
let db = try makeInMemoryMessageDB()
|
||||
|
||||
let now = Date()
|
||||
try db.run("INSERT INTO handle(ROWID, id) VALUES (1, '+123')")
|
||||
@ -317,27 +267,7 @@ func messagesReplyToGuidHandlesNoPrefix() throws {
|
||||
|
||||
@Test
|
||||
func messagesExposeThreadOriginatorGuidWhenAvailable() throws {
|
||||
let db = try Connection(.inMemory)
|
||||
try db.execute(
|
||||
"""
|
||||
CREATE TABLE message (
|
||||
ROWID INTEGER PRIMARY KEY,
|
||||
handle_id INTEGER,
|
||||
text TEXT,
|
||||
guid TEXT,
|
||||
associated_message_guid TEXT,
|
||||
associated_message_type INTEGER,
|
||||
thread_originator_guid TEXT,
|
||||
date INTEGER,
|
||||
is_from_me INTEGER,
|
||||
service TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
try db.execute("CREATE TABLE handle (ROWID INTEGER PRIMARY KEY, id TEXT);")
|
||||
try db.execute("CREATE TABLE chat_message_join (chat_id INTEGER, message_id INTEGER);")
|
||||
try db.execute(
|
||||
"CREATE TABLE message_attachment_join (message_id INTEGER, attachment_id INTEGER);")
|
||||
let db = try makeInMemoryMessageDB(includeThreadOriginatorGUID: true)
|
||||
|
||||
let now = Date()
|
||||
try db.run("INSERT INTO handle(ROWID, id) VALUES (1, '+123')")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user