refactor: decode sqlite rows by name
This commit is contained in:
parent
327829a819
commit
edad072a41
@ -10,8 +10,9 @@ extension MessageStore {
|
||||
let sql: String
|
||||
if hasChatMessageJoinMessageDateColumn {
|
||||
sql = """
|
||||
SELECT c.ROWID, IFNULL(c.display_name, c.chat_identifier) AS name, c.chat_identifier,
|
||||
c.service_name, MAX(cmj.message_date) AS last_date,
|
||||
SELECT c.ROWID AS chat_rowid, IFNULL(c.display_name, c.chat_identifier) AS name,
|
||||
c.chat_identifier AS chat_identifier, c.service_name AS service_name,
|
||||
MAX(cmj.message_date) AS last_date,
|
||||
\(accountIDColumn) AS account_id,
|
||||
\(accountLoginColumn) AS account_login,
|
||||
\(lastAddressedHandleColumn) AS last_addressed_handle
|
||||
@ -23,8 +24,9 @@ extension MessageStore {
|
||||
"""
|
||||
} else {
|
||||
sql = """
|
||||
SELECT c.ROWID, IFNULL(c.display_name, c.chat_identifier) AS name, c.chat_identifier,
|
||||
c.service_name, MAX(m.date) AS last_date,
|
||||
SELECT c.ROWID AS chat_rowid, IFNULL(c.display_name, c.chat_identifier) AS name,
|
||||
c.chat_identifier AS chat_identifier, c.service_name AS service_name,
|
||||
MAX(m.date) AS last_date,
|
||||
\(accountIDColumn) AS account_id,
|
||||
\(accountLoginColumn) AS account_login,
|
||||
\(lastAddressedHandleColumn) AS last_addressed_handle
|
||||
@ -38,17 +40,18 @@ extension MessageStore {
|
||||
}
|
||||
return try withConnection { db in
|
||||
var chats: [Chat] = []
|
||||
for row in try db.prepare(sql, limit) {
|
||||
let rows = try db.prepareRowIterator(sql, bindings: [limit])
|
||||
while let row = try rows.failableNext() {
|
||||
chats.append(
|
||||
Chat(
|
||||
id: int64Value(row[0]) ?? 0,
|
||||
identifier: stringValue(row[2]),
|
||||
name: stringValue(row[1]),
|
||||
service: stringValue(row[3]),
|
||||
lastMessageAt: appleDate(from: int64Value(row[4])),
|
||||
accountID: stringValue(row[5]).nilIfEmpty,
|
||||
accountLogin: stringValue(row[6]).nilIfEmpty,
|
||||
lastAddressedHandle: stringValue(row[7]).nilIfEmpty
|
||||
id: try int64Value(row, "chat_rowid") ?? 0,
|
||||
identifier: try stringValue(row, "chat_identifier"),
|
||||
name: try stringValue(row, "name"),
|
||||
service: try stringValue(row, "service_name"),
|
||||
lastMessageAt: try appleDate(from: int64Value(row, "last_date")),
|
||||
accountID: try stringValue(row, "account_id").nilIfEmpty,
|
||||
accountLogin: try stringValue(row, "account_login").nilIfEmpty,
|
||||
lastAddressedHandle: try stringValue(row, "last_addressed_handle").nilIfEmpty
|
||||
))
|
||||
}
|
||||
return chats
|
||||
@ -61,7 +64,7 @@ extension MessageStore {
|
||||
let lastAddressedHandleColumn =
|
||||
hasChatLastAddressedHandleColumn ? "IFNULL(c.last_addressed_handle, '')" : "''"
|
||||
let sql = """
|
||||
SELECT c.ROWID, IFNULL(c.chat_identifier, '') AS identifier, IFNULL(c.guid, '') AS guid,
|
||||
SELECT c.ROWID AS chat_rowid, IFNULL(c.chat_identifier, '') AS identifier, IFNULL(c.guid, '') AS guid,
|
||||
IFNULL(c.display_name, c.chat_identifier) AS name, IFNULL(c.service_name, '') AS service,
|
||||
\(accountIDColumn) AS account_id,
|
||||
\(accountLoginColumn) AS account_login,
|
||||
@ -71,16 +74,17 @@ extension MessageStore {
|
||||
LIMIT 1
|
||||
"""
|
||||
return try withConnection { db in
|
||||
for row in try db.prepare(sql, chatID) {
|
||||
let rows = try db.prepareRowIterator(sql, bindings: [chatID])
|
||||
while let row = try rows.failableNext() {
|
||||
return ChatInfo(
|
||||
id: int64Value(row[0]) ?? 0,
|
||||
identifier: stringValue(row[1]),
|
||||
guid: stringValue(row[2]),
|
||||
name: stringValue(row[3]),
|
||||
service: stringValue(row[4]),
|
||||
accountID: stringValue(row[5]).nilIfEmpty,
|
||||
accountLogin: stringValue(row[6]).nilIfEmpty,
|
||||
lastAddressedHandle: stringValue(row[7]).nilIfEmpty
|
||||
id: try int64Value(row, "chat_rowid") ?? 0,
|
||||
identifier: try stringValue(row, "identifier"),
|
||||
guid: try stringValue(row, "guid"),
|
||||
name: try stringValue(row, "name"),
|
||||
service: try stringValue(row, "service"),
|
||||
accountID: try stringValue(row, "account_id").nilIfEmpty,
|
||||
accountLogin: try stringValue(row, "account_login").nilIfEmpty,
|
||||
lastAddressedHandle: try stringValue(row, "last_addressed_handle").nilIfEmpty
|
||||
)
|
||||
}
|
||||
return nil
|
||||
@ -98,8 +102,9 @@ extension MessageStore {
|
||||
return try withConnection { db in
|
||||
var results: [String] = []
|
||||
var seen = Set<String>()
|
||||
for row in try db.prepare(sql, chatID) {
|
||||
let handle = stringValue(row[0])
|
||||
let rows = try db.prepareRowIterator(sql, bindings: [chatID])
|
||||
while let row = try rows.failableNext() {
|
||||
let handle = try stringValue(row, "id")
|
||||
if handle.isEmpty { continue }
|
||||
if seen.insert(handle).inserted {
|
||||
results.append(handle)
|
||||
|
||||
@ -11,10 +11,10 @@ extension MessageStore {
|
||||
|
||||
static func tableColumns(connection: Connection, table: String) -> Set<String> {
|
||||
do {
|
||||
let rows = try connection.prepare("PRAGMA table_info(\(table))")
|
||||
let rows = try connection.prepareRowIterator("PRAGMA table_info(\(table))")
|
||||
var columns = Set<String>()
|
||||
for row in rows {
|
||||
if let name = row[1] as? String {
|
||||
while let row = try rows.failableNext() {
|
||||
if let name = try row.get(Expression<String?>("name")) {
|
||||
columns.insert(name.lowercased())
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,9 @@ extension MessageStore {
|
||||
let batch = Array(uniqueIDs[start..<end])
|
||||
let placeholders = Array(repeating: "?", count: batch.count).joined(separator: ",")
|
||||
let sql = """
|
||||
SELECT maj.message_id, a.filename, a.transfer_name, a.uti, a.mime_type, a.total_bytes, a.is_sticker
|
||||
SELECT maj.message_id AS message_id, a.filename AS filename,
|
||||
a.transfer_name AS transfer_name, a.uti AS uti, a.mime_type AS mime_type,
|
||||
a.total_bytes AS total_bytes, a.is_sticker AS is_sticker
|
||||
FROM message_attachment_join maj
|
||||
JOIN attachment a ON a.ROWID = maj.attachment_id
|
||||
WHERE maj.message_id IN (\(placeholders))
|
||||
@ -26,14 +28,15 @@ extension MessageStore {
|
||||
"""
|
||||
let bindings: [Binding?] = batch.map { $0 }
|
||||
try withConnection { db in
|
||||
for row in try db.prepare(sql, bindings) {
|
||||
let messageID = int64Value(row[0]) ?? 0
|
||||
let filename = stringValue(row[1])
|
||||
let transferName = stringValue(row[2])
|
||||
let uti = stringValue(row[3])
|
||||
let mimeType = stringValue(row[4])
|
||||
let totalBytes = int64Value(row[5]) ?? 0
|
||||
let isSticker = boolValue(row[6])
|
||||
let rows = try db.prepareRowIterator(sql, bindings: bindings)
|
||||
while let row = try rows.failableNext() {
|
||||
let messageID = try int64Value(row, "message_id") ?? 0
|
||||
let filename = try stringValue(row, "filename")
|
||||
let transferName = try stringValue(row, "transfer_name")
|
||||
let uti = try stringValue(row, "uti")
|
||||
let mimeType = try stringValue(row, "mime_type")
|
||||
let totalBytes = try int64Value(row, "total_bytes") ?? 0
|
||||
let isSticker = try boolValue(row, "is_sticker")
|
||||
metasByMessageID[messageID, default: []].append(
|
||||
AttachmentResolver.metadata(
|
||||
filename: filename,
|
||||
@ -88,8 +91,10 @@ extension MessageStore {
|
||||
).joined(separator: " OR ")
|
||||
let bodyColumn = hasAttributedBody ? "r.attributedBody" : "NULL"
|
||||
let sql = """
|
||||
SELECT r.ROWID, r.associated_message_guid, r.associated_message_type, h.id, r.is_from_me,
|
||||
r.date, IFNULL(r.text, '') AS text, \(bodyColumn) AS body
|
||||
SELECT r.ROWID AS reaction_rowid, r.associated_message_guid AS associated_message_guid,
|
||||
r.associated_message_type AS associated_message_type, h.id AS sender,
|
||||
r.is_from_me AS is_from_me, r.date AS date, IFNULL(r.text, '') AS text,
|
||||
\(bodyColumn) AS body
|
||||
FROM message r
|
||||
LEFT JOIN handle h ON r.handle_id = h.ROWID
|
||||
WHERE r.associated_message_guid IS NOT NULL
|
||||
@ -105,18 +110,19 @@ extension MessageStore {
|
||||
let bindings: [Binding?] = guids.map { $0 } + guids.map { "%/\($0)" }
|
||||
|
||||
try withConnection { db in
|
||||
for row in try db.prepare(sql, bindings) {
|
||||
let associatedGUID = stringValue(row[1])
|
||||
let rows = try db.prepareRowIterator(sql, bindings: bindings)
|
||||
while let row = try rows.failableNext() {
|
||||
let associatedGUID = try stringValue(row, "associated_message_guid")
|
||||
let baseGUID = baseAssociatedMessageGUID(from: associatedGUID)
|
||||
guard let messageID = messageIDByGUID[baseGUID] else { continue }
|
||||
|
||||
let rowID = int64Value(row[0]) ?? 0
|
||||
let typeValue = intValue(row[2]) ?? 0
|
||||
let sender = stringValue(row[3])
|
||||
let isFromMe = boolValue(row[4])
|
||||
let date = appleDate(from: int64Value(row[5]))
|
||||
let text = stringValue(row[6])
|
||||
let body = dataValue(row[7])
|
||||
let rowID = try int64Value(row, "reaction_rowid") ?? 0
|
||||
let typeValue = try intValue(row, "associated_message_type") ?? 0
|
||||
let sender = try stringValue(row, "sender")
|
||||
let isFromMe = try boolValue(row, "is_from_me")
|
||||
let date = try appleDate(from: int64Value(row, "date"))
|
||||
let text = try stringValue(row, "text")
|
||||
let body = try dataValue(row, "body")
|
||||
let resolvedText = text.isEmpty ? TypedStreamParser.parseAttributedBody(body) : text
|
||||
|
||||
var reactions = reactionsByMessageID[messageID, default: []]
|
||||
|
||||
@ -2,22 +2,22 @@ import Foundation
|
||||
import SQLite
|
||||
|
||||
private struct MessageRowColumns {
|
||||
let rowID: Int
|
||||
let chatID: Int?
|
||||
let handleID: Int
|
||||
let sender: Int
|
||||
let text: Int
|
||||
let date: Int
|
||||
let isFromMe: Int
|
||||
let service: Int
|
||||
let isAudioMessage: Int
|
||||
let destinationCallerID: Int
|
||||
let guid: Int
|
||||
let associatedGUID: Int
|
||||
let associatedType: Int
|
||||
let attachments: Int
|
||||
let body: Int
|
||||
let threadOriginatorGUID: Int
|
||||
let rowID: String
|
||||
let chatID: String?
|
||||
let handleID: String
|
||||
let sender: String
|
||||
let text: String
|
||||
let date: String
|
||||
let isFromMe: String
|
||||
let service: String
|
||||
let isAudioMessage: String
|
||||
let destinationCallerID: String
|
||||
let guid: String
|
||||
let associatedGUID: String
|
||||
let associatedType: String
|
||||
let attachments: String
|
||||
let body: String
|
||||
let threadOriginatorGUID: String
|
||||
}
|
||||
|
||||
private struct DecodedMessageRow {
|
||||
@ -56,7 +56,9 @@ extension MessageStore {
|
||||
? " AND (m.associated_message_type IS NULL OR m.associated_message_type < 2000 OR m.associated_message_type > 3006)"
|
||||
: ""
|
||||
var sql = """
|
||||
SELECT m.ROWID, m.handle_id, h.id, IFNULL(m.text, '') AS text, m.date, m.is_from_me, m.service,
|
||||
SELECT m.ROWID AS message_rowid, m.handle_id AS handle_id, h.id AS sender,
|
||||
IFNULL(m.text, '') AS text, m.date AS date, m.is_from_me AS is_from_me,
|
||||
m.service AS service,
|
||||
\(audioMessageColumn) AS is_audio_message, \(destinationCallerColumn) AS destination_caller_id,
|
||||
\(guidColumn) AS guid, \(associatedGuidColumn) AS associated_guid, \(associatedTypeColumn) AS associated_type,
|
||||
(SELECT COUNT(*) FROM message_attachment_join maj WHERE maj.message_id = m.ROWID) AS attachments,
|
||||
@ -93,27 +95,28 @@ extension MessageStore {
|
||||
sql += " ORDER BY m.date DESC LIMIT ?"
|
||||
bindings.append(limit)
|
||||
let columns = MessageRowColumns(
|
||||
rowID: 0,
|
||||
rowID: "message_rowid",
|
||||
chatID: nil,
|
||||
handleID: 1,
|
||||
sender: 2,
|
||||
text: 3,
|
||||
date: 4,
|
||||
isFromMe: 5,
|
||||
service: 6,
|
||||
isAudioMessage: 7,
|
||||
destinationCallerID: 8,
|
||||
guid: 9,
|
||||
associatedGUID: 10,
|
||||
associatedType: 11,
|
||||
attachments: 12,
|
||||
body: 13,
|
||||
threadOriginatorGUID: 14
|
||||
handleID: "handle_id",
|
||||
sender: "sender",
|
||||
text: "text",
|
||||
date: "date",
|
||||
isFromMe: "is_from_me",
|
||||
service: "service",
|
||||
isAudioMessage: "is_audio_message",
|
||||
destinationCallerID: "destination_caller_id",
|
||||
guid: "guid",
|
||||
associatedGUID: "associated_guid",
|
||||
associatedType: "associated_type",
|
||||
attachments: "attachments",
|
||||
body: "body",
|
||||
threadOriginatorGUID: "thread_originator_guid"
|
||||
)
|
||||
|
||||
return try withConnection { db in
|
||||
var messages: [Message] = []
|
||||
for row in try db.prepare(sql, bindings) {
|
||||
let rows = try db.prepareRowIterator(sql, bindings: bindings)
|
||||
while let row = try rows.failableNext() {
|
||||
let decoded = try decodeMessageRow(row, columns: columns, fallbackChatID: chatID)
|
||||
let replyToGUID = replyToGUID(
|
||||
associatedGuid: decoded.associatedGUID,
|
||||
@ -181,7 +184,9 @@ extension MessageStore {
|
||||
}
|
||||
}
|
||||
var sql = """
|
||||
SELECT m.ROWID, cmj.chat_id, m.handle_id, h.id, IFNULL(m.text, '') AS text, m.date, m.is_from_me, m.service,
|
||||
SELECT m.ROWID AS message_rowid, cmj.chat_id AS chat_id, m.handle_id AS handle_id,
|
||||
h.id AS sender, IFNULL(m.text, '') AS text, m.date AS date,
|
||||
m.is_from_me AS is_from_me, m.service AS service,
|
||||
\(audioMessageColumn) AS is_audio_message, \(destinationCallerColumn) AS destination_caller_id,
|
||||
\(guidColumn) AS guid, \(associatedGuidColumn) AS associated_guid, \(associatedTypeColumn) AS associated_type,
|
||||
(SELECT COUNT(*) FROM message_attachment_join maj WHERE maj.message_id = m.ROWID) AS attachments,
|
||||
@ -201,33 +206,32 @@ extension MessageStore {
|
||||
sql += " ORDER BY m.ROWID ASC LIMIT ?"
|
||||
bindings.append(limit)
|
||||
let columns = MessageRowColumns(
|
||||
rowID: 0,
|
||||
chatID: 1,
|
||||
handleID: 2,
|
||||
sender: 3,
|
||||
text: 4,
|
||||
date: 5,
|
||||
isFromMe: 6,
|
||||
service: 7,
|
||||
isAudioMessage: 8,
|
||||
destinationCallerID: 9,
|
||||
guid: 10,
|
||||
associatedGUID: 11,
|
||||
associatedType: 12,
|
||||
attachments: 13,
|
||||
body: 14,
|
||||
threadOriginatorGUID: 15
|
||||
rowID: "message_rowid",
|
||||
chatID: "chat_id",
|
||||
handleID: "handle_id",
|
||||
sender: "sender",
|
||||
text: "text",
|
||||
date: "date",
|
||||
isFromMe: "is_from_me",
|
||||
service: "service",
|
||||
isAudioMessage: "is_audio_message",
|
||||
destinationCallerID: "destination_caller_id",
|
||||
guid: "guid",
|
||||
associatedGUID: "associated_guid",
|
||||
associatedType: "associated_type",
|
||||
attachments: "attachments",
|
||||
body: "body",
|
||||
threadOriginatorGUID: "thread_originator_guid"
|
||||
)
|
||||
|
||||
let balloonBundleIDIndex = 16
|
||||
|
||||
return try withConnection { db in
|
||||
var messages: [Message] = []
|
||||
let urlBalloonProvider = "com.apple.messages.URLBalloonProvider"
|
||||
|
||||
for row in try db.prepare(sql, bindings) {
|
||||
let rows = try db.prepareRowIterator(sql, bindings: bindings)
|
||||
while let row = try rows.failableNext() {
|
||||
let decoded = try decodeMessageRow(row, columns: columns, fallbackChatID: chatID)
|
||||
let balloonBundleID = stringValue(row[balloonBundleIDIndex])
|
||||
let balloonBundleID = try stringValue(row, "balloon_bundle_id")
|
||||
if balloonBundleID == urlBalloonProvider,
|
||||
shouldSkipURLBalloonDuplicate(
|
||||
chatID: decoded.chatID,
|
||||
@ -296,7 +300,9 @@ extension MessageStore {
|
||||
let threadOriginatorColumn =
|
||||
hasThreadOriginatorGUIDColumn ? "m.thread_originator_guid" : "NULL"
|
||||
var sql = """
|
||||
SELECT m.ROWID, cmj.chat_id, m.handle_id, h.id, IFNULL(m.text, '') AS text, m.date, m.is_from_me, m.service,
|
||||
SELECT m.ROWID AS message_rowid, cmj.chat_id AS chat_id, m.handle_id AS handle_id,
|
||||
h.id AS sender, IFNULL(m.text, '') AS text, m.date AS date,
|
||||
m.is_from_me AS is_from_me, m.service AS service,
|
||||
\(audioMessageColumn) AS is_audio_message, \(destinationCallerColumn) AS destination_caller_id,
|
||||
\(guidColumn) AS guid, \(associatedGuidColumn) AS associated_guid, \(associatedTypeColumn) AS associated_type,
|
||||
(SELECT COUNT(*) FROM message_attachment_join maj WHERE maj.message_id = m.ROWID) AS attachments,
|
||||
@ -317,26 +323,27 @@ extension MessageStore {
|
||||
sql += " ORDER BY m.date DESC, m.ROWID DESC LIMIT 1"
|
||||
|
||||
let columns = MessageRowColumns(
|
||||
rowID: 0,
|
||||
chatID: 1,
|
||||
handleID: 2,
|
||||
sender: 3,
|
||||
text: 4,
|
||||
date: 5,
|
||||
isFromMe: 6,
|
||||
service: 7,
|
||||
isAudioMessage: 8,
|
||||
destinationCallerID: 9,
|
||||
guid: 10,
|
||||
associatedGUID: 11,
|
||||
associatedType: 12,
|
||||
attachments: 13,
|
||||
body: 14,
|
||||
threadOriginatorGUID: 15
|
||||
rowID: "message_rowid",
|
||||
chatID: "chat_id",
|
||||
handleID: "handle_id",
|
||||
sender: "sender",
|
||||
text: "text",
|
||||
date: "date",
|
||||
isFromMe: "is_from_me",
|
||||
service: "service",
|
||||
isAudioMessage: "is_audio_message",
|
||||
destinationCallerID: "destination_caller_id",
|
||||
guid: "guid",
|
||||
associatedGUID: "associated_guid",
|
||||
associatedType: "associated_type",
|
||||
attachments: "attachments",
|
||||
body: "body",
|
||||
threadOriginatorGUID: "thread_originator_guid"
|
||||
)
|
||||
|
||||
return try withConnection { db in
|
||||
guard let row = try db.prepare(sql, bindings).makeIterator().next() else { return nil }
|
||||
let rows = try db.prepareRowIterator(sql, bindings: bindings)
|
||||
guard let row = try rows.failableNext() else { return nil }
|
||||
let decoded = try decodeMessageRow(row, columns: columns, fallbackChatID: chatID)
|
||||
let replyToGUID = replyToGUID(
|
||||
associatedGuid: decoded.associatedGUID,
|
||||
@ -365,26 +372,27 @@ extension MessageStore {
|
||||
}
|
||||
|
||||
private func decodeMessageRow(
|
||||
_ row: [Binding?],
|
||||
_ row: Row,
|
||||
columns: MessageRowColumns,
|
||||
fallbackChatID: Int64?
|
||||
) throws -> DecodedMessageRow {
|
||||
let rowID = int64Value(row[columns.rowID]) ?? 0
|
||||
let resolvedChatID = columns.chatID.flatMap { int64Value(row[$0]) } ?? fallbackChatID ?? 0
|
||||
let handleID = int64Value(row[columns.handleID])
|
||||
let sender = stringValue(row[columns.sender])
|
||||
let text = stringValue(row[columns.text])
|
||||
let date = appleDate(from: int64Value(row[columns.date]))
|
||||
let isFromMe = boolValue(row[columns.isFromMe])
|
||||
let service = stringValue(row[columns.service])
|
||||
let isAudioMessage = boolValue(row[columns.isAudioMessage])
|
||||
let destinationCallerID = stringValue(row[columns.destinationCallerID])
|
||||
let guid = stringValue(row[columns.guid])
|
||||
let associatedGUID = stringValue(row[columns.associatedGUID])
|
||||
let associatedType = intValue(row[columns.associatedType])
|
||||
let attachments = intValue(row[columns.attachments]) ?? 0
|
||||
let body = dataValue(row[columns.body])
|
||||
let threadOriginatorGUID = stringValue(row[columns.threadOriginatorGUID])
|
||||
let rowID = try int64Value(row, columns.rowID) ?? 0
|
||||
let resolvedChatID =
|
||||
try columns.chatID.flatMap { try int64Value(row, $0) } ?? fallbackChatID ?? 0
|
||||
let handleID = try int64Value(row, columns.handleID)
|
||||
let sender = try stringValue(row, columns.sender)
|
||||
let text = try stringValue(row, columns.text)
|
||||
let date = try appleDate(from: int64Value(row, columns.date))
|
||||
let isFromMe = try boolValue(row, columns.isFromMe)
|
||||
let service = try stringValue(row, columns.service)
|
||||
let isAudioMessage = try boolValue(row, columns.isAudioMessage)
|
||||
let destinationCallerID = try stringValue(row, columns.destinationCallerID)
|
||||
let guid = try stringValue(row, columns.guid)
|
||||
let associatedGUID = try stringValue(row, columns.associatedGUID)
|
||||
let associatedType = try intValue(row, columns.associatedType)
|
||||
let attachments = try intValue(row, columns.attachments) ?? 0
|
||||
let body = try dataValue(row, columns.body)
|
||||
let threadOriginatorGUID = try stringValue(row, columns.threadOriginatorGUID)
|
||||
|
||||
var resolvedText = text.isEmpty ? TypedStreamParser.parseAttributedBody(body) : text
|
||||
if isAudioMessage, let transcription = try audioTranscription(for: rowID) {
|
||||
|
||||
@ -62,8 +62,11 @@ extension MessageStore {
|
||||
let destinationCallerColumn = hasDestinationCallerID ? "m.destination_caller_id" : "NULL"
|
||||
|
||||
var sql = """
|
||||
SELECT m.ROWID, cmj.chat_id, m.associated_message_type, m.associated_message_guid,
|
||||
m.handle_id, h.id, m.is_from_me, m.date, IFNULL(m.text, '') AS text,
|
||||
SELECT m.ROWID AS reaction_rowid, cmj.chat_id AS chat_id,
|
||||
m.associated_message_type AS associated_message_type,
|
||||
m.associated_message_guid AS associated_message_guid,
|
||||
m.handle_id AS handle_id, h.id AS sender, m.is_from_me AS is_from_me,
|
||||
m.date AS date, IFNULL(m.text, '') AS text,
|
||||
\(destinationCallerColumn) AS destination_caller_id,
|
||||
\(bodyColumn) AS body,
|
||||
orig.ROWID AS orig_rowid
|
||||
@ -87,19 +90,19 @@ extension MessageStore {
|
||||
|
||||
return try withConnection { db in
|
||||
var events: [ReactionEvent] = []
|
||||
for row in try db.prepare(sql, bindings) {
|
||||
let rowID = int64Value(row[0]) ?? 0
|
||||
let resolvedChatID = int64Value(row[1]) ?? chatID ?? 0
|
||||
let typeValue = intValue(row[2]) ?? 0
|
||||
let associatedGUID = stringValue(row[3])
|
||||
// let handleID = int64Value(row[4])
|
||||
var sender = stringValue(row[5])
|
||||
let isFromMe = boolValue(row[6])
|
||||
let date = appleDate(from: int64Value(row[7]))
|
||||
let text = stringValue(row[8])
|
||||
let destinationCallerID = stringValue(row[9])
|
||||
let body = dataValue(row[10])
|
||||
let origRowID = int64Value(row[11])
|
||||
let rows = try db.prepareRowIterator(sql, bindings: bindings)
|
||||
while let row = try rows.failableNext() {
|
||||
let rowID = try int64Value(row, "reaction_rowid") ?? 0
|
||||
let resolvedChatID = try int64Value(row, "chat_id") ?? chatID ?? 0
|
||||
let typeValue = try intValue(row, "associated_message_type") ?? 0
|
||||
let associatedGUID = try stringValue(row, "associated_message_guid")
|
||||
var sender = try stringValue(row, "sender")
|
||||
let isFromMe = try boolValue(row, "is_from_me")
|
||||
let date = try appleDate(from: int64Value(row, "date"))
|
||||
let text = try stringValue(row, "text")
|
||||
let destinationCallerID = try stringValue(row, "destination_caller_id")
|
||||
let body = try dataValue(row, "body")
|
||||
let origRowID = try int64Value(row, "orig_rowid")
|
||||
|
||||
if sender.isEmpty && !destinationCallerID.isEmpty {
|
||||
sender = destinationCallerID
|
||||
|
||||
26
Sources/IMsgCore/MessageStore+SQLRow.swift
Normal file
26
Sources/IMsgCore/MessageStore+SQLRow.swift
Normal file
@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
extension MessageStore {
|
||||
func stringValue(_ row: Row, _ column: String) throws -> String {
|
||||
try row.get(Expression<String?>(column)) ?? ""
|
||||
}
|
||||
|
||||
func int64Value(_ row: Row, _ column: String) throws -> Int64? {
|
||||
try row.get(Expression<Int64?>(column))
|
||||
}
|
||||
|
||||
func intValue(_ row: Row, _ column: String) throws -> Int? {
|
||||
guard let value = try int64Value(row, column) else { return nil }
|
||||
return Int(value)
|
||||
}
|
||||
|
||||
func boolValue(_ row: Row, _ column: String) throws -> Bool {
|
||||
try row.get(Expression<Bool?>(column)) ?? false
|
||||
}
|
||||
|
||||
func dataValue(_ row: Row, _ column: String) throws -> Data {
|
||||
guard let blob = try row.get(Expression<Blob?>(column)) else { return Data() }
|
||||
return Data(blob.bytes)
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ extension MessageStore {
|
||||
let lastAddressedHandleColumn =
|
||||
hasChatLastAddressedHandleColumn ? "IFNULL(c.last_addressed_handle, '')" : "''"
|
||||
let sql = """
|
||||
SELECT c.ROWID, IFNULL(c.chat_identifier, '') AS identifier, IFNULL(c.guid, '') AS guid,
|
||||
SELECT c.ROWID AS chat_rowid, IFNULL(c.chat_identifier, '') AS identifier, IFNULL(c.guid, '') AS guid,
|
||||
IFNULL(c.display_name, c.chat_identifier) AS name, IFNULL(c.service_name, '') AS service,
|
||||
\(accountIDColumn) AS account_id,
|
||||
\(accountLoginColumn) AS account_login,
|
||||
@ -24,16 +24,17 @@ extension MessageStore {
|
||||
"""
|
||||
let bindings: [Binding?] = candidates + candidates
|
||||
return try withConnection { db in
|
||||
guard let row = try db.prepare(sql, bindings).makeIterator().next() else { return nil }
|
||||
let rows = try db.prepareRowIterator(sql, bindings: bindings)
|
||||
guard let row = try rows.failableNext() else { return nil }
|
||||
return ChatInfo(
|
||||
id: int64Value(row[0]) ?? 0,
|
||||
identifier: stringValue(row[1]),
|
||||
guid: stringValue(row[2]),
|
||||
name: stringValue(row[3]),
|
||||
service: stringValue(row[4]),
|
||||
accountID: stringValue(row[5]).nilIfEmpty,
|
||||
accountLogin: stringValue(row[6]).nilIfEmpty,
|
||||
lastAddressedHandle: stringValue(row[7]).nilIfEmpty
|
||||
id: try int64Value(row, "chat_rowid") ?? 0,
|
||||
identifier: try stringValue(row, "identifier"),
|
||||
guid: try stringValue(row, "guid"),
|
||||
name: try stringValue(row, "name"),
|
||||
service: try stringValue(row, "service"),
|
||||
accountID: try stringValue(row, "account_id").nilIfEmpty,
|
||||
accountLogin: try stringValue(row, "account_login").nilIfEmpty,
|
||||
lastAddressedHandle: try stringValue(row, "last_addressed_handle").nilIfEmpty
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -47,7 +48,7 @@ extension MessageStore {
|
||||
|
||||
let placeholders = Array(repeating: "?", count: candidates.count).joined(separator: ",")
|
||||
let sql = """
|
||||
SELECT m.ROWID
|
||||
SELECT m.ROWID AS message_rowid
|
||||
FROM message m
|
||||
LEFT JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
|
||||
LEFT JOIN handle h ON h.ROWID = m.handle_id
|
||||
@ -61,8 +62,9 @@ extension MessageStore {
|
||||
"""
|
||||
let bindings: [Binding?] = [MessageStore.appleEpoch(date)] + candidates
|
||||
return try withConnection { db in
|
||||
guard let row = try db.prepare(sql, bindings).makeIterator().next() else { return nil }
|
||||
return int64Value(row[0])
|
||||
let rows = try db.prepareRowIterator(sql, bindings: bindings)
|
||||
guard let row = try rows.failableNext() else { return nil }
|
||||
return try int64Value(row, "message_rowid")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -210,20 +210,22 @@ extension MessageStore {
|
||||
options: AttachmentQueryOptions = .default
|
||||
) throws -> [AttachmentMeta] {
|
||||
let sql = """
|
||||
SELECT a.filename, a.transfer_name, a.uti, a.mime_type, a.total_bytes, a.is_sticker
|
||||
SELECT a.filename AS filename, a.transfer_name AS transfer_name, a.uti AS uti,
|
||||
a.mime_type AS mime_type, a.total_bytes AS total_bytes, a.is_sticker AS is_sticker
|
||||
FROM message_attachment_join maj
|
||||
JOIN attachment a ON a.ROWID = maj.attachment_id
|
||||
WHERE maj.message_id = ?
|
||||
"""
|
||||
return try withConnection { db in
|
||||
var metas: [AttachmentMeta] = []
|
||||
for row in try db.prepare(sql, messageID) {
|
||||
let filename = stringValue(row[0])
|
||||
let transferName = stringValue(row[1])
|
||||
let uti = stringValue(row[2])
|
||||
let mimeType = stringValue(row[3])
|
||||
let totalBytes = int64Value(row[4]) ?? 0
|
||||
let isSticker = boolValue(row[5])
|
||||
let rows = try db.prepareRowIterator(sql, bindings: [messageID])
|
||||
while let row = try rows.failableNext() {
|
||||
let filename = try stringValue(row, "filename")
|
||||
let transferName = try stringValue(row, "transfer_name")
|
||||
let uti = try stringValue(row, "uti")
|
||||
let mimeType = try stringValue(row, "mime_type")
|
||||
let totalBytes = try int64Value(row, "total_bytes") ?? 0
|
||||
let isSticker = try boolValue(row, "is_sticker")
|
||||
metas.append(
|
||||
AttachmentResolver.metadata(
|
||||
filename: filename,
|
||||
@ -249,8 +251,9 @@ extension MessageStore {
|
||||
LIMIT 1
|
||||
"""
|
||||
return try withConnection { db in
|
||||
for row in try db.prepare(sql, messageID) {
|
||||
let info = dataValue(row[0])
|
||||
let rows = try db.prepareRowIterator(sql, bindings: [messageID])
|
||||
while let row = try rows.failableNext() {
|
||||
let info = try dataValue(row, "user_info")
|
||||
guard !info.isEmpty else { continue }
|
||||
if let transcription = parseAudioTranscription(from: info) {
|
||||
return transcription
|
||||
@ -295,7 +298,8 @@ extension MessageStore {
|
||||
// where X is the part index (0 for single-part messages) and GUID matches the original message's guid
|
||||
let bodyColumn = hasAttributedBody ? "r.attributedBody" : "NULL"
|
||||
let sql = """
|
||||
SELECT r.ROWID, r.associated_message_type, h.id, r.is_from_me, r.date, IFNULL(r.text, '') as text,
|
||||
SELECT r.ROWID AS reaction_rowid, r.associated_message_type AS associated_message_type,
|
||||
h.id AS sender, r.is_from_me AS is_from_me, r.date AS date, IFNULL(r.text, '') AS text,
|
||||
\(bodyColumn) AS body
|
||||
FROM message m
|
||||
JOIN message r ON r.associated_message_guid = m.guid
|
||||
@ -311,14 +315,15 @@ extension MessageStore {
|
||||
return try withConnection { db in
|
||||
var reactions: [Reaction] = []
|
||||
var reactionIndex: [ReactionKey: Int] = [:]
|
||||
for row in try db.prepare(sql, messageID) {
|
||||
let rowID = int64Value(row[0]) ?? 0
|
||||
let typeValue = intValue(row[1]) ?? 0
|
||||
let sender = stringValue(row[2])
|
||||
let isFromMe = boolValue(row[3])
|
||||
let date = appleDate(from: int64Value(row[4]))
|
||||
let text = stringValue(row[5])
|
||||
let body = dataValue(row[6])
|
||||
let rows = try db.prepareRowIterator(sql, bindings: [messageID])
|
||||
while let row = try rows.failableNext() {
|
||||
let rowID = try int64Value(row, "reaction_rowid") ?? 0
|
||||
let typeValue = try intValue(row, "associated_message_type") ?? 0
|
||||
let sender = try stringValue(row, "sender")
|
||||
let isFromMe = try boolValue(row, "is_from_me")
|
||||
let date = try appleDate(from: int64Value(row, "date"))
|
||||
let text = try stringValue(row, "text")
|
||||
let body = try dataValue(row, "body")
|
||||
let resolvedText = text.isEmpty ? TypedStreamParser.parseAttributedBody(body) : text
|
||||
|
||||
if ReactionType.isReactionRemove(typeValue) {
|
||||
|
||||
@ -48,6 +48,19 @@ func chatInfoReturnsMetadata() throws {
|
||||
#expect(info?.lastAddressedHandle == "+15551234567")
|
||||
}
|
||||
|
||||
@Test
|
||||
func sqlRowDecodingThrowsWhenRequiredAliasIsMissing() throws {
|
||||
let db = try Connection(.inMemory)
|
||||
let store = try MessageStore(connection: db, path: ":memory:")
|
||||
try store.withConnection { db in
|
||||
let rows = try db.prepareRowIterator("SELECT 1 AS actual_value")
|
||||
let row = try #require(try rows.failableNext())
|
||||
#expect(throws: (any Error).self) {
|
||||
_ = try store.int64Value(row, "expected_value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func participantsReturnsUniqueHandles() throws {
|
||||
let db = try Connection(.inMemory)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user