/*
* 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
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.sqlite.db.SupportSQLiteProgram
import androidx.sqlite.db.SupportSQLiteQuery
import java.util.Arrays
import java.util.TreeMap
/**
* This class is used as an intermediate place to keep binding arguments so that we can run
* Cursor queries with correct types rather than passing everything as a string.
*
* Because it is relatively a big object, they are pooled and must be released after each use.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class RoomSQLiteQuery private constructor(
@field:VisibleForTesting val capacity: Int
) : SupportSQLiteQuery, SupportSQLiteProgram {
@Volatile
private var query: String? = null
@JvmField
@VisibleForTesting
val longBindings: LongArray
@JvmField
@VisibleForTesting
val doubleBindings: DoubleArray
@JvmField
@VisibleForTesting
val stringBindings: Array<String?>
@JvmField
@VisibleForTesting
val blobBindings: Array<ByteArray?>
@Binding
private val bindingTypes: IntArray
// number of arguments in the query
override var argCount = 0
private set
fun init(query: String, initArgCount: Int) {
this.query = query
argCount = initArgCount
}
init {
// because, 1 based indices... we don't want to offsets everything with 1 all the time.
val limit = capacity + 1
bindingTypes = IntArray(limit)
longBindings = LongArray(limit)
doubleBindings = DoubleArray(limit)
stringBindings = arrayOfNulls(limit)
blobBindings = arrayOfNulls(limit)
}
/**
* Releases the query back to the pool.
*
* After released, the statement might be returned when [.acquire] is called
* so you should never re-use it after releasing.
*/
fun release() {
synchronized(queryPool) {
queryPool[capacity] = this
prunePoolLocked()
}
}
override val sql: String
get() = checkNotNull(this.query)
override fun bindTo(statement: SupportSQLiteProgram) {
for (index in 1..argCount) {
when (bindingTypes[index]) {
NULL -> statement.bindNull(index)
LONG -> statement.bindLong(index, longBindings[index])
DOUBLE -> statement.bindDouble(index, doubleBindings[index])
STRING -> statement.bindString(index, requireNotNull(stringBindings[index]))
BLOB -> statement.bindBlob(index, requireNotNull(blobBindings[index]))
}
}
}
override fun bindNull(index: Int) {
bindingTypes[index] = NULL
}
override fun bindLong(index: Int, value: Long) {
bindingTypes[index] = LONG
longBindings[index] = value
}
override fun bindDouble(index: Int, value: Double) {
bindingTypes[index] = DOUBLE
doubleBindings[index] = value
}
override fun bindString(index: Int, value: String) {
bindingTypes[index] = STRING
stringBindings[index] = value
}
override fun bindBlob(index: Int, value: ByteArray) {
bindingTypes[index] = BLOB
blobBindings[index] = value
}
override fun close() {
// no-op. not calling release because it is internal API.
}
/**
* Copies arguments from another RoomSQLiteQuery into this query.
*
* @param other The other query, which holds the arguments to be copied.
*/
fun copyArgumentsFrom(other: RoomSQLiteQuery) {
val argCount = other.argCount + 1 // +1 for the binding offsets
System.arraycopy(other.bindingTypes, 0, bindingTypes, 0, argCount)
System.arraycopy(other.longBindings, 0, longBindings, 0, argCount)
System.arraycopy(other.stringBindings, 0, stringBindings, 0, argCount)
System.arraycopy(other.blobBindings, 0, blobBindings, 0, argCount)
System.arraycopy(other.doubleBindings, 0, doubleBindings, 0, argCount)
}
override fun clearBindings() {
Arrays.fill(bindingTypes, NULL)
Arrays.fill(stringBindings, null)
Arrays.fill(blobBindings, null)
query = null
// no need to clear others
}
@Retention(AnnotationRetention.SOURCE)
@IntDef(NULL, LONG, DOUBLE, STRING, BLOB)
internal annotation class Binding
companion object {
// Maximum number of queries we'll keep cached.
@VisibleForTesting
const val POOL_LIMIT = 15
// Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
// clear the bigger queries (# of arguments).
@VisibleForTesting
const val DESIRED_POOL_SIZE = 10
@JvmField
@VisibleForTesting
val queryPool = TreeMap<Int, RoomSQLiteQuery>()
/**
* Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
*
* @param supportSQLiteQuery The query to copy from
* @return A new query copied from the provided one.
*/
@JvmStatic
fun copyFrom(supportSQLiteQuery: SupportSQLiteQuery): RoomSQLiteQuery {
val query = acquire(
supportSQLiteQuery.sql,
supportSQLiteQuery.argCount
)
supportSQLiteQuery.bindTo(object : SupportSQLiteProgram by query {})
return query
}
/**
* Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
* given query.
*
* @param query The query to prepare
* @param argumentCount The number of query arguments
* @return A RoomSQLiteQuery that holds the given query and has space for the given number
* of arguments.
*/
@JvmStatic
fun acquire(query: String, argumentCount: Int): RoomSQLiteQuery {
synchronized(queryPool) {
val entry = queryPool.ceilingEntry(argumentCount)
if (entry != null) {
queryPool.remove(entry.key)
val sqliteQuery = entry.value
sqliteQuery.init(query, argumentCount)
return sqliteQuery
}
}
val sqLiteQuery = RoomSQLiteQuery(argumentCount)
sqLiteQuery.init(query, argumentCount)
return sqLiteQuery
}
internal fun prunePoolLocked() {
if (queryPool.size > POOL_LIMIT) {
var toBeRemoved = queryPool.size - DESIRED_POOL_SIZE
val iterator = queryPool.descendingKeySet().iterator()
while (toBeRemoved-- > 0) {
iterator.next()
iterator.remove()
}
}
}
private const val NULL = 1
private const val LONG = 2
private const val DOUBLE = 3
private const val STRING = 4
private const val BLOB = 5
}
}