Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 737af880 authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Add migration for AppDatabase v8

parent 19474a1e
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
@@ -29,6 +29,12 @@ android {
        buildConfigField "String", "userAgent", "\"DAVx5\""

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        kapt {
            arguments {
                arg("room.schemaLocation", "$projectDir/schemas")
            }
        }
    }

    compileOptions {
@@ -51,6 +57,10 @@ android {
		}
    }

    sourceSets {
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }

    buildTypes {
        debug {
        }
@@ -123,6 +133,7 @@ dependencies {
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    androidTestImplementation "androidx.room:room-testing:$room_version"

    implementation 'com.jaredrummler:colorpicker:1.1.0'
    implementation "com.github.AppIntro:AppIntro:${versions.appIntro}"
+298 −0
Original line number Diff line number Diff line
{
  "formatVersion": 1,
  "database": {
    "version": 8,
    "identityHash": "b8699ef3cc4c62e8851df4360fb69e00",
    "entities": [
      {
        "tableName": "service",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "accountName",
            "columnName": "accountName",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "type",
            "columnName": "type",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "principal",
            "columnName": "principal",
            "affinity": "TEXT",
            "notNull": false
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_service_accountName_type",
            "unique": true,
            "columnNames": [
              "accountName",
              "type"
            ],
            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
          }
        ],
        "foreignKeys": []
      },
      {
        "tableName": "homeset",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "serviceId",
            "columnName": "serviceId",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "personal",
            "columnName": "personal",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "url",
            "columnName": "url",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "privBind",
            "columnName": "privBind",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "displayName",
            "columnName": "displayName",
            "affinity": "TEXT",
            "notNull": false
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_homeset_serviceId_url",
            "unique": true,
            "columnNames": [
              "serviceId",
              "url"
            ],
            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
          }
        ],
        "foreignKeys": [
          {
            "table": "service",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "serviceId"
            ],
            "referencedColumns": [
              "id"
            ]
          }
        ]
      },
      {
        "tableName": "collection",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "serviceId",
            "columnName": "serviceId",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "homeSetId",
            "columnName": "homeSetId",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "type",
            "columnName": "type",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "url",
            "columnName": "url",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "privWriteContent",
            "columnName": "privWriteContent",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "privUnbind",
            "columnName": "privUnbind",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "forceReadOnly",
            "columnName": "forceReadOnly",
            "affinity": "INTEGER",
            "notNull": true
          },
          {
            "fieldPath": "displayName",
            "columnName": "displayName",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "description",
            "columnName": "description",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "owner",
            "columnName": "owner",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "color",
            "columnName": "color",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "timezone",
            "columnName": "timezone",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "supportsVEVENT",
            "columnName": "supportsVEVENT",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "supportsVTODO",
            "columnName": "supportsVTODO",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "supportsVJOURNAL",
            "columnName": "supportsVJOURNAL",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "source",
            "columnName": "source",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "sync",
            "columnName": "sync",
            "affinity": "INTEGER",
            "notNull": true
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [
          {
            "name": "index_collection_serviceId_type",
            "unique": false,
            "columnNames": [
              "serviceId",
              "type"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
          },
          {
            "name": "index_collection_homeSetId_type",
            "unique": false,
            "columnNames": [
              "homeSetId",
              "type"
            ],
            "createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
          }
        ],
        "foreignKeys": [
          {
            "table": "service",
            "onDelete": "CASCADE",
            "onUpdate": "NO ACTION",
            "columns": [
              "serviceId"
            ],
            "referencedColumns": [
              "id"
            ]
          },
          {
            "table": "homeset",
            "onDelete": "SET NULL",
            "onUpdate": "NO ACTION",
            "columns": [
              "homeSetId"
            ],
            "referencedColumns": [
              "id"
            ]
          }
        ]
      }
    ],
    "views": [],
    "setupQueries": [
      "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, 'b8699ef3cc4c62e8851df4360fb69e00')"
    ]
  }
}
 No newline at end of file
+37 −0
Original line number Diff line number Diff line
package at.bitfire.davdroid.model

import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test

class AppDatabaseTest {

    val TEST_DB = "test"

    @Rule
    @JvmField
    val helper = MigrationTestHelper(
            InstrumentationRegistry.getInstrumentation(),
            AppDatabase::class.java.canonicalName,
            FrameworkSQLiteOpenHelperFactory()
    )


    @Test
    fun testAllMigrations() {
        helper.createDatabase(TEST_DB, 8).close()

        Room.databaseBuilder(
            InstrumentationRegistry.getInstrumentation().targetContext,
            AppDatabase::class.java,
            TEST_DB
        ).addMigrations(*AppDatabase.migrations).build().apply {
            openHelper.writableDatabase
            close()
        }
    }

}
 No newline at end of file
+137 −132
Original line number Diff line number Diff line
@@ -31,55 +31,31 @@ abstract class AppDatabase: RoomDatabase() {

        override fun createInstance(context: Context): AppDatabase =
                Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "services.db")
                    .addMigrations(
                            Migration1_2,
                            Migration2_3,
                            Migration3_4,
                            Migration4_5,
                            Migration5_6,
                            Migration6_7
                    )
                    .addMigrations(*migrations)
                    .fallbackToDestructiveMigration()   // as a last fallback, recreate database instead of crashing
                    .build()

    }

    fun dump(writer: Writer) {
        val db = openHelper.readableDatabase
        db.beginTransactionNonExclusive()

        // iterate through all tables
        db.query(SQLiteQueryBuilder.buildQueryString(false, "sqlite_master", arrayOf("name"), "type='table'", null, null, null, null)).use { cursorTables ->
            while (cursorTables.moveToNext()) {
                val tableName = cursorTables.getString(0)
        // migrations

                writer.append("$tableName\n")
                db.query("SELECT * FROM $tableName").use { cursor ->
                    val table = TextTable(*cursor.columnNames)
                    val cols = cursor.columnCount
                    // print rows
                    while (cursor.moveToNext()) {
                        val values = Array<String?>(cols) { idx -> cursor.getStringOrNull(idx) }
                        table.addLine(*values)
                    }
                    writer.append(table.toString())
                }
            }
            db.endTransaction()
        }
        val migrations: Array<Migration> = arrayOf(
            object : Migration(7, 8) {
                override fun migrate(db: SupportSQLiteDatabase) {
                    db.execSQL("ALTER TABLE homeset ADD COLUMN personal INTEGER NOT NULL DEFAULT 1")
                    db.execSQL("ALTER TABLE collection ADD COLUMN homeSetId INTEGER DEFAULT NULL REFERENCES homeset(id) ON DELETE SET NULL")
                    db.execSQL("ALTER TABLE collection ADD COLUMN owner TEXT DEFAULT NULL")
                    db.execSQL("CREATE INDEX index_collection_homeSetId_type ON collection(homeSetId, type)")
                }
            },


    // migrations

    object Migration6_7: Migration(6, 7) {
            object : Migration(6, 7) {
                override fun migrate(db: SupportSQLiteDatabase) {
                    db.execSQL("ALTER TABLE homeset ADD COLUMN privBind INTEGER NOT NULL DEFAULT 1")
                    db.execSQL("ALTER TABLE homeset ADD COLUMN displayName TEXT DEFAULT NULL")
                }
    }
            },

    object Migration5_6: Migration(5, 6) {
            object : Migration(5, 6) {
                override fun migrate(db: SupportSQLiteDatabase) {
                    val sql = arrayOf(
                        // migrate "services" to "service": rename columns, make id NOT NULL
@@ -131,9 +107,9 @@ abstract class AppDatabase: RoomDatabase() {
                    )
                    sql.forEach { db.execSQL(it) }
                }
    }
            },

    object Migration4_5: Migration(4, 5) {
            object : Migration(4, 5) {
                override fun migrate(db: SupportSQLiteDatabase) {
                    db.execSQL("ALTER TABLE collections ADD COLUMN privWriteContent INTEGER DEFAULT 0 NOT NULL")
                    db.execSQL("UPDATE collections SET privWriteContent=NOT readOnly")
@@ -143,15 +119,15 @@ abstract class AppDatabase: RoomDatabase() {

                    // there's no DROP COLUMN in SQLite, so just keep the "readOnly" column
                }
    }
            },

    object Migration3_4: Migration(3, 4) {
            object : Migration(3, 4) {
                override fun migrate(db: SupportSQLiteDatabase) {
                    db.execSQL("ALTER TABLE collections ADD COLUMN forceReadOnly INTEGER DEFAULT 0 NOT NULL")
                }
    }
            },

    object Migration2_3: Migration(2, 3) {
            object : Migration(2, 3) {
                override fun migrate(db: SupportSQLiteDatabase) {
                    // We don't have access to the context in a Room migration now, so
                    // we will just drop those settings from old DAVx5 versions.
@@ -179,9 +155,9 @@ abstract class AppDatabase: RoomDatabase() {
                        edit.apply()
                    }*/
                }
    }
            },

    object Migration1_2: Migration(1, 2) {
            object : Migration(1, 2) {
                override fun migrate(db: SupportSQLiteDatabase) {
                    db.execSQL("ALTER TABLE collections ADD COLUMN type TEXT NOT NULL DEFAULT ''")
                    db.execSQL("ALTER TABLE collections ADD COLUMN source TEXT DEFAULT NULL")
@@ -192,5 +168,34 @@ abstract class AppDatabase: RoomDatabase() {
                            arrayOf("caldav", "CALENDAR", "ADDRESS_BOOK"))
                }
            }
        )

    }


    fun dump(writer: Writer) {
        val db = openHelper.readableDatabase
        db.beginTransactionNonExclusive()

        // iterate through all tables
        db.query(SQLiteQueryBuilder.buildQueryString(false, "sqlite_master", arrayOf("name"), "type='table'", null, null, null, null)).use { cursorTables ->
            while (cursorTables.moveToNext()) {
                val tableName = cursorTables.getString(0)

                writer.append("$tableName\n")
                db.query("SELECT * FROM $tableName").use { cursor ->
                    val table = TextTable(*cursor.columnNames)
                    val cols = cursor.columnCount
                    // print rows
                    while (cursor.moveToNext()) {
                        val values = Array<String?>(cols) { idx -> cursor.getStringOrNull(idx) }
                        table.addLine(*values)
                    }
                    writer.append(table.toString())
                }
            }
            db.endTransaction()
        }
    }

}
 No newline at end of file