3220 lines
134 KiB
Rust
3220 lines
134 KiB
Rust
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<DatabaseType>,
|
||
pub object_type: DatabaseObjectType,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub schema: Option<String>,
|
||
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<DatabaseType>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub driver_profile: Option<String>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub target: Option<DatabaseCreationTarget>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub parent: Option<String>,
|
||
pub name: String,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub charset: Option<String>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub collation: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub enum DatabaseCreationTarget {
|
||
Database,
|
||
Schema,
|
||
Catalog,
|
||
Namespace,
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
#[cfg(feature = "duckdb-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<DatabaseType>,
|
||
pub object_type: DatabaseObjectType,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub schema: Option<String>,
|
||
pub name: String,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub signature: Option<String>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub identifier_quote: Option<String>,
|
||
}
|
||
|
||
#[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<DatabaseType>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub schema: Option<String>,
|
||
pub table_name: String,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub cascade: Option<bool>,
|
||
/// 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<String>,
|
||
}
|
||
|
||
#[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<DatabaseType>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub schema: Option<String>,
|
||
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<DatabaseType>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub driver_profile: Option<String>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub schema: Option<String>,
|
||
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<DatabaseType>,
|
||
pub object_type: TableChildObjectType,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub schema: Option<String>,
|
||
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<DatabaseType>,
|
||
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<DatabaseType>,
|
||
pub name: String,
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct DatabasePropertyEditSqlOptions {
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub database_type: Option<DatabaseType>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub driver_profile: Option<String>,
|
||
pub target: DatabasePropertyTarget,
|
||
pub name: String,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub charset: Option<String>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub collation: Option<String>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub comment: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub enum DatabasePropertyTarget {
|
||
Database,
|
||
Schema,
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct DuplicateTableStructureSqlOptions {
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub database_type: Option<DatabaseType>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub schema: Option<String>,
|
||
pub source_name: String,
|
||
pub target_name: String,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub table_comment: Option<String>,
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub column_comments: Vec<DuplicateTableColumnComment>,
|
||
/// 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<String>,
|
||
/// 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<String>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub identifier_quote: Option<String>,
|
||
}
|
||
|
||
#[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<DatabaseType>,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub schema: Option<String>,
|
||
pub source_name: String,
|
||
pub target_name: String,
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub columns: Option<Vec<String>>,
|
||
#[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<String>,
|
||
}
|
||
|
||
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<DatabaseType>, 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<String, String> {
|
||
match options.target.unwrap_or(DatabaseCreationTarget::Database) {
|
||
DatabaseCreationTarget::Database => build_create_database_statement(&options),
|
||
// Schema creation is exposed through the same frontend dialog contract when the tree target is a database node.
|
||
DatabaseCreationTarget::Schema => {
|
||
build_create_schema_sql(SchemaNameSqlOptions { database_type: options.database_type, name: options.name })
|
||
}
|
||
DatabaseCreationTarget::Catalog => Err("Creating catalogs is not supported yet.".to_string()),
|
||
DatabaseCreationTarget::Namespace => Err("Creating namespaces is not supported yet.".to_string()),
|
||
}
|
||
}
|
||
|
||
fn build_create_database_statement(options: &CreateDatabaseSqlOptions) -> Result<String, String> {
|
||
if !supports_create_database_target(options.database_type, 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 <name>`. 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 <name>` 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<DatabaseType>, 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<DatabaseType>, 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<DatabaseType>) -> 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<DatabaseType>, driver_profile: Option<&str>) -> bool {
|
||
supports_create_database_charset(database_type, driver_profile)
|
||
&& matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Goldendb))
|
||
}
|
||
|
||
pub fn supports_database_property_comment(database_type: Option<DatabaseType>) -> bool {
|
||
matches!(
|
||
database_type,
|
||
Some(
|
||
DatabaseType::Postgres
|
||
| DatabaseType::Gaussdb
|
||
| DatabaseType::Kwdb
|
||
| DatabaseType::Kingbase
|
||
| DatabaseType::Highgo
|
||
| DatabaseType::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<DatabaseType>) -> 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<String, String> {
|
||
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<String, String> {
|
||
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<DatabaseType>) -> 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<String, String> {
|
||
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::<u64>().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<DatabaseType>) -> 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<String, String> {
|
||
match options.target {
|
||
DatabasePropertyTarget::Database => {
|
||
if options.comment.is_some() {
|
||
return build_database_comment_sql(options.database_type, &options.name, options.comment.as_deref());
|
||
}
|
||
build_database_charset_sql(&options)
|
||
}
|
||
DatabasePropertyTarget::Schema => {
|
||
build_schema_comment_sql(options.database_type, &options.name, options.comment.as_deref())
|
||
}
|
||
}
|
||
}
|
||
|
||
fn build_database_charset_sql(options: &DatabasePropertyEditSqlOptions) -> Result<String, String> {
|
||
if !supports_database_property_charset(options.database_type, options.driver_profile.as_deref()) {
|
||
return Err(format!(
|
||
"Editing database charset/collation is not supported for {}.",
|
||
database_label(options.database_type)
|
||
));
|
||
}
|
||
let charset = clean_sql_option(options.charset.as_deref());
|
||
let collation = clean_sql_option(options.collation.as_deref());
|
||
if charset.is_empty() && collation.is_empty() {
|
||
return Err("At least one charset or collation value is required.".to_string());
|
||
}
|
||
let mut sql = format!("ALTER DATABASE {}", quote_table_identifier(options.database_type, &options.name));
|
||
if !charset.is_empty() {
|
||
sql.push_str(&format!(" DEFAULT CHARACTER SET {charset}"));
|
||
}
|
||
if !collation.is_empty() {
|
||
sql.push_str(&format!(" DEFAULT COLLATE {collation}"));
|
||
}
|
||
sql.push(';');
|
||
Ok(sql)
|
||
}
|
||
|
||
fn build_database_comment_sql(
|
||
database_type: Option<DatabaseType>,
|
||
name: &str,
|
||
comment: Option<&str>,
|
||
) -> Result<String, String> {
|
||
if !supports_database_property_comment(database_type) {
|
||
return Err(format!("Editing database comments is not supported for {}.", database_label(database_type)));
|
||
}
|
||
Ok(format!("COMMENT ON DATABASE {} IS {};", quote_table_identifier(database_type, name), comment_literal(comment)))
|
||
}
|
||
|
||
fn build_schema_comment_sql(
|
||
database_type: Option<DatabaseType>,
|
||
name: &str,
|
||
comment: Option<&str>,
|
||
) -> Result<String, String> {
|
||
if !supports_database_property_comment(database_type) {
|
||
return Err(format!("Editing schema comments is not supported for {}.", database_label(database_type)));
|
||
}
|
||
Ok(format!("COMMENT ON SCHEMA {} IS {};", quote_table_identifier(database_type, name), comment_literal(comment)))
|
||
}
|
||
|
||
pub fn build_create_schema_sql(options: SchemaNameSqlOptions) -> Result<String, String> {
|
||
if !supports_create_schema_target(options.database_type) {
|
||
return Err(format!("Creating schemas is not supported for {}.", database_label(options.database_type)));
|
||
}
|
||
Ok(format!("CREATE SCHEMA {};", quote_table_identifier(options.database_type, &options.name)))
|
||
}
|
||
|
||
pub fn build_drop_schema_sql(options: SchemaNameSqlOptions) -> String {
|
||
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::<Vec<_>>()
|
||
.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::<Vec<_>>()
|
||
.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::<Vec<_>>()
|
||
.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<DatabaseType>, 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<String, String> {
|
||
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<DatabaseType>) -> 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<DatabaseType>,
|
||
old_name: &str,
|
||
new_name: &str,
|
||
terminate_connections: bool,
|
||
) -> Result<String, String> {
|
||
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<DatabaseType>,
|
||
database_name: &str,
|
||
) -> Result<String, String> {
|
||
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<DatabaseType>, 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<DatabaseType>, 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<DatabaseType>,
|
||
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<DatabaseType>, 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<DatabaseType>) -> 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 <name>, 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<bool>| {
|
||
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::<String>::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");
|
||
}
|
||
}
|
||
}
|