Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gcb/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,14 @@ steps:
env:
- _SCCACHE_VERSION=${_SCCACHE_VERSION}
- _SCCACHE_SHA256=${_SCCACHE_SHA256}
- id: 'Start Spanner Emulator'
name: 'gcr.io/cloud-builders/docker'
args: ['run', '-d', '--network=cloudbuild', '--name=spanner-emulator', 'gcr.io/cloud-spanner-emulator/emulator']
waitFor: ['-']
Comment thread
coryan marked this conversation as resolved.
- id: Run integration tests
name: 'rust:${_RUST_VERSION}-bookworm'
env:
- 'SPANNER_EMULATOR_HOST=spanner-emulator:9010'
script: |
#!/usr/bin/env bash
set -e
Expand Down
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ default-members = [
"tests/protobuf",
"tests/pubsub",
"tests/showcase",
"tests/spanner",
"tests/storage",
]

Expand Down Expand Up @@ -329,6 +330,7 @@ members = [
"tests/protojson-conformance",
"tests/pubsub",
"tests/showcase",
"tests/spanner",
"tests/storage",
"tools/check-copyright",
"tools/minimal-version-helper",
Expand Down Expand Up @@ -496,6 +498,7 @@ google-cloud-aiplatform-v1 = { default-features = false, path = "src/g
google-cloud-compute-v1 = { default-features = false, path = "src/generated/cloud/compute/v1" }
google-cloud-storage = { default-features = false, path = "src/storage" }
google-cloud-pubsub = { default-features = false, path = "src/pubsub" }
google-cloud-spanner = { default-features = false, path = "src/spanner" }
google-cloud-bigquery-v2 = { default-features = false, path = "src/generated/cloud/bigquery/v2" }
google-cloud-iam-credentials-v1 = { default-features = false, path = "src/generated/iam/credentials/v1" }
google-cloud-language-v2 = { default-features = false, path = "src/generated/cloud/language/v2" }
Expand Down
1 change: 1 addition & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ wrappers = [
"integration-tests",
"integration-tests-auth",
"integration-tests-o11y",
"integration-tests-spanner",
"integration-tests-storage",
"test-auth",
"user-guide-samples",
Expand Down
5 changes: 3 additions & 2 deletions src/spanner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ async-trait.workspace = true
base64.workspace = true
bytes.workspace = true
gaxi = { workspace = true, features = ["_internal-common", "_internal-grpc-client", "_internal-grpc-server-streaming"] }
google-cloud-auth = { workspace = true }
google-cloud-gax = { workspace = true }
google-cloud-rpc = { workspace = true }
http.workspace = true
Expand All @@ -44,11 +45,11 @@ serde_with.workspace = true
thiserror.workspace = true
time = { workspace = true, features = ["formatting", "macros", "parsing"] }
tracing.workspace = true
wkt.workspace = true
url.workspace = true
wkt = { workspace = true, features = ["time"] }

[dev-dependencies]
anyhow.workspace = true
google-cloud-auth.workspace = true
mockall.workspace = true
spanner-grpc-mock = { path = "grpc-mock" }
static_assertions.workspace = true
Expand Down
60 changes: 59 additions & 1 deletion src/spanner/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@ use crate::server_streaming::builder;
use gaxi::options::{ClientConfig, Credentials};

pub use crate::database_client::DatabaseClient;
pub use crate::error::SpannerInternalError;
pub use crate::from_value::{ConvertError, FromValue};
pub use crate::read_only_transaction::SingleUseReadOnlyTransaction;
pub use crate::read_only_transaction::SingleUseReadOnlyTransactionBuilder;
pub use crate::result_set::ResultSet;
pub use crate::row::Row;
pub use crate::statement::Statement;
pub use crate::timestamp_bound::TimestampBound;
pub use crate::to_value::ToValue;
pub use crate::types::{Type, TypeCode};
pub use crate::value::{Kind, Value};
pub use wkt::{DurationError, TimestampError};

/// A client for the [Spanner] API.
///
Expand Down Expand Up @@ -61,10 +69,32 @@ impl google_cloud_gax::client_builder::internal::ClientFactory for Factory {
/// A builder for the Spanner client.
pub type ClientBuilder = google_cloud_gax::client_builder::ClientBuilder<Factory, Credentials>;

fn parse_emulator_endpoint(endpoint: &str) -> String {
match url::Url::parse(endpoint) {
Ok(url) if url.has_host() => endpoint.to_string(),
_ => format!("http://{}", endpoint),
}
}

#[allow(dead_code)]
impl Spanner {
pub fn builder() -> ClientBuilder {
google_cloud_gax::client_builder::internal::new_builder(Factory)
let 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.
let Some(endpoint) = std::env::var("SPANNER_EMULATOR_HOST")
.ok()
.filter(|s| !s.is_empty())
else {
return builder;
};

// Determine if we need to prefix the endpoint with a scheme
let full_endpoint = parse_emulator_endpoint(&endpoint);

builder
.with_endpoint(full_endpoint)
.with_credentials(google_cloud_auth::credentials::anonymous::Builder::new().build())
}

/// Returns a new [DatabaseClientBuilder](crate::database_client::DatabaseClientBuilder) for
Expand Down Expand Up @@ -759,4 +789,32 @@ mod tests {
google_cloud_gax::error::rpc::Code::Internal
);
}

#[test]
fn test_parse_emulator_endpoint() {
assert_eq!(
super::parse_emulator_endpoint("localhost:9010"),
"http://localhost:9010"
);
assert_eq!(
super::parse_emulator_endpoint("spanner-emulator:9010"),
"http://spanner-emulator:9010"
);
assert_eq!(
super::parse_emulator_endpoint("http://localhost:9010"),
"http://localhost:9010"
);
assert_eq!(
super::parse_emulator_endpoint("https://localhost:9010"),
"https://localhost:9010"
);
assert_eq!(
super::parse_emulator_endpoint("grpc://localhost:9010"),
"grpc://localhost:9010"
);
assert_eq!(
super::parse_emulator_endpoint("http_localhost:9010"),
"http://http_localhost:9010"
);
}
}
19 changes: 19 additions & 0 deletions src/spanner/src/database_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use crate::client::Spanner;
use crate::model::Session;
use crate::read_only_transaction::SingleUseReadOnlyTransactionBuilder;
use std::sync::Arc;

/// A client for interacting with a specific Spanner database.
Expand Down Expand Up @@ -48,6 +49,24 @@ pub struct DatabaseClient {
pub(crate) session: Arc<Session>,
}

impl DatabaseClient {
/// Returns a builder for a single-use read-only transaction.
///
/// # Example
/// ```
/// # use google_cloud_spanner::client::{Spanner, Statement};
/// # async fn run(spanner: Spanner) -> Result<(), google_cloud_spanner::Error> {
/// let db_client = spanner.database_client("projects/p/instances/i/databases/d").build().await?;
/// let tx = db_client.single_use().build();
/// let mut rs = tx.execute_query(Statement::new("SELECT 1")).await?;
/// # Ok(())
/// # }
/// ```
pub fn single_use(&self) -> SingleUseReadOnlyTransactionBuilder {
SingleUseReadOnlyTransactionBuilder::new(self.clone())
}
}

/// A builder for [DatabaseClient].
pub struct DatabaseClientBuilder {
spanner: Spanner,
Expand Down
36 changes: 36 additions & 0 deletions src/spanner/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// An unexpected error that occurs when the client receives data from Spanner
/// that it cannot properly parse or handle. This typically indicates a bug in
/// the client library or the Spanner service itself, though other causes are possible.
///
/// # Troubleshooting
///
/// This indicates a bug in the client, the service, or a message corrupted
/// while in transit. Please [open an issue] with as much detail as possible.
///
/// [open an issue]: https://github.com/googleapis/google-cloud-rust/issues/new/choose
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SpannerInternalError {
#[error("unexpected data received from Spanner: {0}")]
UnexpectedData(String),
}

impl SpannerInternalError {
pub(crate) fn new(message: impl Into<String>) -> Self {
Self::UnexpectedData(message.into())
}
}
2 changes: 1 addition & 1 deletion src/spanner/src/from_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ impl FromValue for Decimal {
fn from_value(value: &Value, type_: &Type) -> Result<Self, ConvertError> {
if type_.code() != TypeCode::Numeric {
return Err(ConvertError::KindMismatch {
want: crate::value::Kind::String, // TODO: This feels wrong, TypeCode vs Kind
want: crate::value::Kind::String,
got: value.kind(),
});
}
Expand Down
6 changes: 6 additions & 0 deletions src/spanner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,16 @@ pub(crate) mod model {
pub use crate::generated::gapic_dataplane::model::*;
}
pub(crate) mod from_value;
pub(crate) mod read_only_transaction;
pub(crate) mod result_set;
pub(crate) mod row;
pub(crate) mod statement;
pub(crate) mod timestamp_bound;
pub(crate) mod to_value;
pub(crate) mod types;
pub(crate) mod value;

pub(crate) mod error;
mod status;

#[allow(dead_code)]
Expand Down
Loading
Loading