feat(spanner): add execute_query method#4952
Conversation
olavloite
commented
Mar 9, 2026
- 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.
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
coryan
left a comment
There was a problem hiding this comment.
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".
src/spanner/src/client.rs
Outdated
| 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") { |
There was a problem hiding this comment.
Consider:
| 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.
src/spanner/src/client.rs
Outdated
| 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") { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I've used the url crate to parse and check the URL, and only add a scheme if there is no scheme.
src/spanner/src/client.rs
Outdated
| 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(), | ||
| ); |
There was a problem hiding this comment.
An alternative would be:
| 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, | |
| } |
There was a problem hiding this comment.
This is solved in a slightly different way with the url crate.
| /// A builder for [SingleUseReadOnlyTransaction]. | ||
| /// | ||
| /// # Example | ||
| /// ```rust,no_run |
There was a problem hiding this comment.
| /// ```rust,no_run | |
| /// ``` |
That should be enough as the example only defines a function, does not invoke it.
There was a problem hiding this comment.
Removed here and elsewhere
src/spanner/src/timestamp_bound.rs
Outdated
| 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)))?; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Changed. I'm now re-exporting the error from the try_from implementation.
893a509 to
b33d091
Compare
b33d091 to
3db3756
Compare
coryan
left a comment
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Opened an issue and added a TODO referencing it here and elsewhere.
src/spanner/src/statement.rs
Outdated
| #[derive(Clone, Debug, PartialEq)] | ||
| pub struct Statement { | ||
| pub sql: String, | ||
| // TODO: Add support for query parameters. |
There was a problem hiding this comment.
nit: TODO(#KKKK) would be more in the style of this repo.
| fn internal_error(message: &str) -> crate::Error { | ||
| crate::Error::deser(message) | ||
| } |
There was a problem hiding this comment.
Food for thought. You may consider something like:
| 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.
There was a problem hiding this comment.
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<()> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I've asked a colleague from the Spanner team to take a look.
src/spanner/src/timestamp_bound.rs
Outdated
| /// 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. |
There was a problem hiding this comment.
You can also write:
| /// 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).