DatabaseVerifier.kt
/*
* 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.compiler.processing.XElement
import androidx.room.processor.Context
import androidx.room.vo.DatabaseView
import androidx.room.vo.Entity
import androidx.room.vo.EntityOrView
import androidx.room.vo.FtsEntity
import androidx.room.vo.FtsOptions
import androidx.room.vo.Warning
import org.sqlite.JDBC
import org.sqlite.SQLiteJDBCLoader
import java.io.File
import java.sql.Connection
import java.sql.SQLException
import java.util.regex.Pattern
/**
* Builds an in-memory version of the database and verifies the queries against it.
* This class is also used to resolve the return types.
*/
class DatabaseVerifier private constructor(
val connection: Connection,
val context: Context,
entities: List<Entity>,
views: List<DatabaseView>
) {
val entitiesAndViews: List<EntityOrView> = entities + views
companion object {
private const val CONNECTION_URL = "jdbc:sqlite::memory:"
/**
* Taken from:
* https://github.com/robolectric/robolectric/blob/master/shadows/framework/
* src/main/java/org/robolectric/shadows/ShadowSQLiteConnection.java#L94
*
* This is actually not accurate because it might swap anything since it does not parse
* SQL. That being said, for the verification purposes, it does not matter and clearly
* much easier than parsing and rebuilding the query.
*/
private val COLLATE_LOCALIZED_UNICODE_PATTERN = Pattern.compile(
"\s+COLLATE\s+(LOCALIZED|UNICODE)", Pattern.CASE_INSENSITIVE
)
init {
verifyTempDir()
// Synchronize on a bootstrap loaded class so that parallel runs of Room in the same JVM
// with isolated class loaders (such as the Gradle daemon) don't conflict with each
// other when extracting the native library. SQLiteJDBCLoader already handles
// multiple library versions, process isolation and multiple class loaders by using
// UUID named library files.
synchronized(System::class.java) {
SQLiteJDBCLoader.initialize() // extract and loads native library
JDBC.isValidURL(CONNECTION_URL) // call to register driver
}
}
private fun verifyTempDir() {
val defaultTempDir = System.getProperty("java.io.tmpdir")
val tempDir = System.getProperty("org.sqlite.tmpdir", defaultTempDir)
checkNotNull(tempDir) {
"Room needs the java.io.tmpdir or org.sqlite.tmpdir system property to be set to " +
"setup SQLite."
}
File(tempDir).also {
check(
it.isDirectory &&
(it.exists() || it.mkdirs()) &&
it.canRead() &&
it.canWrite()
) {
"The temp dir [$tempDir] needs to be a directory, must be readable, writable " +
"and allow executables. Please, provide a temporary directory that " +
"fits the requirements via the 'org.sqlite.tmpdir' property."
}
}
}
/**
* Tries to create a verifier but returns null if it cannot find the driver.
*/
fun create(
context: Context,
element: XElement,
entities: List<Entity>,
views: List<DatabaseView>
): DatabaseVerifier? {
try {
val connection = JDBC.createConnection(CONNECTION_URL, java.util.Properties())
return DatabaseVerifier(connection, context, entities, views)
} catch (ex: Exception) {
context.logger.w(
Warning.CANNOT_CREATE_VERIFICATION_DATABASE, element,
DatabaseVerificationErrors.cannotCreateConnection(ex)
)
return null
}
}
}
init {
entities.forEach { entity ->
val stmt = connection.createStatement()
val createTableQuery = if (entity is FtsEntity &&
!FtsOptions.defaultTokenizers.contains(entity.ftsOptions.tokenizer)
) {
// Custom FTS tokenizer used, use create statement without custom tokenizer
// since the DB used for verification probably doesn't have the tokenizer.
entity.getCreateTableQueryWithoutTokenizer()
} else {
entity.createTableQuery
}
try {
stmt.executeUpdate(stripLocalizeCollations(createTableQuery))
} catch (e: SQLException) {
context.logger.e(entity.element, "${e.message}")
}
entity.indices.forEach {
stmt.executeUpdate(it.createQuery(entity.tableName))
}
}
views.forEach { view ->
val stmt = connection.createStatement()
try {
stmt.executeUpdate(stripLocalizeCollations(view.createViewQuery))
} catch (e: SQLException) {
context.logger.e(view.element, "${e.message}")
}
}
}
fun analyze(sql: String): QueryResultInfo {
return try {
val stmt = connection.prepareStatement(stripLocalizeCollations(sql))
QueryResultInfo(stmt.columnInfo())
} catch (ex: SQLException) {
QueryResultInfo(emptyList(), ex)
}
}
private fun stripLocalizeCollations(sql: String) =
COLLATE_LOCALIZED_UNICODE_PATTERN.matcher(sql).replaceAll(" COLLATE NOCASE")
fun closeConnection(context: Context) {
if (!connection.isClosed) {
try {
connection.close()
} catch (t: Throwable) {
// ignore.
context.logger.d("failed to close the database connection ${t.message}")
}
}
}
}