fix: Ensure clientId and clientSecret are non null during db migration (#1103)
Previous migration code could crash if the `clientId` or `clientSecret` columns were null during the migration (unclear how that could happen but there's at least one user report of this crash). Re-write the migration to set these columns to the empty string if NULL first.
This commit is contained in:
parent
04190f0a2b
commit
71f39b3823
|
@ -26,9 +26,9 @@ import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.DeleteColumn
|
import androidx.room.DeleteColumn
|
||||||
import androidx.room.RenameColumn
|
import androidx.room.RenameColumn
|
||||||
import androidx.room.RenameTable
|
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.migration.AutoMigrationSpec
|
import androidx.room.migration.AutoMigrationSpec
|
||||||
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import app.pachli.core.database.dao.AccountDao
|
import app.pachli.core.database.dao.AccountDao
|
||||||
import app.pachli.core.database.dao.AnnouncementsDao
|
import app.pachli.core.database.dao.AnnouncementsDao
|
||||||
|
@ -89,7 +89,6 @@ import java.util.TimeZone
|
||||||
AutoMigration(from = 5, to = 6),
|
AutoMigration(from = 5, to = 6),
|
||||||
AutoMigration(from = 6, to = 7, spec = AppDatabase.MIGRATE_6_7::class),
|
AutoMigration(from = 6, to = 7, spec = AppDatabase.MIGRATE_6_7::class),
|
||||||
AutoMigration(from = 7, to = 8, spec = AppDatabase.MIGRATE_7_8::class),
|
AutoMigration(from = 7, to = 8, spec = AppDatabase.MIGRATE_7_8::class),
|
||||||
AutoMigration(from = 8, to = 9, spec = AppDatabase.MIGRATE_8_9::class),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
@ -159,58 +158,74 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
@DeleteColumn("DraftEntity", "scheduledAt")
|
@DeleteColumn("DraftEntity", "scheduledAt")
|
||||||
@RenameColumn("DraftEntity", "scheduledAtLong", "scheduledAt")
|
@RenameColumn("DraftEntity", "scheduledAtLong", "scheduledAt")
|
||||||
class MIGRATE_7_8 : AutoMigrationSpec
|
class MIGRATE_7_8 : AutoMigrationSpec
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
val MIGRATE_8_9 = object : Migration(8, 9) {
|
||||||
* Populates new tables with default data for existing accounts.
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
*
|
// clientId and clientSecret were made non-nullable. Before migrating data convert
|
||||||
* Sets up:
|
// any existing NULL values to the empty string.
|
||||||
*
|
db.execSQL("UPDATE `AccountEntity` SET `clientId` = '' WHERE `clientId` IS NULL")
|
||||||
* - InstanceInfoEntity
|
db.execSQL("UPDATE `AccountEntity` SET `clientSecret` = '' WHERE `clientSecret` IS NULL")
|
||||||
* - ServerEntity
|
|
||||||
* - ContentFiltersEntity
|
|
||||||
*/
|
|
||||||
@DeleteColumn("InstanceEntity", "emojiList")
|
|
||||||
@RenameColumn(
|
|
||||||
"InstanceEntity",
|
|
||||||
fromColumnName = "maximumTootCharacters",
|
|
||||||
toColumnName = "maxPostCharacters",
|
|
||||||
)
|
|
||||||
@RenameTable(fromTableName = "InstanceEntity", toTableName = "InstanceInfoEntity")
|
|
||||||
class MIGRATE_8_9 : AutoMigrationSpec {
|
|
||||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
|
||||||
db.beginTransaction()
|
|
||||||
|
|
||||||
val accountCursor = db.query("SELECT id, domain FROM AccountEntity")
|
// Migrate the tables.
|
||||||
with(accountCursor) {
|
//
|
||||||
while (moveToNext()) {
|
// - Mark AccountEntity.clientId and .clientSecret as NON NULL
|
||||||
val accountId = getLong(0)
|
// - Delete InstanceEntity.emojiList
|
||||||
val domain = getString(1)
|
// - Rename InstanceEntity.maximumTootCharacters to .maxPostCharacters
|
||||||
|
// - Rename InstanceEntity to InstanceInfoEntity
|
||||||
|
db.execSQL("CREATE TABLE IF NOT EXISTS `EmojisEntity` (`accountId` INTEGER NOT NULL, `emojiList` TEXT NOT NULL, PRIMARY KEY(`accountId`), FOREIGN KEY(`accountId`) REFERENCES `AccountEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)")
|
||||||
|
db.execSQL("CREATE TABLE IF NOT EXISTS `MastodonListEntity` (`accountId` INTEGER NOT NULL, `listId` TEXT NOT NULL, `title` TEXT NOT NULL, `repliesPolicy` TEXT NOT NULL, `exclusive` INTEGER NOT NULL, PRIMARY KEY(`accountId`, `listId`), FOREIGN KEY(`accountId`) REFERENCES `AccountEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)")
|
||||||
|
db.execSQL("CREATE TABLE IF NOT EXISTS `ServerEntity` (`accountId` INTEGER NOT NULL, `serverKind` TEXT NOT NULL, `version` TEXT NOT NULL, `capabilities` TEXT NOT NULL, PRIMARY KEY(`accountId`), FOREIGN KEY(`accountId`) REFERENCES `AccountEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)")
|
||||||
|
db.execSQL("CREATE TABLE IF NOT EXISTS `ContentFiltersEntity` (`accountId` INTEGER NOT NULL, `version` TEXT NOT NULL, `contentFilters` TEXT NOT NULL, PRIMARY KEY(`accountId`), FOREIGN KEY(`accountId`) REFERENCES `AccountEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)")
|
||||||
|
db.execSQL("CREATE TABLE IF NOT EXISTS `AnnouncementEntity` (`accountId` INTEGER NOT NULL, `announcementId` TEXT NOT NULL, `announcement` TEXT NOT NULL, PRIMARY KEY(`accountId`), FOREIGN KEY(`accountId`) REFERENCES `AccountEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)")
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `_new_AccountEntity` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT NOT NULL, `clientSecret` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `profileHeaderPictureUrl` TEXT NOT NULL DEFAULT '', `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationsSeveredRelationships` INTEGER NOT NULL DEFAULT true, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `notificationMarkerId` TEXT NOT NULL DEFAULT '0', `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT, `locked` INTEGER NOT NULL DEFAULT 0)",
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"INSERT INTO `_new_AccountEntity` (`id`,`domain`,`accessToken`,`clientId`,`clientSecret`,`isActive`,`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`,`notificationsMentioned`,`notificationsFollowed`,`notificationsFollowRequested`,`notificationsReblogged`,`notificationsFavorited`,`notificationsPolls`,`notificationsSubscriptions`,`notificationsSignUps`,`notificationsUpdates`,`notificationsReports`,`notificationsSeveredRelationships`,`notificationSound`,`notificationVibration`,`notificationLight`,`defaultPostPrivacy`,`defaultMediaSensitivity`,`defaultPostLanguage`,`alwaysShowSensitiveMedia`,`alwaysOpenSpoiler`,`mediaPreviewEnabled`,`lastNotificationId`,`notificationMarkerId`,`emojis`,`tabPreferences`,`notificationsFilter`,`oauthScopes`,`unifiedPushUrl`,`pushPubKey`,`pushPrivKey`,`pushAuth`,`pushServerKey`,`lastVisibleHomeTimelineStatusId`,`locked`) SELECT `id`,`domain`,`accessToken`,`clientId`,`clientSecret`,`isActive`,`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`,`notificationsMentioned`,`notificationsFollowed`,`notificationsFollowRequested`,`notificationsReblogged`,`notificationsFavorited`,`notificationsPolls`,`notificationsSubscriptions`,`notificationsSignUps`,`notificationsUpdates`,`notificationsReports`,`notificationsSeveredRelationships`,`notificationSound`,`notificationVibration`,`notificationLight`,`defaultPostPrivacy`,`defaultMediaSensitivity`,`defaultPostLanguage`,`alwaysShowSensitiveMedia`,`alwaysOpenSpoiler`,`mediaPreviewEnabled`,`lastNotificationId`,`notificationMarkerId`,`emojis`,`tabPreferences`,`notificationsFilter`,`oauthScopes`,`unifiedPushUrl`,`pushPubKey`,`pushPrivKey`,`pushAuth`,`pushServerKey`,`lastVisibleHomeTimelineStatusId`,`locked` FROM `AccountEntity`",
|
||||||
|
)
|
||||||
|
db.execSQL("DROP TABLE `AccountEntity`")
|
||||||
|
db.execSQL("ALTER TABLE `_new_AccountEntity` RENAME TO `AccountEntity`")
|
||||||
|
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `AccountEntity` (`domain`, `accountId`)")
|
||||||
|
db.execSQL("CREATE TABLE IF NOT EXISTS `_new_InstanceInfoEntity` (`instance` TEXT NOT NULL, `maxPostCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, `enabledTranslation` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`instance`))")
|
||||||
|
db.execSQL("INSERT INTO `_new_InstanceInfoEntity` (`instance`,`maxPostCharacters`,`maxPollOptions`,`maxPollOptionLength`,`minPollDuration`,`maxPollDuration`,`charactersReservedPerUrl`,`version`,`videoSizeLimit`,`imageSizeLimit`,`imageMatrixLimit`,`maxMediaAttachments`,`maxFields`,`maxFieldNameLength`,`maxFieldValueLength`) SELECT `instance`,`maximumTootCharacters`,`maxPollOptions`,`maxPollOptionLength`,`minPollDuration`,`maxPollDuration`,`charactersReservedPerUrl`,`version`,`videoSizeLimit`,`imageSizeLimit`,`imageMatrixLimit`,`maxMediaAttachments`,`maxFields`,`maxFieldNameLength`,`maxFieldValueLength` FROM `InstanceEntity`")
|
||||||
|
db.execSQL("DROP TABLE `InstanceEntity`")
|
||||||
|
db.execSQL("ALTER TABLE `_new_InstanceInfoEntity` RENAME TO `InstanceInfoEntity`")
|
||||||
|
|
||||||
val instanceInfoEntityValues = ContentValues().apply {
|
// Populate the new tables with default data for existing accounts.
|
||||||
put("instance", domain)
|
//
|
||||||
put("enabledTranslation", 0)
|
// Sets up:
|
||||||
}
|
//
|
||||||
db.insert("InstanceInfoEntity", CONFLICT_IGNORE, instanceInfoEntityValues)
|
// - InstanceInfoEntity
|
||||||
|
// - ServerEntity
|
||||||
|
// - ContentFiltersEntity
|
||||||
|
val accountCursor = db.query("SELECT id, domain FROM AccountEntity")
|
||||||
|
with(accountCursor) {
|
||||||
|
while (moveToNext()) {
|
||||||
|
val accountId = getLong(0)
|
||||||
|
val domain = getString(1)
|
||||||
|
|
||||||
val serverEntityValues = ContentValues().apply {
|
val instanceInfoEntityValues = ContentValues().apply {
|
||||||
put("accountId", accountId)
|
put("instance", domain)
|
||||||
put("serverKind", "UNKNOWN")
|
put("enabledTranslation", 0)
|
||||||
put("version", "0.0.1")
|
|
||||||
put("capabilities", "{}")
|
|
||||||
}
|
|
||||||
db.insert("ServerEntity", CONFLICT_IGNORE, serverEntityValues)
|
|
||||||
|
|
||||||
val contentFiltersEntityValues = ContentValues().apply {
|
|
||||||
put("accountId", accountId)
|
|
||||||
put("version", ContentFilterVersion.V1.name)
|
|
||||||
put("contentFilters", "[]")
|
|
||||||
}
|
|
||||||
db.insert("ContentFiltersEntity", CONFLICT_IGNORE, contentFiltersEntityValues)
|
|
||||||
}
|
}
|
||||||
}
|
db.insert("InstanceInfoEntity", CONFLICT_IGNORE, instanceInfoEntityValues)
|
||||||
|
|
||||||
db.setTransactionSuccessful()
|
val serverEntityValues = ContentValues().apply {
|
||||||
db.endTransaction()
|
put("accountId", accountId)
|
||||||
|
put("serverKind", "UNKNOWN")
|
||||||
|
put("version", "0.0.1")
|
||||||
|
put("capabilities", "{}")
|
||||||
|
}
|
||||||
|
db.insert("ServerEntity", CONFLICT_IGNORE, serverEntityValues)
|
||||||
|
|
||||||
|
val contentFiltersEntityValues = ContentValues().apply {
|
||||||
|
put("accountId", accountId)
|
||||||
|
put("version", ContentFilterVersion.V1.name)
|
||||||
|
put("contentFilters", "[]")
|
||||||
|
}
|
||||||
|
db.insert("ContentFiltersEntity", CONFLICT_IGNORE, contentFiltersEntityValues)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import androidx.room.Room
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import app.pachli.core.database.AppDatabase
|
import app.pachli.core.database.AppDatabase
|
||||||
import app.pachli.core.database.Converters
|
import app.pachli.core.database.Converters
|
||||||
|
import app.pachli.core.database.MIGRATE_8_9
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -41,6 +42,7 @@ object DatabaseModule {
|
||||||
return Room.databaseBuilder(appContext, AppDatabase::class.java, "pachliDB")
|
return Room.databaseBuilder(appContext, AppDatabase::class.java, "pachliDB")
|
||||||
.addTypeConverter(converters)
|
.addTypeConverter(converters)
|
||||||
.allowMainThreadQueries()
|
.allowMainThreadQueries()
|
||||||
|
.addMigrations(MIGRATE_8_9)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue