mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: [...] 后续扩展
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user