feat(schema-diff): add charset comparison option

Closes #10396
This commit is contained in:
t8y2
2026-09-27 14:23:30 +00:00
parent 4329ec51db
commit 20c50550cf
13 changed files with 380 additions and 13 deletions
@@ -121,7 +121,7 @@ watch([sourceDbType, targetDbType], ([src, tgt]) => {
});
const optionTree = computed(() => {
const targetConfig = store.getConfig(targetConnectionId.value);
const dbType = targetConfig?.db_type || "postgres";
const dbType = schemaDiffEngineDatabaseType(targetConfig) || targetConfig?.db_type || "postgres";
return getSchemaDiffOptionsForDbType(dbType);
});
@@ -58,6 +58,15 @@ describe("SchemaDiffOptionsPanel", () => {
expect(normalized.ignoreColumnNameCase).toBe(false);
});
it("exposes charset comparison only for supported MySQL targets and defaults it on", () => {
expect(getSchemaDiffOptionsForDbType("mysql").map((item) => item.id)).toContain("compareCharset");
expect(getSchemaDiffOptionsForDbType("goldendb").map((item) => item.id)).toContain("compareCharset");
expect(getSchemaDiffOptionsForDbType("starrocks").map((item) => item.id)).not.toContain("compareCharset");
expect(getSchemaDiffOptionsForDbType("doris").map((item) => item.id)).not.toContain("compareCharset");
expect(getSchemaDiffOptionsForDbType("postgres").map((item) => item.id)).not.toContain("compareCharset");
expect(normalizeSchemaDiffCompareOptions({}, "mysql").compareCharset).toBe(true);
});
it("keeps unsaved checkbox edits when the parent refreshes equivalent options", async () => {
const options = ref<SchemaDiffCompareOptions>({ ...DEFAULT_POSTGRES_OPTIONS });
const optionTree: SchemaDiffOptionItem[] = [{ id: "views", labelKey: "views", defaultChecked: true }];
@@ -0,0 +1,66 @@
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it, vi } from "vitest";
const CONFIG_KEY = "dbx-schema-diff-configs";
const HISTORY_KEY = "dbx-schema-diff-history";
function storedConfig(compareCharset?: boolean) {
return {
id: "saved",
name: "Saved",
createdAt: 1,
updatedAt: 1,
sourceConnectionId: "source",
sourceDatabase: "app",
sourceSchema: "",
targetConnectionId: "target",
targetDatabase: "app_copy",
targetSchema: "",
options: {
tables: true,
...(compareCharset === undefined ? {} : { compareCharset }),
},
};
}
describe("schema diff charset option persistence", () => {
beforeEach(() => {
localStorage.clear();
vi.resetModules();
});
it("migrates legacy saved configs and history entries to comparison enabled", async () => {
localStorage.setItem(CONFIG_KEY, JSON.stringify([storedConfig()]));
localStorage.setItem(HISTORY_KEY, JSON.stringify([storedConfig()]));
const { useSchemaDiffConfig } = await import("../useSchemaDiffConfig");
const state = useSchemaDiffConfig();
expect(state.configs.value[0].options.compareCharset).toBe(true);
expect(state.recentConfigs.value[0].options.compareCharset).toBe(true);
expect(JSON.parse(localStorage.getItem(CONFIG_KEY) ?? "[]")[0].options.compareCharset).toBe(true);
expect(JSON.parse(localStorage.getItem(HISTORY_KEY) ?? "[]")[0].options.compareCharset).toBe(true);
});
it("preserves and persists an explicitly disabled value", async () => {
localStorage.setItem(CONFIG_KEY, JSON.stringify([storedConfig(false)]));
const { useSchemaDiffConfig } = await import("../useSchemaDiffConfig");
const state = useSchemaDiffConfig();
state.activeConfigId.value = "saved";
expect(state.activeConfig.value?.options.compareCharset).toBe(false);
state.updateActiveConfigOptions({ ...state.activeConfig.value!.options, compareCharset: false });
expect(JSON.parse(localStorage.getItem(CONFIG_KEY) ?? "[]")[0].options.compareCharset).toBe(false);
});
it("migrates legacy imported configs before persisting them", async () => {
const { useSchemaDiffConfig } = await import("../useSchemaDiffConfig");
const state = useSchemaDiffConfig();
expect(state.importConfigs(JSON.stringify(storedConfig()), "replace")).toEqual({ imported: 1, renamed: 0 });
expect(state.configs.value[0].options.compareCharset).toBe(true);
expect(JSON.parse(localStorage.getItem(CONFIG_KEY) ?? "[]")[0].options.compareCharset).toBe(true);
});
});
@@ -31,6 +31,7 @@ async function waitForSession(session: { status: string }) {
test("defaults enable tables and functions compare for common targets", () => {
assert.equal(DEFAULT_MYSQL_OPTIONS.tables, true);
assert.equal(DEFAULT_MYSQL_OPTIONS.functions, true);
assert.equal(DEFAULT_MYSQL_OPTIONS.compareCharset, true);
assert.equal(getDefaultOptionsForDbType("oracle").tables, true);
assert.equal(getDefaultOptionsForDbType("oracle").functions, true);
assert.equal(getDefaultOptionsForDbType("mysql").tables, true);
@@ -82,12 +83,49 @@ test("runs a schema diff session after the dialog is closed and retains the prep
assert.equal(trackerMock.updateCompareTask.mock.calls.at(-1)?.[1].status, "Done");
// MySQL defaults now enable functions compare for same-dialect pairs.
assert.equal(apiMock.listFunctions.mock.calls.length, 2);
assert.equal(apiMock.prepareSchemaDiff.mock.calls[0]?.[0]?.compareCharset, true);
const onOpen = trackerMock.addSchemaDiffTask.mock.calls[0]?.[2] as (() => void) | undefined;
onOpen?.();
assert.equal(openMock.mock.calls.at(-1)?.[0], session.id);
});
test("forwards a disabled charset comparison to the backend", async () => {
apiMock.prepareSchemaDiff.mockClear();
apiMock.prepareSchemaDiff.mockResolvedValue({
diffs: [],
functionDiffs: [],
sequenceDiffs: [],
ruleDiffs: [],
ownerDiffs: [],
renameCandidates: [],
syncSql: "",
rollbackSyncSql: "",
});
const session = startSchemaDiffSession(
{
sourceConnectionId: "source",
sourceDatabase: "app",
sourceSchema: "",
targetConnectionId: "target",
targetDatabase: "warehouse",
targetSchema: "",
sourceDbType: "mysql",
targetDbType: "mysql",
options: { compareCharset: false },
ignoreComments: false,
label: "charset disabled",
},
{ tableListLoader: { load: vi.fn().mockResolvedValue([]) } },
);
await waitForSession(session);
assert.equal(session.status, "completed");
assert.equal(apiMock.prepareSchemaDiff.mock.calls[0]?.[0]?.compareCharset, false);
});
test("loads routines for mysql↔mysql when functions is enabled", async () => {
apiMock.prepareSchemaDiff.mockClear();
apiMock.listFunctions.mockClear();
@@ -11,12 +11,29 @@ const configs = ref<SchemaDiffConfig[]>(loadConfigsFromStorage());
const activeConfigId = ref<string>("");
const recentConfigs = ref<SchemaDiffConfig[]>(loadHistoryFromStorage());
function migrateStoredConfig(config: SchemaDiffConfig): SchemaDiffConfig {
return {
...config,
options: {
...config.options,
compareCharset: config.options?.compareCharset ?? true,
},
};
}
function parseStoredConfigs(raw: string | null, storageKey: string): SchemaDiffConfig[] {
if (!raw) return [];
const parsed = JSON.parse(raw) as SchemaDiffConfig[];
if (!Array.isArray(parsed)) return [];
const migrated = parsed.map(migrateStoredConfig);
const serialized = JSON.stringify(migrated);
if (serialized !== raw) localStorage.setItem(storageKey, serialized);
return migrated;
}
function loadConfigsFromStorage(): SchemaDiffConfig[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw) as SchemaDiffConfig[];
return Array.isArray(parsed) ? parsed : [];
return parseStoredConfigs(localStorage.getItem(STORAGE_KEY), STORAGE_KEY);
} catch {
return [];
}
@@ -24,10 +41,7 @@ function loadConfigsFromStorage(): SchemaDiffConfig[] {
function loadHistoryFromStorage(): SchemaDiffConfig[] {
try {
const raw = localStorage.getItem(HISTORY_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw) as SchemaDiffConfig[];
return Array.isArray(parsed) ? parsed : [];
return parseStoredConfigs(localStorage.getItem(HISTORY_KEY), HISTORY_KEY);
} catch {
return [];
}
@@ -136,7 +150,8 @@ export function useSchemaDiffConfig() {
}
for (const item of items) {
let name = item.name;
const migrated = migrateStoredConfig(item);
let name = migrated.name;
if (configs.value.some((c) => c.name === name && c.id !== item.id)) {
let counter = 1;
let candidate = `${name} (${counter})`;
@@ -149,7 +164,7 @@ export function useSchemaDiffConfig() {
}
const newId = uuid();
configs.value.push({
...item,
...migrated,
id: newId,
name,
createdAt: Date.now(),
@@ -226,6 +226,7 @@ async function runSchemaDiffSession(session: SchemaDiffSession, dependencies: Sc
ignoreComments: input.ignoreComments,
cascadeDelete: sessionOptions.cascadeDelete ?? false,
compareColumnOrder: sessionOptions.compareColumnOrder,
compareCharset: sessionOptions.compareCharset,
ignoreTableNameCase: sessionOptions.ignoreTableNameCase,
ignoreColumnNameCase: sessionOptions.ignoreColumnNameCase,
detectRenames: sessionOptions.detectRenames ?? false,
+1
View File
@@ -7594,6 +7594,7 @@ export default {
cascadeDelete: "Use CASCADE delete",
sequenceLastValues: "Compare sequence last values",
compareColumnOrder: "Compare column order",
compareCharset: "Compare character sets and collations",
ignoreTableNameCase: "Ignore table name case",
ignoreColumnNameCase: "Ignore column name case",
detectRenames: "Detect renames",
+1
View File
@@ -7560,6 +7560,7 @@ export default withEnglishFallback({
cascadeDelete: "使用级联删除",
sequenceLastValues: "比较序列最后值",
compareColumnOrder: "比较字段顺序",
compareCharset: "比较字符集和排序规则",
ignoreTableNameCase: "忽略表名大小写",
ignoreColumnNameCase: "忽略字段名大小写",
detectRenames: "检测重命名",
@@ -242,6 +242,7 @@ export interface SchemaDiffPreparationOptions {
ignoreComments?: boolean;
cascadeDelete?: boolean;
compareColumnOrder?: boolean;
compareCharset?: boolean;
ignoreTableNameCase?: boolean;
ignoreColumnNameCase?: boolean;
detectRenames?: boolean;
@@ -30,10 +30,13 @@ export const POSTGRES_SCHEMA_DIFF_OPTIONS: SchemaDiffOptionItem[] = [
{ id: "enableRollback", labelKey: "schemaDiff.options.enableRollback", defaultChecked: false },
];
export const MYSQL_SCHEMA_DIFF_OPTIONS: SchemaDiffOptionItem[] = [...POSTGRES_SCHEMA_DIFF_OPTIONS, { id: "compareCharset", labelKey: "schemaDiff.options.compareCharset", defaultChecked: true }];
export const SCHEMA_DIFF_OPTIONS_BY_DB_TYPE: Record<string, SchemaDiffOptionItem[]> = {
postgres: POSTGRES_SCHEMA_DIFF_OPTIONS,
opengauss: POSTGRES_SCHEMA_DIFF_OPTIONS,
// mysql: [...] 后续扩展
mysql: MYSQL_SCHEMA_DIFF_OPTIONS,
goldendb: MYSQL_SCHEMA_DIFF_OPTIONS,
// sqlserver: [...] 后续扩展
};
+3
View File
@@ -27,6 +27,7 @@ export interface SchemaDiffCompareOptions {
cascadeDelete: boolean;
sequenceLastValues: boolean;
compareColumnOrder: boolean;
compareCharset: boolean;
ignoreTableNameCase: boolean;
ignoreColumnNameCase: boolean;
tableIncludePattern: string;
@@ -114,6 +115,7 @@ export const DEFAULT_POSTGRES_OPTIONS: SchemaDiffCompareOptions = {
cascadeDelete: false,
sequenceLastValues: true,
compareColumnOrder: false,
compareCharset: true,
ignoreTableNameCase: false,
ignoreColumnNameCase: false,
tableIncludePattern: "",
@@ -151,6 +153,7 @@ export const DEFAULT_MYSQL_OPTIONS: SchemaDiffCompareOptions = {
cascadeDelete: false,
sequenceLastValues: false,
compareColumnOrder: false,
compareCharset: true,
ignoreTableNameCase: false,
ignoreColumnNameCase: false,
tableIncludePattern: "",
@@ -51,6 +51,7 @@ fn prepare_schema_diff_function_signature() {
ignore_comments: false,
cascade_delete: false,
compare_column_order: false,
compare_charset: true,
ignore_table_name_case: false,
ignore_column_name_case: false,
detect_renames: false,
@@ -152,6 +153,7 @@ fn schema_diff_preparation_field_names() {
ignore_comments: false,
cascade_delete: false,
compare_column_order: false,
compare_charset: true,
ignore_table_name_case: false,
ignore_column_name_case: false,
detect_renames: false,
+228 -1
View File
@@ -449,6 +449,8 @@ pub struct SchemaDiffPreparationOptions {
pub cascade_delete: bool,
#[serde(default)]
pub compare_column_order: bool,
#[serde(default = "default_compare_charset")]
pub compare_charset: bool,
#[serde(default)]
pub ignore_table_name_case: bool,
#[serde(default)]
@@ -483,6 +485,10 @@ pub struct SchemaDiffPreparationOptions {
pub table_mappings: Vec<SchemaDiffTableMapping>,
}
const fn default_compare_charset() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MissingRollbackObject {
@@ -1167,6 +1173,7 @@ pub fn diff_columns_with_compatibility(
compatibility_threshold,
field_mappings,
false,
source_dialect == DialectKind::Mysql && target_dialect == DialectKind::Mysql,
)
}
@@ -1181,6 +1188,7 @@ fn diff_columns_with_compatibility_options(
compatibility_threshold: f64,
field_mappings: &[FieldMapping],
ignore_column_name_case: bool,
compare_charset: bool,
) -> (Vec<ColumnDiff>, Vec<ColumnCompatibilityWarning>) {
use crate::sql_dialect::descriptor::TypeMappingMatrix;
@@ -1197,6 +1205,7 @@ fn diff_columns_with_compatibility_options(
None,
None,
ignore_column_name_case,
compare_charset,
);
let mut warnings = Vec::new();
@@ -1647,6 +1656,7 @@ pub fn shard_diff(options: &SchemaDiffPreparationOptions, shard_strategy: &Shard
ignore_comments: options.ignore_comments,
cascade_delete: options.cascade_delete,
compare_column_order: options.compare_column_order,
compare_charset: options.compare_charset,
ignore_table_name_case: options.ignore_table_name_case,
ignore_column_name_case: options.ignore_column_name_case,
source_dialect: options.source_dialect,
@@ -1920,6 +1930,7 @@ impl Default for SchemaDiffPreparationOptions {
ignore_comments: false,
cascade_delete: false,
compare_column_order: false,
compare_charset: true,
ignore_table_name_case: false,
ignore_column_name_case: false,
detect_renames: false,
@@ -2090,6 +2101,7 @@ pub fn prepare_schema_diff(options: SchemaDiffPreparationOptions) -> SchemaDiffP
options.compatibility_threshold,
&options.field_mappings,
options.ignore_column_name_case,
compare_mysql_column_charset(&options),
);
all_warnings.extend(warnings);
}
@@ -2278,6 +2290,17 @@ fn resolve_schema_diff_table_names(
SchemaDiffTableNameResolution { pairs, source_only, target_only }
}
fn compare_mysql_column_charset(options: &SchemaDiffPreparationOptions) -> bool {
if !options.compare_charset || !matches!(options.database_type, DatabaseType::Mysql | DatabaseType::Goldendb) {
return false;
}
let target_dialect =
options.target_dialect.unwrap_or_else(|| DialectKind::from_database_type(options.database_type));
let source_dialect = options.source_dialect.unwrap_or(target_dialect);
source_dialect == DialectKind::Mysql && target_dialect == DialectKind::Mysql
}
fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec<TableDiff> {
let source_details: HashMap<&str, &TableSchemaDetail> =
options.source_details.iter().map(|detail| (detail.name.as_str(), detail)).collect();
@@ -2573,6 +2596,7 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec<TableDiff> {
options.source_dialect,
options.target_dialect,
options.ignore_column_name_case,
compare_mysql_column_charset(options),
);
let index_diffs = diff_indexes_with_options(&source.indexes, &target.indexes, options.ignore_column_name_case);
let normalized_source_fks: Vec<ForeignKeyInfo> = source
@@ -3164,6 +3188,17 @@ fn column_types_equal_for_dialects(
}
}
fn mysql_charset_value(value: Option<&str>) -> Option<&str> {
value.map(str::trim).filter(|value| !value.is_empty())
}
fn mysql_charset_values_differ(source: Option<&str>, target: Option<&str>) -> bool {
match (mysql_charset_value(source), mysql_charset_value(target)) {
(Some(source), Some(target)) => !source.eq_ignore_ascii_case(target),
_ => false,
}
}
fn column_type_similarity_score(source_type: &str, target_type: &str) -> f64 {
let s = ColumnType::parse(source_type).base_type.to_ascii_lowercase();
let t = ColumnType::parse(target_type).base_type.to_ascii_lowercase();
@@ -3237,6 +3272,7 @@ fn diff_columns_with_dialect_options(
source_dialect,
target_dialect,
false,
source_dialect == Some(DialectKind::Mysql) && target_dialect == Some(DialectKind::Mysql),
)
}
@@ -3288,6 +3324,7 @@ fn diff_columns_with_identifier_options(
source_dialect: Option<DialectKind>,
target_dialect: Option<DialectKind>,
ignore_column_name_case: bool,
compare_charset: bool,
) -> Vec<ColumnDiff> {
let mut diffs = Vec::new();
let column_matches = resolve_column_matches(source, target, ignore_column_name_case);
@@ -3322,6 +3359,27 @@ fn diff_columns_with_identifier_options(
source_column.column_default.as_deref().unwrap_or("NULL")
));
}
if compare_charset
&& mysql_charset_values_differ(
source_column.character_set.as_deref(),
target_column.character_set.as_deref(),
)
{
changes.push(format!(
"character set: {} → {}",
mysql_charset_value(target_column.character_set.as_deref()).unwrap_or_default(),
mysql_charset_value(source_column.character_set.as_deref()).unwrap_or_default()
));
}
if compare_charset
&& mysql_charset_values_differ(source_column.collation.as_deref(), target_column.collation.as_deref())
{
changes.push(format!(
"collation: {} → {}",
mysql_charset_value(target_column.collation.as_deref()).unwrap_or_default(),
mysql_charset_value(source_column.collation.as_deref()).unwrap_or_default()
));
}
if !ignore_comments
&& source_column.comment.as_deref().unwrap_or_default()
!= target_column.comment.as_deref().unwrap_or_default()
@@ -4091,11 +4149,28 @@ fn column_modifier_tail(
}
fn column_def(col: &ColumnInfo, db_type: DatabaseType, source_dialect: Option<DialectKind>) -> String {
column_def_with_charset(col, db_type, source_dialect, false)
}
fn column_def_with_charset(
col: &ColumnInfo,
db_type: DatabaseType,
source_dialect: Option<DialectKind>,
include_charset: bool,
) -> String {
if db_type == DatabaseType::SqlServer {
return sqlserver_column_definition(col, &col.data_type, source_dialect, None);
}
let profile = profile_for(db_type);
let mut definition = format!("{} {}", quote_id(&col.name, db_type), col.data_type);
if include_charset && matches!(db_type, DatabaseType::Mysql | DatabaseType::Goldendb) {
if let Some(character_set) = mysql_charset_value(col.character_set.as_deref()) {
definition.push_str(&format!(" CHARACTER SET {}", quote_id(character_set, db_type)));
}
if let Some(collation) = mysql_charset_value(col.collation.as_deref()) {
definition.push_str(&format!(" COLLATE {}", quote_id(collation, db_type)));
}
}
definition.push_str(&column_modifier_tail(&profile, col, &col.data_type, db_type, source_dialect, false));
// Suffix-style auto-increment is only valid in MySQL-family ALTER clauses
// (ADD/MODIFY/CHANGE). Other dialects' identity clauses are order-sensitive
@@ -6327,9 +6402,12 @@ fn generate_schema_sync_sql_inner(
} else if profile.alter_uses_modify_column {
if column.changes.iter().any(|change| !change.starts_with("order:")) {
let modify_keyword = profile.alter_modify_keyword();
let include_charset = column.changes.iter().any(|change| {
change.starts_with("character set:") || change.starts_with("collation:")
});
parts.push(format!(
" {modify_keyword} {}",
column_def(&mapped, db_type, source_dialect)
column_def_with_charset(&mapped, db_type, source_dialect, include_charset)
));
}
} else {
@@ -6796,6 +6874,154 @@ mod tests {
}
}
fn mysql_charset_options(compare_charset: bool) -> SchemaDiffPreparationOptions {
let mut source_column = column("name", "varchar(64)", None);
source_column.character_set = Some("utf8mb4".to_string());
source_column.collation = Some("utf8mb4_0900_ai_ci".to_string());
let mut target_column = column("name", "varchar(64)", None);
target_column.character_set = Some("latin1".to_string());
target_column.collation = Some("latin1_swedish_ci".to_string());
SchemaDiffPreparationOptions {
source_tables: vec![table_info("users", "TABLE")],
target_tables: vec![table_info("users", "TABLE")],
source_details: vec![TableSchemaDetail {
name: "users".to_string(),
columns: vec![source_column],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
ddl: None,
}],
target_details: vec![TableSchemaDetail {
name: "users".to_string(),
columns: vec![target_column],
indexes: Vec::new(),
foreign_keys: Vec::new(),
triggers: Vec::new(),
ddl: None,
}],
database_type: DatabaseType::Mysql,
source_dialect: Some(DialectKind::Mysql),
target_dialect: Some(DialectKind::Mysql),
compare_charset,
..Default::default()
}
}
#[test]
fn mysql_charset_comparison_is_enabled_by_default_and_serializes_as_camel_case() {
let legacy: SchemaDiffPreparationOptions =
serde_json::from_value(serde_json::json!({ "databaseType": "mysql" })).unwrap();
assert!(legacy.compare_charset);
let json = serde_json::to_value(legacy).unwrap();
assert_eq!(json["compareCharset"], true);
assert!(json.get("compare_charset").is_none());
let disabled: SchemaDiffPreparationOptions =
serde_json::from_value(serde_json::json!({ "databaseType": "mysql", "compareCharset": false })).unwrap();
assert!(!disabled.compare_charset);
}
#[test]
fn mysql_charset_only_difference_is_reported_when_enabled() {
let result = prepare_schema_diff(mysql_charset_options(true));
let columns = result.diffs[0].columns.as_ref().expect("column diff");
assert_eq!(
columns[0].changes,
vec![
"character set: latin1 → utf8mb4".to_string(),
"collation: latin1_swedish_ci → utf8mb4_0900_ai_ci".to_string(),
]
);
assert!(result.sync_sql.contains("CHARACTER SET `utf8mb4` COLLATE `utf8mb4_0900_ai_ci`"));
}
#[test]
fn mysql_charset_only_difference_is_suppressed_when_disabled_and_restored_when_reenabled() {
assert!(prepare_schema_diff(mysql_charset_options(false)).diffs.is_empty());
assert_eq!(prepare_schema_diff(mysql_charset_options(true)).diffs.len(), 1);
}
#[test]
fn mysql_charset_missing_metadata_is_unknown() {
let mut options = mysql_charset_options(true);
options.target_details[0].columns[0].character_set = None;
options.target_details[0].columns[0].collation = None;
options.source_details[0].ddl =
Some("CREATE TABLE users (name varchar(64)) DEFAULT CHARSET=utf8mb4".to_string());
options.target_details[0].ddl =
Some("CREATE TABLE users (name varchar(64)) DEFAULT CHARSET=latin1".to_string());
assert!(prepare_schema_diff(options).diffs.is_empty());
}
#[test]
fn goldendb_charset_metadata_uses_the_mysql_dialect() {
let mut options = mysql_charset_options(true);
options.database_type = DatabaseType::Goldendb;
options.source_dialect = None;
options.target_dialect = None;
let result = prepare_schema_diff(options);
assert_eq!(result.diffs.len(), 1);
assert!(result.sync_sql.contains("CHARACTER SET `utf8mb4` COLLATE `utf8mb4_0900_ai_ci`"));
}
#[test]
fn disabling_mysql_charset_comparison_preserves_other_column_and_key_differences() {
let mut options = mysql_charset_options(false);
let source_column = &mut options.source_details[0].columns[0];
source_column.data_type = "varchar(128)".to_string();
source_column.is_nullable = false;
source_column.column_default = Some("source".to_string());
source_column.comment = Some("source comment".to_string());
let target_column = &mut options.target_details[0].columns[0];
target_column.is_nullable = true;
target_column.column_default = Some("target".to_string());
target_column.comment = Some("target comment".to_string());
options.source_details[0].indexes.push(index(IndexInfo {
name: "uq_users_name".to_string(),
columns: vec!["name".to_string()],
is_unique: true,
is_primary: false,
filter: None,
index_type: None,
included_columns: None,
comment: None,
key_is_expression: Vec::new(),
column_opclasses: Vec::new(),
key_options: Vec::new(),
constraint_backed: false,
}));
let result = prepare_schema_diff(options);
let table = &result.diffs[0];
let column_changes = &table.columns.as_ref().expect("column diff")[0].changes;
assert!(column_changes.iter().any(|change| change.starts_with("type:")));
assert!(column_changes.iter().any(|change| change.starts_with("nullable:")));
assert!(column_changes.iter().any(|change| change.starts_with("default:")));
assert!(column_changes.iter().any(|change| change.starts_with("comment:")));
assert!(!column_changes.iter().any(|change| change.starts_with("character set:")));
assert!(!column_changes.iter().any(|change| change.starts_with("collation:")));
assert!(table.indexes.as_ref().is_some_and(|indexes| {
indexes.iter().any(|index| {
index.diff_type == "added"
&& index.source.as_ref().is_some_and(|info| info.is_unique && info.name == "uq_users_name")
})
}));
}
#[test]
fn mysql_charset_metadata_is_not_compared_unless_both_dialects_are_mysql() {
let mut options = mysql_charset_options(true);
options.source_dialect = Some(DialectKind::Postgres);
assert!(prepare_schema_diff(options).diffs.is_empty());
}
#[test]
fn oracle_schema_sync_drops_indexes_without_if_exists() {
assert_eq!(
@@ -13058,6 +13284,7 @@ mod tests {
ignore_comments: false,
cascade_delete: false,
compare_column_order: false,
compare_charset: true,
ignore_table_name_case: false,
ignore_column_name_case: false,
detect_renames: false,