feat(spanner): add Google Cloud Spanner support

This commit is contained in:
Frost authored and GitHub committed 2026-08-20 16:33:09 +08:00
1 parent 2cc8d78e64
commit f494f90c30
74 files changed
+2920 -53

No files matched your search

@@ -63,6 +63,11 @@ pub fn is_schema_aware(database_type: DatabaseType) -> bool {
| DatabaseType::Sqlite
| DatabaseType::DuckDb
| DatabaseType::Iris
// Spanner supports named schemas; the PostgreSQL dialect defaults to `public`.
// GoogleSQL's default schema is the empty string, which the blank-schema filters
// in `qualified_table_name` / `table_data_qualified_table_name` drop along with
// the dot separator (`` `s`.`t` `` with an empty `s` is a Spanner syntax error).
| DatabaseType::Spanner
)
}
@@ -670,9 +670,8 @@ pub fn profile_for(db_type: DatabaseType) -> DdlDialectProfile {
ClickHouse => clickhouse_profile(),
DuckDb | Questdb | SapHana | Teradata | Snowflake | Trino | PrestoSql | Hive | Kyuubi | Impala | Spark
| Db2 | Informix | Bigquery | Kylin | Ignite | Ignite3 | Oscar | Tdengine | Iotdb | Databricks | Jdbc => {
conservative_ansi(db_type)
}
| Db2 | Informix | Bigquery | Spanner | Kylin | Ignite | Ignite3 | Oscar | Tdengine | Iotdb | Databricks
| Jdbc => conservative_ansi(db_type),
// Non-tabular / not applicable for relational CREATE TABLE
Redis | MongoDb | DynamoDb | Elasticsearch | Easysearch | Meilisearch | Qdrant | Milvus | Weaviate
@@ -136,6 +136,11 @@ pub fn quote_table_identifier(database_type: Option<DatabaseType>, name: &str) -
| DatabaseType::Tdengine
| DatabaseType::Access
| DatabaseType::Bigquery
// GoogleSQL (Spanner's default dialect) quotes identifiers with backticks;
// double quotes are string literals there, so `SELECT * FROM "users"` is a
// syntax error. PostgreSQL-dialect Spanner databases override this through
// the identifier quote reported by the agent on connect.
| DatabaseType::Spanner
| DatabaseType::Questdb,
) => {
format!("`{}`", name.replace('`', "``"))
@@ -385,6 +385,11 @@ pub(crate) fn uses_connection_identifier_quote(
identifier_quote: Option<&str>,
) -> bool {
database_type == Some(DatabaseType::Kingbase)
// Spanner is dual-dialect: GoogleSQL uses backticks, the PostgreSQL dialect uses
// double quotes, and only the connected agent knows which. Unconditional like
// Kingbase — when no quote was reported the callers fall back to
// `quote_table_identifier`, whose static mapping is GoogleSQL-correct.
|| database_type == Some(DatabaseType::Spanner)
|| (database_type == Some(DatabaseType::Informix) && identifier_quote.is_some())
|| (matches!(database_type, Some(DatabaseType::Gaussdb | DatabaseType::OpenGauss | DatabaseType::Postgres))
&& identifier_quote.is_some())
+38
View File
@@ -34,6 +34,37 @@ fn quotes_identifiers_by_database_type() {
assert_eq!(quote_table_identifier(Some(DatabaseType::Jdbc), "users_1"), "users_1");
assert_eq!(quote_table_identifier(Some(DatabaseType::Jdbc), "user name"), "user name");
assert_eq!(quote_table_identifier(Some(DatabaseType::Iotdb), "root.test.device2"), "root.test.device2");
assert_eq!(quote_table_identifier(Some(DatabaseType::Spanner), "user`name"), "`user``name`");
}
/// Spanner databases are created in one of two immutable dialects. The connected
/// agent reports the correct identifier quote; when it is missing the static mapping
/// must fall back to GoogleSQL (backticks), because GoogleSQL treats double quotes as
/// string literals and would reject `SELECT * FROM "users"`.
#[test]
fn quotes_spanner_identifiers_by_connection_dialect() {
// GoogleSQL dialect: agent reports a backtick.
assert_eq!(quote_table_data_identifier(Some(DatabaseType::Spanner), "order", Some("`")), "`order`");
assert_eq!(quote_table_data_identifier(Some(DatabaseType::Spanner), "user`name", Some("`")), "`user``name`");
// PostgreSQL dialect: agent reports a double quote.
assert_eq!(quote_table_data_identifier(Some(DatabaseType::Spanner), "order", Some("\"")), "\"order\"");
assert_eq!(quote_table_data_identifier(Some(DatabaseType::Spanner), "user\"name", Some("\"")), "\"user\"\"name\"");
// No quote reported (metadata probe failed / caller outside the desktop store):
// fall back to the GoogleSQL default rather than the ANSI double quote.
assert_eq!(quote_table_data_identifier(Some(DatabaseType::Spanner), "order", None), "`order`");
// GoogleSQL's default schema is the empty string and must not produce `` ``.`t` ``,
// which Spanner rejects with `Invalid empty identifier`.
assert_eq!(
table_data_qualified_table_name(Some(DatabaseType::Spanner), Some(""), "singers", Some("`")),
"`singers`"
);
assert_eq!(
table_data_qualified_table_name(Some(DatabaseType::Spanner), Some("public"), "singers", Some("\"")),
"\"public\".\"singers\""
);
}
#[test]
@@ -82,6 +113,11 @@ fn qualifies_schema_only_for_schema_aware_databases() {
assert_eq!(qualified_table_name(Some(DatabaseType::Informix), Some("xtdpcky"), "users"), "xtdpcky.users");
assert_eq!(qualified_table_name(Some(DatabaseType::Sqlite), Some("analytics"), "users"), "\"analytics\".\"users\"");
assert_eq!(qualified_table_name(Some(DatabaseType::Jdbc), Some("cbsdw_dwd"), "dwd_test_df"), "dwd_test_df");
// GoogleSQL's default schema is the empty string: the qualifier (and its dot) must be
// dropped entirely, otherwise Spanner reports `Invalid empty identifier`.
assert_eq!(qualified_table_name(Some(DatabaseType::Spanner), Some(""), "users"), "`users`");
assert_eq!(qualified_table_name(Some(DatabaseType::Spanner), None, "users"), "`users`");
assert_eq!(qualified_table_name(Some(DatabaseType::Spanner), Some("public"), "users"), "`public`.`users`");
assert_eq!(qualified_table_name(Some(DatabaseType::Iotdb), Some("root.test"), "device2"), "root.test.device2");
assert_eq!(
qualified_table_name(Some(DatabaseType::Iotdb), Some("root.test"), "root.test.device2"),
@@ -127,6 +163,8 @@ fn maps_table_pagination_strategy_by_database_type() {
TablePaginationStrategy::Unbounded
);
assert_eq!(table_pagination_strategy(Some(DatabaseType::Jdbc)), TablePaginationStrategy::AgentMaxRows);
// Both Spanner dialects support `LIMIT n OFFSET m`; pin the fallback.
assert_eq!(table_pagination_strategy(Some(DatabaseType::Spanner)), TablePaginationStrategy::LimitOffset);
assert_eq!(table_pagination_strategy(None), TablePaginationStrategy::LimitOffset);
}