1 /* <lambda>null2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.sqlite.inspection.test 18 19 import android.database.sqlite.SQLiteDatabase 20 import androidx.sqlite.inspection.SqliteInspectorProtocol.ErrorContent.ErrorCode.ERROR_NO_OPEN_DATABASE_WITH_REQUESTED_ID_VALUE 21 import androidx.sqlite.inspection.test.MessageFactory.createGetSchemaCommand 22 import androidx.sqlite.inspection.test.MessageFactory.createTrackDatabasesCommand 23 import androidx.test.ext.junit.runners.AndroidJUnit4 24 import androidx.test.filters.MediumTest 25 import androidx.test.filters.SdkSuppress 26 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 27 import com.google.common.truth.Truth.assertThat 28 import kotlinx.coroutines.runBlocking 29 import org.junit.Rule 30 import org.junit.Test 31 import org.junit.rules.TemporaryFolder 32 import org.junit.runner.RunWith 33 34 @MediumTest 35 @RunWith(AndroidJUnit4::class) 36 @SdkSuppress(minSdkVersion = 26) 37 class GetSchemaTest { 38 @get:Rule val testEnvironment = SqliteInspectorTestEnvironment() 39 40 @get:Rule val temporaryFolder = TemporaryFolder(getInstrumentation().context.cacheDir) 41 42 @Test 43 fun test_get_schema_complex_tables() { 44 test_get_schema( 45 listOf( 46 Database( 47 "db1", 48 Table( 49 "table1", 50 Column("t", "TEXT"), 51 Column("nu", "NUMERIC"), 52 Column("i", "INTEGER"), 53 Column("r", "REAL"), 54 Column("b", "BLOB") 55 ), 56 Table("table2", Column("id", "INTEGER"), Column("name", "TEXT")), 57 Table("table3a", Column("c1", "INT"), Column("c2", "INT", primaryKey = 1)), 58 Table( 59 "table3b", // compound-primary-key 60 Column("c1", "INT", primaryKey = 2), 61 Column("c2", "INT", primaryKey = 1) 62 ), 63 Table( 64 "table4", // compound-primary-key, two unique columns 65 Column("c1", "INT", primaryKey = 1), 66 Column("c2", "INT", primaryKey = 2, isUnique = true), 67 Column("c3", "INT", isUnique = true) 68 ), 69 Table( 70 "table5", // mix: unique, primary key, notNull 71 Column("c1", "INT", isNotNull = true), 72 Column("c2", "INT", primaryKey = 1, isUnique = true), 73 Column("c3", "INT", isUnique = true, isNotNull = true) 74 ), 75 Table( 76 "table6", // compound-unique-constraint-indices in [onDatabaseCreated] 77 Column("c1", "INT"), 78 Column("c2uuu", "INT", isUnique = true), 79 Column("c3", "INT"), 80 Column("c4u", "INT", isUnique = true) 81 ) 82 ) 83 ), 84 onDatabaseCreated = { db -> 85 // compound-unique-constraint-indices 86 listOf( 87 "create index index6_12 on 'table6' ('c1', 'c2uuu')", 88 "create index index6_23 on 'table6' ('c2uuu', 'c3')" 89 ) 90 .forEach { query -> db.execSQL(query, emptyArray()) } 91 92 // sanity check: verifies if the above index adding operations succeeded 93 val indexCountTable6 = 94 db.rawQuery("select count(*) from pragma_index_list('table6')", null).let { 95 it.moveToNext() 96 val count = it.getString(0) 97 it.close() 98 count 99 } 100 101 assertThat(indexCountTable6).isEqualTo("4") 102 } 103 ) 104 } 105 106 @Test 107 fun test_get_schema_multiple_databases() { 108 test_get_schema( 109 listOf( 110 Database("db3", Table("t3", Column("c3", "BLOB"))), 111 Database("db2", Table("t2", Column("c2", "TEXT"))), 112 Database("db1", Table("t1", Column("c1", "TEXT"))) 113 ) 114 ) 115 } 116 117 @Test 118 fun test_get_schema_views() { 119 val c1 = Column("c1", "INT") 120 val c2 = Column("c2", "INT") 121 val c3 = Column("c3", "INT") 122 test_get_schema( 123 listOf( 124 Database( 125 "db1", 126 Table("t1", c1, c2), 127 Table("t2", c1, c2, c3), 128 Table( 129 "v1", 130 listOf(c1, c2), 131 isView = true, 132 viewQuery = "select t1.c1, t2.c2 from t1 inner join t2 on t1.c1 = t2.c2" 133 ) 134 ) 135 ) 136 ) 137 } 138 139 @Test 140 fun test_get_schema_auto_increment() = runBlocking { 141 val databaseId = 142 testEnvironment.inspectDatabase( 143 Database("db1").createInstance(temporaryFolder).also { 144 it.execSQL("CREATE TABLE t1 (c2 INTEGER PRIMARY KEY AUTOINCREMENT)") 145 it.execSQL("INSERT INTO t1 VALUES(3)") 146 } 147 ) 148 testEnvironment.sendCommand(createGetSchemaCommand(databaseId)).let { response -> 149 val tableNames = response.getSchema.tablesList.map { it.name } 150 assertThat(tableNames).isEqualTo(listOf("t1")) 151 } 152 } 153 154 @Test 155 fun test_get_schema_wrong_database_id() = runBlocking { 156 val databaseId = 123456789 157 testEnvironment.sendCommand(createGetSchemaCommand(databaseId)).let { response -> 158 assertThat(response.hasErrorOccurred()).isEqualTo(true) 159 val error = response.errorOccurred.content 160 assertThat(error.message) 161 .contains("Unable to perform an operation on database (id=$databaseId).") 162 assertThat(error.message).contains("The database may have already been closed.") 163 assertThat(error.recoverability.isRecoverable).isEqualTo(true) 164 assertThat(error.errorCodeValue) 165 .isEqualTo(ERROR_NO_OPEN_DATABASE_WITH_REQUESTED_ID_VALUE) 166 } 167 } 168 169 private fun test_get_schema( 170 alreadyOpenDatabases: List<Database>, 171 onDatabaseCreated: (SQLiteDatabase) -> Unit = {} 172 ) = runBlocking { 173 assertThat(alreadyOpenDatabases).isNotEmpty() // sanity check 174 175 testEnvironment.registerAlreadyOpenDatabases( 176 alreadyOpenDatabases.map { 177 it.createInstance(temporaryFolder).also { db -> onDatabaseCreated(db) } 178 } 179 ) 180 testEnvironment.sendCommand(createTrackDatabasesCommand()) 181 val databaseConnections = 182 alreadyOpenDatabases.indices.map { testEnvironment.receiveEvent().databaseOpened } 183 184 val schemas = 185 databaseConnections 186 .sortedBy { it.path } 187 .map { 188 testEnvironment.sendCommand(createGetSchemaCommand(it.databaseId)).getSchema 189 } 190 191 alreadyOpenDatabases 192 .sortedBy { it.name } 193 .zipSameSize(schemas) 194 .forEach { (expectedSchema, actualSchema) -> 195 val expectedTables = expectedSchema.tables.sortedBy { it.name } 196 val actualTables = actualSchema.tablesList.sortedBy { it.name } 197 198 expectedTables.zipSameSize(actualTables).forEach { (expectedTable, actualTable) -> 199 assertThat(actualTable.name).isEqualTo(expectedTable.name) 200 assertThat(actualTable.isView).isEqualTo(expectedTable.isView) 201 202 val expectedColumns = expectedTable.columns.sortedBy { it.name } 203 val actualColumns = actualTable.columnsList.sortedBy { it.name } 204 205 expectedColumns 206 .adjustForSinglePrimaryKey() 207 .zipSameSize(actualColumns) 208 .forEach { (expectedColumn, actualColumnProto) -> 209 val actualColumn = 210 Column( 211 name = actualColumnProto.name, 212 type = actualColumnProto.type, 213 primaryKey = actualColumnProto.primaryKey, 214 isNotNull = actualColumnProto.isNotNull, 215 isUnique = actualColumnProto.isUnique 216 ) 217 assertThat(actualColumn).isEqualTo(expectedColumn) 218 } 219 } 220 } 221 } 222 223 // The sole primary key in a table is by definition unique 224 private fun List<Column>.adjustForSinglePrimaryKey(): List<Column> = 225 if (this.count { it.isPrimaryKey } > 1) this 226 else this.map { if (it.isPrimaryKey) it.copy(isUnique = true) else it } 227 228 /** Same as [List.zip] but ensures both lists are the same size. */ 229 private fun <A, B> List<A>.zipSameSize(other: List<B>): List<Pair<A, B>> { 230 assertThat(this.size).isEqualTo(other.size) 231 return this.zip(other) 232 } 233 } 234