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:
Nik Clayton 2024-11-16 00:24:43 +01:00 committed by GitHub
parent 04190f0a2b
commit 71f39b3823
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 65 additions and 48 deletions

View File

@ -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)
}
} }
} }
} }

View File

@ -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()
} }