Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ import com.google.firebase.dataconnect.util.CoroutineUtils
import com.google.firebase.dataconnect.util.GrpcBidiFlow
import com.google.firebase.dataconnect.util.GrpcBidiFlowListenerMessageFormatter
import com.google.firebase.dataconnect.util.IdStringGenerator
import com.google.firebase.dataconnect.util.ImmutableByteArray
import com.google.firebase.dataconnect.util.NullableReference
import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto
import com.google.firebase.dataconnect.util.ProtoUtil.calculateSha512
import com.google.firebase.dataconnect.util.ProtoUtil.toCompactString
import com.google.firebase.dataconnect.util.ProtoUtil.toDataConnectPath
import com.google.firebase.dataconnect.util.ProtoUtil.toStructProto
Expand Down Expand Up @@ -94,7 +92,7 @@ internal class DataConnectGrpcRPCs(
host: String,
sslEnabled: Boolean,
@get:VisibleForTesting val connectorResourceName: String,
nonBlockingCoroutineDispatcher: CoroutineDispatcher,
private val nonBlockingCoroutineDispatcher: CoroutineDispatcher,
private val blockingCoroutineDispatcher: CoroutineDispatcher,
private val grpcMetadata: DataConnectGrpcMetadata,
private val cacheSettings: CacheSettings?,
Expand Down Expand Up @@ -242,22 +240,27 @@ internal class DataConnectGrpcRPCs(
private class QueryCacheInfo(
val cacheDb: DataConnectCacheDatabase,
val authUid: AuthUid?,
val queryId: ImmutableByteArray,
val queryId: QueryId,
val maxAge: DurationProto,
)

private suspend fun queryCacheInfo(
authToken: DataConnectAuth.GetAuthTokenResult?,
request: ExecuteQueryRequest,
) =
lazyCacheDb.get().ref?.let { (cacheDb, maxAge) ->
): QueryCacheInfo? {
val queryId =
withContext(nonBlockingCoroutineDispatcher) {
calculateQueryId(request.operationName, request.variables)
}
return lazyCacheDb.get().ref?.let { (cacheDb, maxAge) ->
QueryCacheInfo(
cacheDb,
authUid = authToken?.authUid,
queryId = request.calculateQueryId(),
queryId = queryId,
maxAge = maxAge,
)
}
}
Comment thread
dconeybe marked this conversation as resolved.

suspend fun executeQuery(
requestId: String,
Expand Down Expand Up @@ -784,9 +787,6 @@ internal class DataConnectGrpcRPCs(
}
}

private fun ExecuteQueryRequest.calculateQueryId(): ImmutableByteArray =
variables.calculateSha512(preamble = operationName)

@JvmName("getEntityIdForPathFunction_ExecuteQueryResponse")
private fun ExecuteQueryResponse.getEntityIdForPathFunction(): GetEntityIdForPathFunction? =
if (extensions.dataConnectCount == 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2026 Google LLC
*
* 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 com.google.firebase.dataconnect.core

import androidx.annotation.WorkerThread
import com.google.firebase.dataconnect.util.ImmutableByteArray
import com.google.firebase.dataconnect.util.ProtoUtil.calculateSha512
import com.google.protobuf.Struct

/**
* Represents bytes that comprise a stable ID for a Data Connect query.
*
* These query IDs are "stable" in the sense that a given operation name/variables pair will
* _always_ generate the same ID. Also, the probability of two queries that differ, even by one
* byte, in their operation name or variables, have an effectively zero chance of having the same
* query ID. Finally, the ID of a given operation name/variables pair will be the same across
* application restarts and device resets.
*
* These properties make Query IDs represented by this type suitable for storing in persistence as a
* key for data related to a query, such as cached query results.
*
* Use [calculateQueryId] to calculate the Query ID for a query.
*/
@JvmInline
internal value class QueryId(val bytes: ImmutableByteArray) {
override fun toString() = "QueryId(${bytes.to0xHexString()})"
}

/**
* Calculates the [QueryId] for a Data Connect query with the given operation name and variables.
*
* This computation is CPU intensive and, therefore, should _never_ be called on the main thread.
*/
@WorkerThread
internal fun calculateQueryId(operationName: String, variables: Struct): QueryId =
QueryId(variables.calculateSha512(preamble = operationName))
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.database.sqlite.SQLiteDatabase
import com.google.firebase.dataconnect.core.DataConnectAuth.AuthUid
import com.google.firebase.dataconnect.core.Logger
import com.google.firebase.dataconnect.core.LoggerGlobals.warn
import com.google.firebase.dataconnect.core.QueryId
import com.google.firebase.dataconnect.sqlite.DataConnectCacheDatabase.GetQueryResultResult
import com.google.firebase.dataconnect.sqlite.SQLiteDatabaseExts.execSQL
import com.google.firebase.dataconnect.sqlite.SQLiteDatabaseExts.getLastInsertRowId
Expand Down Expand Up @@ -222,14 +223,11 @@ internal class DataConnectCacheDatabase(
val expiryProto: QueryResultExpiry,
)

private fun SQLiteDatabase.getQuery(
user: SqliteUserId,
queryId: ImmutableByteArray
): GetQueryResult? =
private fun SQLiteDatabase.getQuery(user: SqliteUserId, queryId: QueryId): GetQueryResult? =
rawQuery(
logger,
"SELECT id, data, expiry, flags FROM queries WHERE user_id=? AND query_id=?",
bindArgs = arrayOf(user.sqliteRowId, queryId.peek()),
bindArgs = arrayOf(user.sqliteRowId, queryId.bytes.peek()),
) { cursor ->
if (!cursor.moveToNext()) {
null
Expand All @@ -255,7 +253,7 @@ internal class DataConnectCacheDatabase(
parseResult.onFailure {
logger.warn(it) {
"Parsing QueryResultProto failed for id=$id, user=$user, " +
"queryId=${queryId.to0xHexString()}, flags=$flags [ykb2vwrcge]"
"queryId=$queryId, flags=$flags [ykb2vwrcge]"
}
}

Expand All @@ -264,7 +262,7 @@ internal class DataConnectCacheDatabase(
expiryParseResult.onFailure {
logger.warn(it) {
"Parsing QueryResultExpiry failed for id=$id, user=$user, " +
"queryId=${queryId.to0xHexString()}, flags=$flags [x9k2c3b8y1]"
"queryId=$queryId, flags=$flags [x9k2c3b8y1]"
}
}

Expand All @@ -274,7 +272,7 @@ internal class DataConnectCacheDatabase(

private fun SQLiteDatabase.insertQuery(
user: SqliteUserId,
queryId: ImmutableByteArray,
queryId: QueryId,
queryResultProtoBytes: ImmutableByteArray,
expiryProtoBytes: ImmutableByteArray,
): SqliteQueryId {
Expand All @@ -287,7 +285,7 @@ internal class DataConnectCacheDatabase(
""",
arrayOf(
user.sqliteRowId,
queryId.peek(),
queryId.bytes.peek(),
queryResultProtoBytes.peek(),
expiryProtoBytes.peek()
)
Expand Down Expand Up @@ -453,7 +451,7 @@ internal class DataConnectCacheDatabase(

suspend fun getQueryResult(
authUid: AuthUid?,
queryId: ImmutableByteArray,
queryId: QueryId,
currentTimeMillis: Long,
staleResult: KClass<out GetQueryResultResult>,
): GetQueryResultResult {
Expand Down Expand Up @@ -509,7 +507,7 @@ internal class DataConnectCacheDatabase(
rehydrateResult.onFailure {
logger.warn {
"rehydrateQueryResult failed for id=${sqliteQueryId.sqliteRowId}, " +
"queryId=${queryId.to0xHexString()} [knpe3t4f5b]"
"queryId=$queryId [knpe3t4f5b]"
}
}

Expand All @@ -526,14 +524,14 @@ internal class DataConnectCacheDatabase(

suspend fun insertQueryResult(
authUid: AuthUid?,
queryId: ImmutableByteArray,
queryId: QueryId,
queryData: Struct,
maxAge: DurationProto,
currentTimeMillis: Long,
getEntityIdForPath: GetEntityIdForPathFunction?,
) {
require(queryId.size > 0) {
"queryId.size=${queryId.size}, but must be greater than zero [ab4em538tb]"
require(queryId.bytes.size > 0) {
"queryId.bytes.size=${queryId.bytes.size}, but must be greater than zero [ab4em538tb]"
}
val (queryResultProto, entityStructById) = dehydrateQueryResult(queryData, getEntityIdForPath)
val queryResultProtoBytes = ImmutableByteArray.adopt(queryResultProto.toByteArray())
Expand Down
Loading
Loading