1use {
2 crate::{error::SpannerDbErr, proxy::SpannerProxy},
3 gcloud_gax::conn::Environment,
4 gcloud_googleapis::spanner::admin::{
5 database::v1::{CreateDatabaseRequest, DatabaseDialect as GrpcDatabaseDialect},
6 instance::v1::{CreateInstanceRequest, Instance},
7 },
8 gcloud_spanner::{
9 admin::{client::Client as AdminClient, AdminClientConfig},
10 client::{Client, ClientConfig},
11 },
12 sea_orm::{Database, DatabaseConnection, DbErr},
13 std::sync::Arc,
14};
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub enum DatabaseDialect {
19 #[default]
20 GoogleStandardSql,
21 PostgreSql,
22}
23
24impl From<DatabaseDialect> for i32 {
25 fn from(dialect: DatabaseDialect) -> Self {
26 match dialect {
27 DatabaseDialect::GoogleStandardSql => GrpcDatabaseDialect::GoogleStandardSql.into(),
28 DatabaseDialect::PostgreSql => GrpcDatabaseDialect::Postgresql.into(),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Default)]
35pub struct InstanceConfig {
36 pub display_name: Option<String>,
38 pub config: Option<String>,
41 pub node_count: Option<i32>,
43 pub processing_units: Option<i32>,
45}
46
47#[derive(Debug, Clone)]
49pub struct CreateOptions {
50 pub create_instance_if_not_exists: bool,
55
56 pub create_database_if_not_exists: bool,
58
59 pub instance_config: InstanceConfig,
61
62 pub database_dialect: DatabaseDialect,
64}
65
66impl Default for CreateOptions {
67 fn default() -> Self {
68 Self {
69 create_instance_if_not_exists: false,
70 create_database_if_not_exists: true,
71 instance_config: InstanceConfig::default(),
72 database_dialect: DatabaseDialect::default(),
73 }
74 }
75}
76
77impl CreateOptions {
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn with_instance_creation(mut self) -> Self {
83 self.create_instance_if_not_exists = true;
84 self
85 }
86
87 pub fn with_dialect(mut self, dialect: DatabaseDialect) -> Self {
88 self.database_dialect = dialect;
89 self
90 }
91
92 pub fn with_instance_config(mut self, config: InstanceConfig) -> Self {
93 self.instance_config = config;
94 self
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct DatabasePath {
101 pub project: String,
102 pub instance: String,
103 pub database: String,
104}
105
106impl DatabasePath {
107 pub fn parse(path: &str) -> Result<Self, DbErr> {
111 let parts: Vec<&str> = path.split('/').collect();
112
113 if parts.len() != 6
114 || parts[0] != "projects"
115 || parts[2] != "instances"
116 || parts[4] != "databases"
117 {
118 return Err(DbErr::Custom(format!(
119 "Invalid database path format. Expected: projects/{{project}}/instances/{{instance}}/databases/{{database}}, got: {}",
120 path
121 )));
122 }
123
124 Ok(Self {
125 project: parts[1].to_string(),
126 instance: parts[3].to_string(),
127 database: parts[5].to_string(),
128 })
129 }
130
131 pub fn full_path(&self) -> String {
133 format!(
134 "projects/{}/instances/{}/databases/{}",
135 self.project, self.instance, self.database
136 )
137 }
138
139 pub fn project_path(&self) -> String {
141 format!("projects/{}", self.project)
142 }
143
144 pub fn instance_path(&self) -> String {
146 format!("projects/{}/instances/{}", self.project, self.instance)
147 }
148}
149
150pub fn ensure_tls() {
155 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
156}
157
158pub struct SpannerDatabase;
159
160impl SpannerDatabase {
161 pub async fn connect(database: &str) -> Result<DatabaseConnection, DbErr> {
172 let config = if std::env::var("SPANNER_EMULATOR_HOST").is_ok() {
173 ClientConfig::default()
174 } else {
175 ensure_tls();
176 ClientConfig::default().with_auth().await.map_err(|e| {
177 SpannerDbErr::Connection(format!("Failed to authenticate with GCP: {}", e))
178 })?
179 };
180 Self::connect_with_config(database, config).await
181 }
182
183 pub async fn connect_with_config(
185 database: &str,
186 config: ClientConfig,
187 ) -> Result<DatabaseConnection, DbErr> {
188 let client = Client::new(database, config)
189 .await
190 .map_err(|e| SpannerDbErr::Connection(e.to_string()))?;
191
192 let proxy = SpannerProxy::new(Arc::new(client));
193 Database::connect_proxy(sea_orm::DbBackend::MySql, Arc::new(Box::new(proxy))).await
194 }
195
196 pub async fn connect_with_emulator(database: &str) -> Result<DatabaseConnection, DbErr> {
198 Self::connect_with_emulator_host(database, "localhost:9010").await
199 }
200
201 pub async fn connect_with_emulator_host(
203 database: &str,
204 emulator_host: &str,
205 ) -> Result<DatabaseConnection, DbErr> {
206 let config = ClientConfig {
207 environment: Environment::Emulator(emulator_host.to_string()),
208 ..Default::default()
209 };
210 Self::connect_with_config(database, config).await
211 }
212
213 pub async fn connect_or_create_with_emulator(
236 database: &str,
237 options: CreateOptions,
238 ) -> Result<DatabaseConnection, DbErr> {
239 Self::connect_or_create_with_emulator_host(database, "localhost:9010", options).await
240 }
241
242 pub async fn connect_or_create_with_emulator_host(
246 database: &str,
247 emulator_host: &str,
248 options: CreateOptions,
249 ) -> Result<DatabaseConnection, DbErr> {
250 let path = DatabasePath::parse(database)?;
251
252 if options.create_instance_if_not_exists {
253 ensure_instance(&path, &options.instance_config, emulator_host).await?;
254 }
255
256 if options.create_database_if_not_exists {
257 ensure_database(&path, options.database_dialect, emulator_host).await?;
258 }
259
260 let config = ClientConfig {
261 environment: Environment::Emulator(emulator_host.to_string()),
262 ..Default::default()
263 };
264 Self::connect_with_config(database, config).await
265 }
266}
267
268pub async fn ensure_instance(
269 path: &DatabasePath,
270 config: &InstanceConfig,
271 emulator_host: &str,
272) -> Result<bool, DbErr> {
273 let admin_config = AdminClientConfig {
274 environment: Environment::Emulator(emulator_host.to_string()),
275 ..Default::default()
276 };
277 let admin_client = AdminClient::new(admin_config)
278 .await
279 .map_err(|e| SpannerDbErr::Connection(format!("Failed to create admin client: {}", e)))?;
280
281 let display_name = config
282 .display_name
283 .clone()
284 .unwrap_or_else(|| format!("{} Instance", path.instance));
285
286 let instance_config = config.config.clone().unwrap_or_default();
287
288 let mut instance = Instance {
289 name: path.instance_path(),
290 config: instance_config,
291 display_name,
292 ..Default::default()
293 };
294
295 if let Some(node_count) = config.node_count {
296 instance.node_count = node_count;
297 }
298 if let Some(processing_units) = config.processing_units {
299 instance.processing_units = processing_units;
300 }
301
302 let result = admin_client
303 .instance()
304 .create_instance(
305 CreateInstanceRequest {
306 parent: path.project_path(),
307 instance_id: path.instance.clone(),
308 instance: Some(instance),
309 },
310 None,
311 )
312 .await;
313
314 match result {
315 Ok(mut op) => {
316 op.wait(None).await.map_err(|e| {
317 SpannerDbErr::Connection(format!("Instance creation failed: {}", e))
318 })?;
319 Ok(true)
320 }
321 Err(e) => {
322 let err_str = e.to_string();
323 if err_str.contains("AlreadyExists") || err_str.contains("already exists") {
324 Ok(false)
325 } else {
326 Err(SpannerDbErr::Connection(format!("Failed to create instance: {}", e)).into())
327 }
328 }
329 }
330}
331
332pub async fn ensure_database(
333 path: &DatabasePath,
334 dialect: DatabaseDialect,
335 emulator_host: &str,
336) -> Result<bool, DbErr> {
337 let admin_config = AdminClientConfig {
338 environment: Environment::Emulator(emulator_host.to_string()),
339 ..Default::default()
340 };
341 let admin_client = AdminClient::new(admin_config)
342 .await
343 .map_err(|e| SpannerDbErr::Connection(format!("Failed to create admin client: {}", e)))?;
344
345 let result = admin_client
346 .database()
347 .create_database(
348 CreateDatabaseRequest {
349 parent: path.instance_path(),
350 create_statement: format!("CREATE DATABASE `{}`", path.database),
351 extra_statements: vec![],
352 encryption_config: None,
353 database_dialect: dialect.into(),
354 proto_descriptors: vec![],
355 },
356 None,
357 )
358 .await;
359
360 match result {
361 Ok(mut op) => {
362 op.wait(None).await.map_err(|e| {
363 SpannerDbErr::Connection(format!("Database creation failed: {}", e))
364 })?;
365 Ok(true)
366 }
367 Err(e) => {
368 let err_str = e.to_string();
369 if err_str.contains("AlreadyExists") || err_str.contains("already exists") {
370 Ok(false)
371 } else {
372 Err(SpannerDbErr::Connection(format!("Failed to create database: {}", e)).into())
373 }
374 }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_database_path_parse() {
384 let path = DatabasePath::parse("projects/my-project/instances/my-instance/databases/my-db")
385 .expect("Should parse valid path");
386
387 assert_eq!(path.project, "my-project");
388 assert_eq!(path.instance, "my-instance");
389 assert_eq!(path.database, "my-db");
390 assert_eq!(path.project_path(), "projects/my-project");
391 assert_eq!(
392 path.instance_path(),
393 "projects/my-project/instances/my-instance"
394 );
395 assert_eq!(
396 path.full_path(),
397 "projects/my-project/instances/my-instance/databases/my-db"
398 );
399 }
400
401 #[test]
402 fn test_database_path_parse_invalid() {
403 assert!(DatabasePath::parse("invalid/path").is_err());
404 assert!(DatabasePath::parse("projects/p/instances/i").is_err());
405 assert!(DatabasePath::parse("").is_err());
406 }
407
408 #[test]
409 fn test_create_options_default() {
410 let options = CreateOptions::default();
411 assert!(!options.create_instance_if_not_exists);
412 assert!(options.create_database_if_not_exists);
413 assert_eq!(options.database_dialect, DatabaseDialect::GoogleStandardSql);
414 }
415}