Skip to content

Conversation

@aseering
Copy link

This change adds support for ClientContext in Options and ensures it is propagated to ExecuteSql, Read, Commit, and BeginTransaction requests. It aligns with go/spanner-client-scoped-session-state design.

ClientContext allows passing opaque, RPC-scoped side-channel information (like application-level user context) to Spanner. This implementation supports setting ClientContext at the Client, Database, and Request levels, with request-level options taking precedence.

Key changes:

  • Added ClientContext to types/spanner.py and exposed it.
  • Updated Client.init to accept a default client_context.
  • Added helpers for merging ClientContext with correct precedence.
  • Updated Snapshot, Transaction, Batch, and Database wrappers to propagate the context.
  • Added comprehensive unit tests in tests/unit/test_client_context.py.

Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

  • Make sure to open an issue as a bug/issue before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
  • Ensure the tests and linter pass
  • Code coverage does not decrease (if any source code was changed)
  • Appropriate docs were updated (if necessary)

Fixes #<issue_number_goes_here> 🦕

This change adds support for ClientContext in Options and ensures it is
propagated to ExecuteSql, Read, Commit, and BeginTransaction requests.
It aligns with go/spanner-client-scoped-session-state design.

ClientContext allows passing opaque, RPC-scoped side-channel information
(like application-level user context) to Spanner. This implementation
supports setting ClientContext at the Client, Database, and Request levels,
with request-level options taking precedence.

Key changes:
- Added ClientContext to types/spanner.py and exposed it.
- Updated Client.__init__ to accept a default client_context.
- Added helpers for merging ClientContext with correct precedence.
- Updated Snapshot, Transaction, Batch, and Database wrappers to propagate the context.
- Added comprehensive unit tests in tests/unit/test_client_context.py.
@aseering aseering requested review from a team as code owners February 11, 2026 20:35
@product-auto-label product-auto-label bot added size: l Pull request size is large. api: spanner Issues related to the googleapis/python-spanner API. labels Feb 11, 2026
@gemini-code-assist
Copy link

Summary of Changes

Hello @aseering, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces ClientContext functionality to the Spanner client library, allowing users to attach opaque, RPC-scoped side-channel information to their requests. This enhancement provides a flexible mechanism for passing application-level user context, improving the client's ability to integrate with secure views and other context-aware Spanner features. The changes ensure that this context is consistently propagated and merged throughout the client's operations, respecting a clear precedence hierarchy.

Highlights

  • ClientContext Support: Added support for ClientContext in Spanner Options and ensured its propagation to ExecuteSql, Read, Commit, and BeginTransaction requests, aligning with the go/spanner-client-scoped-session-state design.
  • Context Propagation Levels: Implemented ClientContext setting at the Client, Database, and Request levels, with request-level options taking precedence during merging.
  • Helper Functions: Introduced new helper functions, _merge_client_context and _merge_request_options, to handle the merging logic for ClientContext with correct precedence.
  • API Integration: Updated Client.__init__, Snapshot, Transaction, Batch, and MutationGroups wrappers to accept and propagate client_context.
  • Unit Tests: Added comprehensive unit tests to tests/unit/test_client_context.py to verify the correct merging and propagation of ClientContext across different components and levels.
Changelog
  • google/cloud/spanner_v1/init.py
    • Imported and exposed the new ClientContext type.
  • google/cloud/spanner_v1/_helpers.py
    • Imported ClientContext and RequestOptions.
    • Added _merge_client_context function to merge two ClientContext objects, with the 'merge' argument taking precedence.
    • Added _merge_request_options function to merge a ClientContext into RequestOptions.
  • google/cloud/spanner_v1/batch.py
    • Imported _merge_client_context, _merge_request_options, and ClientContext.
    • Modified _BatchBase.__init__ to accept an optional client_context argument and store it.
    • Updated commit method to merge client-level and batch-level client_context into request_options.
    • Modified MutationGroups.__init__ to accept an optional client_context argument and store it.
    • Updated batch_write method to merge client-level and mutation group-level client_context into request_options.
  • google/cloud/spanner_v1/client.py
    • Imported ClientContext.
    • Modified Client.__init__ to accept an optional client_context argument and store it as _client_context.
  • google/cloud/spanner_v1/database.py
    • Updated snapshot method's docstring to mention client_context.
    • Modified batch method to accept an optional client_context argument.
    • Modified mutation_groups method to accept an optional client_context argument.
    • Modified batch_snapshot method to accept an optional client_context argument.
    • Updated run_in_transaction method's docstring to mention client_context.
    • Modified BatchCheckout.__init__ to accept an optional client_context argument and pass it to the Batch constructor.
    • Modified MutationGroupsCheckout.__init__ to accept an optional client_context argument and pass it to the MutationGroups constructor.
    • Modified BatchSnapshot.__init__ to accept an optional client_context argument and store it.
  • google/cloud/spanner_v1/snapshot.py
    • Imported _merge_client_context, _merge_request_options, and ClientContext.
    • Modified _SnapshotBase.__init__ to accept an optional client_context argument and store it.
    • Updated read method to merge client-level and snapshot-level client_context into request_options.
    • Updated execute_sql method to merge client-level and snapshot-level client_context into request_options.
    • Updated _begin_transaction method to merge client-level and snapshot-level client_context into request_options.
    • Modified Snapshot.__init__ to accept an optional client_context argument and pass it to the _SnapshotBase constructor.
  • google/cloud/spanner_v1/transaction.py
    • Imported _merge_client_context and _merge_request_options.
    • Modified Transaction.__init__ to accept an optional client_context argument and pass it to the _SnapshotBase constructor.
    • Updated commit method to merge client-level and transaction-level client_context into request_options.
    • Updated execute_update method to merge client-level and transaction-level client_context into request_options.
    • Updated batch_update method to merge client-level and transaction-level client_context into request_options.
  • google/cloud/spanner_v1/types/init.py
    • Exported the new ClientContext type.
  • google/cloud/spanner_v1/types/spanner.py
    • Defined the ClientContext proto message with a secure_context map field.
    • Added a client_context field to the RequestOptions proto message.
  • tests/unit/test_client_context.py
    • Added a new test file to verify the functionality of ClientContext.
    • Tested _merge_client_context and _merge_request_options for various merge scenarios.
    • Verified Client initialization with client_context.
    • Confirmed client_context propagation in Snapshot.execute_sql, Transaction.commit, Batch.commit, Transaction.execute_update, MutationGroups.batch_write, and BatchSnapshot.
    • Included a test to ensure client_context is not supported in Transaction.rollback.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for ClientContext throughout the Spanner client library, enabling its propagation to various requests. However, a critical flaw was identified in the _merge_client_context helper function, which modifies the base context object in place. This can lead to cross-request context leakage, potentially resulting in unauthorized data access or privilege escalation if the context is used for security-sensitive decisions. Additionally, there are suggestions to enhance code quality, such as replacing type() is dict checks with isinstance() for type validation and a refactoring suggestion to improve logic clarity.

Comment on lines +217 to +225
combined = base or ClientContext()
if type(combined) is dict:
combined = ClientContext(combined)

merge = merge or ClientContext()
if type(merge) is dict:
merge = ClientContext(merge)

type(combined).pb(combined).MergeFrom(type(merge).pb(merge))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The _merge_client_context function critically modifies the base object in place, leading to cross-request context leakage. This occurs because combined = base or ClientContext() creates a reference to the base object, and subsequent operations modify the underlying protobuf message of the base object. This in-place modification causes client_context from one request to leak into all subsequent requests using the same Client instance.

This is a high-severity vulnerability as ClientContext is used for SECURE_CONTEXT() calls in Spanner, which are vital for row-level security or authorization decisions in Secure Views. Such leakage can result in unauthorized data access or privilege escalation. The provided code suggestion addresses this by ensuring proper merging without in-place modification, and also incorporates robust type checking using isinstance().

Suggested change
combined = base or ClientContext()
if type(combined) is dict:
combined = ClientContext(combined)
merge = merge or ClientContext()
if type(merge) is dict:
merge = ClientContext(merge)
type(combined).pb(combined).MergeFrom(type(merge).pb(merge))
combined_pb = ClientContext()._pb
if base:
combined_pb.MergeFrom(ClientContext(base)._pb if isinstance(base, dict) else base._pb)
if merge:
combined_pb.MergeFrom(ClientContext(merge)._pb if isinstance(merge, dict) else merge._pb)
combined = ClientContext(combined_pb)

Comment on lines +222 to +223
if type(merge) is dict:
merge = ClientContext(merge)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For type checking, it's generally better to use isinstance(obj, cls) rather than type(obj) is cls. isinstance correctly handles subclasses, making the code more robust.

Suggested change
if type(merge) is dict:
merge = ClientContext(merge)
if isinstance(merge, dict):
merge = ClientContext(merge)

Comment on lines +253 to +254
elif type(request_options) is dict:
request_options = RequestOptions(request_options)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For type checking, it's generally better to use isinstance(obj, cls) rather than type(obj) is cls. isinstance correctly handles subclasses, making the code more robust.

Suggested change
elif type(request_options) is dict:
request_options = RequestOptions(request_options)
elif isinstance(request_options, dict):
request_options = RequestOptions(request_options)

Comment on lines +70 to +72
if client_context is not None:
if type(client_context) is dict:
client_context = ClientContext(client_context)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For type checking, it's generally better to use isinstance(obj, cls) rather than type(obj) is cls. isinstance correctly handles subclasses, making the code more robust.

Suggested change
if client_context is not None:
if type(client_context) is dict:
client_context = ClientContext(client_context)
if isinstance(client_context, dict):
client_context = ClientContext(client_context)
elif not isinstance(client_context, ClientContext):

Comment on lines +351 to +353
if client_context is not None:
if type(client_context) is dict:
client_context = ClientContext(client_context)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For type checking, it's generally better to use isinstance(obj, cls) rather than type(obj) is cls. isinstance correctly handles subclasses, making the code more robust.

Suggested change
if client_context is not None:
if type(client_context) is dict:
client_context = ClientContext(client_context)
if isinstance(client_context, dict):
client_context = ClientContext(client_context)
elif not isinstance(client_context, ClientContext):

Comment on lines 257 to +259
experimental_host=None,
disable_builtin_metrics=False,
client_context=None,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For type checking, it's generally better to use isinstance(obj, cls) rather than type(obj) is cls. isinstance correctly handles subclasses, making the code more robust. This pattern of using type() is dict appears in several places in this PR and should be updated for consistency.

Suggested change
experimental_host=None,
disable_builtin_metrics=False,
client_context=None,
if isinstance(client_context, dict):
client_context = ClientContext(client_context)
elif not isinstance(client_context, ClientContext):

Comment on lines +216 to +218
if type(client_context) is dict:
client_context = ClientContext(client_context)
elif not isinstance(client_context, ClientContext):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For type checking, it's generally better to use isinstance(obj, cls) rather than type(obj) is cls. isinstance correctly handles subclasses, making the code more robust.

Suggested change
if type(client_context) is dict:
client_context = ClientContext(client_context)
elif not isinstance(client_context, ClientContext):
if isinstance(client_context, dict):
client_context = ClientContext(client_context)
elif not isinstance(client_context, ClientContext):

Comment on lines +948 to +962
client_context = _merge_client_context(
database._instance._client._client_context, self._client_context
)
if client_context:
begin_request_kwargs["request_options"] = _merge_request_options(
begin_request_kwargs.get("request_options"), client_context
)

if transaction_tag:
request_options = begin_request_kwargs.get("request_options")
if request_options is None:
request_options = RequestOptions(transaction_tag=transaction_tag)
else:
request_options.transaction_tag = transaction_tag
begin_request_kwargs["request_options"] = request_options

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for creating and updating request_options with client_context and transaction_tag can be simplified for better readability and maintainability. Consider building the request_options object in a more linear fashion before assigning it to begin_request_kwargs.

Suggested change
client_context = _merge_client_context(
database._instance._client._client_context, self._client_context
)
if client_context:
begin_request_kwargs["request_options"] = _merge_request_options(
begin_request_kwargs.get("request_options"), client_context
)
if transaction_tag:
request_options = begin_request_kwargs.get("request_options")
if request_options is None:
request_options = RequestOptions(transaction_tag=transaction_tag)
else:
request_options.transaction_tag = transaction_tag
begin_request_kwargs["request_options"] = request_options
request_options = begin_request_kwargs.get("request_options")
client_context = _merge_client_context(
database._instance._client._client_context, self._client_context
)
request_options = _merge_request_options(request_options, client_context)
if transaction_tag:
if request_options is None:
request_options = RequestOptions()
request_options.transaction_tag = transaction_tag
if request_options:
begin_request_kwargs["request_options"] = request_options

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: spanner Issues related to the googleapis/python-spanner API. size: l Pull request size is large.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants