use serde::{Deserialize, Serialize}; use crate::models::connection::DatabaseType; use crate::sql_dialect::{ is_schema_aware, profile_for, qualified_table_name, quote_table_data_identifier, quote_table_identifier, uses_connection_identifier_quote, DdlDialectProfile, }; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum DatabaseObjectType { Table, View, MaterializedView, Procedure, Function, Event, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum TableChildObjectType { Column, Index, ForeignKey, Trigger, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RenameObjectSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, pub object_type: DatabaseObjectType, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub old_name: String, pub new_name: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateDatabaseSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub driver_profile: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub target: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub parent: Option, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub charset: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub collation: Option, } #[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-sidecar")] pub struct DuckDbAttachDatabaseSqlOptions { pub path: String, pub name: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SqliteAttachDatabaseSqlOptions { pub path: String, pub name: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DropObjectSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, pub object_type: DatabaseObjectType, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub signature: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub identifier_quote: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TableAdminSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub cascade: Option, /// Quote character reported by the connected server, for types whose quote is not a property of /// the database type alone. See [`qualified_name_with_quote`]. #[serde(default, skip_serializing_if = "Option::is_none")] pub identifier_quote: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct VacuumTableSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, #[serde(default)] pub full: bool, #[serde(default)] pub analyze: bool, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MysqlAutoIncrementSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub driver_profile: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, pub value: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DropTableChildObjectSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, pub object_type: TableChildObjectType, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, pub name: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DatabaseNameSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, pub name: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SchemaNameSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, 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, #[serde(default, skip_serializing_if = "Option::is_none")] pub driver_profile: Option, pub target: DatabasePropertyTarget, pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub charset: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub collation: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub comment: Option, } #[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 { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub source_name: String, pub target_name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub table_comment: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub column_comments: Vec, /// Source primary-key columns to recreate on the clone. SQL Server's /// `SELECT ... INTO` copies columns but drops constraints, so the clone /// needs an explicit `ALTER TABLE ... ADD CONSTRAINT ... PRIMARY KEY`. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub primary_key_columns: Vec, /// Pre-computed primary-key constraint name for the clone. Callers derive /// it from the source index names so the generated `PK_{target}` respects /// SQL Server's 128-character identifier limit and avoids a name that /// already exists on the source table. #[serde(default, skip_serializing_if = "Option::is_none")] pub primary_key_constraint_name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub identifier_quote: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DuplicateTableColumnComment { pub name: String, pub comment: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CopyTableDataSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub database_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub source_name: String, pub target_name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub columns: Option>, #[serde(default)] pub postgres_overriding_system_value: bool, #[serde(default)] pub sqlserver_identity_insert: bool, #[serde(default)] pub dameng_identity_insert: bool, #[serde(default)] pub normalize_new_target_name: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub identifier_quote: Option, } const MYSQL_COMPATIBLE_PROFILES: &[&str] = &["mysql", "mariadb", "tidb", "oceanbase", "custom_mysql"]; const CREATE_DATABASE_CHARSET_UNSUPPORTED_PROFILES: &[&str] = &["doris", "selectdb", "starrocks"]; pub fn supports_create_database_charset(database_type: Option, driver_profile: Option<&str>) -> bool { let normalized_profile = driver_profile.map(str::to_ascii_lowercase); if normalized_profile .as_deref() .is_some_and(|profile| CREATE_DATABASE_CHARSET_UNSUPPORTED_PROFILES.contains(&profile)) { return false; } matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Goldendb)) || normalized_profile.as_deref().is_some_and(|profile| MYSQL_COMPATIBLE_PROFILES.contains(&profile)) } pub fn build_create_database_sql(options: CreateDatabaseSqlOptions) -> Result { 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 { if !supports_create_database_target(options.database_type, options.driver_profile.as_deref()) { return Err(format!("Creating databases is not supported for {}.", database_label(options.database_type))); } if is_informix_family(options.database_type, options.driver_profile.as_deref()) { // Informix / GBase 8s accept only a bare `CREATE DATABASE `. The new database // inherits the instance default locale, and the MySQL `CHARACTER SET`/`COLLATE` // clauses are invalid syntax here. Database names are ordinary identifiers, so quote // them with the Informix rule (unquoted for simple identifiers) rather than the // default double-quote path that `DatabaseType::Gbase` would otherwise take. let name = quote_table_identifier(Some(DatabaseType::Informix), &options.name); let locale = clean_sql_option(options.charset.as_deref()); if locale.is_empty() { return Ok(format!("CREATE DATABASE {name};")); } // Informix has no charset clause in `CREATE DATABASE`; the new database inherits the // creating session's DB_LOCALE. Carry the chosen locale as a directive the GBase 8s agent // honors by running the statement on a sysmaster session pinned to that DB_LOCALE. return Ok(format!("-- DBX_DB_LOCALE={locale}\nCREATE DATABASE {name};")); } 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 Ok(format!("CREATE DATABASE {name};")); } let collate_clause = if collation.is_empty() { String::new() } else { format!(" COLLATE {collation}") }; Ok(format!("CREATE DATABASE {name} CHARACTER SET {charset}{collate_clause};")) } /// Whether the connection belongs to the Informix family: standalone Informix, or GBase 8s /// (which shares `DatabaseType::Gbase` with the MySQL-based GBase 8a and is only distinguishable /// through the `gbase8s` driver profile). Informix-family servers accept `CREATE DATABASE` but /// have no `CREATE SCHEMA ` statement (a "schema" is the table owner), so the create /// targets differ from the rest of the `Gbase` family. fn is_informix_family(database_type: Option, driver_profile: Option<&str>) -> bool { match database_type { Some(DatabaseType::Informix) => true, Some(DatabaseType::Gbase) => driver_profile.is_some_and(|profile| profile.eq_ignore_ascii_case("gbase8s")), _ => false, } } pub fn supports_create_database_target(database_type: Option, driver_profile: Option<&str>) -> bool { // Informix / GBase 8s create namespaces with `CREATE DATABASE`, so they are valid targets // even though they are absent from the explicit list below. if is_informix_family(database_type, driver_profile) { return true; } 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) -> bool { matches!( database_type, Some( DatabaseType::Postgres | DatabaseType::Redshift | DatabaseType::SqlServer | DatabaseType::Db2 | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase | DatabaseType::Yashandb | DatabaseType::Dameng | DatabaseType::Databricks | DatabaseType::SapHana | DatabaseType::Teradata | DatabaseType::Vertica | DatabaseType::Exasol | DatabaseType::OpenGauss | DatabaseType::Gbase | DatabaseType::Trino | DatabaseType::PrestoSql | DatabaseType::H2 | DatabaseType::Xugu | DatabaseType::Oscar | DatabaseType::Iris | DatabaseType::Snowflake ) ) } pub fn supports_database_property_charset(database_type: Option, 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) -> bool { matches!( database_type, Some( DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase | DatabaseType::OpenGauss | DatabaseType::Yashandb ) ) } #[cfg(feature = "duckdb-sidecar")] pub fn build_duckdb_attach_database_sql(options: DuckDbAttachDatabaseSqlOptions) -> String { format!( "ATTACH {} AS {};", quote_sql_string(&options.path), quote_table_identifier(Some(DatabaseType::DuckDb), &options.name) ) } pub fn build_sqlite_attach_database_sql(options: SqliteAttachDatabaseSqlOptions) -> String { format!( "ATTACH DATABASE {} AS {};", quote_sql_string(&options.path), quote_table_identifier(Some(DatabaseType::Sqlite), &options.name) ) } pub fn build_create_user_sql(username: &str, password: &str, tablespace: &str) -> String { format!( "CREATE USER {} IDENTIFIED BY {} DEFAULT TABLESPACE {};", quote_table_identifier(Some(DatabaseType::Dameng), username), quote_sql_string(password), quote_table_identifier(Some(DatabaseType::Dameng), tablespace) ) } pub fn build_drop_object_sql(options: DropObjectSqlOptions) -> String { let signature = if matches!(options.database_type, Some(DatabaseType::Postgres)) && matches!(options.object_type, DatabaseObjectType::Function | DatabaseObjectType::Procedure) { options.signature.as_deref().map(|value| format!("({value})")).unwrap_or_default() } else { String::new() }; format!( "DROP {} {}{};", object_type_keyword(options.object_type), qualified_name_with_quote( options.database_type, options.schema.as_deref(), &options.name, options.identifier_quote.as_deref(), ), signature ) } pub fn build_drop_table_sql(options: TableAdminSqlOptions) -> String { let table = qualified_name_with_quote( options.database_type, options.schema.as_deref(), &options.table_name, options.identifier_quote.as_deref(), ); // IoTDB is the one engine whose drop target is a derived path pattern rather than the // qualified name, so it cannot be expressed as a profile template. if matches!(options.database_type, Some(DatabaseType::Iotdb)) { return format!("DELETE TIMESERIES {};", iotdb_timeseries_pattern(&table)); } let cascade = if options.cascade.unwrap_or(false) && supports_drop_table_cascade(options.database_type) { " CASCADE" } else { "" }; // Unknown database type: fall back to the ANSI shape rather than guessing a profile. let Some(database_type) = options.database_type else { return format!("DROP TABLE {table}{cascade};"); }; DdlDialectProfile::render_template( profile_for(database_type).drop_table_template, &[("table", &table), ("cascade", cascade)], ) } fn supports_drop_table_cascade(database_type: Option) -> bool { database_type.is_some_and(|database_type| profile_for(database_type).drop_table_supports_cascade) } pub fn build_drop_table_child_object_sql(options: DropTableChildObjectSqlOptions) -> Result { let database_type = options.database_type; let table = qualified_name(database_type, options.schema.as_deref(), &options.table_name); let name = quote_rename_identifier(database_type, &options.name); match options.object_type { TableChildObjectType::Column => Ok(format!("ALTER TABLE {table} DROP COLUMN {name};")), TableChildObjectType::Index => { if matches!(database_type, Some(DatabaseType::ClickHouse | DatabaseType::Redshift)) { return Err(format!("Dropping indexes is not supported for {}.", database_label(database_type))); } if matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Goldendb | DatabaseType::SqlServer)) { return Ok(format!("DROP INDEX {name} ON {table};")); } if matches!( database_type, Some( DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::OpenGauss | DatabaseType::Questdb | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase | DatabaseType::Kingbase | DatabaseType::Oracle | DatabaseType::Dameng | DatabaseType::OceanbaseOracle | DatabaseType::Iris | DatabaseType::Sqlite ) ) && options.schema.as_deref().is_some_and(|schema| !schema.is_empty()) { let schema = quote_rename_identifier(database_type, options.schema.as_deref().unwrap()); return Ok(format!("DROP INDEX {schema}.{name};")); } Ok(format!("DROP INDEX {name};")) } TableChildObjectType::ForeignKey => { if matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Goldendb)) { Ok(format!("ALTER TABLE {table} DROP FOREIGN KEY {name};")) } else { Ok(format!("ALTER TABLE {table} DROP CONSTRAINT {name};")) } } TableChildObjectType::Trigger => { if matches!( database_type, Some( DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::OpenGauss | DatabaseType::Questdb | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase | DatabaseType::Kingbase ) ) { Ok(format!("DROP TRIGGER {name} ON {table};")) } else if matches!(database_type, Some(DatabaseType::SqlServer)) { Ok(format!("DROP TRIGGER {name};")) } else if database_type.is_some_and(is_schema_aware) && options.schema.as_deref().is_some_and(|schema| !schema.is_empty()) && !matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Goldendb)) { let schema = quote_rename_identifier(database_type, options.schema.as_deref().unwrap()); Ok(format!("DROP TRIGGER {schema}.{name};")) } else { Ok(format!("DROP TRIGGER {name};")) } } } } pub fn build_empty_table_sql(options: TableAdminSqlOptions) -> String { let table = qualified_name_with_quote( options.database_type, options.schema.as_deref(), &options.table_name, options.identifier_quote.as_deref(), ); match options.database_type { Some(DatabaseType::ClickHouse) => format!("ALTER TABLE {table} DELETE WHERE 1 = 1;"), // BigQuery and Spanner both reject an unqualified `DELETE FROM t`; DML requires a // WHERE clause. Some(DatabaseType::Bigquery | DatabaseType::Spanner) => format!("DELETE FROM {table} WHERE TRUE;"), Some( DatabaseType::Cassandra | DatabaseType::Hive | DatabaseType::Kyuubi | DatabaseType::Argo | DatabaseType::Kylin | DatabaseType::Questdb // StarRocks and Doris reject a WHERE-less DELETE (StarRocks analyzer: // "Where clause is not set"), and both officially support TRUNCATE TABLE // as the way to clear a whole table. | DatabaseType::Doris | DatabaseType::StarRocks, ) => { format!("TRUNCATE TABLE {table};") } Some(DatabaseType::Iotdb) => format!("DELETE FROM {};", iotdb_timeseries_pattern(&table)), _ => format!("DELETE FROM {table};"), } } pub fn build_truncate_table_sql(options: TableAdminSqlOptions) -> String { let table = qualified_name_with_quote( options.database_type, options.schema.as_deref(), &options.table_name, options.identifier_quote.as_deref(), ); if matches!(options.database_type, Some(DatabaseType::Iotdb)) { format!("DELETE FROM {};", iotdb_timeseries_pattern(&table)) } else if matches!(options.database_type, Some(DatabaseType::Spanner)) { // Spanner has no TRUNCATE statement (it reports `Unknown statement`), and its DML // requires a WHERE clause. Unlike BigQuery it cannot use the TRUNCATE fallback. format!("DELETE FROM {table} WHERE TRUE;") } else if matches!(options.database_type, Some(DatabaseType::Sqlite | DatabaseType::DuckDb)) { format!("DELETE FROM {table};") } else { // TRUNCATE CASCADE is PostgreSQL-family syntax; other dialects keep their existing default. let cascade = if options.cascade.unwrap_or(false) && supports_truncate_table_cascade(options.database_type) { " CASCADE" } else { "" }; format!("TRUNCATE TABLE {table}{cascade};") } } pub fn build_vacuum_table_sql(options: VacuumTableSqlOptions) -> Result { if !supports_vacuum_table(options.database_type) { return Err(format!("VACUUM is not supported for {}.", database_label(options.database_type))); } let table = qualified_name(options.database_type, options.schema.as_deref(), &options.table_name); let full = if options.full { " FULL" } else { "" }; let analyze = if options.analyze { " ANALYZE" } else { "" }; Ok(format!("VACUUM{full}{analyze} {table};")) } fn supports_vacuum_table(database_type: Option) -> bool { matches!( database_type, Some( DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase | DatabaseType::OpenGauss ) ) } pub fn build_mysql_auto_increment_sql(options: MysqlAutoIncrementSqlOptions) -> Result { let profile = options.driver_profile.as_deref().map(str::trim).unwrap_or_default(); if options.database_type != Some(DatabaseType::Mysql) || (!profile.is_empty() && !profile.eq_ignore_ascii_case("mysql")) { return Err("Setting AUTO_INCREMENT is supported only for native MySQL connections.".to_string()); } let value = options.value.as_str(); if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) || value.starts_with('0') || value.parse::().is_err() { return Err("AUTO_INCREMENT must be a decimal integer from 1 to 18446744073709551615.".to_string()); } let table = if options.schema.as_deref().is_some_and(|schema| !schema.is_empty()) { format!( "{}.{}", quote_rename_identifier(options.database_type, options.schema.as_deref().unwrap()), quote_rename_identifier(options.database_type, &options.table_name) ) } else { quote_rename_identifier(options.database_type, &options.table_name) }; Ok(format!("ALTER TABLE {table} AUTO_INCREMENT = {value};")) } fn supports_truncate_table_cascade(database_type: Option) -> bool { matches!( database_type, Some( DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase | DatabaseType::OpenGauss ) ) } pub fn build_drop_database_sql(options: DatabaseNameSqlOptions) -> String { format!("DROP DATABASE {};", quote_table_identifier(options.database_type, &options.name)) } pub fn build_update_database_properties_sql(options: DatabasePropertyEditSqlOptions) -> Result { 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 { 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, name: &str, comment: Option<&str>, ) -> Result { 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, name: &str, comment: Option<&str>, ) -> Result { 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 { 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 { let schema = quote_table_identifier(options.database_type, &options.name); if options.database_type == Some(DatabaseType::OceanbaseOracle) { // OceanBase Oracle mode models schemas as users, so dropping one drops the user. format!("DROP USER {schema} CASCADE;") } else if matches!( options.database_type, Some(DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::Dameng) ) { format!("DROP SCHEMA {schema} CASCADE;") } else { format!("DROP SCHEMA {schema};") } } pub fn build_duplicate_table_structure_sql(options: DuplicateTableStructureSqlOptions) -> String { let source = qualified_name_with_quote( options.database_type, options.schema.as_deref(), &options.source_name, options.identifier_quote.as_deref(), ); let target = qualified_duplicate_target_name(options.database_type, options.schema.as_deref(), &options.target_name); let structure_sql = if matches!( options.database_type, Some(DatabaseType::Mysql | DatabaseType::Kyuubi | DatabaseType::Impala | DatabaseType::Argo) ) { format!("CREATE TABLE {target} LIKE {source};") } else if options.database_type == Some(DatabaseType::Questdb) { format!("CREATE TABLE {target} (LIKE {source});") } else if options.database_type.is_some_and(is_postgres_like_structure_copy) { format!("CREATE TABLE {target} (LIKE {source} INCLUDING ALL);") } else if options.database_type == Some(DatabaseType::SqlServer) { format!("SELECT TOP 0 * INTO {target} FROM {source};") } else if options.database_type.is_some_and(uses_false_predicate_duplicate_structure) { format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 1=0") } else { // `WHERE 1=0` rather than `WHERE 0`: PostgreSQL-family engines (HighGo, Kingbase, // Vastbase, ...) and DuckDB require a boolean in WHERE and reject a bare integer // with "argument of WHERE must be type boolean, not type integer" (#9950). // `1=0` is a valid false predicate in every dialect, including the permissive // MySQL/SQLite-style engines that also accepted `0`. format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 1=0;") }; let mut comment_sql = Vec::new(); if let Some(database_type) = options.database_type.filter(|database_type| supports_duplicate_table_comment(*database_type)) { if let Some(comment) = options.table_comment.as_deref().filter(|comment| !comment.trim().is_empty()) { comment_sql.push(format!( "COMMENT ON TABLE {target} IS {}", quote_duplicate_table_comment(database_type, comment) )); } } if let Some(database_type @ (DatabaseType::Dameng | DatabaseType::Vastbase)) = options.database_type { comment_sql.extend(options.column_comments.iter().filter_map(|column| { if column.comment.trim().is_empty() { return None; } let column_name = quote_table_identifier(options.database_type, &column.name); Some(format!( "COMMENT ON COLUMN {target}.{column_name} IS {}", quote_duplicate_table_comment(database_type, &column.comment) )) })); } // `SELECT ... INTO` copies the IDENTITY property but not constraints, so the cloned table // would silently lose its primary key (t8y2/dbx#8931). Recreate it from the source metadata. let mut constraint_sql = Vec::new(); if options.database_type == Some(DatabaseType::SqlServer) && !options.primary_key_columns.is_empty() { let raw_constraint_name: String = match options.primary_key_constraint_name.as_deref() { Some(name) => name.to_string(), None => format!("PK_{}", options.target_name), }; let constraint_name = quote_table_identifier(options.database_type, &raw_constraint_name); let key_columns = options .primary_key_columns .iter() .map(|column| quote_table_identifier(options.database_type, column)) .collect::>() .join(", "); constraint_sql .push(format!("ALTER TABLE {target} ADD CONSTRAINT {constraint_name} PRIMARY KEY ({key_columns})")); } let mut trailing_sql = constraint_sql; trailing_sql.extend(comment_sql); if trailing_sql.is_empty() { return structure_sql; } format!("{};\n{};", structure_sql.trim_end_matches(';'), trailing_sql.join(";\n")) } pub fn build_copy_table_data_sql(options: CopyTableDataSqlOptions) -> String { let source = qualified_name_with_quote( options.database_type, options.schema.as_deref(), &options.source_name, options.identifier_quote.as_deref(), ); let target = if options.normalize_new_target_name { qualified_duplicate_target_name(options.database_type, options.schema.as_deref(), &options.target_name) } else { qualified_name_with_quote( options.database_type, options.schema.as_deref(), &options.target_name, options.identifier_quote.as_deref(), ) }; let Some(columns) = options.columns.filter(|columns| !columns.is_empty()) else { return format!("INSERT INTO {target} SELECT * FROM {source};"); }; let source_column_list = columns .iter() .map(|column| quote_table_identifier(options.database_type, column)) .collect::>() .join(", "); // The unquoted Oracle clone DDL creates the target columns case-folded while the source // keeps its exact stored spelling, so the INSERT target list must reference the folded // forms and the SELECT list keeps reading the source exactly. let target_column_list = if options.normalize_new_target_name && options.database_type == Some(DatabaseType::Oracle) { columns .iter() .map(|column| crate::table_structure_sql::oracle_new_object_reference(column)) .collect::>() .join(", ") } else { source_column_list.clone() }; let postgres_override = if options.postgres_overriding_system_value && matches!(options.database_type, Some(DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::Kwdb)) { " OVERRIDING SYSTEM VALUE" } else { "" }; let insert_sql = format!( "INSERT INTO {target} ({target_column_list}){postgres_override} SELECT {source_column_list} FROM {source};" ); let needs_identity_insert = (options.sqlserver_identity_insert && options.database_type == Some(DatabaseType::SqlServer)) || (options.dameng_identity_insert && options.database_type == Some(DatabaseType::Dameng)); if needs_identity_insert { return format!("SET IDENTITY_INSERT {target} ON;\n{insert_sql}\nSET IDENTITY_INSERT {target} OFF;"); } insert_sql } pub fn supports_object_rename(database_type: Option, object_type: DatabaseObjectType) -> bool { let Some(database_type) = database_type else { return false; }; if database_type == DatabaseType::SqlServer { return true; } if matches!(object_type, DatabaseObjectType::Procedure | DatabaseObjectType::Function) { return false; } if matches!( database_type, DatabaseType::Sqlite | DatabaseType::Rqlite | DatabaseType::Turso | DatabaseType::CloudflareD1 | DatabaseType::DuckDb ) { return object_type == DatabaseObjectType::Table; } if matches!(database_type, DatabaseType::Mysql | DatabaseType::Goldendb) { return matches!(object_type, DatabaseObjectType::Table | DatabaseObjectType::View); } if is_postgres_like_rename(database_type) || is_oracle_like_rename(database_type) { return matches!( object_type, DatabaseObjectType::Table | DatabaseObjectType::View | DatabaseObjectType::MaterializedView ); } false } pub fn build_rename_object_sql(options: RenameObjectSqlOptions) -> Result { let database_type = options.database_type; if !supports_object_rename(database_type, options.object_type) { return Err(format!( "Renaming {} is not supported for {}.", object_type_keyword(options.object_type), database_label(database_type) )); } if database_type == Some(DatabaseType::SqlServer) { return Ok(format!( "EXEC sp_rename {}, {}, N'OBJECT';", sqlserver_string(&sqlserver_object_name(options.schema.as_deref(), &options.old_name)), sqlserver_string(&options.new_name) )); } if matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Goldendb)) { return Ok(format!( "RENAME TABLE {} TO {};", qualified_name(database_type, options.schema.as_deref(), &options.old_name), qualified_name(database_type, options.schema.as_deref(), &options.new_name) )); } if matches!( database_type, Some( DatabaseType::Sqlite | DatabaseType::Rqlite | DatabaseType::Turso | DatabaseType::CloudflareD1 | DatabaseType::DuckDb ) ) { return Ok(format!( "ALTER TABLE {} RENAME TO {};", qualified_name(database_type, options.schema.as_deref(), &options.old_name), quote_rename_identifier(database_type, &options.new_name) )); } if database_type .is_some_and(|database_type| is_postgres_like_rename(database_type) || is_oracle_like_rename(database_type)) { return Ok(format!( "ALTER {} {} RENAME TO {};", object_type_keyword(options.object_type), qualified_name(database_type, options.schema.as_deref(), &options.old_name), quote_rename_identifier(database_type, &options.new_name) )); } Err(format!( "Renaming {} is not supported for {}.", object_type_keyword(options.object_type), database_label(database_type) )) } pub fn supports_database_rename(database_type: Option) -> bool { matches!( database_type, Some( DatabaseType::Postgres | DatabaseType::Redshift | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase | DatabaseType::OpenGauss ) ) } pub fn build_rename_database_sql( database_type: Option, old_name: &str, new_name: &str, terminate_connections: bool, ) -> Result { if !supports_database_rename(database_type) { return Err(format!("Renaming databases is not supported for {}.", database_label(database_type))); } let mut parts = Vec::new(); if terminate_connections { let escaped = old_name.replace('\'', "''"); parts.push(format!( "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{escaped}' AND pid <> pg_backend_pid();" )); } parts.push(format!( "ALTER DATABASE {} RENAME TO {};", quote_table_identifier(database_type, old_name), quote_rename_identifier(database_type, new_name) )); Ok(parts.join("\n")) } /// Generates a preflight SQL query that checks whether the database can be renamed. /// Returns a single-row result with: /// - active_connections: number of connections to the database (excluding self) /// - prepared_transactions: number of prepared transactions in the database /// - is_owner: whether the current user owns the database pub fn build_rename_database_preflight_sql( database_type: Option, database_name: &str, ) -> Result { if !supports_database_rename(database_type) { return Err(format!("Renaming databases is not supported for {}.", database_label(database_type))); } let escaped = database_name.replace('\'', "''"); Ok(format!( "SELECT (SELECT count(*) FROM pg_stat_activity WHERE datname = '{escaped}' AND pid <> pg_backend_pid()) AS active_connections, (SELECT count(*) FROM pg_prepared_xacts WHERE database = '{escaped}') AS prepared_transactions, (SELECT pg_catalog.pg_get_userbyid(datdba) = current_user AS is_owner FROM pg_database WHERE datname = '{escaped}') AS is_owner;" )) } fn is_postgres_like_rename(database_type: DatabaseType) -> bool { // openGauss 兼容 PG 的 ALTER TABLE/VIEW ... RENAME TO 语法,与 gaussdb 等同开放重命名能力。 matches!( database_type, DatabaseType::Postgres | DatabaseType::Redshift | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::OpenGauss | DatabaseType::Kingbase | DatabaseType::Highgo | DatabaseType::Uxdb | DatabaseType::Vastbase ) } fn is_oracle_like_rename(database_type: DatabaseType) -> bool { // 神通 Oscar 实测支持 `ALTER TABLE old RENAME TO new`(PG 风格,与 Dameng/Oracle 一致)。 // OceanBase Oracle 模式同样接受该语法。 matches!( database_type, DatabaseType::Oracle | DatabaseType::Dameng | DatabaseType::OceanbaseOracle | DatabaseType::Oscar ) } fn is_postgres_like_structure_copy(database_type: DatabaseType) -> bool { matches!( database_type, DatabaseType::Postgres | DatabaseType::Redshift | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::OpenGauss | DatabaseType::Questdb ) } fn supports_duplicate_table_comment(database_type: DatabaseType) -> bool { matches!( database_type, DatabaseType::Postgres | DatabaseType::Redshift | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::OpenGauss | DatabaseType::Dameng | DatabaseType::Vastbase ) } fn uses_false_predicate_duplicate_structure(database_type: DatabaseType) -> bool { matches!(database_type, DatabaseType::Oracle | DatabaseType::Dameng | DatabaseType::Iris) } fn sqlserver_string(value: &str) -> String { format!("N'{}'", value.replace('\'', "''")) } fn quote_rename_identifier(database_type: Option, name: &str) -> String { if matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Goldendb)) { format!("`{}`", name.replace('`', "``")) } else { quote_table_identifier(database_type, name) } } fn qualified_name(database_type: Option, schema: Option<&str>, name: &str) -> String { if matches!(database_type, Some(DatabaseType::Iotdb)) { return qualified_table_name(database_type, schema, name); } if database_type.is_some_and(is_schema_aware) && schema.is_some_and(|schema| !schema.is_empty()) { format!( "{}.{}", quote_rename_identifier(database_type, schema.unwrap()), quote_rename_identifier(database_type, name) ) } else { quote_rename_identifier(database_type, name) } } /// Same as [`qualified_name`], but honours the quote character the connected server reported. /// /// The static per-type mapping cannot describe a database whose quote depends on the connection. /// Cloud Spanner is the case that forces this: GoogleSQL quotes with backticks while its PostgreSQL /// dialect quotes with double quotes, the dialect is fixed when the database is created, and only /// the connected agent knows which one applies. Emitting the static answer produces admin SQL that /// is a syntax error for every PostgreSQL-dialect Spanner database. /// /// Falls back to [`qualified_name`] whenever no connection quote applies, so every other database /// type — and Spanner itself before the agent has reported a quote — keeps its existing output. fn qualified_name_with_quote( database_type: Option, schema: Option<&str>, name: &str, identifier_quote: Option<&str>, ) -> String { if !uses_connection_identifier_quote(database_type, identifier_quote) { return qualified_name(database_type, schema, name); } let quoted = |value: &str| quote_table_data_identifier(database_type, value, identifier_quote); if database_type.is_some_and(is_schema_aware) && schema.is_some_and(|schema| !schema.is_empty()) { format!("{}.{}", quoted(schema.unwrap()), quoted(name)) } else { quoted(name) } } fn qualified_duplicate_target_name(database_type: Option, schema: Option<&str>, name: &str) -> String { // Both duplicate-target normalizations emit the spelling a freshly created clone resolves // to: Oracle's clone DDL creates plain identifiers unquoted (uppercase fold), and Dameng's // clone keeps the same fold convention through its profile. Every other type stays exact. let target = match database_type { Some(DatabaseType::Oracle) => crate::table_structure_sql::oracle_new_object_reference(name), Some(DatabaseType::Dameng) => profile_for(DatabaseType::Dameng).quote_ident(name), _ => return qualified_name(database_type, schema, name), }; if schema.is_some_and(|schema| !schema.is_empty()) { format!("{}.{}", quote_rename_identifier(database_type, schema.unwrap()), target) } else { target } } fn iotdb_timeseries_pattern(path: &str) -> String { let path = path.trim().trim_end_matches(';'); if path.ends_with(".*") || path.ends_with(".**") { path.to_string() } else { format!("{path}.*") } } fn sqlserver_object_name(schema: Option<&str>, name: &str) -> String { schema .filter(|schema| !schema.is_empty()) .map(|schema| format!("{schema}.{name}")) .unwrap_or_else(|| name.to_string()) } fn object_type_keyword(object_type: DatabaseObjectType) -> &'static str { match object_type { DatabaseObjectType::Table => "TABLE", DatabaseObjectType::View => "VIEW", DatabaseObjectType::MaterializedView => "MATERIALIZED VIEW", DatabaseObjectType::Procedure => "PROCEDURE", DatabaseObjectType::Function => "FUNCTION", DatabaseObjectType::Event => "EVENT", } } fn clean_sql_option(value: Option<&str>) -> String { value.unwrap_or("").trim().replace([';', ' ', '\n', '\r', '\t'], "") } fn quote_sql_string(value: &str) -> String { format!("'{}'", value.replace('\'', "''")) } fn quote_duplicate_table_comment(database_type: DatabaseType, value: &str) -> String { // Dameng rejects Postgres E'...' escape strings; keep standard '' escaping // (same as DamengAgent COMMENT ON / COMMENT ON COLUMN generation). if database_type == DatabaseType::Dameng { return quote_sql_string(value); } if !value.contains('\\') && !value.chars().any(|character| character.is_ascii_control()) { return quote_sql_string(value); } let mut escaped = String::with_capacity(value.len()); for character in value.chars() { match character { '\\' => escaped.push_str("\\\\"), '\n' => escaped.push_str("\\n"), '\r' => escaped.push_str("\\r"), '\t' => escaped.push_str("\\t"), '\x08' => escaped.push_str("\\b"), '\x0c' => escaped.push_str("\\f"), '\'' => escaped.push_str("\\'"), character if character.is_ascii_control() => { const HEX: &[u8; 16] = b"0123456789ABCDEF"; let byte = character as u8; escaped.push_str("\\x"); escaped.push(HEX[(byte >> 4) as usize] as char); escaped.push(HEX[(byte & 0x0F) as usize] as char); } character => escaped.push(character), } } let prefix = if database_type == DatabaseType::Redshift { "" } else { "E" }; format!("{prefix}'{escaped}'") } 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) -> String { database_type .and_then(|database_type| serde_json::to_value(database_type).ok()) .and_then(|value| value.as_str().map(str::to_string)) .unwrap_or_else(|| "this database".to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn builds_mysql_create_database_sql_with_charset_and_collation() { assert_eq!( 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;" ); } #[test] fn builds_goldendb_create_database_sql_with_mysql_charset_options() { assert_eq!( 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;" ); } #[test] fn omits_create_database_charset_for_doris_family() { for (database_type, driver_profile) in [ (DatabaseType::Doris, None), (DatabaseType::StarRocks, None), (DatabaseType::Mysql, Some("doris")), (DatabaseType::Mysql, Some("selectdb")), (DatabaseType::Mysql, Some("starrocks")), ] { assert_eq!( build_create_database_sql(CreateDatabaseSqlOptions { database_type: Some(database_type), driver_profile: driver_profile.map(str::to_string), target: None, parent: None, name: "analytics".to_string(), charset: Some("utf8mb4".to_string()), collation: Some("utf8mb4_unicode_ci".to_string()), }) .unwrap(), "CREATE DATABASE `analytics`;" ); } } #[test] fn omits_create_database_charset_for_non_mysql_types() { assert_eq!( 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\";" ); } #[test] fn builds_vastbase_create_database_sql_without_mysql_charset_options() { assert_eq!( 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_informix_family_create_database_without_mysql_clause() { // GBase 8s shares DatabaseType::Gbase with GBase 8a and is only identified by the // gbase8s driver profile; with no locale chosen it emits a bare, unquoted CREATE DATABASE. assert_eq!( build_create_database_sql(CreateDatabaseSqlOptions { database_type: Some(DatabaseType::Gbase), driver_profile: Some("gbase8s".to_string()), target: None, parent: None, name: "app_db".to_string(), charset: None, collation: None, }) .unwrap(), "CREATE DATABASE app_db;" ); // Standalone Informix behaves the same. assert_eq!( build_create_database_sql(CreateDatabaseSqlOptions { database_type: Some(DatabaseType::Informix), driver_profile: None, target: None, parent: None, name: "app_db".to_string(), charset: None, collation: None, }) .unwrap(), "CREATE DATABASE app_db;" ); } #[test] fn informix_family_create_database_carries_locale_directive() { // A chosen charset becomes the new database's DB_LOCALE via a directive the agent honors, // because Informix cannot express a codeset in CREATE DATABASE. assert_eq!( build_create_database_sql(CreateDatabaseSqlOptions { database_type: Some(DatabaseType::Gbase), driver_profile: Some("gbase8s".to_string()), target: None, parent: None, name: "app_db".to_string(), charset: Some("zh_CN.utf8".to_string()), collation: None, }) .unwrap(), "-- DBX_DB_LOCALE=zh_CN.utf8\nCREATE DATABASE app_db;" ); } #[test] fn gbase8a_is_not_a_create_database_target() { assert!(build_create_database_sql(CreateDatabaseSqlOptions { database_type: Some(DatabaseType::Gbase), driver_profile: Some("gbase8a".to_string()), target: None, parent: None, name: "app_db".to_string(), charset: None, collation: None, }) .unwrap_err() .contains("Creating databases is not supported")); } #[test] fn rejects_create_schema_for_informix_effective_dialect() { // The frontend collapses GBase 8s to the Informix dialect for schema DDL; Informix has no // CREATE SCHEMA , so it must be rejected rather than emitting invalid SQL. assert!(build_create_schema_sql(SchemaNameSqlOptions { database_type: Some(DatabaseType::Informix), name: "app".to_string(), }) .unwrap_err() .contains("Creating schemas is not supported")); } #[test] fn create_database_target_distinguishes_gbase_profiles() { assert!(supports_create_database_target(Some(DatabaseType::Gbase), Some("gbase8s"))); assert!(!supports_create_database_target(Some(DatabaseType::Gbase), Some("gbase8a"))); assert!(!supports_create_database_target(Some(DatabaseType::Gbase), None)); assert!(supports_create_database_target(Some(DatabaseType::Informix), None)); } #[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"))); assert!(supports_create_database_charset(Some(DatabaseType::Goldendb), Some("goldendb"))); assert!(!supports_create_database_charset(Some(DatabaseType::Mysql), Some("doris"))); assert!(!supports_create_database_charset(Some(DatabaseType::Doris), None)); assert!(!supports_create_database_charset(Some(DatabaseType::StarRocks), None)); assert!(!supports_create_database_charset(Some(DatabaseType::Postgres), None)); } #[cfg(feature = "duckdb-sidecar")] #[test] fn builds_duckdb_attach_sql() { assert_eq!( build_duckdb_attach_database_sql(DuckDbAttachDatabaseSqlOptions { path: "/Users/me/O'Reilly analytics.duckdb".to_string(), name: "report db".to_string(), }), "ATTACH '/Users/me/O''Reilly analytics.duckdb' AS \"report db\";" ); } #[test] fn builds_sqlite_attach_sql() { assert_eq!( build_sqlite_attach_database_sql(SqliteAttachDatabaseSqlOptions { path: "/Users/me/O'Reilly data.sqlite".to_string(), name: "report db".to_string(), }), "ATTACH DATABASE '/Users/me/O''Reilly data.sqlite' AS \"report db\";" ); } #[test] fn builds_dameng_create_user_sql_with_escaped_values() { assert_eq!( build_create_user_sql("app\"user", "pa'ss", "main\"space"), "CREATE USER \"app\"\"user\" IDENTIFIED BY 'pa''ss' DEFAULT TABLESPACE \"main\"\"space\";" ); } #[test] fn builds_vacuum_table_sql_for_all_options() { let base = VacuumTableSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), table_name: "events".to_string(), full: false, analyze: false, }; assert_eq!(build_vacuum_table_sql(base.clone()).unwrap(), "VACUUM \"public\".\"events\";"); assert_eq!( build_vacuum_table_sql(VacuumTableSqlOptions { analyze: true, ..base.clone() }).unwrap(), "VACUUM ANALYZE \"public\".\"events\";" ); assert_eq!( build_vacuum_table_sql(VacuumTableSqlOptions { full: true, ..base.clone() }).unwrap(), "VACUUM FULL \"public\".\"events\";" ); assert_eq!( build_vacuum_table_sql(VacuumTableSqlOptions { full: true, analyze: true, ..base }).unwrap(), "VACUUM FULL ANALYZE \"public\".\"events\";" ); } #[test] fn vacuum_is_restricted_to_supported_postgres_family_types() { for database_type in [ DatabaseType::Postgres, DatabaseType::Gaussdb, DatabaseType::OpenGauss, DatabaseType::Kingbase, DatabaseType::Vastbase, ] { assert!(build_vacuum_table_sql(VacuumTableSqlOptions { database_type: Some(database_type), schema: None, table_name: "events".to_string(), full: false, analyze: false, }) .is_ok()); } assert!(build_vacuum_table_sql(VacuumTableSqlOptions { database_type: Some(DatabaseType::Mysql), schema: None, table_name: "events".to_string(), full: false, analyze: false, }) .is_err()); } #[test] fn builds_drop_and_clear_table_sql() { let options = TableAdminSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), table_name: "events".to_string(), cascade: None, identifier_quote: None, }; assert_eq!(build_drop_table_sql(options.clone()), "DROP TABLE \"public\".\"events\";"); assert_eq!( build_drop_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), table_name: "events".to_string(), cascade: Some(true), identifier_quote: None, }), "DROP TABLE \"public\".\"events\" CASCADE;" ); assert_eq!( build_drop_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Mysql), schema: None, table_name: "events".to_string(), cascade: Some(true), identifier_quote: None, }), "DROP TABLE `events`;" ); assert_eq!(build_empty_table_sql(options.clone()), "DELETE FROM \"public\".\"events\";"); assert_eq!(build_truncate_table_sql(options.clone()), "TRUNCATE TABLE \"public\".\"events\";"); assert_eq!( build_truncate_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), table_name: "events".to_string(), cascade: Some(true), identifier_quote: None, }), "TRUNCATE TABLE \"public\".\"events\" CASCADE;" ); assert_eq!( build_truncate_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Mysql), schema: None, table_name: "events".to_string(), cascade: Some(true), identifier_quote: None, }), "TRUNCATE TABLE `events`;" ); assert_eq!( build_empty_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::ClickHouse), schema: None, table_name: "PresetSubjectInfo".to_string(), cascade: None, identifier_quote: None, }), "ALTER TABLE `PresetSubjectInfo` DELETE WHERE 1 = 1;" ); assert_eq!( build_truncate_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::ClickHouse), schema: None, table_name: "PresetSubjectInfo".to_string(), cascade: None, identifier_quote: None, }), "TRUNCATE TABLE `PresetSubjectInfo`;" ); assert_eq!( build_empty_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Bigquery), schema: None, table_name: "events".to_string(), cascade: None, identifier_quote: None, }), "DELETE FROM `events` WHERE TRUE;" ); // Spanner rejects both a WHERE-less DELETE and TRUNCATE TABLE (`Unknown statement`), // so unlike BigQuery it must not fall through to the TRUNCATE default. assert_eq!( build_empty_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Spanner), schema: None, table_name: "events".to_string(), cascade: None, identifier_quote: None, }), "DELETE FROM `events` WHERE TRUE;" ); assert_eq!( build_truncate_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Spanner), schema: None, table_name: "events".to_string(), cascade: None, identifier_quote: None, }), "DELETE FROM `events` WHERE TRUE;" ); // A `public` schema means the PostgreSQL dialect, where backticks are a syntax error, so the // quote has to come from the connection rather than from the static per-type mapping. assert_eq!( build_truncate_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Spanner), schema: Some("public".to_string()), table_name: "events".to_string(), cascade: Some(true), identifier_quote: Some("\"".to_string()), }), "DELETE FROM \"public\".\"events\" WHERE TRUE;" ); assert_eq!( build_empty_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Spanner), schema: Some("public".to_string()), table_name: "events".to_string(), cascade: None, identifier_quote: Some("\"".to_string()), }), "DELETE FROM \"public\".\"events\" WHERE TRUE;" ); // GoogleSQL reports a backtick, which matches the static mapping. assert_eq!( build_truncate_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Spanner), schema: None, table_name: "OrderItems".to_string(), cascade: None, identifier_quote: Some("`".to_string()), }), "DELETE FROM `OrderItems` WHERE TRUE;" ); // Before the agent has reported a quote the static GoogleSQL answer is the only one // available; this documents the fallback rather than a PostgreSQL-dialect expectation. assert_eq!( build_truncate_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Spanner), schema: Some("public".to_string()), table_name: "events".to_string(), cascade: None, identifier_quote: None, }), "DELETE FROM `public`.`events` WHERE TRUE;" ); assert_eq!( build_empty_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Cassandra), schema: None, table_name: "events".to_string(), cascade: None, identifier_quote: None, }), "TRUNCATE TABLE \"events\";" ); assert_eq!( build_truncate_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::DuckDb), schema: None, table_name: "events".to_string(), cascade: None, identifier_quote: None, }), "DELETE FROM \"events\";" ); assert_eq!( build_drop_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Iotdb), schema: Some("root.test".to_string()), table_name: "DCU_101".to_string(), cascade: None, identifier_quote: None, }), "DELETE TIMESERIES root.test.DCU_101.*;" ); assert_eq!( build_empty_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Iotdb), schema: Some("root.test".to_string()), table_name: "root.test.DCU_101".to_string(), cascade: None, identifier_quote: None, }), "DELETE FROM root.test.DCU_101.*;" ); assert_eq!( build_truncate_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Iotdb), schema: Some("root.test".to_string()), table_name: "DCU_101".to_string(), cascade: None, identifier_quote: None, }), "DELETE FROM root.test.DCU_101.*;" ); assert_eq!( build_empty_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Questdb), schema: None, table_name: "table_sample".to_string(), cascade: None, identifier_quote: None, }), "TRUNCATE TABLE `table_sample`;" ); assert_eq!( build_truncate_table_sql(TableAdminSqlOptions { database_type: Some(DatabaseType::Questdb), schema: None, table_name: "table_sample".to_string(), cascade: None, identifier_quote: None, }), "TRUNCATE TABLE `table_sample`;" ); // StarRocks and Doris require a WHERE clause on DELETE (StarRocks analyzer: // "Where clause is not set"), so clearing a table must emit the same // TRUNCATE TABLE statement the truncate action already produces. for database_type in [DatabaseType::Doris, DatabaseType::StarRocks] { assert_eq!( build_empty_table_sql(TableAdminSqlOptions { database_type: Some(database_type), schema: None, table_name: "events".to_string(), cascade: None, identifier_quote: None, }), "TRUNCATE TABLE `events`;" ); } } /// `build_drop_table_sql` renders `DdlDialectProfile::drop_table_template`, so the six /// profile families plus DuckDB (which resolves to `conservative_ansi`, not the SQLite /// family) must all produce a valid statement. #[test] fn builds_drop_table_sql_per_profile_family() { let drop_sql = |database_type: DatabaseType, schema: Option<&str>, cascade: Option| { build_drop_table_sql(TableAdminSqlOptions { database_type: Some(database_type), schema: schema.map(str::to_string), table_name: "events".to_string(), cascade, identifier_quote: None, }) }; assert_eq!(drop_sql(DatabaseType::Mysql, None, None), "DROP TABLE `events`;"); assert_eq!(drop_sql(DatabaseType::Postgres, Some("public"), None), "DROP TABLE \"public\".\"events\";"); assert_eq!(drop_sql(DatabaseType::Oracle, Some("APP"), None), "DROP TABLE \"APP\".\"events\";"); assert_eq!(drop_sql(DatabaseType::SqlServer, Some("dbo"), None), "DROP TABLE [dbo].[events];"); assert_eq!(drop_sql(DatabaseType::Sqlite, None, None), "DROP TABLE \"events\";"); assert_eq!(drop_sql(DatabaseType::DuckDb, None, None), "DROP TABLE \"events\";"); // No database type: the ANSI shape, with the default double-quote from // `qualified_name_with_quote`, and no CASCADE. assert_eq!( build_drop_table_sql(TableAdminSqlOptions { database_type: None, schema: None, table_name: "events".to_string(), cascade: Some(true), identifier_quote: None, }), "DROP TABLE \"events\";" ); // CASCADE stays limited to the PostgreSQL-family profiles. Oracle spells it // `CASCADE CONSTRAINTS` and is deliberately excluded; Firebird, Vertica and Exasol // share the PostgreSQL profile but opt out. assert_eq!( drop_sql(DatabaseType::Postgres, Some("public"), Some(true)), "DROP TABLE \"public\".\"events\" CASCADE;" ); assert_eq!(drop_sql(DatabaseType::OpenGauss, None, Some(true)), "DROP TABLE \"events\" CASCADE;"); assert_eq!(drop_sql(DatabaseType::Firebird, None, Some(true)), "DROP TABLE \"events\";"); assert_eq!(drop_sql(DatabaseType::Vertica, None, Some(true)), "DROP TABLE \"events\";"); assert_eq!(drop_sql(DatabaseType::Oracle, None, Some(true)), "DROP TABLE \"events\";"); assert_eq!(drop_sql(DatabaseType::DuckDb, None, Some(true)), "DROP TABLE \"events\";"); // InfluxDB drops a measurement; the profile template carries the whole shape. assert_eq!(drop_sql(DatabaseType::InfluxDb, None, None), "DROP MEASUREMENT \"events\";"); } #[test] fn builds_mysql_auto_increment_sql_and_rejects_invalid_values() { for value in ["1", "9007199254740993", "18446744073709551615"] { assert_eq!( build_mysql_auto_increment_sql(MysqlAutoIncrementSqlOptions { database_type: Some(DatabaseType::Mysql), driver_profile: Some("mysql".to_string()), schema: Some("sales`archive".to_string()), table_name: "order`items".to_string(), value: value.to_string(), }), Ok(format!("ALTER TABLE `sales``archive`.`order``items` AUTO_INCREMENT = {value};")) ); } for value in ["", "0", "01", " 1", "1 ", "+1", "-1", "1.0", "1e3", "1; DROP TABLE users", "18446744073709551616"] { assert!( build_mysql_auto_increment_sql(MysqlAutoIncrementSqlOptions { database_type: Some(DatabaseType::Mysql), driver_profile: None, schema: None, table_name: "events".to_string(), value: value.to_string(), }) .is_err(), "value {value:?} should be rejected" ); } for (database_type, driver_profile) in [ (Some(DatabaseType::Postgres), None), (Some(DatabaseType::Jdbc), Some("mysql".to_string())), (Some(DatabaseType::Mysql), Some("mariadb".to_string())), (Some(DatabaseType::Mysql), Some("tidb".to_string())), (Some(DatabaseType::Mysql), Some("oceanbase".to_string())), (Some(DatabaseType::Mysql), Some("dolt".to_string())), (Some(DatabaseType::Goldendb), Some("goldendb".to_string())), (Some(DatabaseType::Doris), Some("selectdb".to_string())), (Some(DatabaseType::StarRocks), Some("starrocks".to_string())), ] { assert!(build_mysql_auto_increment_sql(MysqlAutoIncrementSqlOptions { database_type, driver_profile, schema: None, table_name: "events".to_string(), value: "1".to_string(), }) .is_err()); } } #[test] fn builds_drop_object_database_and_schema_sql() { assert_eq!( build_drop_object_sql(DropObjectSqlOptions { database_type: Some(DatabaseType::SqlServer), object_type: DatabaseObjectType::Procedure, schema: Some("dbo".to_string()), name: "refresh_cache".to_string(), signature: None, identifier_quote: None, }), "DROP PROCEDURE [dbo].[refresh_cache];" ); assert_eq!( build_drop_object_sql(DropObjectSqlOptions { database_type: Some(DatabaseType::Postgres), object_type: DatabaseObjectType::Function, schema: Some("public".to_string()), name: "calc".to_string(), signature: Some("integer, integer".to_string()), identifier_quote: None, }), "DROP FUNCTION \"public\".\"calc\"(integer, integer);" ); assert_eq!( build_drop_database_sql(DatabaseNameSqlOptions { database_type: Some(DatabaseType::Mysql), name: "app db".to_string(), }), "DROP DATABASE `app db`;" ); assert_eq!( 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::Dameng), 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), name: "analytics".to_string(), }), "DROP SCHEMA \"analytics\" CASCADE;" ); assert_eq!( build_drop_schema_sql(SchemaNameSqlOptions { database_type: Some(DatabaseType::Dameng), name: "analytics".to_string(), }), "DROP SCHEMA \"analytics\" CASCADE;" ); assert_eq!( build_drop_schema_sql(SchemaNameSqlOptions { database_type: Some(DatabaseType::Postgres), name: "analytics".to_string(), }), "DROP SCHEMA \"analytics\" CASCADE;" ); assert_eq!( build_drop_schema_sql(SchemaNameSqlOptions { database_type: Some(DatabaseType::Mysql), name: "analytics".to_string(), }), "DROP SCHEMA `analytics`;" ); assert_eq!( build_drop_schema_sql(SchemaNameSqlOptions { database_type: Some(DatabaseType::OceanbaseOracle), name: "analytics".to_string(), }), "DROP USER \"analytics\" CASCADE;" ); assert_eq!( build_drop_schema_sql(SchemaNameSqlOptions { database_type: Some(DatabaseType::OceanbaseOracle), name: "ana\"lytics".to_string(), }), "DROP USER \"ana\"\"lytics\" CASCADE;" ); } #[test] fn builds_drop_table_child_object_sql() { assert_eq!( build_drop_table_child_object_sql(DropTableChildObjectSqlOptions { database_type: Some(DatabaseType::Postgres), object_type: TableChildObjectType::Column, schema: Some("public".to_string()), table_name: "orders".to_string(), name: "status".to_string(), }) .unwrap(), "ALTER TABLE \"public\".\"orders\" DROP COLUMN \"status\";" ); assert_eq!( build_drop_table_child_object_sql(DropTableChildObjectSqlOptions { database_type: Some(DatabaseType::Mysql), object_type: TableChildObjectType::Index, schema: None, table_name: "orders".to_string(), name: "idx_orders_status".to_string(), }) .unwrap(), "DROP INDEX `idx_orders_status` ON `orders`;" ); assert_eq!( build_drop_table_child_object_sql(DropTableChildObjectSqlOptions { database_type: Some(DatabaseType::Postgres), object_type: TableChildObjectType::Index, schema: Some("public".to_string()), table_name: "orders".to_string(), name: "idx_orders_status".to_string(), }) .unwrap(), "DROP INDEX \"public\".\"idx_orders_status\";" ); assert_eq!( build_drop_table_child_object_sql(DropTableChildObjectSqlOptions { database_type: Some(DatabaseType::Sqlite), object_type: TableChildObjectType::Index, schema: Some("analytics".to_string()), table_name: "orders".to_string(), name: "idx_orders_status".to_string(), }) .unwrap(), "DROP INDEX \"analytics\".\"idx_orders_status\";" ); assert_eq!( build_drop_table_child_object_sql(DropTableChildObjectSqlOptions { database_type: Some(DatabaseType::Mysql), object_type: TableChildObjectType::ForeignKey, schema: None, table_name: "orders".to_string(), name: "fk_orders_user".to_string(), }) .unwrap(), "ALTER TABLE `orders` DROP FOREIGN KEY `fk_orders_user`;" ); assert_eq!( build_drop_table_child_object_sql(DropTableChildObjectSqlOptions { database_type: Some(DatabaseType::SqlServer), object_type: TableChildObjectType::ForeignKey, schema: Some("dbo".to_string()), table_name: "orders".to_string(), name: "fk_orders_user".to_string(), }) .unwrap(), "ALTER TABLE [dbo].[orders] DROP CONSTRAINT [fk_orders_user];" ); assert_eq!( build_drop_table_child_object_sql(DropTableChildObjectSqlOptions { database_type: Some(DatabaseType::Postgres), object_type: TableChildObjectType::Trigger, schema: Some("public".to_string()), table_name: "orders".to_string(), name: "orders_audit".to_string(), }) .unwrap(), "DROP TRIGGER \"orders_audit\" ON \"public\".\"orders\";" ); assert_eq!( build_drop_table_child_object_sql(DropTableChildObjectSqlOptions { database_type: Some(DatabaseType::Questdb), object_type: TableChildObjectType::Column, schema: Some("public".to_string()), table_name: "orders".to_string(), name: "status".to_string(), }) .unwrap(), "ALTER TABLE `orders` DROP COLUMN `status`;" ); } #[test] fn duplicate_table_structure_vastbase_preserves_table_and_column_comments() { let sql = build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Vastbase), schema: Some("业\"务".to_string()), source_name: "订\"单".to_string(), target_name: "订\"单_副本".to_string(), table_comment: Some(" 客户's;订单 ".to_string()), column_comments: vec![ DuplicateTableColumnComment { name: "备\"注".to_string(), comment: "用户's;备注".to_string() }, DuplicateTableColumnComment { name: "路径".to_string(), comment: "C:\\订单\n明细\t'".to_string() }, DuplicateTableColumnComment { name: "空".to_string(), comment: String::new() }, DuplicateTableColumnComment { name: "空白".to_string(), comment: " \t\n ".to_string() }, ], primary_key_columns: vec!["编号".to_string()], primary_key_constraint_name: Some("PK_副本".to_string()), identifier_quote: Some("\"".to_string()), }); let expected_statements = vec![ "CREATE TABLE \"业\"\"务\".\"订\"\"单_副本\" AS SELECT * FROM \"业\"\"务\".\"订\"\"单\" WHERE 1=0", "COMMENT ON TABLE \"业\"\"务\".\"订\"\"单_副本\" IS ' 客户''s;订单 '", "COMMENT ON COLUMN \"业\"\"务\".\"订\"\"单_副本\".\"备\"\"注\" IS '用户''s;备注'", "COMMENT ON COLUMN \"业\"\"务\".\"订\"\"单_副本\".\"路径\" IS E'C:\\\\订单\\n明细\\t\\''", ]; assert_eq!(sql, format!("{};", expected_statements.join(";\n"))); assert_eq!(crate::sql::split_sql_statements_for_database(&sql, DatabaseType::Vastbase), expected_statements); } #[test] fn duplicate_table_structure_vastbase_comments_are_independent_and_optional() { for table_comment in [None, Some(""), Some(" \t\n "), Some("表注释"), Some("路径\\'\n归档")] { for column_comment in [None, Some(""), Some(" \t\n "), Some("字段注释")] { for schema in [None, Some("")] { let sql = build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Vastbase), schema: schema.map(str::to_string), source_name: "source".to_string(), target_name: "copy".to_string(), table_comment: table_comment.map(str::to_string), column_comments: column_comment .map(|comment| DuplicateTableColumnComment { name: "note".to_string(), comment: comment.to_string(), }) .into_iter() .collect(), primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }); let mut expected = "CREATE TABLE \"copy\" AS SELECT * FROM \"source\" WHERE 1=0;".to_string(); match table_comment { Some("表注释") => expected.push_str("\nCOMMENT ON TABLE \"copy\" IS '表注释';"), Some("路径\\'\n归档") => { expected.push_str("\nCOMMENT ON TABLE \"copy\" IS E'路径\\\\\\'\\n归档';") } _ => {} } if column_comment == Some("字段注释") { expected.push_str("\nCOMMENT ON COLUMN \"copy\".\"note\" IS '字段注释';"); } assert_eq!(sql, expected); } } } } #[test] fn duplicate_table_structure_comments_keep_other_dialects_unchanged() { for (database_type, expected) in [ ( Some(DatabaseType::Postgres), "CREATE TABLE \"copy\" (LIKE \"source\" INCLUDING ALL);\nCOMMENT ON TABLE \"copy\" IS '表注释';", ), (Some(DatabaseType::Mysql), "CREATE TABLE `copy` LIKE `source`;"), (Some(DatabaseType::Highgo), "CREATE TABLE \"copy\" AS SELECT * FROM \"source\" WHERE 1=0;"), (Some(DatabaseType::Kingbase), "CREATE TABLE \"copy\" AS SELECT * FROM \"source\" WHERE 1=0;"), (Some(DatabaseType::DuckDb), "CREATE TABLE \"copy\" AS SELECT * FROM \"source\" WHERE 1=0;"), (None, "CREATE TABLE \"copy\" AS SELECT * FROM \"source\" WHERE 1=0;"), ] { assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type, schema: None, source_name: "source".to_string(), target_name: "copy".to_string(), table_comment: Some("表注释".to_string()), column_comments: vec![DuplicateTableColumnComment { name: "note".to_string(), comment: "字段注释".to_string(), }], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), expected, "{database_type:?}" ); } } #[test] fn copy_table_data_vastbase_keeps_insert_and_target_quoting() { for normalize_new_target_name in [false, true] { for columns in [None, Some(vec![]), Some(vec!["编\"号".to_string(), "备注".to_string()])] { let expected = if columns.as_ref().is_some_and(|columns| !columns.is_empty()) { "INSERT INTO \"业\"\"务\".\"订\"\"单_副本\" (\"编\"\"号\", \"备注\") SELECT \"编\"\"号\", \"备注\" FROM \"业\"\"务\".\"订\"\"单\";" } else { "INSERT INTO \"业\"\"务\".\"订\"\"单_副本\" SELECT * FROM \"业\"\"务\".\"订\"\"单\";" }; assert_eq!( build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::Vastbase), schema: Some("业\"务".to_string()), source_name: "订\"单".to_string(), target_name: "订\"单_副本".to_string(), columns, postgres_overriding_system_value: false, sqlserver_identity_insert: false, dameng_identity_insert: false, normalize_new_target_name, identifier_quote: Some("\"".to_string()), }), expected ); } } } #[test] fn duplicate_table_structure_uses_boolean_false_predicate_for_pg_family_fallbacks() { // Regression for #9950: the generic fallback used `WHERE 0`. PostgreSQL-family // engines require a boolean there, so cloning a HighGo/Kingbase/Vastbase table // failed with "argument of WHERE must be type boolean, not type integer". for database_type in [ DatabaseType::Highgo, DatabaseType::Kingbase, DatabaseType::Vastbase, DatabaseType::DuckDb, DatabaseType::Sqlite, ] { assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(database_type), schema: Some("public".to_string()), source_name: "users".to_string(), target_name: "users_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE \"public\".\"users_copy\" AS SELECT * FROM \"public\".\"users\" WHERE 1=0;", "{database_type:?}" ); } // MySQL keeps the LIKE form, so the shared fallback must not have swallowed it. assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Mysql), schema: None, source_name: "users".to_string(), target_name: "users_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE `users_copy` LIKE `users`;" ); } #[test] fn builds_duplicate_table_structure_sql() { assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Mysql), schema: None, source_name: "users".to_string(), target_name: "users_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE `users_copy` LIKE `users`;" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), source_name: "users".to_string(), target_name: "users_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE \"public\".\"users_copy\" (LIKE \"public\".\"users\" INCLUDING ALL);" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Impala), schema: Some("dbx_demo".to_string()), source_name: "connection_test".to_string(), target_name: "connection_test_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE `dbx_demo`.`connection_test_copy` LIKE `dbx_demo`.`connection_test`;" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Kyuubi), schema: Some("dbx_demo".to_string()), source_name: "orders".to_string(), target_name: "orders_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE `dbx_demo`.`orders_copy` LIKE `dbx_demo`.`orders`;" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), source_name: "customer_orders".to_string(), target_name: "customer_orders_copy".to_string(), table_comment: Some(" Customer's orders; archive ".to_string()), column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE \"public\".\"customer_orders_copy\" (LIKE \"public\".\"customer_orders\" INCLUDING ALL);\nCOMMENT ON TABLE \"public\".\"customer_orders_copy\" IS ' Customer''s orders; archive ';" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Kwdb), schema: Some("public".to_string()), source_name: "users".to_string(), target_name: "users_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE \"public\".\"users_copy\" (LIKE \"public\".\"users\" INCLUDING ALL);" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::SqlServer), schema: Some("dbo".to_string()), source_name: "users".to_string(), target_name: "users_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "SELECT TOP 0 * INTO [dbo].[users_copy] FROM [dbo].[users];" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Oracle), schema: Some("HR".to_string()), source_name: "USERS".to_string(), target_name: "USERS_COPY".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE \"HR\".USERS_COPY AS SELECT * FROM \"HR\".\"USERS\" WHERE 1=0" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::SqlServer), schema: Some("dbo".to_string()), source_name: "users".to_string(), target_name: "users_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "SELECT TOP 0 * INTO [dbo].[users_copy] FROM [dbo].[users];" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::SqlServer), schema: None, source_name: "users".to_string(), target_name: "users_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec!["id".to_string(), "seq no".to_string()], primary_key_constraint_name: None, identifier_quote: None, }), "SELECT TOP 0 * INTO [users_copy] FROM [users];\nALTER TABLE [users_copy] ADD CONSTRAINT [PK_users_copy] PRIMARY KEY ([id], [seq no]);" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::SqlServer), schema: None, source_name: "users".to_string(), target_name: "users_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec!["id".to_string()], primary_key_constraint_name: Some("PK_users_copy_2".to_string()), identifier_quote: None, }), "SELECT TOP 0 * INTO [users_copy] FROM [users];\nALTER TABLE [users_copy] ADD CONSTRAINT [PK_users_copy_2] PRIMARY KEY ([id]);" ); let dameng_sql = build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Dameng), schema: Some("APP".to_string()), source_name: "USERS".to_string(), target_name: "users_copy".to_string(), table_comment: Some("测试'克隆".to_string()), column_comments: vec![ DuplicateTableColumnComment { name: "DISPLAY\"NAME".to_string(), comment: " Owner's; display name".to_string(), }, DuplicateTableColumnComment { name: "STATUS".to_string(), comment: "active ".to_string() }, DuplicateTableColumnComment { name: "EMPTY".to_string(), comment: " \t\n".to_string() }, ], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }); assert_eq!( dameng_sql, "CREATE TABLE \"APP\".USERS_COPY AS SELECT * FROM \"APP\".\"USERS\" WHERE 1=0;\nCOMMENT ON TABLE \"APP\".USERS_COPY IS '测试''克隆';\nCOMMENT ON COLUMN \"APP\".USERS_COPY.\"DISPLAY\"\"NAME\" IS ' Owner''s; display name';\nCOMMENT ON COLUMN \"APP\".USERS_COPY.\"STATUS\" IS 'active ';" ); assert_eq!( crate::sql::split_sql_statements_for_database(&dameng_sql, DatabaseType::Dameng), vec![ "CREATE TABLE \"APP\".USERS_COPY AS SELECT * FROM \"APP\".\"USERS\" WHERE 1=0".to_string(), "COMMENT ON TABLE \"APP\".USERS_COPY IS '测试''克隆'".to_string(), "COMMENT ON COLUMN \"APP\".USERS_COPY.\"DISPLAY\"\"NAME\" IS ' Owner''s; display name'".to_string(), "COMMENT ON COLUMN \"APP\".USERS_COPY.\"STATUS\" IS 'active '".to_string(), ] ); // Control chars / backslashes must not switch Dameng to Postgres E'...' literals. let dameng_escape_sql = build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Dameng), schema: Some("APP".to_string()), source_name: "USERS".to_string(), target_name: "users_copy".to_string(), table_comment: Some("line1\\path\nline2".to_string()), column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }); assert_eq!( dameng_escape_sql, "CREATE TABLE \"APP\".USERS_COPY AS SELECT * FROM \"APP\".\"USERS\" WHERE 1=0;\nCOMMENT ON TABLE \"APP\".USERS_COPY IS 'line1\\path\nline2';" ); assert!(!dameng_escape_sql.contains("E'")); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Dameng), schema: Some("APP".to_string()), source_name: "USERS".to_string(), target_name: "UsersCopy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE \"APP\".\"UsersCopy\" AS SELECT * FROM \"APP\".\"USERS\" WHERE 1=0" ); for database_type in [ DatabaseType::Postgres, DatabaseType::Redshift, DatabaseType::Gaussdb, DatabaseType::Kwdb, DatabaseType::OpenGauss, ] { let sql = build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(database_type), schema: Some("public".to_string()), source_name: "source".to_string(), target_name: "copy".to_string(), table_comment: Some("owner\\'s; archive".to_string()), column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }); let expected_literal = if database_type == DatabaseType::Redshift { "'owner\\\\\\'s; archive'" } else { "E'owner\\\\\\'s; archive'" }; assert!(sql.ends_with(&format!("COMMENT ON TABLE \"public\".\"copy\" IS {expected_literal};"))); assert_eq!( crate::sql::split_sql_statements_for_database(&sql, database_type), vec![ "CREATE TABLE \"public\".\"copy\" (LIKE \"public\".\"source\" INCLUDING ALL)".to_string(), format!("COMMENT ON TABLE \"public\".\"copy\" IS {expected_literal}"), ] ); } assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Iris), schema: Some("SQLUSER".to_string()), source_name: "tb_a".to_string(), target_name: "tb_a_copy".to_string(), table_comment: None, column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE \"SQLUSER\".\"tb_a_copy\" AS SELECT * FROM \"SQLUSER\".\"tb_a\" WHERE 1=0" ); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Questdb), schema: None, source_name: "users".to_string(), target_name: "users_copy".to_string(), table_comment: Some("ignored by QuestDB".to_string()), column_comments: vec![], primary_key_columns: vec![], primary_key_constraint_name: None, identifier_quote: None, }), "CREATE TABLE `users_copy` (LIKE `users`);" ); } #[test] fn builds_copy_table_data_sql() { assert_eq!( build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::Sqlite), schema: None, source_name: "users".to_string(), target_name: "users_copy".to_string(), columns: None, postgres_overriding_system_value: false, sqlserver_identity_insert: false, dameng_identity_insert: false, normalize_new_target_name: false, identifier_quote: None, }), "INSERT INTO \"users_copy\" SELECT * FROM \"users\";" ); assert_eq!( build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::Mysql), schema: None, source_name: "users".to_string(), target_name: "users_copy".to_string(), columns: Some(vec!["id".to_string(), "name".to_string()]), postgres_overriding_system_value: false, sqlserver_identity_insert: false, dameng_identity_insert: false, normalize_new_target_name: false, identifier_quote: None, }), "INSERT INTO `users_copy` (`id`, `name`) SELECT `id`, `name` FROM `users`;" ); assert_eq!( build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::Postgres), schema: Some("public".to_string()), source_name: "users".to_string(), target_name: "users_copy".to_string(), columns: Some(vec!["id".to_string(), "name".to_string()]), postgres_overriding_system_value: true, sqlserver_identity_insert: false, dameng_identity_insert: false, normalize_new_target_name: false, identifier_quote: None, }), "INSERT INTO \"public\".\"users_copy\" (\"id\", \"name\") OVERRIDING SYSTEM VALUE SELECT \"id\", \"name\" FROM \"public\".\"users\";" ); assert_eq!( build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::SqlServer), schema: Some("dbo".to_string()), source_name: "users".to_string(), target_name: "users_copy".to_string(), columns: Some(vec!["id".to_string(), "name".to_string()]), postgres_overriding_system_value: false, sqlserver_identity_insert: true, dameng_identity_insert: false, normalize_new_target_name: false, identifier_quote: None, }), "SET IDENTITY_INSERT [dbo].[users_copy] ON;\nINSERT INTO [dbo].[users_copy] ([id], [name]) SELECT [id], [name] FROM [dbo].[users];\nSET IDENTITY_INSERT [dbo].[users_copy] OFF;" ); { let dameng = build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::Dameng), schema: Some("DCSS".to_string()), source_name: "users".to_string(), target_name: "users_copy".to_string(), columns: Some(vec!["id".to_string(), "name".to_string()]), postgres_overriding_system_value: false, sqlserver_identity_insert: false, dameng_identity_insert: true, normalize_new_target_name: false, identifier_quote: None, }); assert!(dameng.starts_with("SET IDENTITY_INSERT "), "dameng copy should enable identity insert: {dameng}"); assert!(dameng.ends_with("OFF;"), "dameng copy should disable identity insert: {dameng}"); assert!( dameng.contains("INSERT INTO ") && dameng.contains(" SELECT "), "dameng copy should carry an INSERT..SELECT: {dameng}" ); // Dameng rejects assigning an identity column unless a column list is specified. assert!( dameng.contains("(\"id\", \"name\")") || dameng.contains("(`id`, `name`)") || dameng.contains("(id, name)") || dameng.contains("([id], [name])"), "dameng copy must use an explicit column list: {dameng}" ); } assert_eq!( build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::Dameng), schema: Some("APP".to_string()), source_name: "users".to_string(), target_name: "users_copy".to_string(), columns: None, postgres_overriding_system_value: false, sqlserver_identity_insert: false, dameng_identity_insert: false, normalize_new_target_name: true, identifier_quote: None, }), "INSERT INTO \"APP\".USERS_COPY SELECT * FROM \"APP\".\"users\";" ); assert_eq!( build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::Dameng), schema: Some("APP".to_string()), source_name: "users".to_string(), target_name: "users_copy".to_string(), columns: None, postgres_overriding_system_value: false, sqlserver_identity_insert: false, dameng_identity_insert: false, normalize_new_target_name: false, identifier_quote: None, }), "INSERT INTO \"APP\".\"users_copy\" SELECT * FROM \"APP\".\"users\";" ); assert_eq!( build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::Oracle), schema: Some("APP".to_string()), source_name: "orders".to_string(), target_name: "orders_copy".to_string(), columns: Some(vec!["user_id".to_string(), "userName".to_string(), "order total".to_string()]), postgres_overriding_system_value: false, sqlserver_identity_insert: false, dameng_identity_insert: false, normalize_new_target_name: true, identifier_quote: None, }), "INSERT INTO \"APP\".orders_copy (user_id, userName, \"order total\") SELECT \"user_id\", \"userName\", \"order total\" FROM \"APP\".\"orders\";" ); assert_eq!( build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::Oracle), schema: Some("APP".to_string()), source_name: "orders".to_string(), target_name: "orders_copy".to_string(), columns: Some(vec!["user_id".to_string()]), postgres_overriding_system_value: false, sqlserver_identity_insert: false, dameng_identity_insert: false, normalize_new_target_name: false, identifier_quote: None, }), "INSERT INTO \"APP\".\"orders_copy\" (\"user_id\") SELECT \"user_id\" FROM \"APP\".\"orders\";" ); } #[test] fn oracle_clone_data_copy_matches_unquoted_create_identifiers() { // Clone-with-data regression: a lowercase user-typed target and quoted-lowercase source // columns. The clone DDL creates plain identifiers unquoted (Oracle stores them folded), // so the data-copy INSERT must reference the created spellings while the SELECT keeps // the exact quoted spelling of the existing source. let column = |name: &str| crate::table_structure_sql::EditableStructureColumn { id: name.to_string(), name: name.to_string(), data_type: "NUMBER".to_string(), is_nullable: true, default_value: String::new(), comment: String::new(), is_primary_key: false, extra: None, original: None, original_position: None, marked_for_drop: false, character_set: String::new(), collation: String::new(), }; let create = crate::table_structure_sql::build_create_table_sql(crate::table_structure_sql::TableStructureSqlOptions { database_type: Some(DatabaseType::Oracle), driver_profile: None, schema: Some("APP".to_string()), table_name: "orders_copy".to_string(), columns: vec![column("user_id"), column("userName")], indexes: Vec::new(), foreign_keys: Vec::new(), triggers: Vec::new(), table_comment: None, original_table_comment: None, mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, table_collation: None, }); assert_eq!(create.warnings, Vec::::new()); assert_eq!( create.statements[0], "CREATE TABLE \"APP\".orders_copy (\n user_id NUMBER,\n userName NUMBER\n);" ); let copy = build_copy_table_data_sql(CopyTableDataSqlOptions { database_type: Some(DatabaseType::Oracle), schema: Some("APP".to_string()), source_name: "orders".to_string(), target_name: "orders_copy".to_string(), columns: Some(vec!["user_id".to_string(), "userName".to_string()]), postgres_overriding_system_value: false, sqlserver_identity_insert: false, dameng_identity_insert: false, normalize_new_target_name: true, identifier_quote: None, }); assert_eq!( copy, "INSERT INTO \"APP\".orders_copy (user_id, userName) SELECT \"user_id\", \"userName\" FROM \"APP\".\"orders\";" ); } #[test] fn builds_mysql_table_and_view_rename_sql() { assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::Mysql), object_type: DatabaseObjectType::Table, schema: None, old_name: "users".to_string(), new_name: "app users".to_string(), }) .unwrap(), "RENAME TABLE `users` TO `app users`;" ); assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::Goldendb), object_type: DatabaseObjectType::View, schema: None, old_name: "active_users".to_string(), new_name: "enabled_users".to_string(), }) .unwrap(), "RENAME TABLE `active_users` TO `enabled_users`;" ); } #[test] fn builds_sqlite_protocol_table_rename_sql() { for database_type in [DatabaseType::Sqlite, DatabaseType::Rqlite, DatabaseType::Turso, DatabaseType::CloudflareD1] { let expected = if database_type == DatabaseType::Sqlite { "ALTER TABLE \"main\".\"users\" RENAME TO \"app users\";" } else { "ALTER TABLE \"users\" RENAME TO \"app users\";" }; assert!(supports_object_rename(Some(database_type), DatabaseObjectType::Table)); assert!(!supports_object_rename(Some(database_type), DatabaseObjectType::View)); assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(database_type), object_type: DatabaseObjectType::Table, schema: Some("main".to_string()), old_name: "users".to_string(), new_name: "app users".to_string(), }) .unwrap(), expected ); } } #[test] fn builds_duckdb_table_rename_sql() { assert!(supports_object_rename(Some(DatabaseType::DuckDb), DatabaseObjectType::Table)); assert!(!supports_object_rename(Some(DatabaseType::DuckDb), DatabaseObjectType::View)); assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::DuckDb), object_type: DatabaseObjectType::Table, schema: Some("main".to_string()), old_name: "users".to_string(), new_name: "app users".to_string(), }) .unwrap(), "ALTER TABLE \"main\".\"users\" RENAME TO \"app users\";" ); } #[test] fn builds_postgres_table_and_view_rename_sql() { assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::Postgres), object_type: DatabaseObjectType::Table, schema: Some("public".to_string()), old_name: "orders".to_string(), new_name: "archived orders".to_string(), }) .unwrap(), "ALTER TABLE \"public\".\"orders\" RENAME TO \"archived orders\";" ); assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::Postgres), object_type: DatabaseObjectType::View, schema: Some("public".to_string()), old_name: "active_users".to_string(), new_name: "enabled_users".to_string(), }) .unwrap(), "ALTER VIEW \"public\".\"active_users\" RENAME TO \"enabled_users\";" ); } #[test] fn builds_opengauss_table_and_view_rename_sql() { // openGauss 兼容 PG 重命名语法,与 gaussdb 等 PG 系数据库保持一致(右键“重命名”菜单入口)。 assert!(supports_object_rename(Some(DatabaseType::OpenGauss), DatabaseObjectType::Table)); assert!(supports_object_rename(Some(DatabaseType::OpenGauss), DatabaseObjectType::View)); assert!(supports_object_rename(Some(DatabaseType::OpenGauss), DatabaseObjectType::MaterializedView)); assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::OpenGauss), object_type: DatabaseObjectType::Table, schema: Some("public".to_string()), old_name: "orders".to_string(), new_name: "archived orders".to_string(), }) .unwrap(), "ALTER TABLE \"public\".\"orders\" RENAME TO \"archived orders\";" ); assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::OpenGauss), object_type: DatabaseObjectType::View, schema: Some("public".to_string()), old_name: "active_users".to_string(), new_name: "enabled_users".to_string(), }) .unwrap(), "ALTER VIEW \"public\".\"active_users\" RENAME TO \"enabled_users\";" ); } #[test] fn builds_sqlserver_routine_rename_sql() { assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::SqlServer), object_type: DatabaseObjectType::Function, schema: Some("dbo".to_string()), old_name: "fn_total".to_string(), new_name: "fn_order_total".to_string(), }) .unwrap(), "EXEC sp_rename N'dbo.fn_total', N'fn_order_total', N'OBJECT';" ); assert!(supports_object_rename(Some(DatabaseType::SqlServer), DatabaseObjectType::Procedure)); } #[test] fn builds_oracle_family_table_and_view_rename_sql() { assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::Oracle), object_type: DatabaseObjectType::Table, schema: Some("HR".to_string()), old_name: "EMPLOYEES".to_string(), new_name: "STAFF".to_string(), }) .unwrap(), "ALTER TABLE \"HR\".\"EMPLOYEES\" RENAME TO \"STAFF\";" ); assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::Dameng), object_type: DatabaseObjectType::View, schema: Some("SYSDBA".to_string()), old_name: "ACTIVE_USERS".to_string(), new_name: "ENABLED_USERS".to_string(), }) .unwrap(), "ALTER VIEW \"SYSDBA\".\"ACTIVE_USERS\" RENAME TO \"ENABLED_USERS\";" ); // OceanBase in Oracle mode accepts the same statement; without it the transfer // rename-then-drop path has no way to back up a target table. assert!(supports_object_rename(Some(DatabaseType::OceanbaseOracle), DatabaseObjectType::Table)); assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::OceanbaseOracle), object_type: DatabaseObjectType::Table, schema: Some("APP".to_string()), old_name: "ORDERS".to_string(), new_name: "ORDERS__DBX_BAK".to_string(), }) .unwrap(), "ALTER TABLE \"APP\".\"ORDERS\" RENAME TO \"ORDERS__DBX_BAK\";" ); } #[test] fn builds_oscar_table_rename_sql() { // 神通实测支持 `ALTER TABLE old RENAME TO new`(issue #5505 探测)。 assert_eq!( build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::Oscar), object_type: DatabaseObjectType::Table, schema: Some("SYSDBA".to_string()), old_name: "OLD_USERS".to_string(), new_name: "NEW_USERS".to_string(), }) .unwrap(), "ALTER TABLE \"SYSDBA\".\"OLD_USERS\" RENAME TO \"NEW_USERS\";" ); } #[test] fn rejects_unsupported_direct_routine_renames() { assert!(!supports_object_rename(Some(DatabaseType::Oracle), DatabaseObjectType::Function)); assert!(!supports_object_rename(Some(DatabaseType::Dameng), DatabaseObjectType::Procedure)); assert!(build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::Dameng), object_type: DatabaseObjectType::Procedure, schema: Some("SYSDBA".to_string()), old_name: "REFRESH_CACHE".to_string(), new_name: "REFRESH_CACHE_V2".to_string(), }) .unwrap_err() .contains("Renaming PROCEDURE is not supported")); assert!(build_rename_object_sql(RenameObjectSqlOptions { database_type: Some(DatabaseType::Mysql), object_type: DatabaseObjectType::Procedure, schema: None, old_name: "refresh_cache".to_string(), new_name: "refresh_cache_v2".to_string(), }) .unwrap_err() .contains("Renaming PROCEDURE is not supported")); } #[test] fn builds_postgres_database_rename_sql() { assert!(supports_database_rename(Some(DatabaseType::Postgres))); assert_eq!( build_rename_database_sql(Some(DatabaseType::Postgres), "old_db", "new db", false).unwrap(), "ALTER DATABASE \"old_db\" RENAME TO \"new db\";" ); // With terminate_connections let sql = build_rename_database_sql(Some(DatabaseType::Postgres), "old_db", "new db", true).unwrap(); assert!(sql.contains("pg_terminate_backend"), "should include terminate"); assert!(sql.contains("ALTER DATABASE"), "should include rename"); } #[test] fn builds_rename_database_preflight_sql() { let sql = build_rename_database_preflight_sql(Some(DatabaseType::Postgres), "my_db").unwrap(); assert!(sql.contains("active_connections"), "should check active connections"); assert!(sql.contains("prepared_transactions"), "should check prepared transactions"); assert!(sql.contains("is_owner"), "should check ownership"); assert!(sql.contains("pg_stat_activity"), "should query pg_stat_activity"); assert!(sql.contains("pg_prepared_xacts"), "should query pg_prepared_xacts"); assert!(sql.contains("pg_database"), "should query pg_database"); } #[test] fn rejects_unsupported_database_rename() { assert!(!supports_database_rename(Some(DatabaseType::Mysql))); assert!(!supports_database_rename(Some(DatabaseType::Sqlite))); assert!(build_rename_database_sql(Some(DatabaseType::Mysql), "old_db", "new_db", false).is_err()); assert!(build_rename_database_preflight_sql(Some(DatabaseType::Mysql), "old_db").is_err()); } #[test] fn builds_various_postgres_family_database_rename_sql() { for db_type in [ DatabaseType::Gaussdb, DatabaseType::Kingbase, DatabaseType::Highgo, DatabaseType::Vastbase, DatabaseType::OpenGauss, DatabaseType::Redshift, ] { assert!(supports_database_rename(Some(db_type)), "{db_type:?} should support database rename"); let sql = build_rename_database_sql(Some(db_type), "old_db", "new_db", false).unwrap(); assert!(sql.contains("ALTER DATABASE"), "{db_type:?}: missing ALTER DATABASE"); assert!(sql.contains("RENAME TO"), "{db_type:?}: missing RENAME TO"); } } }