From 7af811ea12e9d28cbee170c9a3f234c882c7bb79 Mon Sep 17 00:00:00 2001 From: Rahul Malhotra <16497903+rahulmalhotra@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:53:01 +0530 Subject: [PATCH 1/3] Fix version number under Questionnaire basics section (#2771) --- docs/use/SDCL/Author-questionnaires.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/use/SDCL/Author-questionnaires.md b/docs/use/SDCL/Author-questionnaires.md index 985e51c79a..1dccece760 100644 --- a/docs/use/SDCL/Author-questionnaires.md +++ b/docs/use/SDCL/Author-questionnaires.md @@ -93,7 +93,7 @@ Let's look at a more complex Questionnaire, focused on the top-level `item` elem There are [several options](https://www.hl7.org/fhir/valueset-item-type.html) for the `type` member of `item` objects. The Structured Data Capture Library selects the UI component to use when rendering based on the type. This example also uses the `group` type where `text` acts as section headers and child item objects are logically grouped. -Some Questionnaire elements control validation or rendering logic. For example, item `1.1` is required, and item `2.1.1` is only shown if item `2.1` is `true`. +Some Questionnaire elements control validation or rendering logic. For example, item `1.1` is required, and item `2.2` is only shown if item `2.1` is `true`. The next example of an item object uses extensions from the SDC implementation guide and also demonstrates the `choice` type: From 87fa2ae6a8ca0dde143cce07a58987f5252dd578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=E2=89=A1ZRS?= <12814349+LZRS@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:31:00 +0300 Subject: [PATCH 2/3] Validate QuestionnaireItem.Group with repeats as repeating group (#2755) * Validate QuestionnaireItem.Group with repeats as repeating group Instead of just as group * Add tests for repeating groups in QuestionnaireResponseValidatorTest * Update licence year to fix spotless --- .../QuestionnaireResponseValidator.kt | 23 +-- .../QuestionnaireResponseValidatorTest.kt | 139 +++++++++++++++++- 2 files changed, 152 insertions(+), 10 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt index 616549caf7..e0a74edcad 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 Google LLC + * Copyright 2022-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -151,10 +151,12 @@ object QuestionnaireResponseValidator { questionnaireResponseItemValidator: QuestionnaireResponseItemValidator, linkIdToValidationResultMap: MutableMap>, ): Map> { - when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) { - Questionnaire.QuestionnaireItemType.DISPLAY, - Questionnaire.QuestionnaireItemType.NULL, -> Unit - Questionnaire.QuestionnaireItemType.GROUP -> + checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" } + when { + questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY || + questionnaireItem.type == Questionnaire.QuestionnaireItemType.NULL -> Unit + questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && + !questionnaireItem.repeats -> // Nested items under group // http://www.hl7.org/fhir/questionnaireresponse-definitions.html#QuestionnaireResponse.item.item validateQuestionnaireResponseItems( @@ -262,10 +264,13 @@ object QuestionnaireResponseValidator { questionnaireItem: Questionnaire.QuestionnaireItemComponent, questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent, ) { - when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) { - Questionnaire.QuestionnaireItemType.DISPLAY, - Questionnaire.QuestionnaireItemType.NULL, -> Unit - Questionnaire.QuestionnaireItemType.GROUP -> + checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" } + + when { + questionnaireItem.type == Questionnaire.QuestionnaireItemType.DISPLAY || + questionnaireItem.type == Questionnaire.QuestionnaireItemType.NULL -> Unit + questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && + !questionnaireItem.repeats -> // Nested items under group // http://www.hl7.org/fhir/questionnaireresponse-definitions.html#QuestionnaireResponse.item.item checkQuestionnaireResponseItems(questionnaireItem.item, questionnaireResponseItem.item) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidatorTest.kt index 6b3dfacf26..36fd666d41 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidatorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 Google LLC + * Copyright 2022-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import android.content.Context import android.os.Build import androidx.test.core.app.ApplicationProvider import com.google.android.fhir.datacapture.extensions.EXTENSION_HIDDEN_URL +import com.google.android.fhir.datacapture.extensions.packRepeatedGroups import com.google.common.truth.Truth.assertThat import java.math.BigDecimal import kotlinx.coroutines.test.runTest @@ -596,6 +597,79 @@ class QuestionnaireResponseValidatorTest { ) } + @Test + fun `validation fails for required item in a questionnaire repeating group item with answer value`() { + val questionnaire1 = + Questionnaire().apply { + url = "questionnaire-1" + addItem( + Questionnaire.QuestionnaireItemComponent( + StringType("group-1"), + Enumeration( + Questionnaire.QuestionnaireItemTypeEnumFactory(), + Questionnaire.QuestionnaireItemType.GROUP, + ), + ) + .apply { + repeats = true + addItem( + Questionnaire.QuestionnaireItemComponent( + StringType("question-0"), + Enumeration( + Questionnaire.QuestionnaireItemTypeEnumFactory(), + Questionnaire.QuestionnaireItemType.INTEGER, + ), + ) + .apply { required = true }, + ) + }, + ) + } + + val questionnaireResponse1 = + QuestionnaireResponse() + .apply { + questionnaire = "questionnaire-1" + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0")) + .apply { + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = IntegerType(1) + }, + ) + }, + ) + }, + ) + + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0")), + ) + }, + ) + } + .apply { packRepeatedGroups(questionnaire1) } + + runTest { + val result = + QuestionnaireResponseValidator.validateQuestionnaireResponse( + questionnaire1, + questionnaireResponse1, + context, + ) + + assertThat(result.keys).containsExactly("question-0", "group-1") + assertThat(result["question-0"]!!.first()).isInstanceOf(Invalid::class.java) + assertThat((result["question-0"]!!.first() as Invalid).getSingleStringValidationMessage()) + .isEqualTo("Missing answer for required field.") + } + } + @Test fun `check passes if questionnaire response matches questionnaire`() { QuestionnaireResponseValidator.checkQuestionnaireResponse( @@ -1653,6 +1727,69 @@ class QuestionnaireResponseValidatorTest { ) } + @Test + fun `check fails for wrong answer type to a nested question in repeating group`() { + assertException_checkQuestionnaireResponse_throwsIllegalArgumentException( + Questionnaire().apply { + url = "questionnaire-1" + addItem( + Questionnaire.QuestionnaireItemComponent( + StringType("group-1"), + Enumeration( + Questionnaire.QuestionnaireItemTypeEnumFactory(), + Questionnaire.QuestionnaireItemType.GROUP, + ), + ) + .apply { + repeats = true + addItem( + Questionnaire.QuestionnaireItemComponent( + StringType("question-0"), + Enumeration( + Questionnaire.QuestionnaireItemTypeEnumFactory(), + Questionnaire.QuestionnaireItemType.INTEGER, + ), + ), + ) + }, + ) + }, + QuestionnaireResponse().apply { + questionnaire = "questionnaire-1" + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0")) + .apply { + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = IntegerType(1) + }, + ) + }, + ) + }, + ) + + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("group-1")).apply { + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent(StringType("question-0")) + .apply { + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = DecimalType(2.0) + }, + ) + }, + ) + }, + ) + }, + "Mismatching question type INTEGER and answer type decimal for question-0", + ) + } + private fun assertException_checkQuestionnaireResponse_throwsIllegalArgumentException( questionnaire: Questionnaire, questionnaireResponse: QuestionnaireResponse, From a21e8243678c64eef10d7feef9dead5324d58500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=E2=89=A1ZRS?= <12814349+LZRS@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:24:45 +0300 Subject: [PATCH 3/3] Include index_name column in index for tables used in sorting (#2753) * Include index_name column in index for tables used in sorting * Add index migration tests * Add index_value/index_from to make sorting index covering * Update Licence year to fix spotless --- .../10.json | 1032 +++++++++++++++++ .../db/impl/ResourceDatabaseMigrationTest.kt | 68 +- .../android/fhir/db/impl/DatabaseImpl.kt | 3 +- .../android/fhir/db/impl/ResourceDatabase.kt | 33 +- .../fhir/db/impl/entities/DateIndexEntity.kt | 4 +- .../db/impl/entities/DateTimeIndexEntity.kt | 4 +- .../db/impl/entities/NumberIndexEntity.kt | 4 +- .../db/impl/entities/StringIndexEntity.kt | 4 +- 8 files changed, 1140 insertions(+), 12 deletions(-) create mode 100644 engine/schemas/com.google.android.fhir.db.impl.ResourceDatabase/10.json diff --git a/engine/schemas/com.google.android.fhir.db.impl.ResourceDatabase/10.json b/engine/schemas/com.google.android.fhir.db.impl.ResourceDatabase/10.json new file mode 100644 index 0000000000..e13506e55d --- /dev/null +++ b/engine/schemas/com.google.android.fhir.db.impl.ResourceDatabase/10.json @@ -0,0 +1,1032 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "b22554c7d1b5c2289ddc13d5d26c7eca", + "entities": [ + { + "tableName": "ResourceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceUuid` BLOB NOT NULL, `resourceType` TEXT NOT NULL, `resourceId` TEXT NOT NULL, `serializedResource` TEXT NOT NULL, `versionId` TEXT, `lastUpdatedRemote` INTEGER, `lastUpdatedLocal` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resourceId", + "columnName": "resourceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedResource", + "columnName": "serializedResource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionId", + "columnName": "versionId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdatedRemote", + "columnName": "lastUpdatedRemote", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastUpdatedLocal", + "columnName": "lastUpdatedLocal", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ResourceEntity_resourceUuid", + "unique": true, + "columnNames": [ + "resourceUuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ResourceEntity_resourceUuid` ON `${TABLE_NAME}` (`resourceUuid`)" + }, + { + "name": "index_ResourceEntity_resourceType_resourceId", + "unique": true, + "columnNames": [ + "resourceType", + "resourceId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ResourceEntity_resourceType_resourceId` ON `${TABLE_NAME}` (`resourceType`, `resourceId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "StringIndexEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceUuid` BLOB NOT NULL, `resourceType` TEXT NOT NULL, `index_name` TEXT NOT NULL, `index_path` TEXT NOT NULL, `index_value` TEXT NOT NULL, FOREIGN KEY(`resourceUuid`) REFERENCES `ResourceEntity`(`resourceUuid`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.name", + "columnName": "index_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.path", + "columnName": "index_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.value", + "columnName": "index_value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_StringIndexEntity_resourceType_index_name_index_value", + "unique": false, + "columnNames": [ + "resourceType", + "index_name", + "index_value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StringIndexEntity_resourceType_index_name_index_value` ON `${TABLE_NAME}` (`resourceType`, `index_name`, `index_value`)" + }, + { + "name": "index_StringIndexEntity_resourceUuid_index_name_index_value", + "unique": false, + "columnNames": [ + "resourceUuid", + "index_name", + "index_value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_StringIndexEntity_resourceUuid_index_name_index_value` ON `${TABLE_NAME}` (`resourceUuid`, `index_name`, `index_value`)" + } + ], + "foreignKeys": [ + { + "table": "ResourceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "resourceUuid" + ], + "referencedColumns": [ + "resourceUuid" + ] + } + ] + }, + { + "tableName": "ReferenceIndexEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceUuid` BLOB NOT NULL, `resourceType` TEXT NOT NULL, `index_name` TEXT NOT NULL, `index_path` TEXT NOT NULL, `index_value` TEXT NOT NULL, FOREIGN KEY(`resourceUuid`) REFERENCES `ResourceEntity`(`resourceUuid`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.name", + "columnName": "index_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.path", + "columnName": "index_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.value", + "columnName": "index_value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ReferenceIndexEntity_resourceType_index_name_index_value", + "unique": false, + "columnNames": [ + "resourceType", + "index_name", + "index_value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ReferenceIndexEntity_resourceType_index_name_index_value` ON `${TABLE_NAME}` (`resourceType`, `index_name`, `index_value`)" + }, + { + "name": "index_ReferenceIndexEntity_resourceUuid", + "unique": false, + "columnNames": [ + "resourceUuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_ReferenceIndexEntity_resourceUuid` ON `${TABLE_NAME}` (`resourceUuid`)" + } + ], + "foreignKeys": [ + { + "table": "ResourceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "resourceUuid" + ], + "referencedColumns": [ + "resourceUuid" + ] + } + ] + }, + { + "tableName": "TokenIndexEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceUuid` BLOB NOT NULL, `resourceType` TEXT NOT NULL, `index_name` TEXT NOT NULL, `index_path` TEXT NOT NULL, `index_system` TEXT, `index_value` TEXT NOT NULL, FOREIGN KEY(`resourceUuid`) REFERENCES `ResourceEntity`(`resourceUuid`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.name", + "columnName": "index_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.path", + "columnName": "index_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.system", + "columnName": "index_system", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "index.value", + "columnName": "index_value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_TokenIndexEntity_resourceType_index_name_index_value_resourceUuid", + "unique": false, + "columnNames": [ + "resourceType", + "index_name", + "index_value", + "resourceUuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TokenIndexEntity_resourceType_index_name_index_value_resourceUuid` ON `${TABLE_NAME}` (`resourceType`, `index_name`, `index_value`, `resourceUuid`)" + }, + { + "name": "index_TokenIndexEntity_resourceUuid", + "unique": false, + "columnNames": [ + "resourceUuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TokenIndexEntity_resourceUuid` ON `${TABLE_NAME}` (`resourceUuid`)" + } + ], + "foreignKeys": [ + { + "table": "ResourceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "resourceUuid" + ], + "referencedColumns": [ + "resourceUuid" + ] + } + ] + }, + { + "tableName": "QuantityIndexEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceUuid` BLOB NOT NULL, `resourceType` TEXT NOT NULL, `index_name` TEXT NOT NULL, `index_path` TEXT NOT NULL, `index_system` TEXT NOT NULL, `index_code` TEXT NOT NULL, `index_value` REAL NOT NULL, FOREIGN KEY(`resourceUuid`) REFERENCES `ResourceEntity`(`resourceUuid`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.name", + "columnName": "index_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.path", + "columnName": "index_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.system", + "columnName": "index_system", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.code", + "columnName": "index_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.value", + "columnName": "index_value", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_QuantityIndexEntity_resourceType_index_name_index_value_index_code", + "unique": false, + "columnNames": [ + "resourceType", + "index_name", + "index_value", + "index_code" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_QuantityIndexEntity_resourceType_index_name_index_value_index_code` ON `${TABLE_NAME}` (`resourceType`, `index_name`, `index_value`, `index_code`)" + }, + { + "name": "index_QuantityIndexEntity_resourceUuid", + "unique": false, + "columnNames": [ + "resourceUuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_QuantityIndexEntity_resourceUuid` ON `${TABLE_NAME}` (`resourceUuid`)" + } + ], + "foreignKeys": [ + { + "table": "ResourceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "resourceUuid" + ], + "referencedColumns": [ + "resourceUuid" + ] + } + ] + }, + { + "tableName": "UriIndexEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceUuid` BLOB NOT NULL, `resourceType` TEXT NOT NULL, `index_name` TEXT NOT NULL, `index_path` TEXT NOT NULL, `index_value` TEXT NOT NULL, FOREIGN KEY(`resourceUuid`) REFERENCES `ResourceEntity`(`resourceUuid`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.name", + "columnName": "index_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.path", + "columnName": "index_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.value", + "columnName": "index_value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_UriIndexEntity_resourceType_index_name_index_value", + "unique": false, + "columnNames": [ + "resourceType", + "index_name", + "index_value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UriIndexEntity_resourceType_index_name_index_value` ON `${TABLE_NAME}` (`resourceType`, `index_name`, `index_value`)" + }, + { + "name": "index_UriIndexEntity_resourceUuid", + "unique": false, + "columnNames": [ + "resourceUuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_UriIndexEntity_resourceUuid` ON `${TABLE_NAME}` (`resourceUuid`)" + } + ], + "foreignKeys": [ + { + "table": "ResourceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "resourceUuid" + ], + "referencedColumns": [ + "resourceUuid" + ] + } + ] + }, + { + "tableName": "DateIndexEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceUuid` BLOB NOT NULL, `resourceType` TEXT NOT NULL, `index_name` TEXT NOT NULL, `index_path` TEXT NOT NULL, `index_from` INTEGER NOT NULL, `index_to` INTEGER NOT NULL, FOREIGN KEY(`resourceUuid`) REFERENCES `ResourceEntity`(`resourceUuid`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.name", + "columnName": "index_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.path", + "columnName": "index_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.from", + "columnName": "index_from", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index.to", + "columnName": "index_to", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_DateIndexEntity_resourceType_index_name_resourceUuid_index_from_index_to", + "unique": false, + "columnNames": [ + "resourceType", + "index_name", + "resourceUuid", + "index_from", + "index_to" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DateIndexEntity_resourceType_index_name_resourceUuid_index_from_index_to` ON `${TABLE_NAME}` (`resourceType`, `index_name`, `resourceUuid`, `index_from`, `index_to`)" + }, + { + "name": "index_DateIndexEntity_resourceUuid_index_name_index_from", + "unique": false, + "columnNames": [ + "resourceUuid", + "index_name", + "index_from" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DateIndexEntity_resourceUuid_index_name_index_from` ON `${TABLE_NAME}` (`resourceUuid`, `index_name`, `index_from`)" + } + ], + "foreignKeys": [ + { + "table": "ResourceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "resourceUuid" + ], + "referencedColumns": [ + "resourceUuid" + ] + } + ] + }, + { + "tableName": "DateTimeIndexEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceUuid` BLOB NOT NULL, `resourceType` TEXT NOT NULL, `index_name` TEXT NOT NULL, `index_path` TEXT NOT NULL, `index_from` INTEGER NOT NULL, `index_to` INTEGER NOT NULL, FOREIGN KEY(`resourceUuid`) REFERENCES `ResourceEntity`(`resourceUuid`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.name", + "columnName": "index_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.path", + "columnName": "index_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.from", + "columnName": "index_from", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index.to", + "columnName": "index_to", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_DateTimeIndexEntity_resourceType_index_name_resourceUuid_index_from_index_to", + "unique": false, + "columnNames": [ + "resourceType", + "index_name", + "resourceUuid", + "index_from", + "index_to" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DateTimeIndexEntity_resourceType_index_name_resourceUuid_index_from_index_to` ON `${TABLE_NAME}` (`resourceType`, `index_name`, `resourceUuid`, `index_from`, `index_to`)" + }, + { + "name": "index_DateTimeIndexEntity_resourceUuid_index_name_index_from", + "unique": false, + "columnNames": [ + "resourceUuid", + "index_name", + "index_from" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DateTimeIndexEntity_resourceUuid_index_name_index_from` ON `${TABLE_NAME}` (`resourceUuid`, `index_name`, `index_from`)" + } + ], + "foreignKeys": [ + { + "table": "ResourceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "resourceUuid" + ], + "referencedColumns": [ + "resourceUuid" + ] + } + ] + }, + { + "tableName": "NumberIndexEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceUuid` BLOB NOT NULL, `resourceType` TEXT NOT NULL, `index_name` TEXT NOT NULL, `index_path` TEXT NOT NULL, `index_value` REAL NOT NULL, FOREIGN KEY(`resourceUuid`) REFERENCES `ResourceEntity`(`resourceUuid`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.name", + "columnName": "index_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.path", + "columnName": "index_path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.value", + "columnName": "index_value", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_NumberIndexEntity_resourceType_index_name_index_value", + "unique": false, + "columnNames": [ + "resourceType", + "index_name", + "index_value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NumberIndexEntity_resourceType_index_name_index_value` ON `${TABLE_NAME}` (`resourceType`, `index_name`, `index_value`)" + }, + { + "name": "index_NumberIndexEntity_resourceUuid_index_name_index_value", + "unique": false, + "columnNames": [ + "resourceUuid", + "index_name", + "index_value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NumberIndexEntity_resourceUuid_index_name_index_value` ON `${TABLE_NAME}` (`resourceUuid`, `index_name`, `index_value`)" + } + ], + "foreignKeys": [ + { + "table": "ResourceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "resourceUuid" + ], + "referencedColumns": [ + "resourceUuid" + ] + } + ] + }, + { + "tableName": "LocalChangeEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceType` TEXT NOT NULL, `resourceId` TEXT NOT NULL, `resourceUuid` BLOB NOT NULL, `timestamp` INTEGER NOT NULL, `type` INTEGER NOT NULL, `payload` TEXT NOT NULL, `versionId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resourceId", + "columnName": "resourceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "payload", + "columnName": "payload", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionId", + "columnName": "versionId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_LocalChangeEntity_resourceType_resourceId", + "unique": false, + "columnNames": [ + "resourceType", + "resourceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LocalChangeEntity_resourceType_resourceId` ON `${TABLE_NAME}` (`resourceType`, `resourceId`)" + }, + { + "name": "index_LocalChangeEntity_resourceUuid", + "unique": false, + "columnNames": [ + "resourceUuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LocalChangeEntity_resourceUuid` ON `${TABLE_NAME}` (`resourceUuid`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "PositionIndexEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `resourceUuid` BLOB NOT NULL, `resourceType` TEXT NOT NULL, `index_latitude` REAL NOT NULL, `index_longitude` REAL NOT NULL, FOREIGN KEY(`resourceUuid`) REFERENCES `ResourceEntity`(`resourceUuid`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUuid", + "columnName": "resourceUuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "resourceType", + "columnName": "resourceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index.latitude", + "columnName": "index_latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "index.longitude", + "columnName": "index_longitude", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_PositionIndexEntity_resourceType_index_latitude_index_longitude", + "unique": false, + "columnNames": [ + "resourceType", + "index_latitude", + "index_longitude" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PositionIndexEntity_resourceType_index_latitude_index_longitude` ON `${TABLE_NAME}` (`resourceType`, `index_latitude`, `index_longitude`)" + }, + { + "name": "index_PositionIndexEntity_resourceUuid", + "unique": false, + "columnNames": [ + "resourceUuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_PositionIndexEntity_resourceUuid` ON `${TABLE_NAME}` (`resourceUuid`)" + } + ], + "foreignKeys": [ + { + "table": "ResourceEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "resourceUuid" + ], + "referencedColumns": [ + "resourceUuid" + ] + } + ] + }, + { + "tableName": "LocalChangeResourceReferenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `localChangeId` INTEGER NOT NULL, `resourceReferenceValue` TEXT NOT NULL, `resourceReferencePath` TEXT, FOREIGN KEY(`localChangeId`) REFERENCES `LocalChangeEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localChangeId", + "columnName": "localChangeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceReferenceValue", + "columnName": "resourceReferenceValue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resourceReferencePath", + "columnName": "resourceReferencePath", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_LocalChangeResourceReferenceEntity_resourceReferenceValue", + "unique": false, + "columnNames": [ + "resourceReferenceValue" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LocalChangeResourceReferenceEntity_resourceReferenceValue` ON `${TABLE_NAME}` (`resourceReferenceValue`)" + }, + { + "name": "index_LocalChangeResourceReferenceEntity_localChangeId", + "unique": false, + "columnNames": [ + "localChangeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LocalChangeResourceReferenceEntity_localChangeId` ON `${TABLE_NAME}` (`localChangeId`)" + } + ], + "foreignKeys": [ + { + "table": "LocalChangeEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "localChangeId" + ], + "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, 'b22554c7d1b5c2289ddc13d5d26c7eca')" + ] + } +} \ No newline at end of file diff --git a/engine/src/androidTest/java/com/google/android/fhir/db/impl/ResourceDatabaseMigrationTest.kt b/engine/src/androidTest/java/com/google/android/fhir/db/impl/ResourceDatabaseMigrationTest.kt index 01b9312cf2..62cf04e962 100644 --- a/engine/src/androidTest/java/com/google/android/fhir/db/impl/ResourceDatabaseMigrationTest.kt +++ b/engine/src/androidTest/java/com/google/android/fhir/db/impl/ResourceDatabaseMigrationTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -418,6 +418,72 @@ class ResourceDatabaseMigrationTest { assertThat(retrievedTask).isEqualTo(bedNetTask) } + @Test + fun migrate9To10_should_execute_with_no_exception(): Unit = runBlocking { + val patient1Id = "patient-001" + val patient1ResourceUuid = "e2c79e28-ed4d-4029-a12c-108d1eb5bedb" + val patient1: String = + Patient() + .apply { + id = patient1Id + addName(HumanName().apply { addGiven("Brad") }) + } + .let { iParser.encodeResourceToString(it) } + + val patient2Id = "patient-002" + val patient2ResourceUuid = "541782b3-48f5-4c36-bd20-cae265e974e7" + val patient2: String = + Patient() + .apply { + id = patient2Id + addName(HumanName().apply { addGiven("Alex") }) + } + .let { iParser.encodeResourceToString(it) } + + helper.createDatabase(DB_NAME, 9).apply { + execSQL( + "INSERT INTO ResourceEntity (resourceUuid, resourceType, resourceId, serializedResource) VALUES ('$patient1ResourceUuid', 'Patient', '$patient1', '$patient1');", + ) + execSQL( + "INSERT INTO ResourceEntity (resourceUuid, resourceType, resourceId, serializedResource) VALUES ('$patient2ResourceUuid', 'Patient', '$patient2', '$patient2');", + ) + + close() + } + + val migratedDatabase = helper.runMigrationsAndValidate(DB_NAME, 10, true, Migration_9_10) + + val patientResult1: String? + val patientResult2: String? + + migratedDatabase.let { database -> + database + .query( + """ + SELECT a.serializedResource + FROM ResourceEntity a + LEFT JOIN StringIndexEntity b + ON a.resourceUuid = b.resourceUuid AND b.index_name = 'name' + WHERE a.resourceType = 'Patient' + GROUP BY a.resourceUuid + HAVING MAX(IFNULL(b.index_value,0)) >= -9223372036854775808 + ORDER BY IFNULL(b.index_value, -9223372036854775808) ASC + """ + .trimIndent(), + ) + .let { + it.moveToFirst() + patientResult1 = it.getString(0) + it.moveToNext() + patientResult2 = it.getString(0) + } + } + migratedDatabase.close() + + assertThat(patientResult1).isEqualTo(patient2) + assertThat(patientResult2).isEqualTo(patient1) + } + companion object { const val DB_NAME = "migration_tests.db" } diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt b/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt index 1e8333ea5b..c6b3d0ad71 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -115,6 +115,7 @@ internal class DatabaseImpl( MIGRATION_6_7, MIGRATION_7_8, Migration_8_9, + Migration_9_10, ) } .build() diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/ResourceDatabase.kt b/engine/src/main/java/com/google/android/fhir/db/impl/ResourceDatabase.kt index 8b6c6a483d..7fee49452e 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/ResourceDatabase.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/ResourceDatabase.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ import org.json.JSONObject PositionIndexEntity::class, LocalChangeResourceReferenceEntity::class, ], - version = 9, + version = 10, exportSchema = true, ) @TypeConverters(DbTypeConverters::class) @@ -226,3 +226,32 @@ internal val Migration_8_9 = } } } + +internal val Migration_9_10 = + object : Migration(9, 10) { + override fun migrate(database: SupportSQLiteDatabase) { + database.beginTransaction() + try { + database.execSQL("DROP INDEX IF EXISTS `index_DateIndexEntity_resourceUuid`;") + database.execSQL("DROP INDEX IF EXISTS `index_DateTimeIndexEntity_resourceUuid`;") + database.execSQL("DROP INDEX IF EXISTS `index_NumberIndexEntity_resourceUuid`;") + database.execSQL("DROP INDEX IF EXISTS `index_StringIndexEntity_resourceUuid`;") + + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_DateIndexEntity_resourceUuid_index_name_index_from` ON `DateIndexEntity` (`resourceUuid`, `index_name`, `index_from`);", + ) + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_DateTimeIndexEntity_resourceUuid_index_name_index_from` ON `DateTimeIndexEntity` (`resourceUuid`, `index_name`, `index_from`);", + ) + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_NumberIndexEntity_resourceUuid_index_name_index_value` ON `NumberIndexEntity` (`resourceUuid`, `index_name`, `index_value`);", + ) + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_StringIndexEntity_resourceUuid_index_name_index_value` ON `StringIndexEntity` (`resourceUuid`, `index_name`, `index_value`);", + ) + database.setTransactionSuccessful() + } finally { + database.endTransaction() + } + } + } diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/entities/DateIndexEntity.kt b/engine/src/main/java/com/google/android/fhir/db/impl/entities/DateIndexEntity.kt index 5963c73c08..1d9b666384 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/entities/DateIndexEntity.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/entities/DateIndexEntity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ import org.hl7.fhir.r4.model.ResourceType // https://github.com/google/android-fhir/issues/2040 Index(value = ["resourceType", "index_name", "resourceUuid", "index_from", "index_to"]), // Keep this index for faster foreign lookup - Index(value = ["resourceUuid"]), + Index(value = ["resourceUuid", "index_name", "index_from"]), ], foreignKeys = [ diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/entities/DateTimeIndexEntity.kt b/engine/src/main/java/com/google/android/fhir/db/impl/entities/DateTimeIndexEntity.kt index 4d8a724fd4..9bf1465ee0 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/entities/DateTimeIndexEntity.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/entities/DateTimeIndexEntity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ import org.hl7.fhir.r4.model.ResourceType // https://github.com/google/android-fhir/issues/2040 Index(value = ["resourceType", "index_name", "resourceUuid", "index_from", "index_to"]), // Keep this index for faster foreign lookup - Index(value = ["resourceUuid"]), + Index(value = ["resourceUuid", "index_name", "index_from"]), ], foreignKeys = [ diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/entities/NumberIndexEntity.kt b/engine/src/main/java/com/google/android/fhir/db/impl/entities/NumberIndexEntity.kt index e547535b32..5907ae8261 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/entities/NumberIndexEntity.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/entities/NumberIndexEntity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Google LLC + * Copyright 2021-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import org.hl7.fhir.r4.model.ResourceType [ Index(value = ["resourceType", "index_name", "index_value"]), // keep this index for faster foreign lookup - Index(value = ["resourceUuid"]), + Index(value = ["resourceUuid", "index_name", "index_value"]), ], foreignKeys = [ diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/entities/StringIndexEntity.kt b/engine/src/main/java/com/google/android/fhir/db/impl/entities/StringIndexEntity.kt index a308b4747f..a97c6ce56a 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/entities/StringIndexEntity.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/entities/StringIndexEntity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Google LLC + * Copyright 2021-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import org.hl7.fhir.r4.model.ResourceType [ Index(value = ["resourceType", "index_name", "index_value"]), // keep this index for faster foreign lookup - Index(value = ["resourceUuid"]), + Index(value = ["resourceUuid", "index_name", "index_value"]), ], foreignKeys = [