Skip to content

feat(spanner): add execute_query method#4952

Merged
olavloite merged 5 commits intogoogleapis:mainfrom
olavloite:spanner-execute-query
Mar 11, 2026
Merged

feat(spanner): add execute_query method#4952
olavloite merged 5 commits intogoogleapis:mainfrom
olavloite:spanner-execute-query

Conversation

@olavloite
Copy link
Contributor

  • Adds a method for creating a single-use read-only transaction and for executing a query using this transaction.
  • Adds an integration test using the Spanner emulator to execute a query to show that the client works end-to-end.

Adds a method for creating a single-use read-only transaction and for
executing a query using this transaction. Also adds an integration
test using the Spanner emulator to show that the client works end-to-end.
@olavloite olavloite requested a review from a team as a code owner March 9, 2026 17:37
@product-auto-label product-auto-label bot added the api: spanner Issues related to the Spanner API. label Mar 9, 2026
@codecov
Copy link

codecov bot commented Mar 9, 2026

Codecov Report

❌ Patch coverage is 0% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.85%. Comparing base (8b70ea8) to head (f10dcef).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
src/spanner/src/error.rs 0.00% 2 Missing ⚠️
src/spanner/src/statement.rs 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4952      +/-   ##
==========================================
- Coverage   93.89%   93.85%   -0.05%     
==========================================
  Files         221      223       +2     
  Lines        8601     8605       +4     
==========================================
  Hits         8076     8076              
- Misses        525      529       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Collaborator

@coryan coryan left a comment

Choose a reason for hiding this comment

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

I think using Error::binding() and Error::service for local errors is a deal-breaker for me.

In the future, please keep PRs under 500 lines, 1,000 in a pinch. I have been known to reject larger PRs with a simple "split please".

let mut builder = google_cloud_gax::client_builder::internal::new_builder(Factory);
// The Spanner client should automatically use the Spanner emulator if the
// SPANNER_EMULATOR_HOST environment variable is set.
if let Ok(endpoint) = std::env::var("SPANNER_EMULATOR_HOST") {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider:

Suggested change
if let Ok(endpoint) = std::env::var("SPANNER_EMULATOR_HOST") {
let Ok(endpoint) = std::env::var("SPANNER_EMULATOR_HOST") else {
return builder;
};

Saves you some indentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

if let Ok(endpoint) = std::env::var("SPANNER_EMULATOR_HOST") {
if !endpoint.is_empty() {
// Determine if we need to prefix the endpoint with a scheme
let full_endpoint = if endpoint.starts_with("http") {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmmm... What about "http_carlos_is_evil.com" ? If you need to parse the endpoint, consider the url crate, which I think we already link.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've used the url crate to parse and check the URL, and only add a scheme if there is no scheme.

Comment on lines +77 to +87
if !endpoint.is_empty() {
// Determine if we need to prefix the endpoint with a scheme
let full_endpoint = if endpoint.starts_with("http") {
endpoint
} else {
format!("http://{}", endpoint)
};

builder = builder.with_endpoint(full_endpoint).with_credentials(
google_cloud_auth::credentials::anonymous::Builder::new().build(),
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

An alternative would be:

Suggested change
if !endpoint.is_empty() {
// Determine if we need to prefix the endpoint with a scheme
let full_endpoint = if endpoint.starts_with("http") {
endpoint
} else {
format!("http://{}", endpoint)
};
builder = builder.with_endpoint(full_endpoint).with_credentials(
google_cloud_auth::credentials::anonymous::Builder::new().build(),
);
let anonymous = || google_cloud_auth::credentials::anonymous::Builder::new().build();
builder = match endpoint {
e if e.starts_with("https://) => builder.with_endpoint(e).with_credentials(anonymous()),
e if e.starts_with("http://) => builder.with_endpoint(e).with_credentials(anonymous()),
e if !e.is_empty() => builder.with_endpoint(format!("http://{e}").with_credentials(anonymous()),
_ => builder,
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is solved in a slightly different way with the url crate.

/// A builder for [SingleUseReadOnlyTransaction].
///
/// # Example
/// ```rust,no_run
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// ```rust,no_run
/// ```

That should be enough as the example only defines a function, does not invoke it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed here and elsewhere

let seconds = timestamp.unix_timestamp();
let nanos = timestamp.nanosecond();
let timestamp = wkt::Timestamp::new(seconds, nanos as i32)
.map_err(|e| crate::Error::binding(format!("timestamp out of range: {}", e)))?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Error::binding() is intended for an RPC that is missing the inputs for a HTTP request. In general, google_cloud_gax::error::Error is intended for RPC errors. Please avoid using it elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed. I'm now re-exporting the error from the try_from implementation.

@olavloite olavloite force-pushed the spanner-execute-query branch from 893a509 to b33d091 Compare March 10, 2026 11:07
@olavloite olavloite force-pushed the spanner-execute-query branch from b33d091 to 3db3756 Compare March 10, 2026 12:12
Copy link
Collaborator

@coryan coryan left a comment

Choose a reason for hiding this comment

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

LGTM.

PS: When you respond to each comment one by one I get one email for each. If you press "Start review" you can batch all your responses. Just saying.

// See the License for the specific language governing permissions and
// limitations under the License.

#![allow(dead_code)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: these are easy to forget, consider opening an issue to track them / remind yourself.

FWIW, we use TODO(#NNNN) where NNNN is the GitHub issue number.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Opened an issue and added a TODO referencing it here and elsewhere.

#[derive(Clone, Debug, PartialEq)]
pub struct Statement {
pub sql: String,
// TODO: Add support for query parameters.
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: TODO(#KKKK) would be more in the style of this repo.

Comment on lines +150 to +152
fn internal_error(message: &str) -> crate::Error {
crate::Error::deser(message)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Food for thought. You may consider something like:

Suggested change
fn internal_error(message: &str) -> crate::Error {
crate::Error::deser(message)
}
fn internal_error(e: MyErrorType) -> crate::Error {
crate::Error::deser(e)
}

And then application developers can use Error::source() to get more details about the problem.

In this case it is unclear how that would be better than an error message, as the application can do very little with such an error other than logging.

Oh, you may want to include something in these messages about where to file bugs. That was either a bug in the library or the service, the customer may need some hints on how to proceed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added an SpannerInternalError enum following the example from the Storage and PubSub clients, and added a hint to the documentation regarding filing an issue (example also taken from the Storage client). The internal error is then wrapped in crate::Error::deser.

/// the first value in the `values` array of the subsequent `PartialResultSet`.
///
/// This function handles the concatenation of split `StringValue` and `ListValue` types.
fn merge_values(target: &mut prost_types::Value, source: prost_types::Value) -> crate::Result<()> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

As a reminder: you are the Spanner expert, I am not an effective reviewer as to the correctness of this code. If you want an (informed) second opinion maybe ask one of your colleagues to do a drive-by.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've asked a colleague from the Spanner team to take a look.

/// Returns a strong timestamp bound. Strong reads are guaranteed to see the
/// effects of all transactions that have committed before the start of the read.
///
/// See <https://docs.cloud.google.com/spanner/docs/timestamp-bounds#strong> for more information.
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can also write:

Suggested change
/// See <https://docs.cloud.google.com/spanner/docs/timestamp-bounds#strong> for more information.
/// See the [Timestamp bound types] for more information.
///
/// [Timestamp bound types]: https://docs.cloud.google.com/spanner/docs/timestamp-bounds#strong

Which may read better in the rendered documentation (try cargo doc --no-deps -p google-cloud-spanner locally).

@olavloite olavloite merged commit d66dc1c into googleapis:main Mar 11, 2026
35 checks passed
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 Spanner API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants