Skip to main content

google_cloud_spanner/
omni.rs

1// Copyright 2026 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Spanner Omni instance types and configuration utilities.
16
17pub use crate::client::SpannerBuilderExt;
18
19/// Specifies the type of Spanner instance to connect to (`Cloud` or `Omni`).
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
21#[non_exhaustive]
22pub enum InstanceType {
23    /// Google Cloud Spanner instance (default).
24    #[default]
25    Cloud,
26    /// Spanner Omni instance.
27    Omni,
28}
29
30/// Helper function to check if an endpoint string uses plaintext (`http://`).
31pub(crate) fn is_plaintext_endpoint(endpoint: &str) -> bool {
32    let trimmed = endpoint.trim();
33    if let Ok(parsed_url) = url::Url::parse(trimmed) {
34        parsed_url.scheme() == "http"
35    } else {
36        trimmed.starts_with("http://")
37    }
38}
39
40/// Helper function to format database resource names for Spanner Omni.
41///
42/// If project or instance IDs are omitted, defaults to `projects/default/instances/default/databases/{database}`.
43pub(crate) fn format_database_name(name: &str) -> String {
44    let trimmed = name.trim();
45    if trimmed.is_empty() {
46        return trimmed.to_string();
47    }
48
49    let parts: Vec<&str> = trimmed.split('/').collect();
50    match parts.as_slice() {
51        [
52            "projects",
53            _project,
54            "instances",
55            _instance,
56            "databases",
57            _database,
58        ] => trimmed.to_string(),
59        ["instances", instance, "databases", database] => {
60            format!(
61                "projects/default/instances/{}/databases/{}",
62                instance, database
63            )
64        }
65        ["databases", database] => {
66            format!("projects/default/instances/default/databases/{}", database)
67        }
68        [database] => {
69            format!("projects/default/instances/default/databases/{}", database)
70        }
71        _ => trimmed.to_string(),
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::client::Spanner;
79
80    #[test]
81    fn test_format_database_name() {
82        assert_eq!(
83            format_database_name("projects/p/instances/i/databases/d"),
84            "projects/p/instances/i/databases/d"
85        );
86        assert_eq!(
87            format_database_name("instances/i/databases/d"),
88            "projects/default/instances/i/databases/d"
89        );
90        assert_eq!(
91            format_database_name("databases/d"),
92            "projects/default/instances/default/databases/d"
93        );
94        assert_eq!(
95            format_database_name("retail-sample"),
96            "projects/default/instances/default/databases/retail-sample"
97        );
98    }
99
100    #[test]
101    fn test_is_plaintext_endpoint() {
102        assert!(is_plaintext_endpoint("http://localhost:15000"));
103        assert!(!is_plaintext_endpoint("https://spanner.internal:15000"));
104        assert!(!is_plaintext_endpoint("127.0.0.1:15000"));
105        assert!(is_plaintext_endpoint("http://not a valid url:1234"));
106    }
107
108    #[tokio::test]
109    async fn test_spanner_with_instance_type() {
110        let spanner = Spanner::builder()
111            .with_instance_type(InstanceType::Omni)
112            .build()
113            .await
114            .expect("build client");
115        assert_eq!(spanner.instance_type(), InstanceType::Omni);
116    }
117
118    #[tokio::test]
119    #[ignore = "requires live Omni instance at localhost:15000"]
120    async fn test_query_local_omni_instance() {
121        let spanner = Spanner::builder()
122            .with_endpoint("http://localhost:15000")
123            .with_instance_type(InstanceType::Omni)
124            .build()
125            .await
126            .expect("build client");
127
128        let db_client = spanner
129            .database_client("retail-sample")
130            .build()
131            .await
132            .expect("build db client");
133
134        let tx = db_client.single_use().build();
135        let mut rs = tx
136            .execute_query("SELECT * FROM Products LIMIT 5")
137            .await
138            .expect("execute query");
139
140        let mut count = 0;
141        while let Some(row) = rs.next().await {
142            let row = row.expect("read row");
143            println!("Fetched Omni row {}: {:?}", count, row);
144            count += 1;
145        }
146        println!("Successfully queried Omni! Total rows fetched: {}", count);
147    }
148}