Skip to content

feat: dataconnect: DataConnectOperationException added to support partial errors #6794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Mar 24, 2025

Conversation

dconeybe
Copy link
Contributor

@dconeybe dconeybe commented Mar 21, 2025

This PR adds the DataConnectOperationException class, a subclass of the previously-existing DataConnectException class. This new DataConnectOperationException class is thrown by invocations of QueryRef.execute() and MutationRef.execute() in the case where a response is received from the backend, but the response indicates that the operation could not be executed to completion, including the case that the client SDK fails to decode the response to a higher-level object. Other kinds of errors, such as networking errors, will still be reported as they were previously.

Client code can catch DataConnectOperationException and check its response property to get details about any errors that occurred and any data that was received. If the data was able to be decoded, despite the errors, then it will be available. Also the "raw" data (a Map<String, Any?>) property will give access to the raw, undecoded data, if any was sent from the backend.

This feature is intended to support "partial errors", where an operation can partially succeed and partially fail, and the client application wants to take some special behavior in the case of a partial success.

For example, suppose this database schema and connector definition:

type Person @table {
  name: String!
}

# Notice how both "inserts" use the same ID; this means that one of them
# will necessarily fail because you can't have two rows with the same ID.
mutation InsertMultiplePeople($id: UUID!, $name1: String!, $name2: String!) {
  person1: person_insert(data: { id: $id, name: $name1 })
  person2: person_insert(data: { id: $id, name: $name2 })
}

Here is some code that handles the partial error that will occur if this mutation were to ever be executed:

import com.google.firebase.dataconnect.DataConnectOperationException
import com.google.firebase.dataconnect.DataConnectOperationFailureResponse
import com.google.firebase.dataconnect.DataConnectPathSegment
import com.myapp.myconnector.InsertTwoFoosWithSameIdMutation.Data

suspend fun demo(id: UUID, connector: DemoConnector): Data {
  val result = connector.insertTwoFoosWithSameId.runCatching { execute(id) }
  result.onSuccess {
    println("Weird... inserting _both_ entries with ID $id succeeded 🤷")
    return@demo it.data
  }

  val exception = result.exceptionOrNull()!!
  if (exception !is DataConnectOperationException) {
    throw exception
  }

  // Print warnings messages about which of "foo1" and "foo2" failed to
  // be inserted by the query. This information is gleaned from the list of
  // errors provided in the DataConnectOperationFailureResponse.
  val response: DataConnectOperationFailureResponse<*> = exception.response
  val errors = response.errors

  val error1 = errors.firstOrNull {
    it.path == listOf(DataConnectPathSegment.Field("person1"))
  }
  if (error1 == null) {
    println("Inserting 1st entry with ID $id succeeded")
  } else {
    println("Inserting 1st entry with ID $id failed: ${error1.message}")
  }

  val error2 = errors.firstOrNull 
    it.path == listOf(DataConnectPathSegment.Field("person2"))
  }
  if (error2 == null) {
    println("Inserting 2nd entry with ID $id succeeded")
  } else {
    println("Inserting 2nd entry with ID $id failed: ${error2.message}")
  }

  // If decoding the response was actually successful, then return
  // the decoded response.
  val data = response.data as? Data
  if (data != null) {
    return data
  }

  throw exception
}

Copy link
Contributor

github-actions bot commented Mar 21, 2025

📝 PRs merging into main branch

Our main branch should always be in a releasable state. If you are working on a larger change, or if you don't want this change to see the light of the day just yet, consider using a feature branch first, and only merge into the main branch when the code complete and ready to be released.

1 similar comment
Copy link
Contributor

📝 PRs merging into main branch

Our main branch should always be in a releasable state. If you are working on a larger change, or if you don't want this change to see the light of the day just yet, consider using a feature branch first, and only merge into the main branch when the code complete and ready to be released.

@dconeybe dconeybe changed the title dataconnect: DataConnectOperationException added to support partial errors feat: dataconnect: DataConnectOperationException added to support partial errors Mar 21, 2025
@dconeybe dconeybe requested a review from aashishpatil-g March 21, 2025 21:36
@google-oss-bot
Copy link
Contributor

google-oss-bot commented Mar 21, 2025

Coverage Report 1

Affected Products

  • firebase-dataconnect

    Overall coverage changed from ? (75be716) to 14.80% (a0fc704) by ?.

    60 individual files with coverage change

    FilenameBase (75be716)Merge (a0fc704)Diff
    AlphanumericStringUtil.kt?0.00%?
    AnyValue.kt?0.00%?
    AnyValueSerializer.kt?0.00%?
    Comparisons.kt?0.00%?
    ConnectorConfig.kt?33.33%?
    CoroutineExceptionHandler.kt?50.00%?
    DataConnectAppCheck.kt?71.43%?
    DataConnectAuth.kt?71.43%?
    DataConnectCredentialsTokenManager.kt?32.32%?
    DataConnectException.kt?0.00%?
    DataConnectGrpcClient.kt?0.00%?
    DataConnectGrpcMetadata.kt?65.82%?
    DataConnectGrpcRPCs.kt?0.00%?
    DataConnectOperationException.kt?0.00%?
    DataConnectOperationFailureResponse.kt?0.00%?
    DataConnectOperationFailureResponseImpl.kt?0.00%?
    DataConnectPathSegment.kt?0.00%?
    DataConnectSettings.kt?33.33%?
    DataConnectUntypedData.kt?0.00%?
    DataConnectUntypedVariables.kt?0.00%?
    ExperimentalFirebaseDataConnect.kt?0.00%?
    FirebaseDataConnect.kt?50.00%?
    FirebaseDataConnectFactory.kt?29.17%?
    FirebaseDataConnectImpl.kt?46.67%?
    FirebaseDataConnectRegistrar.kt?100.00%?
    GeneratedConnector.kt?0.00%?
    GeneratedMutation.kt?0.00%?
    GeneratedOperation.kt?0.00%?
    GeneratedQuery.kt?0.00%?
    Globals.kt?0.00%?
    JavaTimeLocalDateSerializer.kt?0.00%?
    KotlinxDatetimeLocalDateSerializer.kt?0.00%?
    LiveQueries.kt?0.00%?
    LiveQuery.kt?0.00%?
    LocalDate.kt?0.00%?
    LocalDateSerializer.kt?0.00%?
    Logger.kt?73.68%?
    LogLevel.kt?72.73%?
    MutationRef.kt?0.00%?
    MutationRefImpl.kt?12.36%?
    NullableReference.kt?40.00%?
    NullOutputStream.kt?0.00%?
    OperationRef.kt?0.00%?
    OperationRefImpl.kt?20.45%?
    OptionalVariable.kt?0.00%?
    ProtoStructDecoder.kt?0.00%?
    ProtoStructEncoder.kt?0.00%?
    ProtoUtil.kt?0.00%?
    QueryManager.kt?0.00%?
    QueryRef.kt?0.00%?
    QueryRefImpl.kt?14.08%?
    QuerySubscription.kt?0.00%?
    QuerySubscriptionImpl.kt?0.00%?
    QuerySubscriptionInternal.kt?0.00%?
    ReferenceCounted.kt?0.00%?
    RegisteredDataDeserialzer.kt?0.00%?
    SequencedReference.kt?0.00%?
    SuspendingLazy.kt?31.58%?
    TimestampSerializer.kt?0.00%?
    UUIDSerializer.kt?0.00%?

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/GHhmZyVInw.html

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

1 similar comment
@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

Copy link
Contributor

github-actions bot commented Mar 21, 2025

Test Results

   66 files  + 2     66 suites  +2   1m 16s ⏱️ +4s
  552 tests +10    551 ✅ +10  1 💤 ±0  0 ❌ ±0 
1 104 runs  +20  1 102 ✅ +20  2 💤 ±0  0 ❌ ±0 

Results for commit 3b00fb4. ± Comparison against base commit 75be716.

This pull request removes 40 and adds 50 tests. Note that renamed tests count towards both.
com.google.firebase.dataconnect.DataConnectErrorUnitTest ‑ equals() should return false for a different type
com.google.firebase.dataconnect.DataConnectErrorUnitTest ‑ equals() should return false for null
com.google.firebase.dataconnect.DataConnectErrorUnitTest ‑ equals() should return false when locations differ
com.google.firebase.dataconnect.DataConnectErrorUnitTest ‑ equals() should return false when message differs only in character case
com.google.firebase.dataconnect.DataConnectErrorUnitTest ‑ equals() should return false when only message differs
com.google.firebase.dataconnect.DataConnectErrorUnitTest ‑ equals() should return false when path differs
com.google.firebase.dataconnect.DataConnectErrorUnitTest ‑ equals() should return true for an equal instance
com.google.firebase.dataconnect.DataConnectErrorUnitTest ‑ equals() should return true for the exact same instance
com.google.firebase.dataconnect.DataConnectErrorUnitTest ‑ hashCode() should return a different value if locations is different
com.google.firebase.dataconnect.DataConnectErrorUnitTest ‑ hashCode() should return a different value if message is different
…
com.google.firebase.dataconnect.DataConnectPathSegmentFieldUnitTest ‑ constructor should set field property
com.google.firebase.dataconnect.DataConnectPathSegmentFieldUnitTest ‑ equals() should return false for a different type
com.google.firebase.dataconnect.DataConnectPathSegmentFieldUnitTest ‑ equals() should return false for null
com.google.firebase.dataconnect.DataConnectPathSegmentFieldUnitTest ‑ equals() should return false when field differs
com.google.firebase.dataconnect.DataConnectPathSegmentFieldUnitTest ‑ equals() should return true for an equal instance
com.google.firebase.dataconnect.DataConnectPathSegmentFieldUnitTest ‑ equals() should return true for the exact same instance
com.google.firebase.dataconnect.DataConnectPathSegmentFieldUnitTest ‑ hashCode() should return a different value if field is different
com.google.firebase.dataconnect.DataConnectPathSegmentFieldUnitTest ‑ hashCode() should return the same value each time it is invoked on a given object
com.google.firebase.dataconnect.DataConnectPathSegmentFieldUnitTest ‑ hashCode() should return the same value on equal objects
com.google.firebase.dataconnect.DataConnectPathSegmentFieldUnitTest ‑ toString() should return a string equal to the field property
…

♻️ This comment has been updated with latest results.

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Mar 21, 2025

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

# Conflicts:
#	firebase-dataconnect/CHANGELOG.md
@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

4 similar comments
@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

@dconeybe
Copy link
Contributor Author

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

@dconeybe
Copy link
Contributor Author

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

@dconeybe
Copy link
Contributor Author

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

@dconeybe
Copy link
Contributor Author

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

1 similar comment
@dconeybe
Copy link
Contributor Author

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

@dconeybe
Copy link
Contributor Author

There are two failing GitHub Actions, both of which ave been identified as false positives:

  • [API Information / api-information-check (pull_request)](https://github.com/firebase/firebase-android-sdk/actions/runs/14038742853/job/39303279776?pr=6794) is failing because the output of a gradle command happens to contain the string "error", which is being taken as an API change that needs to be accounted for; however, this is a false positive and the heuristic in [api_information.py](https://github.com/firebase/firebase-android-sdk/blob/c1ca0210b83f8d94eea14ee1ffe29a98aac154ca/ci/fireci/fireciplugins/api_information.py#L40) should be improved to detect api errors.
  • Metalava SemVer Check / semver-check (pull_request) is failing because it is erroneously not considering dropping the "beta" suffix of the version as a major version bump.

…reResponse.ErrorInfo.PathSegment moved to a top-level class
@dconeybe dconeybe force-pushed the dconeybe/dataconnect/PartialErrors branch from 3555951 to d036552 Compare March 24, 2025 18:34
@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

The public api surface has changed for the subproject firebase-dataconnect:
error: Method com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo.getPath has changed return type from java.util.List<com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo.PathSegment> to java.util.List<com.google.firebase.dataconnect.DataConnectPathSegment> [ChangedType]
error: Removed class com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo.PathSegment [RemovedInterface]

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-dataconnect_api.txt:

Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly.

@rlazo rlazo merged commit ba69975 into main Mar 24, 2025
38 of 41 checks passed
@rlazo rlazo deleted the dconeybe/dataconnect/PartialErrors branch March 24, 2025 22:10
tejasd pushed a commit that referenced this pull request Apr 1, 2025
…tial errors (#6794)

This PR adds the `DataConnectOperationException` class, a subclass of
the previously-existing `DataConnectException` class. This new
`DataConnectOperationException` class is thrown by invocations of
`QueryRef.execute()` and `MutationRef.execute()` in the case where a
response _is_ received from the backend, but the response indicates that
the operation could not be executed to completion, including the case
that the client SDK fails to decode the response to a higher-level
object. Other kinds of errors, such as networking errors, will still be
reported as they were previously.

Client code can catch `DataConnectOperationException` and check its
`response` property to get details about any errors that occurred and
any data that was received. If the data was able to be decoded, despite
the errors, then it will be available. Also the "raw" data (a
`Map<String, Any?>`) property will give access to the raw, undecoded
data, if any was sent from the backend.

This feature is intended to support "partial errors", where an operation
can partially succeed and partially fail, and the client application
wants to take some special behavior in the case of a partial success.

For example, suppose this database schema and connector definition:

```
type Person @table {
  name: String!
}

# Notice how both "inserts" use the same ID; this means that one of them
# will necessarily fail because you can't have two rows with the same ID.
mutation InsertMultiplePeople($id: UUID!, $name1: String!, $name2: String!) {
  person1: person_insert(data: { id: $id, name: $name1 })
  person2: person_insert(data: { id: $id, name: $name2 })
}
```

Here is some code that handles the partial error that will occur if this
mutation were to ever be executed:

```kt
import com.google.firebase.dataconnect.DataConnectOperationException
import com.google.firebase.dataconnect.DataConnectOperationFailureResponse
import com.google.firebase.dataconnect.DataConnectPathSegment
import com.myapp.myconnector.InsertTwoFoosWithSameIdMutation.Data

suspend fun demo(id: UUID, connector: DemoConnector): Data {
  val result = connector.insertTwoFoosWithSameId.runCatching { execute(id) }
  result.onSuccess {
    println("Weird... inserting _both_ entries with ID $id succeeded 🤷")
    return@demo it.data
  }

  val exception = result.exceptionOrNull()!!
  if (exception !is DataConnectOperationException) {
    throw exception
  }

  // Print warnings messages about which of "foo1" and "foo2" failed to
  // be inserted by the query. This information is gleaned from the list of
  // errors provided in the DataConnectOperationFailureResponse.
  val response: DataConnectOperationFailureResponse<*> = exception.response
  val errors = response.errors

  val error1 = errors.firstOrNull {
    it.path == listOf(DataConnectPathSegment.Field("person1"))
  }
  if (error1 == null) {
    println("Inserting 1st entry with ID $id succeeded")
  } else {
    println("Inserting 1st entry with ID $id failed: ${error1.message}")
  }

  val error2 = errors.firstOrNull 
    it.path == listOf(DataConnectPathSegment.Field("person2"))
  }
  if (error2 == null) {
    println("Inserting 2nd entry with ID $id succeeded")
  } else {
    println("Inserting 2nd entry with ID $id failed: ${error2.message}")
  }

  // If decoding the response was actually successful, then return
  // the decoded response.
  val data = response.data as? Data
  if (data != null) {
    return data
  }

  throw exception
}

```
@firebase firebase locked and limited conversation to collaborators Apr 24, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants