1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-27 19:52:17 -05:00

Use separate table to store the thread structure

This commit is contained in:
cketti 2013-01-11 03:40:35 +01:00
parent 3f84bb54f2
commit 1df88ea153
7 changed files with 617 additions and 291 deletions

View File

@ -711,8 +711,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
public void showThread(Account account, String folderName, long threadRootId) {
LocalSearch tmpSearch = new LocalSearch();
tmpSearch.addAccountUuid(account.getUuid());
tmpSearch.and(Searchfield.THREAD_ROOT, String.valueOf(threadRootId), Attribute.EQUALS);
tmpSearch.or(new SearchCondition(Searchfield.ID, Attribute.EQUALS, String.valueOf(threadRootId)));
tmpSearch.and(Searchfield.THREAD_ID, String.valueOf(threadRootId), Attribute.EQUALS);
MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, true, false);
addMessageListFragment(fragment, true);

View File

@ -3758,8 +3758,9 @@ public class MessagingController implements Runnable {
List<Message> messagesInThreads = new ArrayList<Message>();
for (Message message : messages) {
long rootId = ((LocalMessage) message).getRootId();
long threadId = (rootId == -1) ? message.getId() : rootId;
LocalMessage localMessage = (LocalMessage) message;
long rootId = localMessage.getRootId();
long threadId = (rootId == -1) ? localMessage.getThreadId() : rootId;
Message[] messagesInThread = localStore.getMessagesInThread(threadId);
Collections.addAll(messagesInThreads, messagesInThread);

View File

@ -91,8 +91,12 @@ import com.fsck.k9.mail.store.LocalStore.LocalFolder;
import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.provider.EmailProvider.MessageColumns;
import com.fsck.k9.provider.EmailProvider.SpecialColumns;
import com.fsck.k9.provider.EmailProvider.ThreadColumns;
import com.fsck.k9.search.ConditionsTreeNode;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchSpecification;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
import com.fsck.k9.search.SearchSpecification.Searchfield;
import com.fsck.k9.search.SqlQueryBuilder;
import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshListView;
@ -117,11 +121,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
MessageColumns.ATTACHMENT_COUNT,
MessageColumns.FOLDER_ID,
MessageColumns.PREVIEW,
MessageColumns.THREAD_ROOT,
ThreadColumns.ROOT,
SpecialColumns.ACCOUNT_UUID,
SpecialColumns.FOLDER_NAME,
MessageColumns.THREAD_COUNT,
SpecialColumns.THREAD_COUNT,
};
private static final int ID_COLUMN = 0;
@ -2083,15 +2087,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
accountMapping.put(account, messageIdList);
}
long selectionId;
if (mThreadedList) {
selectionId = (cursor.isNull(THREAD_ROOT_COLUMN)) ?
cursor.getLong(ID_COLUMN) : cursor.getLong(THREAD_ROOT_COLUMN);
} else {
selectionId = cursor.getLong(ID_COLUMN);
}
messageIdList.add(selectionId);
messageIdList.add(cursor.getLong(ID_COLUMN));
}
}
@ -2888,19 +2884,30 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
String accountUuid = mAccountUuids[id];
Account account = mPreferences.getAccount(accountUuid);
String threadId = getThreadId(mSearch);
Uri uri;
String[] projection;
if (mThreadedList) {
boolean needConditions;
if (threadId != null) {
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/thread/" + threadId);
projection = PROJECTION;
needConditions = false;
} else if (mThreadedList) {
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages/threaded");
projection = THREADED_PROJECTION;
needConditions = true;
} else {
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages");
projection = PROJECTION;
needConditions = true;
}
StringBuilder query = new StringBuilder();
List<String> queryArgs = new ArrayList<String>();
if (needConditions) {
SqlQueryBuilder.buildWhereClause(account, mSearch.getConditions(), query, queryArgs);
}
String selection = query.toString();
String[] selectionArgs = queryArgs.toArray(new String[0]);
@ -2911,6 +2918,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
sortOrder);
}
private String getThreadId(LocalSearch search) {
for (ConditionsTreeNode node : search.getLeafSet()) {
SearchCondition condition = node.mCondition;
if (condition.field == Searchfield.THREAD_ID) {
return condition.value;
}
}
return null;
}
private String buildSortOrder() {
String sortColumn = MessageColumns.ID;
switch (mSortType) {

View File

@ -74,7 +74,6 @@ import com.fsck.k9.provider.AttachmentProvider;
import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchSpecification.Attribute;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
import com.fsck.k9.search.SearchSpecification.Searchfield;
import com.fsck.k9.search.SqlQueryBuilder;
@ -96,8 +95,10 @@ public class LocalStore extends Store implements Serializable {
* in the correct order.
*/
static private String GET_MESSAGES_COLS =
"subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, "
+ "bcc_list, reply_to_list, attachment_count, internal_date, message_id, folder_id, preview, thread_root, thread_parent, deleted, read, flagged, answered, forwarded ";
"subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, " +
"bcc_list, reply_to_list, attachment_count, internal_date, messages.message_id, " +
"folder_id, preview, threads.id, threads.root, deleted, read, flagged, answered, " +
"forwarded ";
static private String GET_FOLDER_COLS = "folders.id, name, SUM(read=0), visible_limit, last_updated, status, push_state, last_pushed, SUM(flagged), integrate, top_group, poll_class, push_class, display_class";
@ -117,7 +118,7 @@ public class LocalStore extends Store implements Serializable {
*/
private static final int FLAG_UPDATE_BATCH_SIZE = 500;
public static final int DB_VERSION = 46;
public static final int DB_VERSION = 47;
protected String uUid = null;
@ -204,8 +205,6 @@ public class LocalStore extends Store implements Serializable {
"message_id TEXT, " +
"preview TEXT, " +
"mime_type TEXT, "+
"thread_root INTEGER, " +
"thread_parent INTEGER, " +
"normalized_subject_hash INTEGER, " +
"empty INTEGER, " +
"read INTEGER default 0, " +
@ -226,18 +225,30 @@ public class LocalStore extends Store implements Serializable {
db.execSQL("DROP INDEX IF EXISTS msg_empty");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_empty ON messages (empty)");
db.execSQL("DROP INDEX IF EXISTS msg_thread_root");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_root ON messages (thread_root)");
db.execSQL("DROP INDEX IF EXISTS msg_thread_parent");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_parent ON messages (thread_parent)");
db.execSQL("DROP INDEX IF EXISTS msg_read");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_read ON messages (read)");
db.execSQL("DROP INDEX IF EXISTS msg_flagged");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_flagged ON messages (flagged)");
db.execSQL("DROP TABLE IF EXISTS threads");
db.execSQL("CREATE TABLE threads (" +
"id INTEGER PRIMARY KEY, " +
"message_id INTEGER, " +
"root INTEGER, " +
"parent INTEGER" +
")");
db.execSQL("DROP INDEX IF EXISTS threads_message_id");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_message_id ON threads (message_id)");
db.execSQL("DROP INDEX IF EXISTS threads_root");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_root ON threads (root)");
db.execSQL("DROP INDEX IF EXISTS threads_parent");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_parent ON threads (parent)");
db.execSQL("DROP TABLE IF EXISTS attachments");
db.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER,"
+ "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT,"
@ -539,6 +550,76 @@ public class LocalStore extends Store implements Serializable {
db.execSQL("CREATE INDEX IF NOT EXISTS msg_read ON messages (read)");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_flagged ON messages (flagged)");
}
if (db.getVersion() < 47) {
// Create new 'threads' table
db.execSQL("DROP TABLE IF EXISTS threads");
db.execSQL("CREATE TABLE threads (" +
"id INTEGER PRIMARY KEY, " +
"message_id INTEGER, " +
"root INTEGER, " +
"parent INTEGER" +
")");
// Create indices for new table
db.execSQL("DROP INDEX IF EXISTS threads_message_id");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_message_id ON threads (message_id)");
db.execSQL("DROP INDEX IF EXISTS threads_root");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_root ON threads (root)");
db.execSQL("DROP INDEX IF EXISTS threads_parent");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_parent ON threads (parent)");
// Create entries for all messages in 'threads' table
db.execSQL("INSERT INTO threads (message_id) SELECT id FROM messages");
// Copy thread structure from 'messages' table to 'threads'
Cursor cursor = db.query("messages",
new String[] { "id", "thread_root", "thread_parent" },
null, null, null, null, null);
try {
ContentValues cv = new ContentValues();
while (cursor.moveToNext()) {
cv.clear();
long messageId = cursor.getLong(0);
if (!cursor.isNull(1)) {
long threadRootMessageId = cursor.getLong(1);
db.execSQL("UPDATE threads SET root = (SELECT t.id FROM " +
"threads t WHERE t.message_id = ?) " +
"WHERE message_id = ?",
new String[] {
Long.toString(threadRootMessageId),
Long.toString(messageId)
});
}
if (!cursor.isNull(2)) {
long threadParentMessageId = cursor.getLong(2);
db.execSQL("UPDATE threads SET parent = (SELECT t.id FROM " +
"threads t WHERE t.message_id = ?) " +
"WHERE message_id = ?",
new String[] {
Long.toString(threadParentMessageId),
Long.toString(messageId)
});
}
}
} finally {
cursor.close();
}
// Remove indices for old thread-related columns in 'messages' table
db.execSQL("DROP INDEX IF EXISTS msg_thread_root");
db.execSQL("DROP INDEX IF EXISTS msg_thread_parent");
// Clear out old thread-related columns in 'messages'
ContentValues cv = new ContentValues();
cv.putNull("thread_root");
cv.putNull("thread_parent");
db.update("messages", cv, null, null);
}
}
} catch (SQLiteException e) {
Log.e(K9.LOG_TAG, "Exception while upgrading database. Resetting the DB to v0");
@ -671,8 +752,21 @@ public class LocalStore extends Store implements Serializable {
database.execute(false, new DbCallback<Void>() {
@Override
public Void doDbWork(final SQLiteDatabase db) {
db.execSQL("DELETE FROM messages WHERE deleted = 0 and uid not like 'Local%'");
db.execSQL("update folders set flagged_count = 0, unread_count = 0");
// Delete entries from 'threads' table
db.execSQL("DELETE FROM threads WHERE message_id IN " +
"(SELECT id FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%')");
// Set 'root' and 'parent' of remaining entries in 'thread' table to 'NULL' to make
// sure the thread structure is in a valid state (this may destroy existing valid
// thread trees, but is much faster than adjusting the tree by removing messages
// one by one).
ContentValues cv = new ContentValues();
cv.putNull("root");
cv.putNull("parent");
db.update("threads", cv, null, null);
// Delete entries from 'messages' table
db.execSQL("DELETE FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%'");
return null;
}
});
@ -979,6 +1073,7 @@ public class LocalStore extends Store implements Serializable {
String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY);
String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages " +
"JOIN threads ON (threads.message_id = messages.id) " +
"LEFT JOIN folders ON (folders.id = messages.folder_id) WHERE " +
"((empty IS NULL OR empty != 1) AND deleted = 0)" +
((!StringUtils.isNullOrEmpty(where)) ? " AND (" + where + ")" : "") +
@ -1052,8 +1147,7 @@ public class LocalStore extends Store implements Serializable {
String rootIdString = Long.toString(rootId);
LocalSearch search = new LocalSearch();
search.and(Searchfield.THREAD_ROOT, rootIdString, Attribute.EQUALS);
search.or(new SearchCondition(Searchfield.ID, Attribute.EQUALS, rootIdString));
search.and(Searchfield.THREAD_ID, rootIdString, Attribute.EQUALS);
return searchForMessages(null, search);
}
@ -1947,9 +2041,11 @@ public class LocalStore extends Store implements Serializable {
try {
cursor = db.rawQuery(
"SELECT "
+ GET_MESSAGES_COLS
+ "FROM messages WHERE uid = ? AND folder_id = ?",
"SELECT " +
GET_MESSAGES_COLS +
"FROM messages " +
"JOIN threads ON (threads.message_id = messages.id) " +
"WHERE uid = ? AND folder_id = ?",
new String[] {
message.getUid(), Long.toString(mFolderId)
});
@ -1987,13 +2083,13 @@ public class LocalStore extends Store implements Serializable {
return LocalStore.this.getMessages(
listener,
LocalFolder.this,
"SELECT " + GET_MESSAGES_COLS
+ "FROM messages WHERE (empty IS NULL OR empty != 1) AND "
+ (includeDeleted ? "" : "deleted = 0 AND ")
+ " folder_id = ? ORDER BY date DESC"
, new String[] {
Long.toString(mFolderId)
}
"SELECT " + GET_MESSAGES_COLS +
"FROM messages " +
"JOIN threads ON (threads.message_id = messages.id) " +
"WHERE (empty IS NULL OR empty != 1) AND " +
(includeDeleted ? "" : "deleted = 0 AND ") +
"folder_id = ? ORDER BY date DESC",
new String[] { Long.toString(mFolderId) }
);
} catch (MessagingException e) {
throw new WrappedException(e);
@ -2065,36 +2161,34 @@ public class LocalStore extends Store implements Serializable {
ThreadInfo threadInfo = lDestFolder.doMessageThreading(db, message);
/*
* "Move" the message into the new folder and thread structure
* "Move" the message into the new folder
*/
long id = lMessage.getId();
String[] idArg = new String[] { Long.toString(id) };
long msgId = lMessage.getId();
String[] idArg = new String[] { Long.toString(msgId) };
ContentValues cv = new ContentValues();
cv.put("folder_id", lDestFolder.getId());
cv.put("uid", newUid);
db.update("messages", cv, "id = ?", idArg);
// Create/update entry in 'threads' table for the message in the
// target folder
cv.clear();
cv.put("message_id", msgId);
if (threadInfo.threadId == -1) {
if (threadInfo.rootId != -1) {
cv.put("thread_root", threadInfo.rootId);
cv.put("root", threadInfo.rootId);
}
if (threadInfo.parentId != -1) {
cv.put("thread_parent", threadInfo.parentId);
cv.put("parent", threadInfo.parentId);
}
db.update("messages", cv, "id = ?", idArg);
if (threadInfo.id != -1) {
String[] oldIdArg =
new String[] { Long.toString(threadInfo.id) };
cv.clear();
cv.put("thread_root", id);
db.update("messages", cv, "thread_root = ?", oldIdArg);
cv.clear();
cv.put("thread_parent", id);
db.update("messages", cv, "thread_parent = ?", oldIdArg);
db.insert("threads", null, cv);
} else {
db.update("threads", cv, "id = ?",
new String[] { Long.toString(threadInfo.threadId) });
}
/*
@ -2112,29 +2206,19 @@ public class LocalStore extends Store implements Serializable {
cv.put("read", 1);
cv.put("deleted", 1);
cv.put("folder_id", mFolderId);
cv.put("empty", 0);
String messageId = message.getMessageId();
if (messageId != null) {
cv.put("message_id", messageId);
cv.put("empty", 1);
long rootId = lMessage.getRootId();
if (rootId != -1) {
cv.put("thread_root", rootId);
}
long parentId = lMessage.getParentId();
if (parentId != -1) {
cv.put("thread_parent", parentId);
}
}
final long newId;
if (threadInfo.id != -1) {
if (threadInfo.msgId != -1) {
// There already existed an empty message in the target folder.
// Let's use it as placeholder.
newId = threadInfo.id;
newId = threadInfo.msgId;
db.update("messages", cv, "id = ?",
new String[] { Long.toString(newId) });
@ -2143,27 +2227,14 @@ public class LocalStore extends Store implements Serializable {
}
/*
* Replace all "thread links" to the original message with links to
* the placeholder message.
* Update old entry in 'threads' table to point to the newly
* created placeholder.
*/
String[] whereArgs = new String[] {
Long.toString(mFolderId),
Long.toString(id) };
// Note: If there was an empty message in the target folder we
// already reconnected some messages to point to 'id'. We don't
// want to change those links again, so we limit the update
// statements below to the source folder.
cv.clear();
cv.put("thread_root", newId);
db.update("messages", cv, "folder_id = ? AND thread_root = ?",
whereArgs);
cv.clear();
cv.put("thread_parent", newId);
db.update("messages", cv, "folder_id = ? AND thread_parent = ?",
whereArgs);
cv.put("message_id", newId);
db.update("threads", cv, "id = ?",
new String[] { Long.toString(lMessage.getThreadId()) });
}
} catch (MessagingException e) {
throw new WrappedException(e);
@ -2285,21 +2356,23 @@ public class LocalStore extends Store implements Serializable {
}
private ThreadInfo getThreadInfo(SQLiteDatabase db, String messageId) {
Cursor cursor = db.query("messages",
new String[] { "id", "thread_root", "thread_parent" },
"folder_id=? AND message_id=?",
new String[] { Long.toString(mFolderId), messageId },
null, null, null);
String sql = "SELECT t.id, t.message_id, t.root, t.parent " +
"FROM threads t " +
"JOIN messages m ON (t.message_id = m.id) " +
"WHERE m.folder_id = ? AND m.message_id = ?";
String[] selectionArgs = { Long.toString(mFolderId), messageId };
Cursor cursor = db.rawQuery(sql, selectionArgs);
if (cursor != null) {
try {
if (cursor.getCount() == 1) {
cursor.moveToFirst();
long id = cursor.getLong(0);
long rootId = (cursor.isNull(1)) ? -1 : cursor.getLong(1);
long parentId = (cursor.isNull(2)) ? -1 : cursor.getLong(2);
long threadId = cursor.getLong(0);
long msgId = cursor.getLong(1);
long rootId = (cursor.isNull(2)) ? -1 : cursor.getLong(2);
long parentId = (cursor.isNull(3)) ? -1 : cursor.getLong(3);
return new ThreadInfo(id, messageId, rootId, parentId);
return new ThreadInfo(threadId, msgId, messageId, rootId, parentId);
}
} finally {
cursor.close();
@ -2374,7 +2447,7 @@ public class LocalStore extends Store implements Serializable {
if (oldMessageId == -1) {
// This is a new message. Do the message threading.
ThreadInfo threadInfo = doMessageThreading(db, message);
oldMessageId = threadInfo.id;
oldMessageId = threadInfo.msgId;
rootId = threadInfo.rootId;
parentId = threadInfo.parentId;
}
@ -2437,25 +2510,32 @@ public class LocalStore extends Store implements Serializable {
cv.put("message_id", messageId);
}
if (rootId != -1) {
cv.put("thread_root", rootId);
}
if (parentId != -1) {
cv.put("thread_parent", parentId);
}
long messageUid;
long msgId;
if (oldMessageId == -1) {
messageUid = db.insert("messages", "uid", cv);
msgId = db.insert("messages", "uid", cv);
// Create entry in 'threads' table
cv.clear();
cv.put("message_id", msgId);
if (rootId != -1) {
cv.put("root", rootId);
}
if (parentId != -1) {
cv.put("parent", parentId);
}
db.insert("threads", null, cv);
} else {
db.update("messages", cv, "id = ?", new String[] { Long.toString(oldMessageId) });
messageUid = oldMessageId;
msgId = oldMessageId;
}
for (Part attachment : attachments) {
saveAttachment(messageUid, attachment, copy);
saveAttachment(msgId, attachment, copy);
}
saveHeaders(messageUid, (MimeMessage)message);
saveHeaders(msgId, (MimeMessage)message);
} catch (Exception e) {
throw new MessagingException("Error appending message", e);
}
@ -2852,47 +2932,69 @@ public class LocalStore extends Store implements Serializable {
throw new MessagingException("Cannot call getUidFromMessageId on LocalFolder");
}
private void clearMessagesWhere(final String whereClause, final String[] params) throws MessagingException {
public void clearMessagesOlderThan(long cutoff) throws MessagingException {
open(OpenMode.READ_ONLY);
Message[] messages = LocalStore.this.getMessages(
null,
this,
"SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE (empty IS NULL OR empty != 1) AND (" + whereClause + ")",
params);
"SELECT " + GET_MESSAGES_COLS +
"FROM messages " +
"JOIN threads ON (threads.message_id = messages.id) " +
"WHERE (empty IS NULL OR empty != 1) AND " +
"(folder_id = ? and date < ?)",
new String[] {
Long.toString(mFolderId), Long.toString(cutoff)
});
for (Message message : messages) {
deleteAttachments(message.getUid());
message.destroy();
}
database.execute(false, new DbCallback<Void>() {
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
db.execSQL("DELETE FROM messages WHERE " + whereClause, params);
return null;
}
});
notifyChange();
}
public void clearMessagesOlderThan(long cutoff) throws MessagingException {
final String where = "folder_id = ? and date < ?";
final String[] params = new String[] {
Long.toString(mFolderId), Long.toString(cutoff)
};
public void clearAllMessages() throws MessagingException {
final String[] folderIdArg = new String[] { Long.toString(mFolderId) };
clearMessagesWhere(where, params);
open(OpenMode.READ_ONLY);
try {
database.execute(false, new DbCallback<Void>() {
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
try {
// Get UIDs for all messages to delete
Cursor cursor = db.query("messages", new String[] { "uid" },
"folder_id = ? AND (empty IS NULL OR empty != 1)",
folderIdArg, null, null, null);
try {
// Delete attachments of these messages
while (cursor.moveToNext()) {
deleteAttachments(cursor.getString(0));
}
} finally {
cursor.close();
}
// Delete entries in 'threads' and 'messages'
db.execSQL("DELETE FROM threads WHERE message_id IN " +
"(SELECT id FROM messages WHERE folder_id = ?)", folderIdArg);
db.execSQL("DELETE FROM messages WHERE folder_id = ?", folderIdArg);
return null;
} catch (MessagingException e) {
throw new WrappedException(e);
}
}
});
} catch (WrappedException e) {
throw(MessagingException) e.getCause();
}
public void clearAllMessages() throws MessagingException {
final String where = "folder_id = ?";
final String[] params = new String[] {
Long.toString(mFolderId)
};
notifyChange();
clearMessagesWhere(where, params);
setPushState(null);
setLastPush(0);
setLastChecked(0);
@ -3142,7 +3244,7 @@ public class LocalStore extends Store implements Serializable {
String messageId = message.getMessageId();
// If there's already an empty message in the database, update that
long id = getDatabaseIdByMessageId(db, messageId, true);
ThreadInfo msgThreadInfo = getThreadInfo(db, messageId);
// Get the message IDs from the "References" header line
String[] referencesArray = message.getHeader("References");
@ -3168,56 +3270,73 @@ public class LocalStore extends Store implements Serializable {
if (messageIds == null) {
// This is not a reply, nothing to do for us.
return new ThreadInfo(id, messageId, -1, -1);
return (msgThreadInfo != null) ?
msgThreadInfo : new ThreadInfo(-1, -1, messageId, -1, -1);
}
for (String reference : messageIds) {
ThreadInfo threadInfo = getThreadInfo(db, reference);
if (threadInfo == null) {
// Create placeholder message
// Create placeholder message in 'messages' table
ContentValues cv = new ContentValues();
cv.put("message_id", reference);
cv.put("folder_id", mFolderId);
cv.put("empty", 1);
long newMsgId = db.insert("messages", null, cv);
// Create entry in 'threads' table
cv.clear();
cv.put("message_id", newMsgId);
if (rootId != -1) {
cv.put("thread_root", rootId);
cv.put("root", rootId);
}
if (parentId != -1) {
cv.put("thread_parent", parentId);
cv.put("parent", parentId);
}
parentId = db.insert("messages", null, cv);
parentId = db.insert("threads", null, cv);
if (rootId == -1) {
rootId = parentId;
}
} else {
if (rootId != -1 && threadInfo.rootId == -1 && rootId != threadInfo.id) {
if (rootId != -1 && threadInfo.rootId == -1 && rootId != threadInfo.threadId) {
// We found an existing root container that is not
// the root of our current path (References).
// Connect it to the current parent.
// Let all children know who's the new root
ContentValues cv = new ContentValues();
cv.put("thread_root", rootId);
db.update("messages", cv, "thread_root=?",
new String[] { Long.toString(threadInfo.id) });
cv.put("root", rootId);
db.update("threads", cv, "root = ?",
new String[] { Long.toString(threadInfo.threadId) });
// Connect the message to the current parent
cv.put("thread_parent", parentId);
db.update("messages", cv, "id=?",
new String[] { Long.toString(threadInfo.id) });
cv.put("parent", parentId);
db.update("threads", cv, "id = ?",
new String[] { Long.toString(threadInfo.threadId) });
} else {
rootId = (threadInfo.rootId == -1) ? threadInfo.id : threadInfo.rootId;
rootId = (threadInfo.rootId == -1) ?
threadInfo.threadId : threadInfo.rootId;
}
parentId = threadInfo.id;
parentId = threadInfo.threadId;
}
}
//TODO: set in-reply-to "link" even if one already exists
return new ThreadInfo(id, messageId, rootId, parentId);
long threadId;
long msgId;
if (msgThreadInfo != null) {
threadId = msgThreadInfo.threadId;
msgId = msgThreadInfo.msgId;
} else {
threadId = -1;
msgId = -1;
}
return new ThreadInfo(threadId, msgId, messageId, rootId, parentId);
}
public List<Message> extractNewMessages(final List<Message> messages)
@ -3328,8 +3447,8 @@ public class LocalStore extends Store implements Serializable {
private boolean mHeadersLoaded = false;
private boolean mMessageDirty = false;
private long mThreadId;
private long mRootId;
private long mParentId;
public LocalMessage() {
}
@ -3385,8 +3504,8 @@ public class LocalStore extends Store implements Serializable {
this.mFolder = f;
}
mRootId = (cursor.isNull(15)) ? -1 : cursor.getLong(15);
mParentId = (cursor.isNull(16)) ? -1 : cursor.getLong(16);
mThreadId = (cursor.isNull(15)) ? -1 : cursor.getLong(15);
mRootId = (cursor.isNull(16)) ? -1 : cursor.getLong(16);
boolean deleted = (cursor.getInt(17) == 1);
boolean read = (cursor.getInt(18) == 1);
@ -3656,17 +3775,9 @@ public class LocalStore extends Store implements Serializable {
localFolder.deleteAttachments(mId);
String id = Long.toString(mId);
// Check if this message has children in the thread hierarchy
Cursor cursor = db.query("messages", new String[] { "COUNT(id)" },
"thread_root = ? OR thread_parent = ?",
new String[] {id, id},
null, null, null);
try {
if (cursor.moveToFirst() && cursor.getLong(0) > 0) {
// Make the message an empty message
if (hasThreadChildren(db, mId)) {
// This message has children in the thread structure so we need to
// make it an empty message.
ContentValues cv = new ContentValues();
cv.put("id", mId);
cv.put("folder_id", localFolder.getId());
@ -3674,79 +3785,37 @@ public class LocalStore extends Store implements Serializable {
cv.put("message_id", getMessageId());
cv.put("empty", 1);
if (getRootId() != -1) {
cv.put("thread_root", getRootId());
}
if (getParentId() != -1) {
cv.put("thread_parent", getParentId());
}
db.replace("messages", null, cv);
// Nothing else to do
return null;
}
} finally {
cursor.close();
}
long parentId = getParentId();
// Get the message ID of the parent message if it's empty
long currentId = getEmptyThreadParent(db, mId);
// Check if 'parentId' is empty
cursor = db.query("messages", new String[] { "id" },
"id = ? AND empty = 1",
new String[] { Long.toString(parentId) },
null, null, null);
// Delete the placeholder message
deleteMessageRow(db, mId);
try {
if (cursor.getCount() == 0) {
// If the message isn't empty we skip the loop below
parentId = -1;
}
} finally {
cursor.close();
}
/*
* Walk the thread tree to delete all empty parents without children
*/
while (parentId != -1) {
String parentIdString = Long.toString(parentId);
// Get the parent of the message 'parentId'
cursor = db.query("messages", new String[] { "thread_parent" },
"id = ? AND empty = 1",
new String[] { parentIdString },
null, null, null);
try {
if (cursor.moveToFirst() && !cursor.isNull(0)) {
parentId = cursor.getLong(0);
} else {
parentId = -1;
}
} finally {
cursor.close();
}
// Check if (the old) 'parentId' has any children
cursor = db.query("messages", new String[] { "COUNT(id)" },
"thread_parent = ? AND id != ?",
new String[] { parentIdString, id },
null, null, null);
try {
if (cursor.moveToFirst() && cursor.getLong(0) == 0) {
// If it has no children we can remove it
db.delete("messages", "id = ?",
new String[] { parentIdString });
} else {
while (currentId != -1) {
if (hasThreadChildren(db, currentId)) {
// We made sure there are no empty leaf nodes and can stop now.
break;
}
} finally {
cursor.close();
}
// Get ID of the (empty) parent for the next iteration
long newId = getEmptyThreadParent(db, currentId);
// Delete the empty message
deleteMessageRow(db, currentId);
currentId = newId;
}
// Remove the placeholder message
db.delete("messages", "id = ?", new String[] { id });
} catch (MessagingException e) {
throw new WrappedException(e);
}
@ -3760,6 +3829,77 @@ public class LocalStore extends Store implements Serializable {
notifyChange();
}
/**
* Get ID of the the given message's parent if the parent is an empty message.
*
* @param db
* {@link SQLiteDatabase} instance to access the database.
* @param messageId
* The database ID of the message to get the parent for.
*
* @return Message ID of the parent message if there exists a parent and it is empty.
* Otherwise {@code -1}.
*/
private long getEmptyThreadParent(SQLiteDatabase db, long messageId) {
Cursor cursor = db.rawQuery(
"SELECT m.id " +
"FROM threads t1 " +
"JOIN threads t2 ON (t1.parent = t2.id) " +
"JOIN messages m ON (t2.message_id = m.id) " +
"WHERE t1.message_id = ? AND m.empty = 1",
new String[] { Long.toString(messageId) });
try {
return (cursor.moveToFirst() && !cursor.isNull(0)) ? cursor.getLong(0) : -1;
} finally {
cursor.close();
}
}
/**
* Check whether or not a message has child messages in the thread structure.
*
* @param db
* {@link SQLiteDatabase} instance to access the database.
* @param messageId
* The database ID of the message to get the children for.
*
* @return {@code true} if the message has children. {@code false} otherwise.
*/
private boolean hasThreadChildren(SQLiteDatabase db, long messageId) {
Cursor cursor = db.rawQuery(
"SELECT COUNT(t2.id) " +
"FROM threads t1 " +
"JOIN threads t2 ON (t2.parent = t1.id) " +
"WHERE t1.message_id = ?",
new String[] { Long.toString(messageId) });
try {
return (cursor.moveToFirst() && !cursor.isNull(0) && cursor.getLong(0) > 0L);
} finally {
cursor.close();
}
}
/**
* Delete a message from the 'messages' and 'threads' tables.
*
* @param db
* {@link SQLiteDatabase} instance to access the database.
* @param messageId
* The database ID of the message to delete.
*/
private void deleteMessageRow(SQLiteDatabase db, long messageId) {
String[] idArg = { Long.toString(messageId) };
// Delete the message
db.delete("messages", "id = ?", idArg);
// Delete row in 'threads' table
// TODO: create trigger for 'messages' table to get rid of the row in 'threads' table
db.delete("threads", "message_id = ?", idArg);
}
private void loadHeaders() throws UnavailableStorageException {
ArrayList<LocalMessage> messages = new ArrayList<LocalMessage>();
messages.add(this);
@ -3818,12 +3958,12 @@ public class LocalStore extends Store implements Serializable {
return message;
}
public long getRootId() {
return mRootId;
public long getThreadId() {
return mThreadId;
}
public long getParentId() {
return mParentId;
public long getRootId() {
return mRootId;
}
}
@ -3893,13 +4033,15 @@ public class LocalStore extends Store implements Serializable {
}
static class ThreadInfo {
public final long id;
public final long threadId;
public final long msgId;
public final String messageId;
public final long rootId;
public final long parentId;
public ThreadInfo(long id, String messageId, long rootId, long parentId) {
this.id = id;
public ThreadInfo(long threadId, long msgId, String messageId, long rootId, long parentId) {
this.threadId = threadId;
this.msgId = msgId;
this.messageId = messageId;
this.rootId = rootId;
this.parentId = parentId;
@ -4045,8 +4187,18 @@ public class LocalStore extends Store implements Serializable {
*
* @throws MessagingException
*/
public void setFlag(final List<Long> messageIds, final Flag flag,
final boolean newState, final boolean threadRootIds) throws MessagingException {
public void setFlag(List<Long> messageIds, Flag flag, boolean newState, boolean threadRootIds)
throws MessagingException {
if (threadRootIds) {
setFlagForThreads(messageIds, flag, newState);
} else {
setFlag(messageIds, flag, newState);
}
}
public void setFlag(final List<Long> messageIds, final Flag flag, final boolean newState)
throws MessagingException {
final ContentValues cv = new ContentValues();
@ -4090,11 +4242,67 @@ public class LocalStore extends Store implements Serializable {
db.update("messages", cv, "(empty IS NULL OR empty != 1) AND id" + selectionSet,
selectionArgs);
if (threadRootIds) {
db.update("messages", cv, "(empty IS NULL OR empty != 1) AND thread_root" +
selectionSet, selectionArgs);
}
@Override
public void postDbWork() {
notifyChange();
}
}, FLAG_UPDATE_BATCH_SIZE);
}
public void setFlagForThreads(final List<Long> messageIds, Flag flag, final boolean newState)
throws MessagingException {
final String flagColumn;
switch (flag) {
case SEEN: {
flagColumn = "read";
break;
}
case FLAGGED: {
flagColumn = "flagged";
break;
}
case ANSWERED: {
flagColumn = "answered";
break;
}
case FORWARDED: {
flagColumn = "forwarded";
break;
}
default: {
throw new IllegalArgumentException("Flag must be a special column flag");
}
}
doBatchSetSelection(new BatchSetSelection() {
@Override
public int getListSize() {
return messageIds.size();
}
@Override
public String getListItem(int index) {
return Long.toString(messageIds.get(index));
}
@Override
public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
throws UnavailableStorageException {
db.execSQL("UPDATE messages SET " + flagColumn + " = " + ((newState) ? "1" : "0") +
" WHERE id IN (" +
"SELECT m.id FROM messages h " +
"JOIN threads t1 ON (t1.message_id = h.id) " +
"JOIN threads t2 ON " +
"(t1.root IN (t2.id, t2.root) OR t1.id IN (t2.id, t2.root)) " +
"JOIN messages m ON (t2.message_id = m.id) " +
"WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " +
"AND h.id" + selectionSet + ")",
selectionArgs);
}
@Override
@ -4140,6 +4348,20 @@ public class LocalStore extends Store implements Serializable {
public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
throws UnavailableStorageException {
if (threadedList) {
String sql = "SELECT m.uid, f.name " +
"FROM messages h " +
"JOIN threads t1 ON (t1.message_id = h.id) " +
"JOIN threads t2 ON " +
"(t1.root IN (t2.id, t2.root) OR t1.id IN (t2.id, t2.root)) " +
"JOIN messages m ON (t2.message_id = m.id) " +
"JOIN folders f ON (m.folder_id = f.id) " +
"WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " +
"AND h.id" + selectionSet;
getDataFromCursor(db.rawQuery(sql, selectionArgs));
} else {
String sqlPrefix =
"SELECT m.uid, f.name " +
"FROM messages m " +
@ -4148,10 +4370,6 @@ public class LocalStore extends Store implements Serializable {
String sql = sqlPrefix + "m.id" + selectionSet;
getDataFromCursor(db.rawQuery(sql, selectionArgs));
if (threadedList) {
String threadSql = sqlPrefix + "m.thread_root" + selectionSet;
getDataFromCursor(db.rawQuery(threadSql, selectionArgs));
}
}

View File

@ -54,7 +54,7 @@ public class EmailProvider extends ContentProvider {
private static final int MESSAGE_BASE = 0;
private static final int MESSAGES = MESSAGE_BASE;
private static final int MESSAGES_THREADED = MESSAGE_BASE + 1;
//private static final int MESSAGES_THREAD = MESSAGE_BASE + 2;
private static final int MESSAGES_THREAD = MESSAGE_BASE + 2;
private static final int STATS_BASE = 100;
private static final int STATS = STATS_BASE;
@ -78,8 +78,6 @@ public class EmailProvider extends ContentProvider {
MessageColumns.ATTACHMENT_COUNT,
MessageColumns.FOLDER_ID,
MessageColumns.PREVIEW,
MessageColumns.THREAD_ROOT,
MessageColumns.THREAD_PARENT,
MessageColumns.READ,
MessageColumns.FLAGGED,
MessageColumns.ANSWERED,
@ -95,6 +93,8 @@ public class EmailProvider extends ContentProvider {
MessageColumns.ID
};
private static final String FOLDERS_TABLE = "folders";
private static final String[] FOLDERS_COLUMNS = {
FolderColumns.ID,
FolderColumns.NAME,
@ -112,12 +112,21 @@ public class EmailProvider extends ContentProvider {
FolderColumns.DISPLAY_CLASS
};
private static final String THREADS_TABLE = "threads";
private static final String[] THREADS_COLUMNS = {
ThreadColumns.ID,
ThreadColumns.MESSAGE_ID,
ThreadColumns.ROOT,
ThreadColumns.PARENT
};
static {
UriMatcher matcher = sUriMatcher;
matcher.addURI(AUTHORITY, "account/*/messages", MESSAGES);
matcher.addURI(AUTHORITY, "account/*/messages/threaded", MESSAGES_THREADED);
//matcher.addURI(AUTHORITY, "account/*/thread/#", MESSAGES_THREAD);
matcher.addURI(AUTHORITY, "account/*/thread/#", MESSAGES_THREAD);
matcher.addURI(AUTHORITY, "account/*/stats", STATS);
}
@ -125,6 +134,8 @@ public class EmailProvider extends ContentProvider {
public interface SpecialColumns {
public static final String ACCOUNT_UUID = "account_uuid";
public static final String THREAD_COUNT = "thread_count";
public static final String FOLDER_NAME = "name";
public static final String INTEGRATE = "integrate";
}
@ -145,9 +156,6 @@ public class EmailProvider extends ContentProvider {
public static final String ATTACHMENT_COUNT = "attachment_count";
public static final String FOLDER_ID = "folder_id";
public static final String PREVIEW = "preview";
public static final String THREAD_ROOT = "thread_root";
public static final String THREAD_PARENT = "thread_parent";
public static final String THREAD_COUNT = "thread_count";
public static final String READ = "read";
public static final String FLAGGED = "flagged";
public static final String ANSWERED = "answered";
@ -179,6 +187,13 @@ public class EmailProvider extends ContentProvider {
public static final String DISPLAY_CLASS = "display_class";
}
public interface ThreadColumns {
public static final String ID = "id";
public static final String MESSAGE_ID = "message_id";
public static final String ROOT = "root";
public static final String PARENT = "parent";
}
public interface StatsColumns {
public static final String UNREAD_COUNT = "unread_count";
public static final String FLAGGED_COUNT = "flagged_count";
@ -216,7 +231,8 @@ public class EmailProvider extends ContentProvider {
Cursor cursor = null;
switch (match) {
case MESSAGES:
case MESSAGES_THREADED: {
case MESSAGES_THREADED:
case MESSAGES_THREAD: {
List<String> segments = uri.getPathSegments();
String accountUuid = segments.get(1);
@ -238,11 +254,16 @@ public class EmailProvider extends ContentProvider {
} else if (match == MESSAGES_THREADED) {
cursor = getThreadedMessages(accountUuid, dbProjection, selection,
selectionArgs, sortOrder);
} else if (match == MESSAGES_THREAD) {
String threadId = segments.get(3);
cursor = getThread(accountUuid, dbProjection, threadId, sortOrder);
} else {
throw new RuntimeException("Not implemented");
}
cursor.setNotificationUri(contentResolver, uri);
Uri notificationUri = Uri.withAppendedPath(CONTENT_URI, "account/" + accountUuid +
"/messages");
cursor.setNotificationUri(contentResolver, notificationUri);
cursor = new SpecialColumnsCursor(new IdTrickeryCursor(cursor), projection,
specialColumns);
@ -328,6 +349,7 @@ public class EmailProvider extends ContentProvider {
}
query.append(" FROM messages m " +
"JOIN threads t ON (t.message_id = m.id) " +
"LEFT JOIN folders f ON (m.folder_id = f.id) " +
"WHERE ");
query.append(SqlQueryBuilder.addPrefixToSelection(FIXUP_MESSAGES_COLUMNS,
@ -374,14 +396,20 @@ public class EmailProvider extends ContentProvider {
if (MessageColumns.DATE.equals(columnName)) {
query.append("MAX(m.date) AS " + MessageColumns.DATE);
} else if (MessageColumns.THREAD_COUNT.equals(columnName)) {
query.append("COUNT(h.id) AS " + MessageColumns.THREAD_COUNT);
} else if (SpecialColumns.THREAD_COUNT.equals(columnName)) {
query.append("COUNT(h.id) AS " + SpecialColumns.THREAD_COUNT);
} else if (SpecialColumns.FOLDER_NAME.equals(columnName)) {
query.append("f." + SpecialColumns.FOLDER_NAME + " AS " +
SpecialColumns.FOLDER_NAME);
} else if (SpecialColumns.INTEGRATE.equals(columnName)) {
query.append("f." + SpecialColumns.INTEGRATE + " AS " +
SpecialColumns.INTEGRATE);
} else if (ThreadColumns.ROOT.equals(columnName)) {
// Always return the thread ID of the root message (even for the root
// message itself)
query.append("CASE WHEN t2." + ThreadColumns.ROOT + " IS NULL THEN " +
"t2." + ThreadColumns.ID + " ELSE t2." + ThreadColumns.ROOT +
" END AS " + ThreadColumns.ROOT);
} else {
query.append("m.");
query.append(columnName);
@ -391,8 +419,10 @@ public class EmailProvider extends ContentProvider {
}
query.append(
" FROM messages h JOIN messages m " +
"ON (h.id = m.thread_root OR h.id = m.id) ");
" FROM messages h " +
"LEFT JOIN threads t1 ON (t1.message_id = h.id) " +
"LEFT JOIN threads t2 ON (t1.id = t2.id OR t1.id = t2.root) " +
"LEFT JOIN messages m ON (m.id = t2.message_id) ");
if (Utility.arrayContainsAny(projection, (Object[]) FOLDERS_COLUMNS)) {
query.append("LEFT JOIN folders f ON (m.folder_id = f.id) ");
@ -400,9 +430,9 @@ public class EmailProvider extends ContentProvider {
query.append(
"WHERE " +
"(m.deleted = 0 AND " +
"(m.empty IS NULL OR m.empty != 1) AND " +
"h.thread_root IS NULL) ");
"(t1.root IS NULL AND " +
"m.deleted = 0 AND " +
"(m.empty IS NULL OR m.empty != 1)) ");
if (!StringUtils.isNullOrEmpty(selection)) {
query.append("AND (");
@ -427,6 +457,63 @@ public class EmailProvider extends ContentProvider {
}
}
protected Cursor getThread(String accountUuid, final String[] projection, final String threadId,
final String sortOrder) {
Account account = getAccount(accountUuid);
LockableDatabase database = getDatabase(account);
try {
return database.execute(false, new DbCallback<Cursor>() {
@Override
public Cursor doDbWork(SQLiteDatabase db) throws WrappedException,
UnavailableStorageException {
StringBuilder query = new StringBuilder();
query.append("SELECT ");
boolean first = true;
for (String columnName : projection) {
if (!first) {
query.append(",");
} else {
first = false;
}
if (MessageColumns.ID.equals(columnName)) {
query.append("m." + MessageColumns.ID + " AS " + MessageColumns.ID);
} else {
query.append(columnName);
}
}
query.append(" FROM " + THREADS_TABLE + " t JOIN " + MESSAGES_TABLE + " m " +
"ON (m." + MessageColumns.ID + " = t." + ThreadColumns.MESSAGE_ID +
") ");
if (Utility.arrayContainsAny(projection, (Object[]) FOLDERS_COLUMNS)) {
query.append("LEFT JOIN " + FOLDERS_TABLE + " f " +
"ON (m." + MessageColumns.FOLDER_ID + " = f." + FolderColumns.ID +
") ");
}
query.append("WHERE (t." + ThreadColumns.ID + " = ? OR " +
ThreadColumns.ROOT + " = ?) AND " +
InternalMessageColumns.DELETED + " = 0 AND (" +
InternalMessageColumns.EMPTY + " IS NULL OR " +
InternalMessageColumns.EMPTY + " != 1)");
query.append(" ORDER BY ");
query.append(SqlQueryBuilder.addPrefixToSelection(FIXUP_MESSAGES_COLUMNS,
"m.", sortOrder));
return db.rawQuery(query.toString(), new String[] { threadId, threadId });
}
});
} catch (UnavailableStorageException e) {
throw new RuntimeException("Storage not available", e);
}
}
private Cursor getAccountStats(String accountUuid, String[] columns,
final String selection, final String[] selectionArgs) {

View File

@ -76,7 +76,7 @@ public interface SearchSpecification extends Parcelable {
MESSAGE_CONTENTS,
ATTACHMENT_COUNT,
DELETED,
THREAD_ROOT,
THREAD_ID,
ID,
INTEGRATE,
READ,

View File

@ -66,6 +66,12 @@ public class SqlQueryBuilder {
}
break;
}
case THREAD_ID: {
query.append("threads.id = ? OR threads.root = ?");
selectionArgs.add(condition.value);
selectionArgs.add(condition.value);
break;
}
default: {
appendCondition(condition, query, selectionArgs);
}
@ -149,10 +155,6 @@ public class SqlQueryBuilder {
columnName = "subject";
break;
}
case THREAD_ROOT: {
columnName = "thread_root";
break;
}
case TO: {
columnName = "to_list";
break;
@ -177,6 +179,7 @@ public class SqlQueryBuilder {
columnName = "display_class";
break;
}
case THREAD_ID:
case FOLDER:
case SEARCHABLE: {
// Special cases handled in buildWhereClauseInternal()
@ -258,7 +261,7 @@ public class SqlQueryBuilder {
case FOLDER:
case ID:
case INTEGRATE:
case THREAD_ROOT:
case THREAD_ID:
case READ:
case FLAGGED: {
return true;
@ -272,7 +275,7 @@ public class SqlQueryBuilder {
public static String addPrefixToSelection(String[] columnNames, String prefix, String selection) {
String result = selection;
for (String columnName : columnNames) {
result = result.replaceAll("\\b" + columnName + "\\b", prefix + columnName);
result = result.replaceAll("(?<=^|[^\\.])\\b" + columnName + "\\b", prefix + columnName);
}
return result;