From 5741bc1256303c073e408990e9acd0b6e43e03fc Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 20 Aug 2026 18:50:22 +0800 Subject: [PATCH] feat(gaussdb): support M-mode index metadata and DDL --- .gitignore | 1 + .../structure/TableStructureEditor.vue | 23 +- .../src/lib/table/tableStructureEditorSql.ts | 3 + crates/dbx-core/src/schema.rs | 167 ++++++++ crates/dbx-core/src/table_structure_sql.rs | 9 +- .../src/table_structure_sql/column_format.rs | 4 +- .../src/table_structure_sql/comments.rs | 2 +- .../src/table_structure_sql/create_table.rs | 14 +- .../src/table_structure_sql/dialect.rs | 25 ++ .../src/table_structure_sql/indexes.rs | 55 ++- .../src/table_structure_sql/sqlite_rebuild.rs | 1 + .../dbx-core/src/table_structure_sql/tests.rs | 368 ++++++++++++++++++ .../dbx-core/src/table_structure_sql/types.rs | 5 + .../dbx-core/src/table_structure_sql/util.rs | 1 + .../tests/live_mysql_generated_columns.rs | 1 + .../tests/live_postgres_concurrent_index.rs | 2 + .../tests/live_sqlserver_completion.rs | 1 + 17 files changed, 667 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index fc02f51212..64dae1bc48 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules .claude/ .codex/ +.cortexkit/ .workbuddy/ .superpowers/ .trellis/ diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 02a82d9522..00d78753d1 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -680,8 +680,15 @@ const indexTypesByDb: Record = { sqlserver: ["CLUSTERED", "NONCLUSTERED", "COLUMNSTORE", "NONCLUSTERED COLUMNSTORE", "XML", "SPATIAL"], oracle: ["NORMAL", "BITMAP", "FUNCTION-BASED NORMAL", "FUNCTION-BASED DOMAIN", "DOMAIN", "CLUSTER"], sqlite: ["BTREE"], + "gaussdb-m": ["UBTREE"], }; -const indexTypeOptions = computed(() => (structureCapabilities.value.indexType ? (indexTypesByDb[structureDialect.value] ?? []) : [])); +const indexTypeOptions = computed(() => { + if (!structureCapabilities.value.indexType) return []; + if (connection.value?.driver_profile?.toLowerCase() === "gaussdb-m") { + return indexTypesByDb["gaussdb-m"]; + } + return indexTypesByDb[structureDialect.value] ?? []; +}); interface DefaultValuePreset { label: string; @@ -1342,6 +1349,7 @@ function structureChangeOptions(): BuildTableStructureChangeSqlOptions { tableComment: tableComment.value, originalTableComment: isCreateMode.value ? undefined : originalTableComment.value, partitioned: isPartitionedParent.value, + isGaussdbMMode: connection.value?.driver_profile?.toLowerCase() === "gaussdb-m", }; } @@ -2457,15 +2465,24 @@ function existingIndexNamesForDraft(index: EditableStructureIndex): string[] { } function generatedIndexNameForDraft(index: EditableStructureIndex, columnsForName = index.columns): string { - return generateUniqueIndexName(structureIndexTableName(), columnsForName, existingIndexNamesForDraft(index)); + const name = generateUniqueIndexName(structureIndexTableName(), columnsForName, existingIndexNamesForDraft(index)); + // GaussDB M-mode expects lowercase index names (MySQL-compatible). + return connection.value?.driver_profile?.toLowerCase() === "gaussdb-m" ? name.toLowerCase() : name; } function refreshAutoIndexName(index: EditableStructureIndex, previousColumns = index.columns) { if (index.original || index.nameEdited) return; + const isGaussdbM = connection.value?.driver_profile?.toLowerCase() === "gaussdb-m"; const previousName = generateIndexName(structureIndexTableName(), previousColumns); const previousUniqueName = generateUniqueIndexName(structureIndexTableName(), previousColumns, existingIndexNamesForDraft(index)); const currentName = index.name.trim(); - if (currentName && currentName !== previousName && currentName !== previousUniqueName) return; + if (currentName) { + if (isGaussdbM) { + if (currentName.toLowerCase() !== previousName.toLowerCase() && currentName.toLowerCase() !== previousUniqueName.toLowerCase()) return; + } else if (currentName !== previousName && currentName !== previousUniqueName) { + return; + } + } index.name = generatedIndexNameForDraft(index); } diff --git a/apps/desktop/src/lib/table/tableStructureEditorSql.ts b/apps/desktop/src/lib/table/tableStructureEditorSql.ts index c00a89c402..5e7c7d411d 100644 --- a/apps/desktop/src/lib/table/tableStructureEditorSql.ts +++ b/apps/desktop/src/lib/table/tableStructureEditorSql.ts @@ -87,6 +87,9 @@ export interface BuildTableStructureChangeSqlOptions { * the backend rejects `CREATE INDEX CONCURRENTLY` on such tables (fail * closed) instead of downgrading to a blocking `CREATE INDEX`. */ partitioned?: boolean; + /** When true, the connection is GaussDB M-mode which uses MySQL-compatible + * SQL dialect with backtick quoting. */ + isGaussdbMMode?: boolean; } export interface TableStructureChangeSql { diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index ab5d9544b7..bd745a5527 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -6372,6 +6372,14 @@ async fn list_indexes_core_for_session( { let connections = state.connections.read().await; try_sqlserver!(connections, &pool_key, list_indexes, schema, table); + if let Some(PoolKind::ExternalDriver { config, session, .. }) = connections.get(&pool_key) { + if external_driver_uses_mysql_ddl(config.as_ref()) { + let config = config.clone(); + let session = session.clone(); + drop(connections); + return external_driver_gaussdb_m_indexes(session, config.as_ref(), database, schema, table).await; + } + } if let Some(client) = extract_pool!(&connections, &pool_key, Agent) { drop(connections); let mut client = client.lock().await; @@ -7272,6 +7280,165 @@ fn external_driver_uses_mysql_ddl(config: &ConnectionConfig) -> bool { is_mysql_external_driver_config(config) || gaussdb_uses_m_jdbc_driver(config) } +async fn external_driver_gaussdb_m_indexes( + session: std::sync::Arc, + config: &ConnectionConfig, + database: &str, + schema: &str, + table: &str, +) -> Result, String> { + // Query information_schema.STATISTICS row by row (one row per index column, + // ordered by SEQ_IN_INDEX). + // Docs: https://support.huaweicloud.com/intl/en-us/centralized-m-comp-devg-v8-gaussdb/gaussdb-81-0134.html + // The STATISTICS view has no EXPRESSION column in GaussDB M-mode. Expression + // indexes are not supported via this path. + let sql = format!( + "SELECT INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX, NON_UNIQUE, \ + INDEX_TYPE, INDEX_COMMENT, SUB_PART \ + FROM information_schema.STATISTICS \ + WHERE TABLE_SCHEMA = {} AND TABLE_NAME = {} \ + ORDER BY INDEX_NAME, SEQ_IN_INDEX", + sql_string(schema), + sql_string(table), + ); + log::debug!("[gaussdb-m][list_indexes] sql={sql}"); + + let timeout = agent_metadata_timeout(Some(config)); + + let result: db::QueryResult = session + .invoke_with_timeout( + "executeQuery", + serde_json::json!({ + "connection": config, + "database": database, + "schema": schema, + "sql": sql, + "maxRows": 1000, + "fetchSize": 1000, + "timeoutSecs": 60, + }), + timeout, + ) + .await?; + + log::debug!("[gaussdb-m][list_indexes] row_count={}", result.rows.len()); + + let col_index: std::collections::HashMap = + result.columns.iter().enumerate().map(|(i, name)| (name.to_uppercase(), i)).collect(); + let get_str = |row: &[serde_json::Value], key: &str| -> String { + let upper = key.to_uppercase(); + col_index.get(&upper).and_then(|&i| row.get(i)).and_then(|v| v.as_str()).unwrap_or("").to_string() + }; + let get_i64 = |row: &[serde_json::Value], key: &str| -> Option { + let upper = key.to_uppercase(); + col_index.get(&upper).and_then(|&i| row.get(i)).and_then(|v| v.as_i64()) + }; + + let mut indexes: Vec = Vec::new(); + let mut current_name = String::new(); + let mut current_columns: Vec = Vec::new(); + let mut current_is_expression: Vec = Vec::new(); + let mut current_non_unique: i64 = 1; + let mut current_index_type: Option = None; + let mut current_comment: Option = None; + + for row in &result.rows { + let name = get_str(row, "INDEX_NAME"); + if name.is_empty() { + log::debug!("[gaussdb-m][list_indexes] skipping row with empty INDEX_NAME: {:?}", row); + continue; + } + + if name != current_name { + // Finalize previous index + if !current_name.is_empty() { + let is_primary = + current_name.to_lowercase().ends_with("_pkey") || current_name.eq_ignore_ascii_case("primary"); + indexes.push(db::IndexInfo { + name: current_name.clone(), + columns: current_columns.clone(), + is_unique: current_non_unique == 0, + is_primary, + filter: None, + index_type: current_index_type.clone(), + included_columns: None, + comment: current_comment.clone(), + key_is_expression: current_is_expression.clone(), + }); + } + // Start new index + current_name = name; + current_columns = Vec::new(); + current_is_expression = Vec::new(); + current_non_unique = get_i64(row, "NON_UNIQUE").unwrap_or(1); + current_index_type = { + let t = get_str(row, "INDEX_TYPE"); + if t.is_empty() { + None + } else { + Some(t) + } + }; + current_comment = { + let c = get_str(row, "INDEX_COMMENT"); + if c.is_empty() { + None + } else { + Some(c) + } + }; + } + + // Build column name with prefix/sub_part suffix + let column_name = get_str(row, "COLUMN_NAME"); + if column_name.is_empty() { + continue; + } + + // SUB_PART is bigint in STATISTICS; JDBC plugin sends it as JSON number, + // but may also be null or string. + let sub_part: String = col_index + .get("SUB_PART") + .and_then(|&i| row.get(i)) + .and_then(|v| { + if v.is_null() { + None + } else if let Some(n) = v.as_i64() { + Some(n.to_string()) + } else { + v.as_str().map(|s| s.to_string()) + } + }) + .unwrap_or_default(); + + if !sub_part.is_empty() && sub_part != "0" && sub_part != "NULL" { + // Prefix index: COLUMN_NAME(SUB_PART) like "name(10)" + current_columns.push(format!("{}({})", column_name, sub_part)); + } else { + current_columns.push(column_name); + } + current_is_expression.push(false); + } + + // Finalize last index + if !current_name.is_empty() { + let is_primary = current_name.to_lowercase().ends_with("_pkey") || current_name.eq_ignore_ascii_case("primary"); + indexes.push(db::IndexInfo { + name: current_name, + columns: current_columns, + is_unique: current_non_unique == 0, + is_primary, + filter: None, + index_type: current_index_type, + included_columns: None, + comment: current_comment, + key_is_expression: current_is_expression, + }); + } + + Ok(indexes) +} + fn gaussdb_m_view_object_source_sql( config: &ConnectionConfig, _database: &str, diff --git a/crates/dbx-core/src/table_structure_sql.rs b/crates/dbx-core/src/table_structure_sql.rs index 3cf1833300..6d1e8c9bbd 100644 --- a/crates/dbx-core/src/table_structure_sql.rs +++ b/crates/dbx-core/src/table_structure_sql.rs @@ -20,6 +20,8 @@ pub use create_table::build_create_table_sql; pub use sqlite_rebuild::{apply_sqlite_table_structure_change, preview_sqlite_table_structure_change}; pub use types::*; +use crate::models::connection::DatabaseType; + use columns::build_column_sql; use comments::build_table_comment_sql; use foreign_keys::build_foreign_key_sql; @@ -27,7 +29,12 @@ use indexes::build_index_sql; use triggers::build_trigger_sql; use validation::{validate_concurrent_index_scope, validate_draft}; -pub fn build_table_structure_change_sql(options: TableStructureSqlOptions) -> TableStructureSqlResult { +pub fn build_table_structure_change_sql(mut options: TableStructureSqlOptions) -> TableStructureSqlResult { + // GaussDB M-mode uses MySQL-compatible SQL dialect with backtick quoting. + // Map to StructureDialect::Mysql so DDL is generated correctly. + if options.is_gaussdb_m_mode { + options.database_type = Some(DatabaseType::Mysql); + } // Fail closed: an unsupported concurrent-index request (existing index, or // partitioned parent table) must never degrade into blocking index DDL // behind the caller's back, so the whole plan is refused up front. diff --git a/crates/dbx-core/src/table_structure_sql/column_format.rs b/crates/dbx-core/src/table_structure_sql/column_format.rs index 289dc528fa..dbdc615748 100644 --- a/crates/dbx-core/src/table_structure_sql/column_format.rs +++ b/crates/dbx-core/src/table_structure_sql/column_format.rs @@ -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(" ") diff --git a/crates/dbx-core/src/table_structure_sql/comments.rs b/crates/dbx-core/src/table_structure_sql/comments.rs index 74f7a8ff90..394699f1f3 100644 --- a/crates/dbx-core/src/table_structure_sql/comments.rs +++ b/crates/dbx-core/src/table_structure_sql/comments.rs @@ -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 diff --git a/crates/dbx-core/src/table_structure_sql/create_table.rs b/crates/dbx-core/src/table_structure_sql/create_table.rs index e23b679cac..d86c64495a 100644 --- a/crates/dbx-core/src/table_structure_sql/create_table.rs +++ b/crates/dbx-core/src/table_structure_sql/create_table.rs @@ -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))); } diff --git a/crates/dbx-core/src/table_structure_sql/dialect.rs b/crates/dbx-core/src/table_structure_sql/dialect.rs index 134190f62c..6ee6c3d148 100644 --- a/crates/dbx-core/src/table_structure_sql/dialect.rs +++ b/crates/dbx-core/src/table_structure_sql/dialect.rs @@ -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) -> 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 Some(DatabaseType::ManticoreSearch), StructureDialect::Informix => Some(DatabaseType::Informix), StructureDialect::Questdb => Some(DatabaseType::Questdb), + StructureDialect::GaussdbM => Some(DatabaseType::Mysql), StructureDialect::Unsupported => None, } } diff --git a/crates/dbx-core/src/table_structure_sql/indexes.rs b/crates/dbx-core/src/table_structure_sql/indexes.rs index c04163c266..02e6fc3250 100644 --- a/crates/dbx-core/src/table_structure_sql/indexes.rs +++ b/crates/dbx-core/src/table_structure_sql/indexes.rs @@ -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) -> Vec { - 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))); } diff --git a/crates/dbx-core/src/table_structure_sql/sqlite_rebuild.rs b/crates/dbx-core/src/table_structure_sql/sqlite_rebuild.rs index c5980c9599..32e5b1f397 100644 --- a/crates/dbx-core/src/table_structure_sql/sqlite_rebuild.rs +++ b/crates/dbx-core/src/table_structure_sql/sqlite_rebuild.rs @@ -1517,6 +1517,7 @@ mod tests { table_comment: None, original_table_comment: None, partitioned: false, + is_gaussdb_m_mode: false, } } diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs index d23cb805b7..33ab771a7b 100644 --- a/crates/dbx-core/src/table_structure_sql/tests.rs +++ b/crates/dbx-core/src/table_structure_sql/tests.rs @@ -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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::new()); @@ -5372,3 +5474,269 @@ fn non_postgres_concurrent_flag_is_ignored() { ); } } + +// --------------------------------------------------------------------------- +// GaussDB M-mode index tests +// --------------------------------------------------------------------------- + +fn gaussdb_m_options(columns: Vec) -> 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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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`)")); +} diff --git a/crates/dbx-core/src/table_structure_sql/types.rs b/crates/dbx-core/src/table_structure_sql/types.rs index 812eaa8d3d..7da77515c0 100644 --- a/crates/dbx-core/src/table_structure_sql/types.rs +++ b/crates/dbx-core/src/table_structure_sql/types.rs @@ -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)] diff --git a/crates/dbx-core/src/table_structure_sql/util.rs b/crates/dbx-core/src/table_structure_sql/util.rs index 2a097aafab..03403869c0 100644 --- a/crates/dbx-core/src/table_structure_sql/util.rs +++ b/crates/dbx-core/src/table_structure_sql/util.rs @@ -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('`', "``")) diff --git a/crates/dbx-core/tests/live_mysql_generated_columns.rs b/crates/dbx-core/tests/live_mysql_generated_columns.rs index dd3ecf7703..1897afb61f 100644 --- a/crates/dbx-core/tests/live_mysql_generated_columns.rs +++ b/crates/dbx-core/tests/live_mysql_generated_columns.rs @@ -47,6 +47,7 @@ fn change_options(table_name: &str, columns: Vec) -> Ta table_comment: None, original_table_comment: None, partitioned: false, + is_gaussdb_m_mode: false, } } diff --git a/crates/dbx-core/tests/live_postgres_concurrent_index.rs b/crates/dbx-core/tests/live_postgres_concurrent_index.rs index 2a4b10ebd1..6ef1157dde 100644 --- a/crates/dbx-core/tests/live_postgres_concurrent_index.rs +++ b/crates/dbx-core/tests/live_postgres_concurrent_index.rs @@ -60,6 +60,7 @@ async fn live_postgres_concurrent_index_builds_valid_index() { table_comment: None, original_table_comment: None, partitioned: false, + is_gaussdb_m_mode: false, }); assert!(result.warnings.is_empty(), "{:?}", result.warnings); assert_eq!( @@ -189,6 +190,7 @@ async fn live_postgres_partitioned_parent_concurrent_request_rejected() { table_comment: None, original_table_comment: None, partitioned: true, + is_gaussdb_m_mode: false, }); assert_eq!( result.warnings, diff --git a/crates/dbx-core/tests/live_sqlserver_completion.rs b/crates/dbx-core/tests/live_sqlserver_completion.rs index 18eec3361a..74d211891e 100644 --- a/crates/dbx-core/tests/live_sqlserver_completion.rs +++ b/crates/dbx-core/tests/live_sqlserver_completion.rs @@ -1006,6 +1006,7 @@ async fn live_sqlserver_table_structure_default_changes_drop_existing_constraint table_comment: None, original_table_comment: None, partitioned: false, + is_gaussdb_m_mode: false, }); assert_eq!(result.warnings, Vec::::new()); assert_eq!(result.statements.len(), 4);