package com.fsck.k9.mailstore; import java.util.List; import java.util.Locale; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import timber.log.Timber; import com.fsck.k9.Account; import com.fsck.k9.BuildConfig; import com.fsck.k9.K9; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; import com.fsck.k9.mailstore.migrations.Migrations; import com.fsck.k9.mailstore.migrations.MigrationsHelper; import com.fsck.k9.preferences.Storage; import static com.fsck.k9.mailstore.LocalStore.DB_VERSION; import static java.lang.String.format; import static java.util.Locale.US; class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { private final LocalStore localStore; StoreSchemaDefinition(LocalStore localStore) { this.localStore = localStore; } @Override public int getVersion() { return LocalStore.DB_VERSION; } @Override public void doDbUpgrade(final SQLiteDatabase db) { try { upgradeDatabase(db); } catch (Exception e) { if (BuildConfig.DEBUG) { throw new Error("Exception while upgrading database", e); } Timber.e(e, "Exception while upgrading database. Resetting the DB to v0"); db.setVersion(0); upgradeDatabase(db); } } private void upgradeDatabase(final SQLiteDatabase db) { Timber.i("Upgrading database from version %d to version %d", db.getVersion(), DB_VERSION); db.beginTransaction(); try { // schema version 29 was when we moved to incremental updates // in the case of a new db or a < v29 db, we blow away and start from scratch if (db.getVersion() < 29) { dbCreateDatabaseFromScratch(db); } else { RealMigrationsHelper migrationsHelper = new RealMigrationsHelper(localStore); Migrations.upgradeDatabase(db, migrationsHelper); } db.setVersion(LocalStore.DB_VERSION); db.setTransactionSuccessful(); } finally { db.endTransaction(); } if (db.getVersion() != LocalStore.DB_VERSION) { throw new RuntimeException("Database upgrade failed!"); } } private static void dbCreateDatabaseFromScratch(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS folders"); db.execSQL("CREATE TABLE folders (" + "id INTEGER PRIMARY KEY," + "name TEXT, " + "last_updated INTEGER, " + "unread_count INTEGER, " + "visible_limit INTEGER, " + "status TEXT, " + "push_state TEXT, " + "last_pushed INTEGER, " + "flagged_count INTEGER default 0, " + "integrate INTEGER, " + "top_group INTEGER, " + "poll_class TEXT, " + "push_class TEXT, " + "display_class TEXT, " + "notify_class TEXT default '"+ Folder.FolderClass.INHERITED.name() + "', " + "more_messages TEXT default \"unknown\"" + ")"); db.execSQL("CREATE INDEX IF NOT EXISTS folder_name ON folders (name)"); db.execSQL("DROP TABLE IF EXISTS messages"); db.execSQL("CREATE TABLE messages (" + "id INTEGER PRIMARY KEY, " + "deleted INTEGER default 0, " + "folder_id INTEGER, " + "uid TEXT, " + "subject TEXT, " + "date INTEGER, " + "flags TEXT, " + "sender_list TEXT, " + "to_list TEXT, " + "cc_list TEXT, " + "bcc_list TEXT, " + "reply_to_list TEXT, " + "attachment_count INTEGER, " + "internal_date INTEGER, " + "message_id TEXT, " + "preview_type TEXT default \"none\", " + "preview TEXT, " + "mime_type TEXT, "+ "normalized_subject_hash INTEGER, " + "empty INTEGER default 0, " + "read INTEGER default 0, " + "flagged INTEGER default 0, " + "answered INTEGER default 0, " + "forwarded INTEGER default 0, " + "message_part_id INTEGER" + ")"); db.execSQL("DROP TABLE IF EXISTS message_parts"); db.execSQL("CREATE TABLE message_parts (" + "id INTEGER PRIMARY KEY, " + "type INTEGER NOT NULL, " + "root INTEGER, " + "parent INTEGER NOT NULL, " + "seq INTEGER NOT NULL, " + "mime_type TEXT, " + "decoded_body_size INTEGER, " + "display_name TEXT, " + "header TEXT, " + "encoding TEXT, " + "charset TEXT, " + "data_location INTEGER NOT NULL, " + "data BLOB, " + "preamble TEXT, " + "epilogue TEXT, " + "boundary TEXT, " + "content_id TEXT, " + "server_extra TEXT" + ")"); db.execSQL("CREATE TRIGGER set_message_part_root " + "AFTER INSERT ON message_parts " + "BEGIN " + "UPDATE message_parts SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " + "END"); db.execSQL("CREATE INDEX IF NOT EXISTS msg_uid ON messages (uid, folder_id)"); db.execSQL("DROP INDEX IF EXISTS msg_folder_id"); db.execSQL("DROP INDEX IF EXISTS msg_folder_id_date"); db.execSQL("CREATE INDEX IF NOT EXISTS msg_folder_id_deleted_date ON messages (folder_id,deleted,internal_date)"); 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_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 INDEX IF EXISTS msg_composite"); db.execSQL("CREATE INDEX IF NOT EXISTS msg_composite ON messages (deleted, empty,folder_id,flagged,read)"); 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 TRIGGER IF EXISTS set_thread_root"); db.execSQL("CREATE TRIGGER set_thread_root " + "AFTER INSERT ON threads " + "BEGIN " + "UPDATE threads SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " + "END"); db.execSQL("DROP TABLE IF EXISTS pending_commands"); db.execSQL("CREATE TABLE pending_commands " + "(id INTEGER PRIMARY KEY, command TEXT, data TEXT)"); db.execSQL("DROP TRIGGER IF EXISTS delete_folder"); db.execSQL("CREATE TRIGGER delete_folder BEFORE DELETE ON folders BEGIN DELETE FROM messages WHERE old.id = folder_id; END;"); db.execSQL("DROP TRIGGER IF EXISTS delete_message"); db.execSQL("CREATE TRIGGER delete_message " + "BEFORE DELETE ON messages " + "BEGIN " + "DELETE FROM message_parts WHERE root = OLD.message_part_id; " + "DELETE FROM messages_fulltext WHERE docid = OLD.id; " + "END"); db.execSQL("DROP TABLE IF EXISTS messages_fulltext"); db.execSQL("CREATE VIRTUAL TABLE messages_fulltext USING fts4 (fulltext)"); } private static class RealMigrationsHelper implements MigrationsHelper { private final LocalStore localStore; public RealMigrationsHelper(LocalStore localStore) { this.localStore = localStore; } @Override public LocalStore getLocalStore() { return localStore; } @Override public Storage getStorage() { return localStore.getStorage(); } @Override public Account getAccount() { return localStore.getAccount(); } @Override public Context getContext() { return localStore.getContext(); } @Override public String serializeFlags(List<Flag> flags) { return LocalStore.serializeFlags(flags); } } }