use adbc_core::error::{Result, Status};
use adbc_core::options::{OptionDatabase, OptionValue};
use adbc_core::{Database, Driver, Optionable};
use google_cloud_auth::credentials::anonymous::Builder as AnonymousCredentials;
use google_cloud_spanner::client::{DatabaseClient, Spanner};
use crate::connection::SpannerConnection;
use crate::error::{err, from_spanner, invalid_argument, invalid_state};
use crate::runtime::{new_runtime, SharedRuntime};
use crate::{OPTION_DATABASE, OPTION_EMULATOR, OPTION_ENDPOINT};
pub struct SpannerDriver {
runtime: SharedRuntime,
}
impl SpannerDriver {
pub fn try_new() -> Result<Self> {
Ok(Self {
runtime: new_runtime()?,
})
}
}
impl Default for SpannerDriver {
fn default() -> Self {
Self::try_new().expect("failed to initialize the Spanner ADBC driver Tokio runtime")
}
}
impl Driver for SpannerDriver {
type DatabaseType = SpannerDatabase;
fn new_database(&mut self) -> Result<Self::DatabaseType> {
Ok(SpannerDatabase::new(self.runtime.clone()))
}
fn new_database_with_opts(
&mut self,
opts: impl IntoIterator<Item = (OptionDatabase, OptionValue)>,
) -> Result<Self::DatabaseType> {
let mut database = SpannerDatabase::new(self.runtime.clone());
for (key, value) in opts {
database.set_option(key, value)?;
}
Ok(database)
}
}
pub struct SpannerDatabase {
runtime: SharedRuntime,
database: Option<String>,
endpoint: Option<String>,
emulator: bool,
}
impl SpannerDatabase {
pub(crate) fn new(runtime: SharedRuntime) -> Self {
Self {
runtime,
database: None,
endpoint: None,
emulator: false,
}
}
pub(crate) fn connect(&self) -> Result<DatabaseClient> {
let database = self.database.clone().ok_or_else(|| {
invalid_state(
"Spanner database path is not set; provide the `uri` or \
`adbc.spanner.database` option (projects/<p>/instances/<i>/databases/<d>)",
)
})?;
let mut endpoint = self.endpoint.clone();
let mut emulator = self.emulator;
if let Ok(host) = std::env::var("SPANNER_EMULATOR_HOST") {
if !host.is_empty() {
if endpoint.is_none() {
endpoint = Some(ensure_scheme(&host));
}
emulator = true;
}
}
self.runtime.block_on(async move {
let mut builder = Spanner::builder();
if let Some(endpoint) = endpoint {
builder = builder.with_endpoint(endpoint);
}
if emulator {
builder = builder.with_credentials(AnonymousCredentials::new().build());
}
let spanner = builder.build().await.map_err(from_spanner)?;
spanner
.database_client(database)
.build()
.await
.map_err(from_spanner)
})
}
}
impl Optionable for SpannerDatabase {
type Option = OptionDatabase;
fn set_option(&mut self, key: Self::Option, value: OptionValue) -> Result<()> {
match &key {
OptionDatabase::Uri => self.database = Some(string_value(&key, value)?),
OptionDatabase::Other(name) if name == OPTION_DATABASE => {
self.database = Some(string_value(&key, value)?)
}
OptionDatabase::Other(name) if name == OPTION_ENDPOINT => {
self.endpoint = Some(string_value(&key, value)?)
}
OptionDatabase::Other(name) if name == OPTION_EMULATOR => {
self.emulator = bool_value(&key, value)?
}
other => {
return Err(invalid_argument(format!(
"unsupported Spanner database option: {}",
option_name(other)
)))
}
}
Ok(())
}
fn get_option_string(&self, key: Self::Option) -> Result<String> {
let value = match &key {
OptionDatabase::Uri => self.database.clone(),
OptionDatabase::Other(name) if name == OPTION_DATABASE => self.database.clone(),
OptionDatabase::Other(name) if name == OPTION_ENDPOINT => self.endpoint.clone(),
OptionDatabase::Other(name) if name == OPTION_EMULATOR => {
Some(self.emulator.to_string())
}
_ => None,
};
value.ok_or_else(|| {
err(
format!("option {} is not set", option_name(&key)),
Status::NotFound,
)
})
}
fn get_option_bytes(&self, key: Self::Option) -> Result<Vec<u8>> {
Ok(self.get_option_string(key)?.into_bytes())
}
fn get_option_int(&self, key: Self::Option) -> Result<i64> {
Err(err(
format!("option {} is not an integer", option_name(&key)),
Status::NotFound,
))
}
fn get_option_double(&self, key: Self::Option) -> Result<f64> {
Err(err(
format!("option {} is not a double", option_name(&key)),
Status::NotFound,
))
}
}
impl Database for SpannerDatabase {
type ConnectionType = SpannerConnection;
fn new_connection(&self) -> Result<Self::ConnectionType> {
let client = self.connect()?;
Ok(SpannerConnection::new(self.runtime.clone(), client))
}
fn new_connection_with_opts(
&self,
opts: impl IntoIterator<Item = (adbc_core::options::OptionConnection, OptionValue)>,
) -> Result<Self::ConnectionType> {
let mut connection = self.new_connection()?;
for (key, value) in opts {
connection.set_option(key, value)?;
}
Ok(connection)
}
}
fn ensure_scheme(host: &str) -> String {
if host.starts_with("http://") || host.starts_with("https://") {
host.to_string()
} else {
format!("http://{host}")
}
}
fn option_name(key: &OptionDatabase) -> String {
key.as_ref().to_string()
}
fn string_value(key: &OptionDatabase, value: OptionValue) -> Result<String> {
match value {
OptionValue::String(s) => Ok(s),
_ => Err(invalid_argument(format!(
"option {} requires a string value",
option_name(key)
))),
}
}
fn bool_value(key: &OptionDatabase, value: OptionValue) -> Result<bool> {
match value {
OptionValue::String(s) => match s.to_ascii_lowercase().as_str() {
"true" | "1" | "yes" => Ok(true),
"false" | "0" | "no" => Ok(false),
other => Err(invalid_argument(format!(
"option {} expects a boolean, got {other:?}",
option_name(key)
))),
},
OptionValue::Int(i) => Ok(i != 0),
_ => Err(invalid_argument(format!(
"option {} requires a boolean value",
option_name(key)
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use adbc_core::error::Status;
fn new_database() -> SpannerDatabase {
SpannerDatabase::new(new_runtime().unwrap())
}
#[test]
fn ensure_scheme_adds_http_prefix() {
assert_eq!(ensure_scheme("localhost:9010"), "http://localhost:9010");
assert_eq!(ensure_scheme("http://host:1"), "http://host:1");
assert_eq!(ensure_scheme("https://host:1"), "https://host:1");
}
#[test]
fn database_options_round_trip() {
let mut db = new_database();
db.set_option(
OptionDatabase::Uri,
OptionValue::String("projects/p/instances/i/databases/d".into()),
)
.unwrap();
db.set_option(
OptionDatabase::Other(OPTION_ENDPOINT.into()),
OptionValue::String("http://localhost:9010".into()),
)
.unwrap();
db.set_option(
OptionDatabase::Other(OPTION_EMULATOR.into()),
OptionValue::String("true".into()),
)
.unwrap();
assert_eq!(
db.get_option_string(OptionDatabase::Uri).unwrap(),
"projects/p/instances/i/databases/d"
);
assert_eq!(
db.get_option_string(OptionDatabase::Other(OPTION_ENDPOINT.into()))
.unwrap(),
"http://localhost:9010"
);
assert!(db.emulator);
}
#[test]
fn the_database_option_is_an_alias_for_uri() {
let mut db = new_database();
db.set_option(
OptionDatabase::Other(OPTION_DATABASE.into()),
OptionValue::String("projects/p/instances/i/databases/d".into()),
)
.unwrap();
assert_eq!(
db.get_option_string(OptionDatabase::Uri).unwrap(),
"projects/p/instances/i/databases/d"
);
}
#[test]
fn connecting_without_a_database_path_is_an_error() {
let db = new_database();
let error = db.connect().unwrap_err();
assert_eq!(error.status, Status::InvalidState);
}
#[test]
fn a_non_string_uri_is_rejected() {
let mut db = new_database();
let error = db
.set_option(OptionDatabase::Uri, OptionValue::Int(42))
.unwrap_err();
assert_eq!(error.status, Status::InvalidArguments);
}
}