blob: bbe29693ad6f5ebc45d393b92fd15b7828e8bac6 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.verifier
import androidx.room.parser.Collate
import androidx.room.parser.SQLTypeAffinity
import androidx.room.parser.SqlParser
import androidx.room.processing.XConstructorElement
import androidx.room.processing.XDeclaredType
import androidx.room.processing.XElement
import androidx.room.processing.XType
import androidx.room.processing.XTypeElement
import androidx.room.processing.XVariableElement
import androidx.room.processor.Context
import androidx.room.testing.TestInvocation
import androidx.room.vo.CallType
import androidx.room.vo.Constructor
import androidx.room.vo.Database
import androidx.room.vo.DatabaseView
import androidx.room.vo.Entity
import androidx.room.vo.Field
import androidx.room.vo.FieldGetter
import androidx.room.vo.FieldSetter
import androidx.room.vo.Fields
import androidx.room.vo.PrimaryKey
import collect
import columnNames
import com.squareup.javapoet.TypeName
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.hasItem
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import simpleRun
import java.sql.Connection
@RunWith(Parameterized::class)
class DatabaseVerifierTest(private val useLocalizedCollation: Boolean) {
@Test
fun testSimpleDatabase() {
simpleRun { invocation ->
val verifier = createVerifier(invocation)
val stmt = verifier.connection.createStatement()
val rs = stmt.executeQuery("select * from sqlite_master WHERE type='table'")
assertThat(
rs.collect { set -> set.getString("name") }, hasItem(`is`("User")))
val table = verifier.connection.prepareStatement("select * from User")
assertThat(table.columnNames(), `is`(listOf("id", "name", "lastName", "ratio")))
assertThat(getPrimaryKeys(verifier.connection, "User"), `is`(listOf("id")))
}.compilesWithoutError()
}
private fun createVerifier(invocation: TestInvocation): DatabaseVerifier {
val db = userDb(invocation)
return DatabaseVerifier.create(invocation.context, mock(XElement::class.java),
db.entities, db.views)!!
}
@Test
fun testFullEntityQuery() {
validQueryTest("select * from User") {
assertThat(it, `is`(
QueryResultInfo(listOf(
ColumnInfo("id", SQLTypeAffinity.INTEGER),
ColumnInfo("name", SQLTypeAffinity.TEXT),
ColumnInfo("lastName", SQLTypeAffinity.TEXT),
ColumnInfo("ratio", SQLTypeAffinity.REAL)
))))
}
}
@Test
fun testPartialFields() {
validQueryTest("select id, lastName from User") {
assertThat(it, `is`(
QueryResultInfo(listOf(
ColumnInfo("id", SQLTypeAffinity.INTEGER),
ColumnInfo("lastName", SQLTypeAffinity.TEXT)
))))
}
}
@Test
fun testRenamedField() {
validQueryTest("select id as myId, lastName from User") {
assertThat(it, `is`(
QueryResultInfo(listOf(
ColumnInfo("myId", SQLTypeAffinity.INTEGER),
ColumnInfo("lastName", SQLTypeAffinity.TEXT)
))))
}
}
@Test
fun testGrouped() {
validQueryTest("select MAX(ratio) from User GROUP BY name") {
assertThat(it, `is`(
QueryResultInfo(listOf(
// unfortunately, we don't get this information
ColumnInfo("MAX(ratio)", SQLTypeAffinity.NULL)
))))
}
}
@Test
fun testConcat() {
validQueryTest("select name || lastName as mergedName from User") {
assertThat(it, `is`(
QueryResultInfo(listOf(
// unfortunately, we don't get this information
ColumnInfo("mergedName", SQLTypeAffinity.NULL)
))))
}
}
@Test
fun testResultWithArgs() {
validQueryTest("select id, name || lastName as mergedName from User where name LIKE ?") {
assertThat(it, `is`(
QueryResultInfo(listOf(
// unfortunately, we don't get this information
ColumnInfo("id", SQLTypeAffinity.INTEGER),
ColumnInfo("mergedName", SQLTypeAffinity.NULL)
))))
}
}
@Test
fun testDeleteQuery() {
validQueryTest("delete from User where name LIKE ?") {
assertThat(it, `is`(QueryResultInfo(emptyList())))
}
}
@Test
fun testUpdateQuery() {
validQueryTest("update User set name = ? WHERE id = ?") {
assertThat(it, `is`(QueryResultInfo(emptyList())))
}
}
@Test
fun testBadQuery() {
simpleRun { invocation ->
val verifier = createVerifier(invocation)
val (_, error) = verifier.analyze("select foo from User")
assertThat(error, notNullValue())
}.compilesWithoutError()
}
@Test
fun testCollate() {
validQueryTest("SELECT id, name FROM user ORDER BY name COLLATE LOCALIZED ASC") {
assertThat(it, `is`(
QueryResultInfo(listOf(
// unfortunately, we don't get this information
ColumnInfo("id", SQLTypeAffinity.INTEGER),
ColumnInfo("name", SQLTypeAffinity.TEXT)
))))
}
}
@Test
fun testCollateBasQuery() {
simpleRun { invocation ->
val verifier = createVerifier(invocation)
val (_, error) = verifier.analyze(
"SELECT id, name FROM user ORDER BY name COLLATE LOCALIZEDASC")
assertThat(error, notNullValue())
}.compilesWithoutError()
}
@Test
fun testFullViewQuery() {
validQueryTest("select * from UserSummary") {
assertThat(it, `is`(
QueryResultInfo(listOf(
ColumnInfo("id", SQLTypeAffinity.INTEGER),
ColumnInfo("name", SQLTypeAffinity.TEXT)
))
))
}
}
@Test
fun testViewNoSuchColumn() {
simpleRun { invocation ->
val verifier = createVerifier(invocation)
val (_, error) = verifier.analyze(
"SELECT ratio FROM UserSummary")
assertThat(error, notNullValue())
assertThat(error?.message, containsString("no such column: ratio"))
}.compilesWithoutError()
}
@Test
fun defaultValue_exprError() {
simpleRun { invocation ->
val db = database(
listOf(
entity(
invocation,
"User",
field(
"id",
primitive(invocation.context, TypeName.INT),
SQLTypeAffinity.INTEGER
),
field(
"name",
invocation.context.COMMON_TYPES.STRING,
SQLTypeAffinity.TEXT,
defaultValue = "(NO_SUCH_CONSTANT)"
)
)
),
emptyList()
)
val element = mock(XElement::class.java)
DatabaseVerifier.create(invocation.context, element, db.entities, db.views)!!
}.failsToCompile().withErrorContaining("default value of column [name]")
}
private fun validQueryTest(sql: String, cb: (QueryResultInfo) -> Unit) {
simpleRun { invocation ->
val verifier = createVerifier(invocation)
val info = verifier.analyze(sql)
cb(info)
}.compilesWithoutError()
}
private fun userDb(invocation: TestInvocation): Database {
val context = invocation.context
return database(
listOf(
entity(
invocation,
"User",
field("id", primitive(context, TypeName.INT), SQLTypeAffinity.INTEGER),
field("name", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT),
field("lastName", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT),
field("ratio", primitive(context, TypeName.FLOAT), SQLTypeAffinity.REAL)
)
),
listOf(
view(
"UserSummary", "SELECT id, name FROM User",
field("id", primitive(context, TypeName.INT), SQLTypeAffinity.INTEGER),
field("name", context.COMMON_TYPES.STRING, SQLTypeAffinity.TEXT)
)
)
)
}
private fun database(entities: List<Entity>, views: List<DatabaseView>): Database {
return Database(
element = mock(XTypeElement::class.java),
type = mock(XType::class.java),
entities = entities,
views = views,
daoMethods = emptyList(),
version = -1,
exportSchema = false,
enableForeignKeys = false)
}
private fun entity(
invocation: TestInvocation,
tableName: String,
vararg fields: Field
): Entity {
val element = invocation.processingEnv.requireTypeElement("NoOp")
return Entity(
element = element,
tableName = tableName,
type = mock(XDeclaredType::class.java),
fields = fields.toList(),
embeddedFields = emptyList(),
indices = emptyList(),
primaryKey = PrimaryKey(null, Fields(fields.take(1)), false),
foreignKeys = emptyList(),
constructor = Constructor(mock(XConstructorElement::class.java), emptyList()),
shadowTableName = null
)
}
private fun view(viewName: String, query: String, vararg fields: Field): DatabaseView {
return DatabaseView(
element = mock(XTypeElement::class.java),
viewName = viewName,
type = mock(XDeclaredType::class.java),
fields = fields.toList(),
embeddedFields = emptyList(),
query = SqlParser.parse(query),
constructor = Constructor(mock(XConstructorElement::class.java), emptyList())
)
}
private fun field(
name: String,
type: XType,
affinity: SQLTypeAffinity,
defaultValue: String? = null
): Field {
val element = mock(XVariableElement::class.java)
doReturn(type).`when`(element).type
val f = Field(
element = element,
name = name,
type = type,
columnName = name,
affinity = affinity,
collate = if (useLocalizedCollation && affinity == SQLTypeAffinity.TEXT) {
Collate.LOCALIZED
} else {
null
},
defaultValue = defaultValue
)
assignGetterSetter(f, name, type)
return f
}
private fun assignGetterSetter(f: Field, name: String, type: XType) {
f.getter = FieldGetter(name, type, CallType.FIELD)
f.setter = FieldSetter(name, type, CallType.FIELD)
}
private fun primitive(context: Context, typeName: TypeName): XType {
return context.processingEnv.requireType(typeName)
}
private fun getPrimaryKeys(connection: Connection, tableName: String): List<String> {
val stmt = connection.createStatement()
val resultSet = stmt.executeQuery("PRAGMA table_info($tableName)")
return resultSet.collect {
Pair(it.getString("name"), it.getInt("pk"))
}
.filter { it.second > 0 }
.sortedBy { it.second }
.map { it.first }
}
companion object {
@Parameterized.Parameters(name = "useLocalizedCollation={0}")
@JvmStatic
fun params() = arrayListOf(true, false)
}
}