Fixed bug with Room Overwriting old data

- Created MigrationsTest
- Created Manual Migration from 9 to 10
- New Database has additional primary key

Signed-off-by: Julius Linus <julius.linus@nextcloud.com>
This commit is contained in:
Julius Linus 2023-12-11 11:10:01 -06:00 committed by rapterjet2004
parent a8345172e2
commit 857d356615
11 changed files with 135 additions and 15 deletions

View file

@ -23,9 +23,10 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import com.github.spotbugs.snom.SpotBugsTask
import com.github.spotbugs.snom.Confidence import com.github.spotbugs.snom.Confidence
import com.github.spotbugs.snom.Effort import com.github.spotbugs.snom.Effort
import com.github.spotbugs.snom.SpotBugsTask
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
@ -91,6 +92,10 @@ android {
} }
} }
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
testInstrumentationRunnerArgument "TEST_SERVER_URL", "${NC_TEST_SERVER_BASEURL}" testInstrumentationRunnerArgument "TEST_SERVER_URL", "${NC_TEST_SERVER_BASEURL}"
testInstrumentationRunnerArgument "TEST_SERVER_USERNAME", "${NC_TEST_SERVER_USERNAME}" testInstrumentationRunnerArgument "TEST_SERVER_USERNAME", "${NC_TEST_SERVER_USERNAME}"
testInstrumentationRunnerArgument "TEST_SERVER_PASSWORD", "${NC_TEST_SERVER_PASSWORD}" testInstrumentationRunnerArgument "TEST_SERVER_PASSWORD", "${NC_TEST_SERVER_PASSWORD}"
@ -239,6 +244,7 @@ dependencies {
implementation "androidx.room:room-rxjava2:${roomVersion}" implementation "androidx.room:room-rxjava2:${roomVersion}"
kapt "androidx.room:room-compiler:${roomVersion}" kapt "androidx.room:room-compiler:${roomVersion}"
implementation "androidx.room:room-ktx:${roomVersion}" implementation "androidx.room:room-ktx:${roomVersion}"
androidTestImplementation "androidx.room:room-testing:2.6.1"
implementation "org.parceler:parceler-api:$parcelerVersion" implementation "org.parceler:parceler-api:$parcelerVersion"
implementation 'eu.davidea:flexible-adapter:5.1.0' implementation 'eu.davidea:flexible-adapter:5.1.0'

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 9, "version": 9,
"identityHash": "666fcc4bbbdf3ff121b8f1ace8fcbcb8", "identityHash": "250a3a56f3943f0d72f7ca0aac08fd1e",
"entities": [ "entities": [
{ {
"tableName": "User", "tableName": "User",
@ -92,7 +92,7 @@
}, },
{ {
"tableName": "ArbitraryStorage", "tableName": "ArbitraryStorage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT NOT NULL, `object` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`accountIdentifier`, `key`))",
"fields": [ "fields": [
{ {
"fieldPath": "accountIdentifier", "fieldPath": "accountIdentifier",
@ -110,7 +110,7 @@
"fieldPath": "storageObject", "fieldPath": "storageObject",
"columnName": "object", "columnName": "object",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": true
}, },
{ {
"fieldPath": "value", "fieldPath": "value",
@ -123,7 +123,8 @@
"autoGenerate": false, "autoGenerate": false,
"columnNames": [ "columnNames": [
"accountIdentifier", "accountIdentifier",
"key" "key",
"object"
] ]
}, },
"indices": [], "indices": [],
@ -133,7 +134,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '666fcc4bbbdf3ff121b8f1ace8fcbcb8')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '250a3a56f3943f0d72f7ca0aac08fd1e')"
] ]
} }
} }

View file

@ -0,0 +1,69 @@
/*
* Nextcloud Talk application
*
* @author Julius Linus
* Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.migrations
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry
import com.nextcloud.talk.data.source.local.Migrations
import com.nextcloud.talk.data.source.local.TalkDatabase
import org.junit.Rule
import org.junit.Test
import java.io.IOException
class MigrationsTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
TalkDatabase::class.java.canonicalName!!,
FrameworkSQLiteOpenHelperFactory()
)
@Test
@Throws(IOException::class)
fun migrateAll() {
// Create earliest version of the database.
helper.createDatabase(TEST_DB, 8).apply {
close()
}
// Open latest version of the database. Room validates the schema
// once all migrations execute.
Room.databaseBuilder(
InstrumentationRegistry.getInstrumentation().targetContext,
TalkDatabase::class.java,
TEST_DB
).addMigrations(
Migrations.MIGRATION_6_8,
Migrations.MIGRATION_7_8,
Migrations.MIGRATION_8_9,
Migrations.MIGRATION_9_10
).build().apply {
openHelper.writableDatabase.close()
}
}
companion object {
private const val TEST_DB = "migration-test"
}
}

View file

@ -25,7 +25,7 @@ import com.nextcloud.talk.data.storage.model.ArbitraryStorage
import io.reactivex.Maybe import io.reactivex.Maybe
class ArbitraryStorageManager(private val arbitraryStoragesRepository: ArbitraryStoragesRepository) { class ArbitraryStorageManager(private val arbitraryStoragesRepository: ArbitraryStoragesRepository) {
fun storeStorageSetting(accountIdentifier: Long, key: String, value: String?, objectString: String?) { fun storeStorageSetting(accountIdentifier: Long, key: String, value: String?, objectString: String) {
arbitraryStoragesRepository.saveArbitraryStorage(ArbitraryStorage(accountIdentifier, key, objectString, value)) arbitraryStoragesRepository.saveArbitraryStorage(ArbitraryStorage(accountIdentifier, key, objectString, value))
} }

View file

@ -166,7 +166,6 @@ class ConversationInfoActivity :
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (databaseStorageModule == null) { if (databaseStorageModule == null) {
databaseStorageModule = DatabaseStorageModule(conversationUser, conversationToken) databaseStorageModule = DatabaseStorageModule(conversationUser, conversationToken)
} }

View file

@ -47,6 +47,13 @@ object Migrations {
} }
} }
val MIGRATION_9_10 = object : Migration(9, 10) {
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 9 to 10")
migrateToTriplePrimaryKeyArbitraryStorage(db)
}
}
fun migrateToRoom(db: SupportSQLiteDatabase) { fun migrateToRoom(db: SupportSQLiteDatabase) {
db.execSQL( db.execSQL(
"CREATE TABLE User_new (" + "CREATE TABLE User_new (" +
@ -124,4 +131,29 @@ object Migrations {
// Change the table name to the correct one // Change the table name to the correct one
db.execSQL("ALTER TABLE ArbitraryStorage_dualPK RENAME TO ArbitraryStorage") db.execSQL("ALTER TABLE ArbitraryStorage_dualPK RENAME TO ArbitraryStorage")
} }
fun migrateToTriplePrimaryKeyArbitraryStorage(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE ArbitraryStorage_triplePK (" +
"accountIdentifier INTEGER NOT NULL, " +
"value TEXT, " +
"\"key\" TEXT NOT NULL, " +
"object TEXT NOT NULL, " +
"PRIMARY KEY(accountIdentifier, \"key\", object)" +
")"
)
// Copy the data
db.execSQL(
"INSERT INTO ArbitraryStorage_triplePK (" +
"accountIdentifier, \"key\", object, value) " +
"SELECT " +
"accountIdentifier, \"key\", object, value " +
"FROM ArbitraryStorage"
)
// Remove the old table
db.execSQL("DROP TABLE ArbitraryStorage")
// Change the table name to the correct one
db.execSQL("ALTER TABLE ArbitraryStorage_triplePK RENAME TO ArbitraryStorage")
}
} }

View file

@ -45,7 +45,7 @@ import java.util.Locale
@Database( @Database(
entities = [UserEntity::class, ArbitraryStorageEntity::class], entities = [UserEntity::class, ArbitraryStorageEntity::class],
version = 9, version = 10,
exportSchema = true exportSchema = true
) )
@TypeConverters( @TypeConverters(
@ -94,9 +94,14 @@ abstract class TalkDatabase : RoomDatabase() {
return Room return Room
.databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName) .databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName)
// comment out openHelperFactory to view the database entries in Android Studio for debugging // NOTE: comment out openHelperFactory to view the database entries in Android Studio for debugging
.openHelperFactory(factory) .openHelperFactory(factory)
.addMigrations(Migrations.MIGRATION_6_8, Migrations.MIGRATION_7_8, Migrations.MIGRATION_8_9) .addMigrations(
Migrations.MIGRATION_6_8,
Migrations.MIGRATION_7_8,
Migrations.MIGRATION_8_9,
Migrations.MIGRATION_9_10
)
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback( .addCallback(
object : RoomDatabase.Callback() { object : RoomDatabase.Callback() {

View file

@ -33,7 +33,9 @@ class ArbitraryStoragesRepositoryImpl(private val arbitraryStoragesDao: Arbitrar
): Maybe<ArbitraryStorage> { ): Maybe<ArbitraryStorage> {
return arbitraryStoragesDao return arbitraryStoragesDao
.getStorageSetting(accountIdentifier, key, objectString) .getStorageSetting(accountIdentifier, key, objectString)
.map { ArbitraryStorageMapper.toModel(it) } .map {
ArbitraryStorageMapper.toModel(it)
}
} }
override fun getAll(): Maybe<List<ArbitraryStorageEntity>> { override fun getAll(): Maybe<List<ArbitraryStorageEntity>> {

View file

@ -27,6 +27,6 @@ import kotlinx.parcelize.Parcelize
data class ArbitraryStorage( data class ArbitraryStorage(
var accountIdentifier: Long, var accountIdentifier: Long,
var key: String, var key: String,
var storageObject: String? = null, var storageObject: String,
var value: String? = null var value: String? = null
) : Parcelable ) : Parcelable

View file

@ -26,7 +26,7 @@ import androidx.room.Entity
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@Entity(tableName = "ArbitraryStorage", primaryKeys = ["accountIdentifier", "key"]) @Entity(tableName = "ArbitraryStorage", primaryKeys = ["accountIdentifier", "key", "object"])
data class ArbitraryStorageEntity( data class ArbitraryStorageEntity(
@ColumnInfo(name = "accountIdentifier") @ColumnInfo(name = "accountIdentifier")
var accountIdentifier: Long = 0, var accountIdentifier: Long = 0,
@ -35,7 +35,7 @@ data class ArbitraryStorageEntity(
var key: String = "", var key: String = "",
@ColumnInfo(name = "object") @ColumnInfo(name = "object")
var storageObject: String? = null, var storageObject: String = "",
@ColumnInfo(name = "value") @ColumnInfo(name = "value")
var value: String? = null var value: String? = null

View file

@ -251,4 +251,10 @@ public class DatabaseStorageModule {
public void setMessageExpiration(int messageExpiration) { public void setMessageExpiration(int messageExpiration) {
this.messageExpiration = messageExpiration; this.messageExpiration = messageExpiration;
} }
@androidx.annotation.NonNull
public String toString() {
return "Conversation token: " + conversationToken
+ "\nAccount Number: " + accountIdentifier;
}
} }