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