feat(gaussdb): support M-mode index metadata and DDL

This commit is contained in:
Andrew authored and GitHub committed 2026-08-20 18:50:22 +08:00
1 parent ba0f864234
commit 5741bc1256
17 files changed
+667 -15

No files matched your search

@@ -43,7 +43,9 @@ pub(super) fn column_definition(dialect: StructureDialect, column: &EditableStru
}
}
}
if matches!(dialect, StructureDialect::Mysql | StructureDialect::Doris) && !clean(&column.comment).is_empty() {
if matches!(dialect, StructureDialect::Mysql | StructureDialect::GaussdbM | StructureDialect::Doris)
&& !clean(&column.comment).is_empty()
{
parts.push(format!("COMMENT {}", quote_string(&clean(&column.comment))));
}
parts.join(" ")
@@ -20,7 +20,7 @@ pub(super) fn build_table_comment_sql(options: &TableStructureSqlOptions, warnin
let table = qualified_table(dialect, options.schema.as_deref(), &options.table_name);
let quoted = quote_string(&clean(new_comment));
match dialect {
StructureDialect::Mysql => {
StructureDialect::Mysql | StructureDialect::GaussdbM => {
vec![format!("ALTER TABLE {table} COMMENT = {quoted};")]
}
StructureDialect::Postgres
@@ -13,6 +13,15 @@ use super::validation::{validate_columns, validate_concurrent_index_scope, valid
use crate::models::connection::DatabaseType;
pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStructureSqlResult {
let capabilities;
let dialect;
if options.is_gaussdb_m_mode {
capabilities = super::dialect::gaussdb_m_capabilities();
dialect = StructureDialect::GaussdbM;
} else {
capabilities = capabilities_for(options.database_type);
dialect = capabilities.dialect;
}
options.table_name = clean(&options.table_name);
let mut warnings = Vec::new();
// Fail closed: a concurrent-index request on a partitioned parent (or on an
@@ -31,9 +40,6 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
if !warnings.is_empty() {
return TableStructureSqlResult { statements: Vec::new(), warnings };
}
let capabilities = capabilities_for(options.database_type);
let dialect = capabilities.dialect;
let table = qualified_table(dialect, options.schema.as_deref(), &options.table_name);
if dialect == StructureDialect::Dameng {
for column in &active_columns {
@@ -100,7 +106,7 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr
if capabilities.comment {
let table_comment = clean(options.table_comment.as_deref().unwrap_or(""));
if !table_comment.is_empty() {
if dialect == StructureDialect::Mysql {
if matches!(dialect, StructureDialect::Mysql | StructureDialect::GaussdbM) {
if let Some(last) = statements.last_mut() {
*last = last.replace(");", &format!(") COMMENT = {};", quote_string(&table_comment)));
}
@@ -19,6 +19,10 @@ pub(super) enum StructureDialect {
ManticoreSearch,
Informix,
Questdb,
// GaussDB M 模式:MySQL 兼容方言,使用反引号引用标识符。DDL 行为接近 MySQL,
// 但使用 UBTREE 索引类型(而非 BTREE),不支持 INCLUDE/FILTER 等。
// DROP INDEX 使用 PostgreSQL 风格(DROP INDEX name),而非 MySQL 的 DROP INDEX name ON table。
GaussdbM,
Unsupported,
}
@@ -67,6 +71,25 @@ impl Default for TableStructureCapabilities {
}
}
pub(super) fn gaussdb_m_capabilities() -> TableStructureCapabilities {
TableStructureCapabilities {
dialect: StructureDialect::GaussdbM,
add_column: true,
drop_column: true,
rename_column: true,
alter_existing_column: true,
reorder_column: true,
comment: true,
create_index: true,
drop_index: true,
rebuild_index: true,
index_type: true,
index_comment: true,
alter_primary_key: true,
..TableStructureCapabilities::default()
}
}
pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStructureCapabilities {
let base = TableStructureCapabilities::default();
match database_type {
@@ -362,6 +385,7 @@ pub(super) fn dialect_label(dialect: StructureDialect) -> String {
StructureDialect::ClickHouse => "clickhouse",
StructureDialect::ManticoreSearch => "manticoresearch",
StructureDialect::Informix => "informix",
StructureDialect::GaussdbM => "gaussdb-m",
StructureDialect::Questdb => "questdb",
StructureDialect::Unsupported => "this database",
}
@@ -385,6 +409,7 @@ pub(super) fn database_type_for_dialect(dialect: StructureDialect) -> Option<Dat
StructureDialect::ManticoreSearch => Some(DatabaseType::ManticoreSearch),
StructureDialect::Informix => Some(DatabaseType::Informix),
StructureDialect::Questdb => Some(DatabaseType::Questdb),
StructureDialect::GaussdbM => Some(DatabaseType::Mysql),
StructureDialect::Unsupported => None,
}
}
@@ -5,8 +5,17 @@ use super::util::{clean, qualified_table, quote_ident, quote_string};
use crate::models::connection::DatabaseType;
pub(super) fn build_index_sql(options: &TableStructureSqlOptions, warnings: &mut Vec<String>) -> Vec<String> {
let capabilities = capabilities_for(options.database_type);
let dialect = capabilities.dialect;
let capabilities;
let dialect;
if options.is_gaussdb_m_mode {
let caps = super::dialect::gaussdb_m_capabilities();
capabilities = caps;
dialect = StructureDialect::GaussdbM;
} else {
let caps = capabilities_for(options.database_type);
capabilities = caps;
dialect = caps.dialect;
}
let table = qualified_table(dialect, options.schema.as_deref(), &options.table_name);
let database_label = database_label(options.database_type);
let mut statements = Vec::new();
@@ -124,6 +133,29 @@ pub(super) fn mysql_index_parts(index_type: &str) -> (String, String) {
}
}
fn gaussdbm_index_parts(index_type: &str) -> (String, String) {
match index_type.to_ascii_uppercase().as_str() {
"BTREE" | "UBTREE" => (String::new(), " USING UBTREE".to_string()),
"HASH" => (String::new(), " USING HASH".to_string()),
_ => (String::new(), String::new()),
}
}
fn gaussdbm_index_column_sql(column: &str, is_expression: bool) -> String {
let trimmed = column.trim();
if is_expression {
return trimmed.to_string();
}
if let Some((name, suffix)) = trimmed.rsplit_once('(') {
if let Some(length) = suffix.strip_suffix(')') {
if !name.trim().is_empty() && !length.is_empty() && length.chars().all(|ch| ch.is_ascii_digit()) {
return format!("{}({length})", quote_ident(StructureDialect::GaussdbM, name.trim()));
}
}
}
quote_ident(StructureDialect::GaussdbM, trimmed)
}
fn mysql_index_column_sql(column: &str) -> String {
let trimmed = column.trim();
// Keep the wrapped expression from MySQL metadata instead of quoting it as a column identifier.
@@ -246,6 +278,8 @@ pub(super) fn build_create_index_statements(
.map(|(i, column)| {
if dialect == StructureDialect::Mysql {
mysql_index_column_sql(column)
} else if dialect == StructureDialect::GaussdbM {
gaussdbm_index_column_sql(column, key_is_expression[i])
} else if dialect == StructureDialect::Postgres {
postgres_index_column_sql(column, key_is_expression[i])
} else {
@@ -270,6 +304,11 @@ pub(super) fn build_create_index_statements(
StructureDialect::Oracle | StructureDialect::Dameng if idx_type == "BITMAP" => {
type_prefix = "BITMAP ".to_string()
}
StructureDialect::GaussdbM => {
let (prefix, using) = gaussdbm_index_parts(&idx_type);
type_prefix = prefix;
using_clause = using;
}
_ => {}
}
}
@@ -288,7 +327,10 @@ pub(super) fn build_create_index_statements(
String::new()
};
let comment = clean(&index.comment);
let comment_clause = if !comment.is_empty() && capabilities.index_comment && dialect == StructureDialect::Mysql {
let comment_clause = if !comment.is_empty()
&& capabilities.index_comment
&& matches!(dialect, StructureDialect::Mysql | StructureDialect::GaussdbM)
{
format!(" COMMENT {}", quote_string(&comment))
} else {
String::new()
@@ -322,8 +364,11 @@ pub(super) fn build_create_index_statements(
statements.push(format!("COMMENT ON INDEX {} IS {};", quote_ident(dialect, &name), quote_string(&comment)));
} else if !comment.is_empty() && capabilities.index_comment && dialect == StructureDialect::SqlServer {
statements.extend(build_sqlserver_index_comment_sql(table, schema, table_name, &name, &comment));
} else if !comment.is_empty() && capabilities.index_comment && dialect == StructureDialect::Mysql {
// MySQL comment is embedded inline in the CREATE INDEX statement above
} else if !comment.is_empty()
&& capabilities.index_comment
&& matches!(dialect, StructureDialect::Mysql | StructureDialect::GaussdbM)
{
// Comment is embedded inline in the CREATE INDEX statement above
} else if !comment.is_empty() && capabilities.index_comment {
warnings.push(format!("Index comments are not supported for {} from this editor.", dialect_label(dialect)));
}
@@ -1517,6 +1517,7 @@ mod tests {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
}
}
@@ -60,6 +60,7 @@ fn structure_change_options(
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
}
}
@@ -113,6 +114,7 @@ fn index_change_options(
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
}
}
@@ -189,6 +191,7 @@ fn builds_mysql_column_and_index_changes() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -403,6 +406,7 @@ fn builds_xugu_type_change_with_native_syntax() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -611,6 +615,7 @@ fn builds_mysql_unsigned_integer_column_with_length_before_attribute() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -644,6 +649,7 @@ fn doris_table_editor_renames_column_without_mysql_change_syntax() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -701,6 +707,7 @@ fn dameng_integer_column_omits_mysql_display_width() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -741,6 +748,7 @@ fn builds_highgo_foreign_key_changes_with_postgres_syntax() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -810,6 +818,7 @@ fn builds_informix_column_and_index_changes() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -864,6 +873,7 @@ fn oracle_does_not_generate_drop_sql_for_all_columns() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.statements, Vec::<String>::new());
@@ -923,6 +933,7 @@ fn oracle_create_table_preserves_character_length_units() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert!(result.statements[0].contains("\"BYTE_COL\" VARCHAR2(12 BYTE)"));
@@ -1010,6 +1021,7 @@ fn iris_drop_index_includes_table_name() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1049,6 +1061,7 @@ fn iris_ignores_comment_changes_but_keeps_supported_column_alters() {
table_comment: Some("new table description".to_string()),
original_table_comment: Some("old table description".to_string()),
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(
@@ -1128,6 +1141,7 @@ fn oracle_compatible_databases_keep_comment_on_sql() {
table_comment: Some("new table description".to_string()),
original_table_comment: Some("old table description".to_string()),
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new(), "{database_type:?}");
@@ -1160,6 +1174,7 @@ fn mysql_create_index_with_comment() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1191,6 +1206,7 @@ fn manticoresearch_builds_create_table_sql_only() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1232,6 +1248,7 @@ fn manticoresearch_builds_add_and_drop_column_sql() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1296,6 +1313,7 @@ fn gbase8a_uses_limited_mysql_ddl() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(
@@ -1364,6 +1382,7 @@ fn gbase8a_allows_mysql_style_column_reorder() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1397,6 +1416,7 @@ fn manticoresearch_does_not_drop_id_column() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.statements, Vec::<String>::new());
@@ -1463,6 +1483,7 @@ fn manticoresearch_warns_when_existing_column_properties_change() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.statements, Vec::<String>::new());
@@ -1496,6 +1517,7 @@ fn manticoresearch_ignores_mysql_column_options() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1532,6 +1554,7 @@ fn manticoresearch_builds_text_column_properties() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1560,6 +1583,7 @@ fn manticoresearch_builds_json_secondary_index_property() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1584,6 +1608,7 @@ fn mysql_create_unique_index_with_comment_and_btree() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1610,6 +1635,7 @@ fn mysql_create_functional_index_preserves_key_part_syntax() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1636,6 +1662,7 @@ fn mysql_add_timestamp_column_drops_invalid_precision() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1662,6 +1689,7 @@ fn mysql_add_timestamp_column_preserves_valid_precision() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1695,6 +1723,7 @@ fn builds_postgres_create_table_with_comments_and_index() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1725,6 +1754,7 @@ fn quotes_expression_like_new_index_columns_without_provenance() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1795,6 +1825,7 @@ fn create_table_trims_table_name_whitespace_for_all_statements() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1830,6 +1861,7 @@ fn warns_for_sqlite_unsafe_column_changes() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.statements, Vec::<String>::new());
@@ -1869,6 +1901,7 @@ fn qualifies_attached_sqlite_table_and_index_changes() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1936,6 +1969,7 @@ fn builds_rqlite_changes_with_sqlite_dialect() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -1964,6 +1998,7 @@ fn builds_kingbase_add_column_without_column_keyword() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2027,6 +2062,7 @@ fn builds_mysql_column_reorder_statements() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2081,6 +2117,7 @@ fn mysql_add_column_before_existing_column_does_not_reorder_shifted_column() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2142,6 +2179,7 @@ fn mysql_existing_column_reorder_does_not_reorder_columns_shifted_by_prior_move(
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2215,6 +2253,7 @@ fn mysql_moving_first_column_to_end_uses_single_reorder_statement() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2238,6 +2277,7 @@ fn builds_sql_server_quoted_column_and_index_statements() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2267,6 +2307,7 @@ fn sqlserver_strips_mysql_display_width_from_fixed_integer_types() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2290,6 +2331,7 @@ fn sqlserver_strips_scale_from_float() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2313,6 +2355,7 @@ fn sqlserver_preserves_float_mantissa_bits() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2361,6 +2404,7 @@ fn sqlserver_default_changes_drop_old_constraints_with_isolated_batches() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2542,6 +2586,7 @@ fn sqlserver_unchanged_foreign_key_does_not_warn_when_saving_other_changes() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2570,6 +2615,7 @@ fn sqlserver_add_column_with_identity() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2598,6 +2644,7 @@ fn dameng_add_column_with_identity() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2625,6 +2672,7 @@ fn dameng_rejects_identity_on_incompatible_type() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.statements, Vec::<String>::new());
@@ -2656,6 +2704,7 @@ fn sqlserver_rejects_identity_on_incompatible_type() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.statements, Vec::<String>::new());
@@ -2690,6 +2739,7 @@ fn sqlserver_changed_foreign_key_still_warns_as_unsupported() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.statements, Vec::<String>::new());
@@ -2729,6 +2779,7 @@ fn sqlserver_unchanged_identity_extra_does_not_mark_existing_column_changed() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2768,6 +2819,7 @@ fn dameng_unchanged_identity_extra_does_not_mark_existing_column_changed() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2814,6 +2866,7 @@ fn dameng_rejects_adding_second_identity_column() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, vec!["Dameng tables can have only one identity column."]);
@@ -2864,6 +2917,7 @@ fn sqlserver_existing_column_identity_change_warns_without_unchanged_foreign_key
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.statements, Vec::<String>::new());
@@ -2894,6 +2948,7 @@ fn builds_duckdb_create_table_statements() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2938,6 +2993,7 @@ fn builds_clickhouse_nullable_comment_and_reorder_statements() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -2983,6 +3039,7 @@ fn builds_h2_schema_qualified_existing_column_statements() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3396,6 +3453,7 @@ fn mysql_create_table_with_auto_increment() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3422,6 +3480,7 @@ fn mysql_create_table_keeps_column_charset_collation_and_comment() {
table_comment: Some("User accounts".to_string()),
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3452,6 +3511,7 @@ fn mysql_compatible_databases_do_not_emit_mysql_column_charset_clauses() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3479,6 +3539,7 @@ fn mysql_create_table_with_on_update_current_timestamp() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3506,6 +3567,7 @@ fn postgres_create_table_with_identity() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3535,6 +3597,7 @@ fn dameng_create_table_with_identity() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3560,6 +3623,7 @@ fn dameng_create_table_preserves_character_length_units() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3593,6 +3657,7 @@ fn dameng_alter_column_preserves_character_length_unit() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3627,6 +3692,7 @@ fn dameng_rejects_multiple_identity_columns() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert!(result.statements.is_empty());
@@ -3654,6 +3720,7 @@ fn dameng_rejects_zero_identity_increment() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert!(result.statements.is_empty());
@@ -3682,6 +3749,7 @@ fn sqlserver_create_table_with_identity() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3705,6 +3773,7 @@ fn mysql_quotes_datetime_literal_default() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3728,6 +3797,7 @@ fn mysql_does_not_quote_current_timestamp() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3752,6 +3822,7 @@ fn mysql_does_not_quote_temporal_function_with_parens() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3775,6 +3846,7 @@ fn mysql_date_literal_default_is_quoted() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3798,6 +3870,7 @@ fn mysql_time_literal_default_is_quoted() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3821,6 +3894,7 @@ fn non_temporal_types_are_not_quoted() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3945,6 +4019,7 @@ fn builds_mysql_foreign_key_changes() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -3973,6 +4048,7 @@ fn builds_mysql_composite_foreign_key() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4004,6 +4080,7 @@ fn builds_oracle_foreign_key_with_supported_actions() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4039,6 +4116,7 @@ fn builds_oracle_foreign_key_replacement() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4072,6 +4150,7 @@ fn builds_mysql_trigger_changes() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4116,6 +4195,7 @@ fn unchanged_postgres_trigger_does_not_block_column_rename() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4143,6 +4223,7 @@ fn changed_postgres_trigger_remains_unsupported() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert!(result.statements.is_empty());
@@ -4175,6 +4256,7 @@ fn rejects_editing_existing_oracle_trigger_without_complete_source() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert!(result.statements.is_empty());
@@ -4197,6 +4279,7 @@ fn builds_oracle_statement_trigger_without_row_clause() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4230,6 +4313,7 @@ fn drops_existing_oracle_trigger_without_reconstructing_it() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4249,6 +4333,7 @@ fn rejects_unsupported_oracle_compound_trigger_shape() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert!(result.statements.is_empty());
@@ -4272,6 +4357,7 @@ fn mysql_varchar_default_is_quoted() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4296,6 +4382,7 @@ fn mysql_char_default_is_quoted() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4319,6 +4406,7 @@ fn mysql_text_default_is_quoted() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4342,6 +4430,7 @@ fn mysql_enum_default_is_quoted() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4365,6 +4454,7 @@ fn mysql_int_default_is_not_quoted() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4494,6 +4584,7 @@ fn mysql_character_column_add_with_charset_collation() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4525,6 +4616,7 @@ fn mysql_numeric_column_omits_charset_collation_in_column_definition() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4566,6 +4658,7 @@ fn mysql_numeric_column_ignores_charset_collation_in_change_detection() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
// No ALTER should be emitted — charset/collation changes on
@@ -4602,6 +4695,7 @@ fn mysql_character_column_detects_charset_collation_change() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4643,6 +4737,7 @@ fn mysql_character_column_preserves_charset_collation_on_other_change() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4789,6 +4884,7 @@ fn oscar_create_table_with_primary_key_and_comments() {
table_comment: Some("user table".to_string()),
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4980,6 +5076,7 @@ fn oscar_drop_index_with_schema_qualifier() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -4999,6 +5096,7 @@ fn oscar_table_comment_uses_comment_on_table() {
table_comment: Some("new comment".to_string()),
original_table_comment: Some("old comment".to_string()),
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -5080,6 +5178,7 @@ fn postgres_partitioned_parent_concurrent_request_rejected() {
table_comment: None,
original_table_comment: None,
partitioned: true,
is_gaussdb_m_mode: false,
});
// Fail closed: PostgreSQL rejects CREATE INDEX CONCURRENTLY on a
@@ -5109,6 +5208,7 @@ fn postgres_partitioned_parent_plain_index_unchanged() {
table_comment: None,
original_table_comment: None,
partitioned: true,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -5150,6 +5250,7 @@ fn postgres_create_table_partitioned_concurrent_request_rejected() {
table_comment: None,
original_table_comment: None,
partitioned: true,
is_gaussdb_m_mode: false,
});
assert_eq!(
@@ -5275,6 +5376,7 @@ fn postgres_create_table_concurrent_index() {
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: false,
});
assert_eq!(result.warnings, Vec::<String>::new());
@@ -5372,3 +5474,269 @@ fn non_postgres_concurrent_flag_is_ignored() {
);
}
}
// ---------------------------------------------------------------------------
// GaussDB M-mode index tests
// ---------------------------------------------------------------------------
fn gaussdb_m_options(columns: Vec<EditableStructureColumn>) -> TableStructureSqlOptions {
TableStructureSqlOptions {
database_type: Some(DatabaseType::Gaussdb),
schema: None,
table_name: "USERS".to_string(),
columns,
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: true,
}
}
fn gaussdb_m_index(name: &str, columns: &[&str]) -> EditableStructureIndex {
EditableStructureIndex {
id: name.to_string(),
name: name.to_string(),
columns: columns.iter().map(|c| c.to_string()).collect(),
is_unique: false,
is_primary: false,
filter: String::new(),
index_type: String::new(),
included_columns: Vec::new(),
comment: String::new(),
concurrently: false,
original: None,
marked_for_drop: false,
}
}
fn gaussdb_m_existing_index(
name: &str,
columns: &[&str],
is_unique: bool,
index_type: Option<&str>,
) -> EditableStructureIndex {
let mut idx = gaussdb_m_index(name, columns);
idx.is_unique = is_unique;
idx.index_type = index_type.unwrap_or("").to_string();
idx.original = Some(IndexInfo {
name: name.to_string(),
columns: columns.iter().map(|c| c.to_string()).collect(),
is_unique,
is_primary: false,
filter: None,
index_type: index_type.map(|s| s.to_string()),
included_columns: None,
comment: None,
key_is_expression: Vec::new(),
});
idx
}
#[test]
fn gaussdb_m_create_index_uses_backtick_quoting() {
let mut options = gaussdb_m_options(vec![column("id")]);
options.indexes = vec![gaussdb_m_index("idx_email", &["email"])];
let result = build_table_structure_change_sql(options);
assert_eq!(result.warnings, Vec::<String>::new());
let sql = result.statements.join("\n");
assert!(sql.contains("CREATE INDEX `idx_email` ON `USERS`"));
assert!(sql.contains("(`email`)"));
}
#[test]
fn gaussdb_m_create_unique_index() {
let mut options = gaussdb_m_options(vec![column("id")]);
let mut idx = gaussdb_m_index("idx_email", &["email"]);
idx.is_unique = true;
options.indexes = vec![idx];
let result = build_table_structure_change_sql(options);
assert_eq!(result.warnings, Vec::<String>::new());
let sql = result.statements.join("\n");
assert!(sql.contains("CREATE UNIQUE INDEX `idx_email` ON `USERS`"));
}
#[test]
fn gaussdb_m_create_index_with_ubtree_using_clause() {
let mut options = gaussdb_m_options(vec![column("id")]);
let mut idx = gaussdb_m_index("idx_email", &["email"]);
idx.index_type = "UBTREE".to_string();
options.indexes = vec![idx];
let result = build_table_structure_change_sql(options);
assert_eq!(result.warnings, Vec::<String>::new());
let sql = result.statements.join("\n");
// GaussDB M-mode maps UBTREE/BTREE to USING UBTREE
assert!(sql.contains("USING UBTREE"), "Expected USING UBTREE, got: {sql}");
}
#[test]
fn gaussdb_m_create_index_with_btree_also_emits_ubtree() {
let mut options = gaussdb_m_options(vec![column("id")]);
let mut idx = gaussdb_m_index("idx_email", &["email"]);
idx.index_type = "BTREE".to_string();
options.indexes = vec![idx];
let result = build_table_structure_change_sql(options);
assert_eq!(result.warnings, Vec::<String>::new());
let sql = result.statements.join("\n");
// BTREE in the DB is also rendered as USING UBTREE for GaussDB M
assert!(sql.contains("USING UBTREE"), "Expected USING UBTREE, got: {sql}");
}
#[test]
fn gaussdb_m_create_index_with_comment() {
let mut options = gaussdb_m_options(vec![column("id")]);
let mut idx = gaussdb_m_index("idx_email", &["email"]);
idx.comment = "index comment".to_string();
options.indexes = vec![idx];
let result = build_table_structure_change_sql(options);
assert_eq!(result.warnings, Vec::<String>::new());
let sql = result.statements.join("\n");
assert!(sql.contains("COMMENT 'index comment'"));
}
#[test]
fn gaussdb_m_drop_index_does_not_use_on_table() {
let mut idx = gaussdb_m_existing_index("idx_email", &["email"], false, None);
idx.marked_for_drop = true;
let options = gaussdb_m_options(vec![column("id")]);
let options = TableStructureSqlOptions { indexes: vec![idx], ..options };
let result = build_table_structure_change_sql(options);
assert_eq!(result.warnings, Vec::<String>::new());
let sql = result.statements.join("\n");
// GaussDB M-mode must NOT use MySQL-style "DROP INDEX ... ON table"
assert!(!sql.contains("ON `USERS`"), "Must not use MySQL ON clause: {sql}");
// Must use PostgreSQL-style "DROP INDEX name"
assert!(sql.contains("DROP INDEX `idx_email`"), "Expected DROP INDEX without ON: {sql}");
}
#[test]
fn gaussdb_m_rebuild_index_drops_and_creates() {
let mut idx = gaussdb_m_existing_index("idx_email", &["email"], false, None);
idx.columns = vec!["email".to_string(), "name".to_string()]; // change: add column
let options = gaussdb_m_options(vec![column("id")]);
let options = TableStructureSqlOptions { indexes: vec![idx], ..options };
let result = build_table_structure_change_sql(options);
assert_eq!(result.warnings, Vec::<String>::new());
let sql = result.statements.join("\n");
assert!(sql.contains("DROP INDEX `idx_email`"), "Must drop old index: {sql}");
assert!(sql.contains("CREATE INDEX `idx_email` ON `USERS`"), "Must recreate index: {sql}");
assert!(sql.contains("(`email`, `name`)"), "Must include new column: {sql}");
}
#[test]
fn gaussdb_m_create_index_with_composite_columns() {
let mut options = gaussdb_m_options(vec![column("id")]);
options.indexes = vec![gaussdb_m_index("idx_name_email", &["last_name", "first_name", "email"])];
let result = build_table_structure_change_sql(options);
assert_eq!(result.warnings, Vec::<String>::new());
let sql = result.statements.join("\n");
assert!(sql.contains("(`last_name`, `first_name`, `email`)"));
}
#[test]
fn gaussdb_m_create_prefix_index_quotes_column_before_length() {
let mut options = gaussdb_m_options(vec![column("email")]);
options.indexes = vec![gaussdb_m_index("idx_email", &["email(10)"])];
let result = build_table_structure_change_sql(options);
assert_eq!(result.warnings, Vec::<String>::new());
let sql = result.statements.join("\n");
assert!(sql.contains("(`email`(10))"), "Expected prefix length outside the quoted identifier: {sql}");
assert!(!sql.contains("`email(10)`"), "Prefix length must not be quoted as part of the identifier: {sql}");
}
#[test]
fn gaussdb_m_create_table_uses_backtick_quoting() {
let cols = vec![column("id"), column("name")];
let mut options = gaussdb_m_options(cols);
options.indexes = vec![gaussdb_m_index("idx_name", &["name"])];
let result = build_create_table_sql(options);
assert!(result.warnings.is_empty());
let sql = result.statements.join("\n");
assert!(sql.contains("CREATE TABLE `USERS`"));
assert!(sql.contains("`id` varchar(255)"));
assert!(sql.contains("`name` varchar(255)"));
assert!(sql.contains("CREATE INDEX `idx_name` ON `USERS`"));
}
#[test]
fn gaussdb_m_create_table_does_not_add_charset_or_collation() {
let mut col = column("name");
col.character_set = "utf8mb4".to_string();
col.collation = "utf8mb4_unicode_ci".to_string();
let options = gaussdb_m_options(vec![col]);
let result = build_create_table_sql(options);
assert!(result.warnings.is_empty());
let sql = result.statements.join("\n");
// GaussDB M must NOT emit MySQL CHARACTER SET/COLLATE clauses
assert!(!sql.contains("CHARACTER SET"), "Must not emit CHARACTER SET: {sql}");
assert!(!sql.contains("COLLATE"), "Must not emit COLLATE: {sql}");
}
#[test]
fn gaussdb_m_create_table_comment_uses_mysql_syntax() {
let options = TableStructureSqlOptions {
table_comment: Some("User accounts table".to_string()),
original_table_comment: None,
..gaussdb_m_options(vec![column("id")])
};
let result = build_create_table_sql(options);
assert!(result.warnings.is_empty());
let sql = result.statements.join("\n");
// GaussDB M uses MySQL-style inline COMMENT = '...'
assert!(sql.contains("COMMENT = 'User accounts table'"), "Expected MySQL-style comment, got: {sql}");
}
#[test]
fn gaussdb_m_rebuild_index_changing_type_from_btree_to_ubtree() {
let mut idx = gaussdb_m_existing_index("idx_email", &["email"], false, Some("BTREE"));
idx.index_type = "UBTREE".to_string();
let options = gaussdb_m_options(vec![column("id")]);
let options = TableStructureSqlOptions { indexes: vec![idx], ..options };
let result = build_table_structure_change_sql(options);
assert_eq!(result.warnings, Vec::<String>::new());
let sql = result.statements.join("\n");
assert!(sql.contains("DROP INDEX `idx_email`"));
assert!(sql.contains("USING UBTREE"));
}
#[test]
fn gaussdb_m_rebuild_index_unchanged_type_does_not_rebuild() {
// When the index type from SHOW INDEX is "BTREE" and the user doesn't
// change it, the editor should send "BTREE" back (which maps to
// USING UBTREE in SQL). But since normalized_index_type("BTREE") ==
// "BTREE" and original.index_type == Some("BTREE"), they match — no rebuild.
let mut idx = gaussdb_m_existing_index("idx_email", &["email"], false, Some("BTREE"));
idx.index_type = "BTREE".to_string(); // same type
// No columns — just test the index itself has no change
let options = TableStructureSqlOptions {
database_type: Some(DatabaseType::Gaussdb),
schema: None,
table_name: "USERS".to_string(),
columns: Vec::new(),
indexes: vec![idx],
foreign_keys: Vec::new(),
triggers: Vec::new(),
table_comment: None,
original_table_comment: None,
partitioned: false,
is_gaussdb_m_mode: true,
};
let result = build_table_structure_change_sql(options);
assert!(result.warnings.is_empty());
assert!(result.statements.is_empty(), "Expected no DDL for unchanged index, got: {:?}", result.statements);
}
#[test]
fn gaussdb_m_create_table_with_primary_key() {
let mut pk_col = column("id");
pk_col.is_primary_key = true;
pk_col.is_nullable = false;
pk_col.data_type = "bigint".to_string();
let options = gaussdb_m_options(vec![pk_col]);
let result = build_create_table_sql(options);
assert!(result.warnings.is_empty());
let sql = result.statements.join("\n");
assert!(sql.contains("PRIMARY KEY (`id`)"));
}
@@ -217,6 +217,11 @@ pub struct TableStructureSqlOptions {
/// will reject or downgrading to a blocking `CREATE INDEX`.
#[serde(default)]
pub partitioned: bool,
/// When true, the connection is GaussDB M-mode which uses MySQL-compatible
/// SQL dialect with backtick quoting. The structure editor maps this to
/// `StructureDialect::Mysql` so that DDL is generated with MySQL syntax.
#[serde(default)]
pub is_gaussdb_m_mode: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -23,6 +23,7 @@ pub(super) fn quote_ident(dialect: StructureDialect, name: &str) -> String {
match dialect {
StructureDialect::Mysql
| StructureDialect::Doris
| StructureDialect::GaussdbM
| StructureDialect::ManticoreSearch
| StructureDialect::Questdb => {
format!("`{}`", name.replace('`', "``"))