diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 3178f5baeb..99435e918a 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -37,6 +37,7 @@ import { invalidateObjectMetadataCache, loadObjectMetadataFacet, type ObjectMeta import { invalidateTableMetadataCache } from "@/lib/metadata/tableMetadataCache"; import { type BuildTableStructureChangeSqlOptions, type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql"; import { buildMysqlAutoIncrementCounterStatement, canEditMysqlAutoIncrementCounter, refreshMysqlAutoIncrementCounterDraft } from "@/lib/table/mysqlAutoIncrementCounter"; +import { mysqlTableCollationSql, parseMysqlTableCollation } from "@/lib/table/mysqlTableCollation"; import { MYSQL_STORAGE_ENGINES_SQL, mysqlTableEngineSql, mysqlTableEngineSqlOption, parseMysqlTableEngineMetadata, refreshMysqlTableEngineDraft, supportsMysqlTableEngine } from "@/lib/table/mysqlTableEngine"; import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/lib/table/tableColumnTemplates"; import { getMysqlDataTypeHelp } from "@/lib/table/mysqlDataTypeHelp"; @@ -928,6 +929,9 @@ const showCharacterSet = computed(() => structureDialect.value === "mysql"); const serverCharsetMetadata = ref(); const charsetMetadataLoading = ref(false); +// The table's own default collation, used only to keep inherited charsets out of the +// generated DDL. Columns keep the real values MySQL reports so the pickers stay filled. +const mysqlTableDefaultCollation = ref(""); const mysqlCharsetOptions = computed(() => { const meta = serverCharsetMetadata.value; @@ -956,6 +960,22 @@ async function loadCharsetMetadata() { } } +async function loadMysqlTableDefaultCollation() { + if (!showCharacterSet.value || isCreateMode.value || !props.connectionId || !props.database || !props.tableName) { + mysqlTableDefaultCollation.value = ""; + return; + } + try { + await store.ensureConnected(props.connectionId); + const result = await api.executeQuery(props.connectionId, props.database, mysqlTableCollationSql(props.database, props.tableName), undefined, undefined, { maxRows: 1 }); + mysqlTableDefaultCollation.value = parseMysqlTableCollation(result); + } catch { + // Optional metadata: without it the DDL just spells out the charset the column + // already has, which is equivalent SQL — never block the editor on this lookup. + mysqlTableDefaultCollation.value = ""; + } +} + function onCharsetChange(column: EditableStructureColumn, charset: string) { column.characterSet = charset; // If the collation is no longer valid for the new charset, clear it so the @@ -1614,6 +1634,7 @@ function structureChangeOptions(): BuildTableStructureChangeSqlOptions { tableComment: tableComment.value, originalTableComment: isCreateMode.value ? undefined : originalTableComment.value, mysqlEngine: mysqlTableEngineSqlOption({ value: mysqlTableEngine.value, originalValue: originalMysqlTableEngine.value }, isCreateMode.value, supportsMysqlEngine.value && !mysqlTableEngineLoading.value && !mysqlTableEngineLoadError.value), + tableCollation: mysqlTableDefaultCollation.value || undefined, partitioned: isPartitionedParent.value, isGaussdbMMode: connection.value?.driver_profile?.toLowerCase() === "gaussdb-m", }; @@ -1772,6 +1793,7 @@ function resetState() { mysqlTableEngineLoadRequestId += 1; mysqlTableEngineLoading.value = false; mysqlTableEngineLoadError.value = ""; + mysqlTableDefaultCollation.value = ""; tableOwner.value = ""; originalTableOwner.value = ""; tableOwnerLoadRequestId += 1; @@ -2040,6 +2062,7 @@ async function loadStructure( // Load live charset/collation metadata from the MySQL server so the column // editor shows the correct options for the server version. void loadCharsetMetadata(); + void loadMysqlTableDefaultCollation(); const nextColumnDrafts = createColumnDrafts(nextColumns, databaseType.value); const hydratedColumnDrafts = supportsCharacterLengthUnits.value && options.characterLengthUnitsAfterSave ? restoreCharacterLengthUnitsAfterSave(databaseType.value, nextColumnDrafts, options.characterLengthUnitsAfterSave) : nextColumnDrafts; columns.value = applyStoredLocalColumnOrder(hydratedColumnDrafts); @@ -3862,6 +3885,7 @@ watch( originalMysqlTableEngine, mysqlTableEngineLoading, mysqlTableEngineLoadError, + mysqlTableDefaultCollation, tableOwner, ddlDraft, columns, diff --git a/apps/desktop/src/lib/__tests__/table/mysqlTableCollation.spec.ts b/apps/desktop/src/lib/__tests__/table/mysqlTableCollation.spec.ts new file mode 100644 index 0000000000..bf931b0c93 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/table/mysqlTableCollation.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import type { QueryResult } from "@/types/database"; +import { mysqlTableCollationSql, parseMysqlTableCollation } from "@/lib/table/mysqlTableCollation"; + +function result(columns: string[], rows: QueryResult["rows"]): QueryResult { + return { columns, rows, affected_rows: 0, execution_time_ms: 1 }; +} + +describe("mysqlTableCollation", () => { + it("builds mode-independent lookup SQL for quoted and Unicode names", () => { + const sql = mysqlTableCollationSql("db'x", "订单\\archive"); + expect(sql).toContain("TABLE_COLLATION AS table_collation"); + expect(sql).toContain("TABLE_SCHEMA = CONVERT(X'64622778' USING utf8mb4)"); + expect(sql).toContain("TABLE_NAME = CONVERT(X'e8aea2e58d955c61726368697665' USING utf8mb4)"); + expect(sql).not.toContain("db'x"); + }); + + it("reads the reported collation regardless of column-name casing", () => { + expect(parseMysqlTableCollation(result(["TABLE_COLLATION"], [["utf8mb4_0900_ai_ci"]]))).toBe("utf8mb4_0900_ai_ci"); + expect(parseMysqlTableCollation(result(["table_collation"], [[" utf8mb4_bin "]]))).toBe("utf8mb4_bin"); + }); + + it("falls back to no table default when the server reports nothing usable", () => { + // MySQL reports TABLE_COLLATION as NULL for views, and the lookup itself may fail. + expect(parseMysqlTableCollation(result(["table_collation"], [[null]]))).toBe(""); + expect(parseMysqlTableCollation(result(["table_collation"], []))).toBe(""); + expect(parseMysqlTableCollation(result(["engine"], [["InnoDB"]]))).toBe(""); + expect(parseMysqlTableCollation(undefined)).toBe(""); + }); +}); diff --git a/apps/desktop/src/lib/table/mysqlTableCollation.ts b/apps/desktop/src/lib/table/mysqlTableCollation.ts new file mode 100644 index 0000000000..6b73bb72d9 --- /dev/null +++ b/apps/desktop/src/lib/table/mysqlTableCollation.ts @@ -0,0 +1,23 @@ +import type { QueryResult } from "@/types/database"; +import { mysqlUtf8Literal } from "@/lib/table/mysqlTableEngine"; + +/** + * Reads the table's default collation. MySQL reports the *effective* collation on every + * character column and never records whether it was written out explicitly, so a column + * matching this value is one that simply inherits the table default. The structure editor + * passes it to the SQL builder, which then leaves the redundant `CHARACTER SET`/`COLLATE` + * clauses out of the generated DDL — the column metadata itself keeps the real values so + * the charset and collation pickers can show what the column currently uses. + */ +export function mysqlTableCollationSql(database: string, table: string): string { + return ["SELECT TABLE_COLLATION AS table_collation", "FROM information_schema.TABLES", `WHERE TABLE_SCHEMA = ${mysqlUtf8Literal(database)}`, ` AND TABLE_NAME = ${mysqlUtf8Literal(table)}`, "LIMIT 1"].join("\n"); +} + +/** Empty string when the server reported nothing usable — MySQL returns NULL for views. */ +export function parseMysqlTableCollation(result: QueryResult | undefined): string { + if (!result) return ""; + const index = result.columns.findIndex((column) => column.trim().toLowerCase() === "table_collation"); + if (index < 0) return ""; + const value = result.rows[0]?.[index]; + return value === null || value === undefined ? "" : String(value).trim(); +} diff --git a/apps/desktop/src/lib/table/mysqlTableEngine.ts b/apps/desktop/src/lib/table/mysqlTableEngine.ts index 721b99a4a3..b29e90fe34 100644 --- a/apps/desktop/src/lib/table/mysqlTableEngine.ts +++ b/apps/desktop/src/lib/table/mysqlTableEngine.ts @@ -13,7 +13,9 @@ export interface MysqlTableEngineDraft { export const MYSQL_STORAGE_ENGINES_SQL = "SHOW ENGINES"; -function mysqlUtf8Literal(value: string): string { +/** Encodes a schema/table name as a byte literal so non-ASCII names survive whatever + * charset the session happens to use. Shared with the table collation lookup. */ +export function mysqlUtf8Literal(value: string): string { const bytes = new TextEncoder().encode(value); const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); return `CONVERT(X'${hex}' USING utf8mb4)`; diff --git a/apps/desktop/src/lib/table/tableStructureEditorSql.ts b/apps/desktop/src/lib/table/tableStructureEditorSql.ts index 319d546843..3560e2c50b 100644 --- a/apps/desktop/src/lib/table/tableStructureEditorSql.ts +++ b/apps/desktop/src/lib/table/tableStructureEditorSql.ts @@ -84,6 +84,10 @@ export interface BuildTableStructureChangeSqlOptions { tableComment?: string; originalTableComment?: string; mysqlEngine?: string; + /** MySQL only: the table's current default collation. Columns whose collation merely + * matches it inherit the table default, so the backend leaves their redundant + * `CHARACTER SET`/`COLLATE` clauses out of the generated DDL. */ + tableCollation?: string; /** The target table is a PostgreSQL partitioned parent (`relkind = 'p'`); * the backend rejects `CREATE INDEX CONCURRENTLY` on such tables (fail * closed) instead of downgrading to a blocking `CREATE INDEX`. */ diff --git a/crates/dbx-core/src/db/mysql.rs b/crates/dbx-core/src/db/mysql.rs index cf0e55ab8d..23e48a1e1b 100644 --- a/crates/dbx-core/src/db/mysql.rs +++ b/crates/dbx-core/src/db/mysql.rs @@ -282,7 +282,7 @@ fn nonblank(value: String) -> Option { } async fn query_first_nonblank_string(conn: &mut mysql_async::Conn, sql: &str) -> Option { - // MySQL reports nullable metadata such as TABLE_COLLATION as NULL for views. + // MySQL reports nullable metadata such as `@@version_comment` as NULL. // Reading it as String makes mysql_async panic during row conversion. match query_first_column::(conn, sql).await { Ok(value) => value.and_then(nonblank), @@ -3697,11 +3697,9 @@ pub async fn list_completion_objects(pool: &MySqlPool, database: &str) -> Result } fn columns_sql(database: &str, table: &str) -> String { - // Query only information_schema.COLUMNS and fetch TABLE_COLLATION separately via - // `table_collation_sql`. A LEFT JOIN onto information_schema.TABLES triggers a - // catastrophic plan on MySQL 5.7 (observed ~8s vs ~1ms without the join), because 5.7's - // TABLES metadata is materialized per-query with poor predicate pushdown. The separate - // lookup matches the `get_columns_show` fallback path and keeps results identical. + // Query only information_schema.COLUMNS. A LEFT JOIN onto information_schema.TABLES + // triggers a catastrophic plan on MySQL 5.7 (observed ~8s vs ~1ms without the join), + // because 5.7's TABLES metadata is materialized per-query with poor predicate pushdown. format!( "SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, \ COLUMN_COMMENT, COLUMN_KEY, NUMERIC_PRECISION, NUMERIC_SCALE, CHARACTER_MAXIMUM_LENGTH, \ @@ -3802,28 +3800,6 @@ fn apply_mysql_generated_column_expressions(columns: &mut [ColumnInfo], expressi } } -fn table_collation_sql(database: &str, table: &str) -> String { - format!( - "SELECT TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = {} AND TABLE_NAME = {} LIMIT 1", - quote_value(database), - quote_value(table), - ) -} - -fn normalize_mysql_column_charset_metadata(columns: &mut [ColumnInfo], table_collation: Option<&str>) { - let Some(table_collation) = table_collation.filter(|value| !value.trim().is_empty()) else { - return; - }; - for column in columns { - if column.collation.as_deref().is_some_and(|value| value.eq_ignore_ascii_case(table_collation)) { - // MySQL reports effective values and does not preserve whether an - // equivalent table-default collation was explicitly written. - column.character_set = None; - column.collation = None; - } - } -} - /// Attempt to reverse CP1252→UTF-8 double-encoding. /// /// When Chinese text is written to MySQL through a connection with the wrong @@ -3964,14 +3940,6 @@ where }; let rows: Vec = result.collect_and_drop().await.map_err(|e| e.to_string())?; - // When database is empty the COLUMNS query returns no rows, so the - // function falls through to get_columns_show and this code path is - // never reached. Skip the collation lookup to avoid a pointless query. - let table_collation = if database.trim().is_empty() { - None - } else { - query_first_nonblank_string(&mut conn, &table_collation_sql(database, table)).await - }; let mut columns: Vec = rows .iter() .filter_map(|row| { @@ -4019,7 +3987,6 @@ where } enrich_mysql_generated_column_expressions(&mut conn, database, table, &mut columns).await; - normalize_mysql_column_charset_metadata(&mut columns, table_collation.as_deref()); Ok(columns) } @@ -4037,11 +4004,6 @@ where result.collect_and_drop().await.map_err(|e| e.to_string())? } }; - let table_collation = if database.trim().is_empty() { - None - } else { - query_first_nonblank_string(&mut conn, &table_collation_sql(database, table)).await - }; let mut columns: Vec = rows .iter() .filter_map(|row| { @@ -4076,7 +4038,6 @@ where }) .collect(); enrich_mysql_generated_column_expressions(&mut conn, database, table, &mut columns).await; - normalize_mysql_column_charset_metadata(&mut columns, table_collation.as_deref()); Ok(columns) } @@ -7106,12 +7067,12 @@ mod tests { } #[test] - fn mysql_columns_sql_uses_column_key_and_table_default_collation() { + fn mysql_columns_sql_uses_column_key_for_primary_keys_without_join() { let sql = columns_sql("app", "users"); assert!(sql.contains("information_schema.COLUMNS")); - // TABLE_COLLATION is fetched separately via `table_collation_sql`; the LEFT JOIN onto - // information_schema.TABLES is intentionally avoided to keep MySQL 5.7 fast. + // The LEFT JOIN onto information_schema.TABLES is intentionally avoided to keep + // MySQL 5.7 fast. assert!(!sql.contains("information_schema.TABLES")); assert!(!sql.contains("TABLE_COLLATION")); assert!(!sql.contains("KEY_COLUMN_USAGE")); @@ -7175,54 +7136,13 @@ mod tests { } #[test] - fn mysql_nullable_table_collation_uses_optional_string_conversion() { + fn mysql_nullable_metadata_uses_optional_string_conversion() { let collation = mysql_async::from_value_opt::>(mysql_async::Value::NULL) .expect("Option must accept NULL MySQL metadata values"); assert_eq!(collation, None); } - #[test] - fn mysql_column_charset_metadata_clears_values_matching_table_default() { - let mut columns = vec![ - ColumnInfo { - name: "inherited_name".to_string(), - character_set: Some("utf8mb4".to_string()), - collation: Some("utf8mb4_unicode_ci".to_string()), - ..Default::default() - }, - ColumnInfo { - name: "explicit_other".to_string(), - character_set: Some("latin1".to_string()), - collation: Some("latin1_bin".to_string()), - ..Default::default() - }, - ColumnInfo { name: "numeric_value".to_string(), ..Default::default() }, - ]; - - normalize_mysql_column_charset_metadata(&mut columns, Some("utf8mb4_unicode_ci")); - - assert_eq!((columns[0].character_set.as_deref(), columns[0].collation.as_deref()), (None, None)); - assert_eq!(columns[1].character_set.as_deref(), Some("latin1")); - assert_eq!(columns[1].collation.as_deref(), Some("latin1_bin")); - assert_eq!((columns[2].character_set.as_deref(), columns[2].collation.as_deref()), (None, None)); - } - - #[test] - fn mysql_column_charset_metadata_preserves_values_without_table_default() { - let mut columns = vec![ColumnInfo { - name: "name".to_string(), - character_set: Some("utf8mb4".to_string()), - collation: Some("utf8mb4_unicode_ci".to_string()), - ..Default::default() - }]; - - normalize_mysql_column_charset_metadata(&mut columns, None); - - assert_eq!(columns[0].character_set.as_deref(), Some("utf8mb4")); - assert_eq!(columns[0].collation.as_deref(), Some("utf8mb4_unicode_ci")); - } - #[test] fn parse_mysql_enum_values_preserves_mysql_literal_edges() { assert_eq!( diff --git a/crates/dbx-core/src/db_admin_sql.rs b/crates/dbx-core/src/db_admin_sql.rs index 12bc866a2d..dee0060b24 100644 --- a/crates/dbx-core/src/db_admin_sql.rs +++ b/crates/dbx-core/src/db_admin_sql.rs @@ -2403,6 +2403,7 @@ mod tests { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(create.warnings, Vec::::new()); assert_eq!( diff --git a/crates/dbx-core/src/table_structure_sql.rs b/crates/dbx-core/src/table_structure_sql.rs index 68825ba6d1..cb64f2c8b2 100644 --- a/crates/dbx-core/src/table_structure_sql.rs +++ b/crates/dbx-core/src/table_structure_sql.rs @@ -31,6 +31,7 @@ pub(crate) use util::{oracle_new_object_reference, sqlserver_unicode_string_lite use crate::models::connection::DatabaseType; +use column_format::strip_inherited_mysql_column_charsets; use columns::{build_column_sql, validate_primary_key_change_scope}; use comments::build_table_comment_sql; use foreign_keys::build_foreign_key_sql; @@ -49,6 +50,7 @@ pub fn build_table_structure_change_sql(mut options: TableStructureSqlOptions) - if options.is_gaussdb_m_mode { options.database_type = Some(DatabaseType::Mysql); } + strip_inherited_mysql_column_charsets(&mut options); let primary_key_errors = validate_primary_key_change_scope(&options); if !primary_key_errors.is_empty() { return TableStructureSqlResult { statements: Vec::new(), warnings: primary_key_errors }; 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 643a619581..ffa7b0b4a9 100644 --- a/crates/dbx-core/src/table_structure_sql/column_format.rs +++ b/crates/dbx-core/src/table_structure_sql/column_format.rs @@ -1,7 +1,41 @@ use super::dialect::{is_oracle_like, StructureDialect}; -use super::types::EditableStructureColumn; +use super::types::{EditableStructureColumn, TableStructureSqlOptions}; use super::util::{clean, format_default_for_sql, normalize_default, quote_ident, quote_string}; +/// Drops the `CHARACTER SET` / `COLLATE` inputs of MySQL columns that merely +/// inherit the table's default collation, so the generated DDL does not spell +/// out clauses the server would apply anyway. +/// +/// MySQL reports the *effective* collation of every character column and never +/// records whether it was written out explicitly, so "equals the table default" +/// is the only signal available. Omitting the clauses is equivalent to keeping +/// them: a column definition without `CHARACTER SET` takes the table default, +/// which is exactly the value being dropped here. This runs on the DDL inputs +/// only — introspection (`get_columns`) still reports the real values so the +/// structure editor can show the column's current charset and collation. +/// +/// The original snapshot is normalized alongside the draft so that an untouched +/// column does not register as a charset change and trigger a needless `MODIFY`. +pub(super) fn strip_inherited_mysql_column_charsets(options: &mut TableStructureSqlOptions) { + let Some(table_collation) = options.table_collation.as_deref().map(str::trim).filter(|value| !value.is_empty()) + else { + return; + }; + let inherits = |collation: &str| collation.trim().eq_ignore_ascii_case(table_collation); + for column in &mut options.columns { + if inherits(&column.collation) { + column.character_set = String::new(); + column.collation = String::new(); + } + if let Some(original) = column.original.as_mut() { + if original.collation.as_deref().is_some_and(inherits) { + original.character_set = None; + original.collation = None; + } + } + } +} + pub(super) fn column_definition(dialect: StructureDialect, column: &EditableStructureColumn) -> String { let data_type = column_data_type(dialect, column); let mut parts = vec![quote_ident(dialect, &column.name), data_type]; 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 e9663c79dc..d8931462a5 100644 --- a/crates/dbx-core/src/table_structure_sql/create_table.rs +++ b/crates/dbx-core/src/table_structure_sql/create_table.rs @@ -1,6 +1,6 @@ use super::column_format::{ column_data_type, column_extra_clause, has_dameng_identity, is_dameng_identity_compatible_type, - is_mysql_character_data_type, is_mysql_timestamp_type, + is_mysql_character_data_type, is_mysql_timestamp_type, strip_inherited_mysql_column_charsets, }; use super::comments::{build_sqlserver_column_comment_sql, build_sqlserver_table_comment_sql}; use super::dialect::{capabilities_for, database_label, StructureDialect}; @@ -26,6 +26,7 @@ pub fn build_create_table_sql(mut options: TableStructureSqlOptions) -> TableStr dialect = capabilities.dialect; } options.table_name = clean(&options.table_name); + strip_inherited_mysql_column_charsets(&mut options); let mut warnings = Vec::new(); warnings.extend(validate_mysql_engine(&options)); // Fail closed: a concurrent-index request on a partitioned parent (or on an 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 022806f67a..b6e6634fed 100644 --- a/crates/dbx-core/src/table_structure_sql/sqlite_rebuild.rs +++ b/crates/dbx-core/src/table_structure_sql/sqlite_rebuild.rs @@ -1519,6 +1519,7 @@ mod tests { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, } } diff --git a/crates/dbx-core/src/table_structure_sql/tests.rs b/crates/dbx-core/src/table_structure_sql/tests.rs index 4b2f37b30d..12aa0687c4 100644 --- a/crates/dbx-core/src/table_structure_sql/tests.rs +++ b/crates/dbx-core/src/table_structure_sql/tests.rs @@ -62,6 +62,7 @@ fn structure_change_options( mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, } } @@ -168,6 +169,7 @@ fn index_change_options( mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, } } @@ -246,6 +248,7 @@ fn builds_mysql_column_and_index_changes() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -462,6 +465,7 @@ fn builds_xugu_type_change_with_native_syntax() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -672,6 +676,7 @@ fn builds_mysql_unsigned_integer_column_with_length_before_attribute() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -707,6 +712,7 @@ fn doris_table_editor_renames_column_without_mysql_change_syntax() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -766,6 +772,7 @@ fn dameng_integer_column_omits_mysql_display_width() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -808,6 +815,7 @@ fn builds_highgo_foreign_key_changes_with_postgres_syntax() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -879,6 +887,7 @@ fn builds_informix_column_and_index_changes() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -935,6 +944,7 @@ fn oracle_does_not_generate_drop_sql_for_all_columns() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.statements, Vec::::new()); @@ -996,6 +1006,7 @@ fn oracle_create_table_preserves_character_length_units() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert!(result.statements[0].contains("BYTE_COL VARCHAR2(12 BYTE)")); @@ -1031,6 +1042,7 @@ fn oracle_create_table_uses_unquoted_identifiers_for_new_objects() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1067,6 +1079,7 @@ fn oracle_create_table_leaves_uppercase_regular_identifier_unquoted() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1102,6 +1115,7 @@ fn oracle_create_table_quotes_special_and_reserved_identifiers() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1140,6 +1154,7 @@ fn oracle_create_table_distinguishes_new_and_referenced_foreign_key_identifiers( mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1181,6 +1196,7 @@ fn oracle_existing_quoted_identifiers_keep_exact_spelling() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1213,6 +1229,7 @@ fn oracle_new_identifier_formatting_does_not_change_other_dialects() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new(), "{database_type:?}"); @@ -1303,6 +1320,7 @@ fn iris_drop_index_includes_table_name() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1344,6 +1362,7 @@ fn iris_ignores_comment_changes_but_keeps_supported_column_alters() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!( @@ -1425,6 +1444,7 @@ fn oracle_compatible_databases_keep_comment_on_sql() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new(), "{database_type:?}"); @@ -1459,6 +1479,7 @@ fn mysql_create_index_with_comment() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1492,6 +1513,7 @@ fn manticoresearch_builds_create_table_sql_only() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1535,6 +1557,7 @@ fn manticoresearch_builds_add_and_drop_column_sql() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1601,6 +1624,7 @@ fn gbase8a_uses_limited_mysql_ddl() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!( @@ -1671,6 +1695,7 @@ fn gbase8a_allows_mysql_style_column_reorder() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1706,6 +1731,7 @@ fn manticoresearch_does_not_drop_id_column() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.statements, Vec::::new()); @@ -1774,6 +1800,7 @@ fn manticoresearch_warns_when_existing_column_properties_change() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.statements, Vec::::new()); @@ -1809,6 +1836,7 @@ fn manticoresearch_ignores_mysql_column_options() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1847,6 +1875,7 @@ fn manticoresearch_builds_text_column_properties() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1877,6 +1906,7 @@ fn manticoresearch_builds_json_secondary_index_property() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1903,6 +1933,7 @@ fn mysql_create_unique_index_with_comment_and_btree() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1931,6 +1962,7 @@ fn mysql_create_functional_index_preserves_key_part_syntax() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1959,6 +1991,7 @@ fn mysql_add_timestamp_column_drops_invalid_precision() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -1987,6 +2020,7 @@ fn mysql_add_timestamp_column_preserves_valid_precision() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2022,6 +2056,7 @@ fn builds_postgres_create_table_with_comments_and_index() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2054,6 +2089,7 @@ fn quotes_expression_like_new_index_columns_without_provenance() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2126,6 +2162,7 @@ fn create_table_trims_table_name_whitespace_for_all_statements() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2163,6 +2200,7 @@ fn warns_for_sqlite_unsafe_column_changes() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.statements, Vec::::new()); @@ -2204,6 +2242,7 @@ fn qualifies_attached_sqlite_table_and_index_changes() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2273,6 +2312,7 @@ fn builds_rqlite_changes_with_sqlite_dialect() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2303,6 +2343,7 @@ fn builds_kingbase_add_column_without_column_keyword() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2368,6 +2409,7 @@ fn builds_mysql_column_reorder_statements() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2424,6 +2466,7 @@ fn mysql_add_column_before_existing_column_does_not_reorder_shifted_column() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2487,6 +2530,7 @@ fn mysql_existing_column_reorder_does_not_reorder_columns_shifted_by_prior_move( mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2562,6 +2606,7 @@ fn mysql_moving_first_column_to_end_uses_single_reorder_statement() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2587,6 +2632,7 @@ fn builds_sql_server_quoted_column_and_index_statements() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2618,6 +2664,7 @@ fn sqlserver_strips_mysql_display_width_from_fixed_integer_types() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2643,6 +2690,7 @@ fn sqlserver_strips_scale_from_float() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2668,6 +2716,7 @@ fn sqlserver_preserves_float_mantissa_bits() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2718,6 +2767,7 @@ fn sqlserver_default_changes_drop_old_constraints_with_isolated_batches() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2930,6 +2980,7 @@ fn sqlserver_unchanged_foreign_key_does_not_warn_when_saving_other_changes() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2960,6 +3011,7 @@ fn sqlserver_add_column_with_identity() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -2990,6 +3042,7 @@ fn dameng_add_column_with_identity() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -3014,6 +3067,7 @@ fn dameng_uppercases_lowercase_column_type() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -3045,6 +3099,7 @@ fn dameng_rejects_identity_on_incompatible_type() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.statements, Vec::::new()); @@ -3078,6 +3133,7 @@ fn sqlserver_rejects_identity_on_incompatible_type() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.statements, Vec::::new()); @@ -3114,6 +3170,7 @@ fn sqlserver_changed_foreign_key_still_warns_as_unsupported() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.statements, Vec::::new()); @@ -3155,6 +3212,7 @@ fn sqlserver_unchanged_identity_extra_does_not_mark_existing_column_changed() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -3196,6 +3254,7 @@ fn dameng_unchanged_identity_extra_does_not_mark_existing_column_changed() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -3450,6 +3509,7 @@ fn dameng_rejects_adding_second_identity_column() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.statements, Vec::::new()); @@ -3503,6 +3563,7 @@ fn sqlserver_existing_column_identity_change_warns_without_unchanged_foreign_key mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.statements, Vec::::new()); @@ -3535,6 +3596,7 @@ fn builds_duckdb_create_table_statements() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -3581,6 +3643,7 @@ fn builds_clickhouse_nullable_comment_and_reorder_statements() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -3628,6 +3691,7 @@ fn builds_h2_schema_qualified_existing_column_statements() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4568,6 +4632,7 @@ fn mysql_create_table_with_auto_increment() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4596,6 +4661,7 @@ fn mysql_create_table_keeps_column_charset_collation_and_comment() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4628,6 +4694,7 @@ fn mysql_compatible_databases_do_not_emit_mysql_column_charset_clauses() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4657,6 +4724,7 @@ fn mysql_create_table_with_on_update_current_timestamp() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4686,6 +4754,7 @@ fn postgres_create_table_with_identity() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4717,6 +4786,7 @@ fn dameng_create_table_with_identity() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4744,6 +4814,7 @@ fn dameng_create_table_preserves_character_length_units() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4779,6 +4850,7 @@ fn dameng_alter_column_preserves_character_length_unit() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4815,6 +4887,7 @@ fn dameng_rejects_multiple_identity_columns() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert!(result.statements.is_empty()); @@ -4844,6 +4917,7 @@ fn dameng_rejects_zero_identity_increment() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert!(result.statements.is_empty()); @@ -4874,6 +4948,7 @@ fn sqlserver_create_table_with_identity() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4899,6 +4974,7 @@ fn mysql_quotes_datetime_literal_default() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4924,6 +5000,7 @@ fn mysql_does_not_quote_current_timestamp() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4950,6 +5027,7 @@ fn mysql_does_not_quote_temporal_function_with_parens() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -4975,6 +5053,7 @@ fn mysql_date_literal_default_is_quoted() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5000,6 +5079,7 @@ fn mysql_time_literal_default_is_quoted() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5025,6 +5105,7 @@ fn non_temporal_types_are_not_quoted() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5151,6 +5232,7 @@ fn builds_mysql_foreign_key_changes() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5181,6 +5263,7 @@ fn builds_mysql_composite_foreign_key() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5214,6 +5297,7 @@ fn builds_oracle_foreign_key_with_supported_actions() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5251,6 +5335,7 @@ fn builds_oracle_foreign_key_replacement() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5287,6 +5372,7 @@ fn builds_mysql_trigger_changes() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5314,6 +5400,7 @@ fn builds_sqlserver_trigger_with_multiple_events() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5357,6 +5444,7 @@ fn rebuilds_changed_sqlserver_trigger_from_complete_metadata_source() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5394,6 +5482,7 @@ fn sqlserver_trigger_edit_restores_disabled_state() { partitioned: false, mysql_engine: None, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!( @@ -5437,6 +5526,7 @@ fn unchanged_postgres_trigger_does_not_block_column_rename() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5467,6 +5557,7 @@ fn changed_postgres_trigger_remains_unsupported() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert!(result.statements.is_empty()); @@ -5502,6 +5593,7 @@ fn rejects_editing_existing_oracle_trigger_without_complete_source() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert!(result.statements.is_empty()); @@ -5526,6 +5618,7 @@ fn builds_oracle_statement_trigger_without_row_clause() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5562,6 +5655,7 @@ fn drops_existing_oracle_trigger_without_reconstructing_it() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5583,6 +5677,7 @@ fn rejects_unsupported_oracle_compound_trigger_shape() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert!(result.statements.is_empty()); @@ -5608,6 +5703,7 @@ fn mysql_varchar_default_is_quoted() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5634,6 +5730,7 @@ fn mysql_char_default_is_quoted() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5659,6 +5756,7 @@ fn mysql_text_default_is_quoted() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5684,6 +5782,7 @@ fn mysql_enum_default_is_quoted() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5709,6 +5808,7 @@ fn mysql_int_default_is_not_quoted() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5840,6 +5940,7 @@ fn mysql_character_column_add_with_charset_collation() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5873,6 +5974,7 @@ fn mysql_numeric_column_omits_charset_collation_in_column_definition() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5916,6 +6018,7 @@ fn mysql_numeric_column_ignores_charset_collation_in_change_detection() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); // No ALTER should be emitted — charset/collation changes on @@ -5954,6 +6057,7 @@ fn mysql_character_column_detects_charset_collation_change() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -5997,6 +6101,7 @@ fn mysql_character_column_preserves_charset_collation_on_other_change() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -6005,6 +6110,305 @@ fn mysql_character_column_preserves_charset_collation_on_other_change() { vec!["ALTER TABLE `users` MODIFY COLUMN `name` varchar(255) CHARACTER SET `utf8mb4` COLLATE `utf8mb4_unicode_ci` DEFAULT 'guest';"] ); } +#[test] +fn mysql_inherited_column_charset_is_omitted_from_generated_ddl() { + // MySQL reports the effective collation of every character column, so a column + // that simply inherits the table default looks identical to one that spells the + // same collation out. Introspection keeps those values (the editor needs them to + // show the column's current charset), and the redundant clauses are dropped here, + // while generating the DDL. + let mut col = column("note"); + col.data_type = "varchar(50)".to_string(); + col.character_set = "utf8mb4".to_string(); + col.collation = "utf8mb4_0900_ai_ci".to_string(); + col.comment = "Free-form note".to_string(); + col.original = Some(ColumnInfo { + name: "note".to_string(), + data_type: "varchar(50)".to_string(), + is_nullable: true, + column_default: None, + is_primary_key: false, + extra: None, + comment: None, + character_set: Some("utf8mb4".to_string()), + collation: Some("utf8mb4_0900_ai_ci".to_string()), + }); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users".to_string(), + columns: vec![col], + indexes: Vec::new(), + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + mysql_engine: None, + partitioned: false, + is_gaussdb_m_mode: false, + table_collation: Some("utf8mb4_0900_ai_ci".to_string()), + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["ALTER TABLE `users` MODIFY COLUMN `note` varchar(50) COMMENT 'Free-form note';"] + ); +} + +#[test] +fn mysql_explicit_column_charset_survives_the_table_default_comparison() { + // A column whose collation differs from the table default must keep its clauses: + // MODIFY COLUMN replaces the whole definition, so dropping them would silently + // convert the column to the table's default character set. + let mut col = column("name"); + col.data_type = "varchar(50)".to_string(); + col.character_set = "utf8mb4".to_string(); + col.collation = "utf8mb4_unicode_ci".to_string(); + col.comment = "Display name".to_string(); + col.original = Some(ColumnInfo { + name: "name".to_string(), + data_type: "varchar(50)".to_string(), + is_nullable: true, + column_default: None, + is_primary_key: false, + extra: None, + comment: None, + character_set: Some("utf8mb4".to_string()), + collation: Some("utf8mb4_unicode_ci".to_string()), + }); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users".to_string(), + columns: vec![col], + indexes: Vec::new(), + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + mysql_engine: None, + partitioned: false, + is_gaussdb_m_mode: false, + table_collation: Some("utf8mb4_0900_ai_ci".to_string()), + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec![ + "ALTER TABLE `users` MODIFY COLUMN `name` varchar(50) CHARACTER SET `utf8mb4` COLLATE `utf8mb4_unicode_ci` COMMENT 'Display name';" + ] + ); +} + +#[test] +fn mysql_inherited_column_charset_does_not_register_as_a_change() { + // The original snapshot is normalized together with the draft, so a column left + // untouched by the user never looks like a charset edit and produces no ALTER. + let mut col = column("note"); + col.data_type = "varchar(50)".to_string(); + col.character_set = "utf8mb4".to_string(); + col.collation = "utf8mb4_0900_ai_ci".to_string(); + col.original = Some(ColumnInfo { + name: "note".to_string(), + data_type: "varchar(50)".to_string(), + is_nullable: true, + column_default: None, + is_primary_key: false, + extra: None, + comment: None, + character_set: Some("utf8mb4".to_string()), + collation: Some("utf8mb4_0900_ai_ci".to_string()), + }); + col.original_position = Some(0); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users".to_string(), + columns: vec![col], + indexes: Vec::new(), + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + mysql_engine: None, + partitioned: false, + is_gaussdb_m_mode: false, + table_collation: Some("utf8mb4_0900_ai_ci".to_string()), + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!(result.statements, Vec::::new()); +} + +#[test] +fn mysql_collation_switched_away_from_the_table_default_is_emitted() { + let mut col = column("note"); + col.data_type = "varchar(50)".to_string(); + col.character_set = "utf8mb4".to_string(); + col.collation = "utf8mb4_bin".to_string(); + col.original = Some(ColumnInfo { + name: "note".to_string(), + data_type: "varchar(50)".to_string(), + is_nullable: true, + column_default: None, + is_primary_key: false, + extra: None, + comment: None, + character_set: Some("utf8mb4".to_string()), + collation: Some("utf8mb4_0900_ai_ci".to_string()), + }); + col.original_position = Some(0); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users".to_string(), + columns: vec![col], + indexes: Vec::new(), + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + mysql_engine: None, + partitioned: false, + is_gaussdb_m_mode: false, + table_collation: Some("utf8mb4_0900_ai_ci".to_string()), + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec!["ALTER TABLE `users` MODIFY COLUMN `note` varchar(50) CHARACTER SET `utf8mb4` COLLATE `utf8mb4_bin`;"] + ); +} + +#[test] +fn mysql_column_charset_switched_to_the_table_default_drops_the_clause() { + // Omitting the clauses is equivalent to writing the table default out, so a column + // moved onto the table default still converts — it just does so implicitly. + let mut col = column("note"); + col.data_type = "varchar(50)".to_string(); + col.character_set = "utf8mb4".to_string(); + col.collation = "utf8mb4_0900_ai_ci".to_string(); + col.original = Some(ColumnInfo { + name: "note".to_string(), + data_type: "varchar(50)".to_string(), + is_nullable: true, + column_default: None, + is_primary_key: false, + extra: None, + comment: None, + character_set: Some("latin1".to_string()), + collation: Some("latin1_bin".to_string()), + }); + col.original_position = Some(0); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users".to_string(), + columns: vec![col], + indexes: Vec::new(), + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + mysql_engine: None, + partitioned: false, + is_gaussdb_m_mode: false, + table_collation: Some("utf8mb4_0900_ai_ci".to_string()), + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!(result.statements, vec!["ALTER TABLE `users` MODIFY COLUMN `note` varchar(50);"]); +} + +#[test] +fn mysql_column_charset_is_kept_when_the_table_default_is_unknown() { + // Without a table default there is nothing to compare against, so the real values + // reported by MySQL are written out rather than guessed away. + let mut col = column("note"); + col.data_type = "varchar(50)".to_string(); + col.character_set = "utf8mb4".to_string(); + col.collation = "utf8mb4_0900_ai_ci".to_string(); + col.comment = "Free-form note".to_string(); + col.original = Some(ColumnInfo { + name: "note".to_string(), + data_type: "varchar(50)".to_string(), + is_nullable: true, + column_default: None, + is_primary_key: false, + extra: None, + comment: None, + character_set: Some("utf8mb4".to_string()), + collation: Some("utf8mb4_0900_ai_ci".to_string()), + }); + + let result = build_table_structure_change_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users".to_string(), + columns: vec![col], + indexes: Vec::new(), + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + mysql_engine: None, + partitioned: false, + is_gaussdb_m_mode: false, + table_collation: None, + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec![ + "ALTER TABLE `users` MODIFY COLUMN `note` varchar(50) CHARACTER SET `utf8mb4` COLLATE `utf8mb4_0900_ai_ci` COMMENT 'Free-form note';" + ] + ); +} + +#[test] +fn mysql_create_table_omits_inherited_column_charset() { + let mut inherited = column("note"); + inherited.data_type = "varchar(50)".to_string(); + inherited.character_set = "utf8mb4".to_string(); + inherited.collation = "utf8mb4_0900_ai_ci".to_string(); + let mut explicit = column("name"); + explicit.data_type = "varchar(50)".to_string(); + explicit.character_set = "utf8mb4".to_string(); + explicit.collation = "utf8mb4_unicode_ci".to_string(); + + let result = build_create_table_sql(TableStructureSqlOptions { + database_type: Some(DatabaseType::Mysql), + schema: None, + table_name: "users".to_string(), + columns: vec![inherited, explicit], + indexes: Vec::new(), + foreign_keys: Vec::new(), + triggers: Vec::new(), + table_comment: None, + original_table_comment: None, + mysql_engine: None, + partitioned: false, + is_gaussdb_m_mode: false, + table_collation: Some("utf8mb4_0900_ai_ci".to_string()), + }); + + assert_eq!(result.warnings, Vec::::new()); + assert_eq!( + result.statements, + vec![ + "CREATE TABLE `users` (\n `note` varchar(50),\n `name` varchar(50) CHARACTER SET `utf8mb4` COLLATE `utf8mb4_unicode_ci`\n);" + ] + ); +} #[test] fn mysql_generated_column_preserves_expression_when_modified() { @@ -6145,6 +6549,7 @@ fn oscar_create_table_with_primary_key_and_comments() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -6338,6 +6743,7 @@ fn oscar_drop_index_with_schema_qualifier() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -6359,6 +6765,7 @@ fn oscar_table_comment_uses_comment_on_table() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -6442,6 +6849,7 @@ fn postgres_partitioned_parent_concurrent_request_rejected() { mysql_engine: None, partitioned: true, is_gaussdb_m_mode: false, + table_collation: None, }); // Fail closed: PostgreSQL rejects CREATE INDEX CONCURRENTLY on a @@ -6473,6 +6881,7 @@ fn postgres_partitioned_parent_plain_index_unchanged() { mysql_engine: None, partitioned: true, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -6516,6 +6925,7 @@ fn postgres_create_table_partitioned_concurrent_request_rejected() { mysql_engine: None, partitioned: true, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!( @@ -6643,6 +7053,7 @@ fn postgres_create_table_concurrent_index() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -6759,6 +7170,7 @@ fn gaussdb_m_options(columns: Vec) -> TableStructureSql mysql_engine: None, partitioned: false, is_gaussdb_m_mode: true, + table_collation: None, } } @@ -6990,6 +7402,7 @@ fn gaussdb_m_rebuild_index_unchanged_type_does_not_rebuild() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: true, + table_collation: None, }; let result = build_table_structure_change_sql(options); assert!(result.warnings.is_empty()); @@ -7037,6 +7450,7 @@ fn mysql_create_table_nullable_timestamp_without_default_gets_explicit_null() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -7066,6 +7480,7 @@ fn mysql_create_table_nullable_timestamp_with_default_still_gets_explicit_null() mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -7093,6 +7508,7 @@ fn mysql_create_table_nullable_datetime_does_not_gain_null_keyword() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); @@ -7120,6 +7536,7 @@ fn mysql_add_column_nullable_timestamp_without_default_gets_explicit_null() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); diff --git a/crates/dbx-core/src/table_structure_sql/types.rs b/crates/dbx-core/src/table_structure_sql/types.rs index c53931ac11..645c287868 100644 --- a/crates/dbx-core/src/table_structure_sql/types.rs +++ b/crates/dbx-core/src/table_structure_sql/types.rs @@ -216,6 +216,14 @@ pub struct TableStructureSqlOptions { pub original_table_comment: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub mysql_engine: Option, + /// MySQL only: the table's current default collation + /// (`information_schema.TABLES.TABLE_COLLATION`). A column whose collation + /// merely matches it inherits the table default, so its `CHARACTER SET` / + /// `COLLATE` clauses are redundant and are dropped from the generated DDL. + /// Introspection keeps reporting the column's real values, which is what + /// the structure editor renders in its charset/collation pickers. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub table_collation: Option, /// Whether the target table is a partitioned parent table (PostgreSQL /// `relkind = 'p'`). PostgreSQL rejects `CREATE INDEX CONCURRENTLY` on /// partitioned parents, so the builder refuses such a request up front diff --git a/crates/dbx-core/tests/live_mysql_generated_columns.rs b/crates/dbx-core/tests/live_mysql_generated_columns.rs index 642f9d7af8..8deaac0c3d 100644 --- a/crates/dbx-core/tests/live_mysql_generated_columns.rs +++ b/crates/dbx-core/tests/live_mysql_generated_columns.rs @@ -49,6 +49,7 @@ fn change_options(table_name: &str, columns: Vec) -> Ta mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, } } diff --git a/crates/dbx-core/tests/live_postgres_concurrent_index.rs b/crates/dbx-core/tests/live_postgres_concurrent_index.rs index 438d548738..3de24282e5 100644 --- a/crates/dbx-core/tests/live_postgres_concurrent_index.rs +++ b/crates/dbx-core/tests/live_postgres_concurrent_index.rs @@ -62,6 +62,7 @@ async fn live_postgres_concurrent_index_builds_valid_index() { mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert!(result.warnings.is_empty(), "{:?}", result.warnings); assert_eq!( @@ -193,6 +194,7 @@ async fn live_postgres_partitioned_parent_concurrent_request_rejected() { mysql_engine: None, partitioned: true, is_gaussdb_m_mode: false, + table_collation: None, }); 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 421f86594b..7a6da9002c 100644 --- a/crates/dbx-core/tests/live_sqlserver_completion.rs +++ b/crates/dbx-core/tests/live_sqlserver_completion.rs @@ -1010,6 +1010,7 @@ async fn live_sqlserver_table_structure_default_changes_drop_existing_constraint mysql_engine: None, partitioned: false, is_gaussdb_m_mode: false, + table_collation: None, }); assert_eq!(result.warnings, Vec::::new()); assert_eq!(result.statements.len(), 4);