diff --git a/docs/contrib/index.md b/docs/contrib/index.md index 2dde9bbe0c..400188ab19 100644 --- a/docs/contrib/index.md +++ b/docs/contrib/index.md @@ -324,3 +324,18 @@ This section defines the process one goes through when making changes to any of * Update your/any dependent PR (PR using the library) with the new _Artifact ID_ and make/trigger the CI **NB:** For a specific example on working with FHIR SDK's Common Library during development, see [Common Library](#common-library). + +# Database migration + +If you are making changes to the database schema (in the `engine` or the `knowledge` module), you +need to consider how applications with Android FHIR SDK dependencies can upgrade to the new schema +without losing or corrupting existing data already on device. This can be done with [Room database +migration](https://developer.android.com/training/data-storage/room/migrating-db-versions). + +> [!TIP] +> A new JSON schema file will be generated under the `schemas` folder in the module when you +update the database version. If you are having trouble with this, make sure you run the gradle +> command with `--rerun-tasks`: +> ``` +> ./gradlew ::build --rerun-tasks +> ``` diff --git a/engine/build.gradle.kts b/engine/build.gradle.kts index 62ec25224e..d18f5f60ed 100644 --- a/engine/build.gradle.kts +++ b/engine/build.gradle.kts @@ -14,6 +14,9 @@ publishArtifact(Releases.Engine) createJacocoTestReportTask() +// Generate database schema in the schemas folder +ksp { arg("room.schemaLocation", "$projectDir/schemas") } + val generateSearchParamsTask = project.tasks.register("generateSearchParamsTask", GenerateSearchParamsTask::class) { srcOutputDir.set(project.layout.buildDirectory.dir("gen/main")) @@ -39,12 +42,6 @@ android { // need to specify this to prevent junit runner from going deep into our dependencies testInstrumentationRunnerArguments["package"] = "com.google.android.fhir" consumerProguardFile("proguard-rules.pro") - - javaCompileOptions { - annotationProcessorOptions { - compilerArgumentProviders(RoomSchemaArgProvider(File(projectDir, "schemas"))) - } - } } sourceSets { diff --git a/knowledge/build.gradle.kts b/knowledge/build.gradle.kts index e4687a987b..0a1f5fed19 100644 --- a/knowledge/build.gradle.kts +++ b/knowledge/build.gradle.kts @@ -14,6 +14,9 @@ publishArtifact(Releases.Knowledge) createJacocoTestReportTask() +// Generate database schema in the schemas folder +ksp { arg("room.schemaLocation", "$projectDir/schemas") } + android { namespace = "com.google.android.fhir.knowledge" compileSdk = Sdk.COMPILE_SDK @@ -25,7 +28,10 @@ android { } sourceSets { - getByName("androidTest").apply { resources.setSrcDirs(listOf("testdata")) } + getByName("androidTest").apply { + resources.setSrcDirs(listOf("testdata")) + assets.srcDirs("$projectDir/schemas") + } getByName("test").apply { resources.setSrcDirs(listOf("testdata")) } } diff --git a/knowledge/schemas/com.google.android.fhir.knowledge.db.KnowledgeDatabase/1.json b/knowledge/schemas/com.google.android.fhir.knowledge.db.KnowledgeDatabase/1.json new file mode 100644 index 0000000000..e8761b3c58 --- /dev/null +++ b/knowledge/schemas/com.google.android.fhir.knowledge.db.KnowledgeDatabase/1.json @@ -0,0 +1,234 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "41dc6411ce57c1eeba3300592f302b07", + "entities": [ + { + "tableName": "ImplementationGuideEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`implementationGuideId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `packageId` TEXT NOT NULL, `version` TEXT, `rootDirectory` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "implementationGuideId", + "columnName": "implementationGuideId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageId", + "columnName": "packageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rootDirectory", + "columnName": "rootDirectory", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "implementationGuideId" + ] + }, + "indices": [ + { + "name": "index_ImplementationGuideEntity_implementationGuideId", + "unique": false, + "columnNames": [ + "implementationGuideId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ImplementationGuideEntity_implementationGuideId` ON `${TABLE_NAME}` (`implementationGuideId`)" + }, + { + "name": "index_ImplementationGuideEntity_packageId_url_version", + "unique": true, + "columnNames": [ + "packageId", + "url", + "version" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ImplementationGuideEntity_packageId_url_version` ON `${TABLE_NAME}` (`packageId`, `url`, `version`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ResourceMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`resourceMetadataId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceType` TEXT NOT NULL, `url` TEXT, `name` TEXT, `version` TEXT, `resourceFile` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "resourceMetadataId", + "columnName": "resourceMetadataId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "resourceFile", + "columnName": "resourceFile", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "resourceMetadataId" + ] + }, + "indices": [ + { + "name": "index_ResourceMetadataEntity_resourceMetadataId", + "unique": false, + "columnNames": [ + "resourceMetadataId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ResourceMetadataEntity_resourceMetadataId` ON `${TABLE_NAME}` (`resourceMetadataId`)" + }, + { + "name": "index_ResourceMetadataEntity_url_version_resourceFile", + "unique": true, + "columnNames": [ + "url", + "version", + "resourceFile" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ResourceMetadataEntity_url_version_resourceFile` ON `${TABLE_NAME}` (`url`, `version`, `resourceFile`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ImplementationGuideResourceMetadataEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `implementationGuideId` INTEGER, `resourceMetadataId` INTEGER NOT NULL, FOREIGN KEY(`implementationGuideId`) REFERENCES `ImplementationGuideEntity`(`implementationGuideId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`resourceMetadataId`) REFERENCES `ResourceMetadataEntity`(`resourceMetadataId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "implementationGuideId", + "columnName": "implementationGuideId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "resourceMetadataId", + "columnName": "resourceMetadataId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ImplementationGuideResourceMetadataEntity_implementationGuideId", + "unique": false, + "columnNames": [ + "implementationGuideId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ImplementationGuideResourceMetadataEntity_implementationGuideId` ON `${TABLE_NAME}` (`implementationGuideId`)" + }, + { + "name": "index_ImplementationGuideResourceMetadataEntity_resourceMetadataId", + "unique": false, + "columnNames": [ + "resourceMetadataId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ImplementationGuideResourceMetadataEntity_resourceMetadataId` ON `${TABLE_NAME}` (`resourceMetadataId`)" + }, + { + "name": "index_ImplementationGuideResourceMetadataEntity_implementationGuideId_resourceMetadataId", + "unique": true, + "columnNames": [ + "implementationGuideId", + "resourceMetadataId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ImplementationGuideResourceMetadataEntity_implementationGuideId_resourceMetadataId` ON `${TABLE_NAME}` (`implementationGuideId`, `resourceMetadataId`)" + } + ], + "foreignKeys": [ + { + "table": "ImplementationGuideEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "implementationGuideId" + ], + "referencedColumns": [ + "implementationGuideId" + ] + }, + { + "table": "ResourceMetadataEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "resourceMetadataId" + ], + "referencedColumns": [ + "resourceMetadataId" + ] + } + ] + } + ], + "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, '41dc6411ce57c1eeba3300592f302b07')" + ] + } +} \ No newline at end of file diff --git a/knowledge/src/main/java/com/google/android/fhir/knowledge/db/KnowledgeDatabase.kt b/knowledge/src/main/java/com/google/android/fhir/knowledge/db/KnowledgeDatabase.kt index b140c443bd..8e86864d3f 100644 --- a/knowledge/src/main/java/com/google/android/fhir/knowledge/db/KnowledgeDatabase.kt +++ b/knowledge/src/main/java/com/google/android/fhir/knowledge/db/KnowledgeDatabase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ import com.google.android.fhir.knowledge.db.entities.ResourceMetadataEntity ImplementationGuideResourceMetadataEntity::class, ], version = 1, - exportSchema = false, + exportSchema = true, ) @TypeConverters(DbTypeConverters::class) internal abstract class KnowledgeDatabase : RoomDatabase() {