feat(spanner): add execute_query method#4952
feat(spanner): add execute_query method#4952olavloite wants to merge 1 commit intogoogleapis:mainfrom
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.95% 93.92% -0.04%
==========================================
Files 221 222 +1
Lines 8538 8540 +2
==========================================
- Hits 8022 8021 -1
- Misses 516 519 +3 ☔ 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".
| 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.
| 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.
| 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, | |
| } |
| - id: 'Start Spanner Emulator' | ||
| name: 'gcr.io/cloud-spanner-emulator/emulator' | ||
| waitFor: ['-'] |
There was a problem hiding this comment.
This is fine for CI, but now I need to install the spanner emulator to do cargo test --all-features, event if I am working on something unrelated.
There was a problem hiding this comment.
Oh I see, you skip the tests if the environment variable is not set. SGTM, thanks.
| /// 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.
| 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.
| // TODO: Re-write this to use the admin clients once those also support the Emulator. | ||
| let rest_endpoint = endpoint.replace("9010", "9020"); | ||
| let rest_endpoint = | ||
| if rest_endpoint.starts_with("http://") || rest_endpoint.starts_with("https://") { | ||
| rest_endpoint | ||
| } else { | ||
| format!("http://{}", rest_endpoint) | ||
| }; |
There was a problem hiding this comment.
Consider using url::Url to make this parsing and modifications to the Url.
| // Provisions the Spanner Emulator with a test instance and database. | ||
| // ALREADY_EXISTS errors that are returned when creating an instance or database are ignored. | ||
| async fn provision_emulator(endpoint: &str) { | ||
| // TODO: Re-write this to use the admin clients once those also support the Emulator. |
There was a problem hiding this comment.
You can certainly override the endpoint and the authentication for them.
| let mut count = 0; | ||
| while let Some(row_result) = rs.next().await { |
There was a problem hiding this comment.
Consider:
| let mut count = 0; | |
| while let Some(row_result) = rs.next().await { | |
| let mut rows = Vec::new(); | |
| while let Some(row) = rs.next().await.transpose()? { | |
| rows.push_back(row) | |
| } | |
| let (row1, row2, row3) = match &rows[..] { | |
| [r1, r2, r3] => (r1, r2, r3), | |
| _ => panic!("unexpected number of rows, got={}, want=3\n{rows:?}", rows.len()); | |
| }; |
And then you can make assertions (maybe in helper functions?) about row1, row2 and row3. You could also capture the results if that is useful.
| ); | ||
| } | ||
| 2 => { | ||
| // Assert row 1 |
There was a problem hiding this comment.
Ugh. I wished there was a better way to write these...