feat(database): add database creation and property editing

This commit is contained in:
t8y2 committed 2026-07-06 02:00:01 +08:00
1 parent 3fdcc2d454
commit 2bfd03cca4
23 files changed
+1101 -57

No files matched your search

+428 -11
View File
@@ -41,6 +41,10 @@ pub struct CreateDatabaseSqlOptions {
pub database_type: Option<DatabaseType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub driver_profile: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<DatabaseCreationTarget>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub charset: Option<String>,
@@ -48,6 +52,15 @@ pub struct CreateDatabaseSqlOptions {
pub collation: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum DatabaseCreationTarget {
Database,
Schema,
Catalog,
Namespace,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg(feature = "duckdb-bundled")]
@@ -105,6 +118,30 @@ pub struct SchemaNameSqlOptions {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DatabasePropertyEditSqlOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub database_type: Option<DatabaseType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub driver_profile: Option<String>,
pub target: DatabasePropertyTarget,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub charset: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collation: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum DatabasePropertyTarget {
Database,
Schema,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DuplicateTableStructureSqlOptions {
@@ -137,22 +174,119 @@ const MYSQL_COMPATIBLE_PROFILES: &[&str] =
&["mysql", "mariadb", "tidb", "oceanbase", "doris", "starrocks", "custom_mysql"];
pub fn supports_create_database_charset(database_type: Option<DatabaseType>, driver_profile: Option<&str>) -> bool {
let normalized_profile = driver_profile.map(str::to_ascii_lowercase);
matches!(
database_type,
Some(DatabaseType::Mysql | DatabaseType::Doris | DatabaseType::StarRocks | DatabaseType::Goldendb)
) || driver_profile.is_some_and(|profile| MYSQL_COMPATIBLE_PROFILES.contains(&profile))
) || normalized_profile.as_deref().is_some_and(|profile| MYSQL_COMPATIBLE_PROFILES.contains(&profile))
}
pub fn build_create_database_sql(options: CreateDatabaseSqlOptions) -> String {
pub fn build_create_database_sql(options: CreateDatabaseSqlOptions) -> Result<String, String> {
match options.target.unwrap_or(DatabaseCreationTarget::Database) {
DatabaseCreationTarget::Database => build_create_database_statement(&options),
// Schema creation is exposed through the same frontend dialog contract when the tree target is a database node.
DatabaseCreationTarget::Schema => {
build_create_schema_sql(SchemaNameSqlOptions { database_type: options.database_type, name: options.name })
}
DatabaseCreationTarget::Catalog => Err("Creating catalogs is not supported yet.".to_string()),
DatabaseCreationTarget::Namespace => Err("Creating namespaces is not supported yet.".to_string()),
}
}
fn build_create_database_statement(options: &CreateDatabaseSqlOptions) -> Result<String, String> {
if !supports_create_database_target(options.database_type) {
return Err(format!("Creating databases is not supported for {}.", database_label(options.database_type)));
}
let name = quote_table_identifier(options.database_type, &options.name);
let charset = clean_sql_option(options.charset.as_deref());
let collation = clean_sql_option(options.collation.as_deref());
if !supports_create_database_charset(options.database_type, options.driver_profile.as_deref()) || charset.is_empty()
{
return format!("CREATE DATABASE {name};");
return Ok(format!("CREATE DATABASE {name};"));
}
let collate_clause = if collation.is_empty() { String::new() } else { format!(" COLLATE {collation}") };
format!("CREATE DATABASE {name} CHARACTER SET {charset}{collate_clause};")
Ok(format!("CREATE DATABASE {name} CHARACTER SET {charset}{collate_clause};"))
}
pub fn supports_create_database_target(database_type: Option<DatabaseType>) -> bool {
matches!(
database_type,
Some(
DatabaseType::Mysql
| DatabaseType::Doris
| DatabaseType::StarRocks
| DatabaseType::Goldendb
| DatabaseType::ClickHouse
| DatabaseType::SqlServer
| DatabaseType::InfluxDb
| DatabaseType::Databend
| DatabaseType::Snowflake
| DatabaseType::Tdengine
| DatabaseType::Postgres
| DatabaseType::Redshift
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::OpenGauss
| DatabaseType::Vastbase
| DatabaseType::Highgo
| DatabaseType::Kingbase
| DatabaseType::Yashandb
)
)
}
pub fn supports_create_schema_target(database_type: Option<DatabaseType>) -> bool {
matches!(
database_type,
Some(
DatabaseType::Postgres
| DatabaseType::Redshift
| DatabaseType::SqlServer
| DatabaseType::Db2
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::Kingbase
| DatabaseType::Highgo
| DatabaseType::Vastbase
| DatabaseType::Yashandb
| DatabaseType::Databricks
| DatabaseType::SapHana
| DatabaseType::Teradata
| DatabaseType::Vertica
| DatabaseType::Exasol
| DatabaseType::OpenGauss
| DatabaseType::Gbase
| DatabaseType::Trino
| DatabaseType::PrestoSql
| DatabaseType::H2
| DatabaseType::Informix
| DatabaseType::Xugu
| DatabaseType::Oscar
| DatabaseType::Iris
| DatabaseType::Snowflake
)
)
}
pub fn supports_database_property_charset(database_type: Option<DatabaseType>, driver_profile: Option<&str>) -> bool {
supports_create_database_charset(database_type, driver_profile)
&& matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Goldendb))
}
pub fn supports_database_property_comment(database_type: Option<DatabaseType>) -> bool {
matches!(
database_type,
Some(
DatabaseType::Postgres
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::Kingbase
| DatabaseType::Highgo
| DatabaseType::Vastbase
| DatabaseType::OpenGauss
| DatabaseType::Yashandb
)
)
}
#[cfg(feature = "duckdb-bundled")]
@@ -292,8 +426,70 @@ pub fn build_drop_database_sql(options: DatabaseNameSqlOptions) -> String {
format!("DROP DATABASE {};", quote_table_identifier(options.database_type, &options.name))
}
pub fn build_create_schema_sql(options: SchemaNameSqlOptions) -> String {
format!("CREATE SCHEMA {};", quote_table_identifier(options.database_type, &options.name))
pub fn build_update_database_properties_sql(options: DatabasePropertyEditSqlOptions) -> Result<String, String> {
match options.target {
DatabasePropertyTarget::Database => {
if options.comment.is_some() {
return build_database_comment_sql(options.database_type, &options.name, options.comment.as_deref());
}
build_database_charset_sql(&options)
}
DatabasePropertyTarget::Schema => {
build_schema_comment_sql(options.database_type, &options.name, options.comment.as_deref())
}
}
}
fn build_database_charset_sql(options: &DatabasePropertyEditSqlOptions) -> Result<String, String> {
if !supports_database_property_charset(options.database_type, options.driver_profile.as_deref()) {
return Err(format!(
"Editing database charset/collation is not supported for {}.",
database_label(options.database_type)
));
}
let charset = clean_sql_option(options.charset.as_deref());
let collation = clean_sql_option(options.collation.as_deref());
if charset.is_empty() && collation.is_empty() {
return Err("At least one charset or collation value is required.".to_string());
}
let mut sql = format!("ALTER DATABASE {}", quote_table_identifier(options.database_type, &options.name));
if !charset.is_empty() {
sql.push_str(&format!(" DEFAULT CHARACTER SET {charset}"));
}
if !collation.is_empty() {
sql.push_str(&format!(" DEFAULT COLLATE {collation}"));
}
sql.push(';');
Ok(sql)
}
fn build_database_comment_sql(
database_type: Option<DatabaseType>,
name: &str,
comment: Option<&str>,
) -> Result<String, String> {
if !supports_database_property_comment(database_type) {
return Err(format!("Editing database comments is not supported for {}.", database_label(database_type)));
}
Ok(format!("COMMENT ON DATABASE {} IS {};", quote_table_identifier(database_type, name), comment_literal(comment)))
}
fn build_schema_comment_sql(
database_type: Option<DatabaseType>,
name: &str,
comment: Option<&str>,
) -> Result<String, String> {
if !supports_database_property_comment(database_type) {
return Err(format!("Editing schema comments is not supported for {}.", database_label(database_type)));
}
Ok(format!("COMMENT ON SCHEMA {} IS {};", quote_table_identifier(database_type, name), comment_literal(comment)))
}
pub fn build_create_schema_sql(options: SchemaNameSqlOptions) -> Result<String, String> {
if !supports_create_schema_target(options.database_type) {
return Err(format!("Creating schemas is not supported for {}.", database_label(options.database_type)));
}
Ok(format!("CREATE SCHEMA {};", quote_table_identifier(options.database_type, &options.name)))
}
pub fn build_drop_schema_sql(options: SchemaNameSqlOptions) -> String {
@@ -523,6 +719,13 @@ fn quote_sql_string(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
fn comment_literal(value: Option<&str>) -> String {
match value.map(str::trim).filter(|value| !value.is_empty()) {
Some(value) => quote_sql_string(value),
None => "NULL".to_string(),
}
}
fn database_label(database_type: Option<DatabaseType>) -> String {
database_type
.and_then(|database_type| serde_json::to_value(database_type).ok())
@@ -540,10 +743,13 @@ mod tests {
build_create_database_sql(CreateDatabaseSqlOptions {
database_type: Some(DatabaseType::Mysql),
driver_profile: Some("mysql".to_string()),
target: None,
parent: None,
name: "app db".to_string(),
charset: Some("utf8mb4".to_string()),
collation: Some("utf8mb4_unicode_ci".to_string()),
}),
})
.unwrap(),
"CREATE DATABASE `app db` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
);
}
@@ -554,10 +760,13 @@ mod tests {
build_create_database_sql(CreateDatabaseSqlOptions {
database_type: Some(DatabaseType::Goldendb),
driver_profile: Some("goldendb".to_string()),
target: None,
parent: None,
name: "app_db".to_string(),
charset: Some("utf8mb4".to_string()),
collation: Some("utf8mb4_unicode_ci".to_string()),
}),
})
.unwrap(),
"CREATE DATABASE `app_db` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
);
}
@@ -568,10 +777,13 @@ mod tests {
build_create_database_sql(CreateDatabaseSqlOptions {
database_type: Some(DatabaseType::Postgres),
driver_profile: None,
target: None,
parent: None,
name: "analytics".to_string(),
charset: Some("utf8mb4".to_string()),
collation: Some("utf8mb4_unicode_ci".to_string()),
}),
})
.unwrap(),
"CREATE DATABASE \"analytics\";"
);
}
@@ -582,14 +794,196 @@ mod tests {
build_create_database_sql(CreateDatabaseSqlOptions {
database_type: Some(DatabaseType::Vastbase),
driver_profile: Some("vastbase".to_string()),
target: None,
parent: None,
name: "app_db".to_string(),
charset: Some("utf8mb4".to_string()),
collation: Some("utf8mb4_unicode_ci".to_string()),
}),
})
.unwrap(),
"CREATE DATABASE \"app_db\";"
);
}
#[test]
fn builds_additional_verified_create_database_targets() {
assert_eq!(
build_create_database_sql(CreateDatabaseSqlOptions {
database_type: Some(DatabaseType::SqlServer),
driver_profile: None,
target: None,
parent: None,
name: "analytics db".to_string(),
charset: None,
collation: None,
})
.unwrap(),
"CREATE DATABASE [analytics db];"
);
assert_eq!(
build_create_database_sql(CreateDatabaseSqlOptions {
database_type: Some(DatabaseType::Snowflake),
driver_profile: None,
target: None,
parent: None,
name: "analytics db".to_string(),
charset: None,
collation: None,
})
.unwrap(),
"CREATE DATABASE \"analytics db\";"
);
assert_eq!(
build_create_database_sql(CreateDatabaseSqlOptions {
database_type: Some(DatabaseType::Databend),
driver_profile: None,
target: None,
parent: None,
name: "analytics db".to_string(),
charset: None,
collation: None,
})
.unwrap(),
"CREATE DATABASE `analytics db`;"
);
assert_eq!(
build_create_database_sql(CreateDatabaseSqlOptions {
database_type: Some(DatabaseType::Tdengine),
driver_profile: None,
target: None,
parent: None,
name: "analytics db".to_string(),
charset: None,
collation: None,
})
.unwrap(),
"CREATE DATABASE `analytics db`;"
);
}
#[test]
fn rejects_unsupported_create_database_targets() {
assert!(build_create_database_sql(CreateDatabaseSqlOptions {
database_type: Some(DatabaseType::Oracle),
driver_profile: None,
target: None,
parent: None,
name: "analytics".to_string(),
charset: None,
collation: None,
})
.unwrap_err()
.contains("Creating databases is not supported"));
assert!(build_create_database_sql(CreateDatabaseSqlOptions {
database_type: Some(DatabaseType::Jdbc),
driver_profile: None,
target: None,
parent: None,
name: "analytics".to_string(),
charset: None,
collation: None,
})
.unwrap_err()
.contains("Creating databases is not supported"));
}
#[test]
fn builds_mysql_database_property_charset_sql() {
assert_eq!(
build_update_database_properties_sql(DatabasePropertyEditSqlOptions {
database_type: Some(DatabaseType::Mysql),
driver_profile: Some("mysql".to_string()),
target: DatabasePropertyTarget::Database,
name: "app db".to_string(),
charset: Some("utf8mb4".to_string()),
collation: Some("utf8mb4_unicode_ci".to_string()),
comment: None,
})
.unwrap(),
"ALTER DATABASE `app db` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;"
);
assert_eq!(
build_update_database_properties_sql(DatabasePropertyEditSqlOptions {
database_type: Some(DatabaseType::Goldendb),
driver_profile: None,
target: DatabasePropertyTarget::Database,
name: "app".to_string(),
charset: Some("utf8mb4".to_string()),
collation: None,
comment: None,
})
.unwrap(),
"ALTER DATABASE `app` DEFAULT CHARACTER SET utf8mb4;"
);
}
#[test]
fn builds_postgres_style_database_and_schema_comment_sql() {
assert_eq!(
build_update_database_properties_sql(DatabasePropertyEditSqlOptions {
database_type: Some(DatabaseType::Postgres),
driver_profile: None,
target: DatabasePropertyTarget::Database,
name: "app db".to_string(),
charset: None,
collation: None,
comment: Some("owner's app".to_string()),
})
.unwrap(),
"COMMENT ON DATABASE \"app db\" IS 'owner''s app';"
);
assert_eq!(
build_update_database_properties_sql(DatabasePropertyEditSqlOptions {
database_type: Some(DatabaseType::Kingbase),
driver_profile: None,
target: DatabasePropertyTarget::Schema,
name: "public".to_string(),
charset: None,
collation: None,
comment: Some("".to_string()),
})
.unwrap(),
"COMMENT ON SCHEMA \"public\" IS NULL;"
);
}
#[test]
fn rejects_unsupported_database_property_sql() {
assert!(build_update_database_properties_sql(DatabasePropertyEditSqlOptions {
database_type: Some(DatabaseType::SqlServer),
driver_profile: None,
target: DatabasePropertyTarget::Database,
name: "master".to_string(),
charset: Some("utf8mb4".to_string()),
collation: None,
comment: None,
})
.unwrap_err()
.contains("charset/collation is not supported"));
assert!(build_update_database_properties_sql(DatabasePropertyEditSqlOptions {
database_type: Some(DatabaseType::Mysql),
driver_profile: None,
target: DatabasePropertyTarget::Database,
name: "app".to_string(),
charset: None,
collation: None,
comment: Some("comment".to_string()),
})
.unwrap_err()
.contains("database comments is not supported"));
assert!(build_update_database_properties_sql(DatabasePropertyEditSqlOptions {
database_type: Some(DatabaseType::DuckDb),
driver_profile: None,
target: DatabasePropertyTarget::Schema,
name: "main".to_string(),
charset: None,
collation: None,
comment: Some("comment".to_string()),
})
.unwrap_err()
.contains("schema comments is not supported"));
}
#[test]
fn recognizes_mysql_compatible_create_database_profiles() {
assert!(supports_create_database_charset(Some(DatabaseType::Mysql), Some("oceanbase")));
@@ -733,9 +1127,32 @@ mod tests {
build_create_schema_sql(SchemaNameSqlOptions {
database_type: Some(DatabaseType::Postgres),
name: "analytics".to_string(),
}),
})
.unwrap(),
"CREATE SCHEMA \"analytics\";"
);
assert_eq!(
build_create_schema_sql(SchemaNameSqlOptions {
database_type: Some(DatabaseType::SqlServer),
name: "analytics".to_string(),
})
.unwrap(),
"CREATE SCHEMA [analytics];"
);
assert_eq!(
build_create_schema_sql(SchemaNameSqlOptions {
database_type: Some(DatabaseType::Db2),
name: "analytics".to_string(),
})
.unwrap(),
"CREATE SCHEMA \"analytics\";"
);
assert!(build_create_schema_sql(SchemaNameSqlOptions {
database_type: Some(DatabaseType::DuckDb),
name: "analytics".to_string(),
})
.unwrap_err()
.contains("Creating schemas is not supported"));
assert_eq!(
build_drop_schema_sql(SchemaNameSqlOptions {
database_type: Some(DatabaseType::Kwdb),