fix(schema-diff): equate MySQL RESTRICT/NO ACTION and refresh metadata
This commit is contained in:
1 parent
452aa46d54
commit
7958f7ad90
18 files changed
+210
-8
No files matched your search
@@ -58,6 +58,14 @@ describe("SchemaDiffOptionsPanel", () => {
|
||||
expect(normalized.ignoreColumnNameCase).toBe(false);
|
||||
});
|
||||
|
||||
it("exposes the refresh-metadata option for every target and defaults it on, including legacy configs", () => {
|
||||
for (const dbType of ["postgres", "mysql", "goldendb", "oracle"]) {
|
||||
expect(getSchemaDiffOptionsForDbType(dbType).map((item) => item.id)).toContain("refreshMetadata");
|
||||
expect(normalizeSchemaDiffCompareOptions({}, dbType).refreshMetadata).toBe(true);
|
||||
}
|
||||
expect(normalizeSchemaDiffCompareOptions({ refreshMetadata: false }, "mysql").refreshMetadata).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");
|
||||
|
||||
@@ -20,6 +20,7 @@ vi.mock("@/lib/backend/api", () => apiMock);
|
||||
vi.mock("@/composables/useDialogSources", () => ({ openSchemaDiffSession: openMock }));
|
||||
vi.mock("@/composables/useExportTracker", () => ({ useExportTracker: () => trackerMock }));
|
||||
vi.mock("@/lib/schema/schemaDiffMetadataLoad", () => ({ loadSchemaDetails: vi.fn().mockResolvedValue([]) }));
|
||||
vi.mock("@/lib/metadata/objectMetadataCache", () => ({ invalidateObjectMetadataCache: vi.fn().mockResolvedValue(undefined) }));
|
||||
|
||||
const { startSchemaDiffSession } = await import("../useSchemaDiffSession.ts");
|
||||
|
||||
@@ -85,6 +86,14 @@ test("applies every exclude rule before loading source and target details", asyn
|
||||
await waitForSession(session);
|
||||
assert.equal(session.status, "completed");
|
||||
assert.equal(vi.mocked(loadSchemaDetails).mock.calls.length, 2);
|
||||
const { invalidateObjectMetadataCache } = await import("@/lib/metadata/objectMetadataCache");
|
||||
assert.deepEqual(
|
||||
vi.mocked(invalidateObjectMetadataCache).mock.calls.map(([match]) => match),
|
||||
[
|
||||
{ connectionId: "source", database: "app" },
|
||||
{ connectionId: "target", database: "warehouse" },
|
||||
],
|
||||
);
|
||||
for (const [tables] of vi.mocked(loadSchemaDetails).mock.calls) {
|
||||
assert.deepEqual(
|
||||
tables.map((table) => table.name),
|
||||
@@ -94,6 +103,34 @@ test("applies every exclude rule before loading source and target details", asyn
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("skips metadata cache invalidation when refreshMetadata is turned off", async () => {
|
||||
const { loadSchemaDetails } = await import("@/lib/schema/schemaDiffMetadataLoad");
|
||||
const { invalidateObjectMetadataCache } = await import("@/lib/metadata/objectMetadataCache");
|
||||
vi.clearAllMocks();
|
||||
apiMock.prepareSchemaDiff.mockResolvedValue({ diffs: [], renameCandidates: [], syncSql: "", rollbackSyncSql: "" });
|
||||
const session = startSchemaDiffSession(
|
||||
{
|
||||
sourceConnectionId: "source",
|
||||
sourceDatabase: "app",
|
||||
sourceSchema: "",
|
||||
targetConnectionId: "target",
|
||||
targetDatabase: "warehouse",
|
||||
targetSchema: "",
|
||||
sourceDbType: "mysql",
|
||||
targetDbType: "mysql",
|
||||
options: { functions: false, refreshMetadata: false },
|
||||
ignoreComments: false,
|
||||
label: "cached compare",
|
||||
},
|
||||
{ tableListLoader: { load: vi.fn().mockResolvedValue([{ name: "users", table_type: "BASE TABLE" }]) } },
|
||||
);
|
||||
await waitForSession(session);
|
||||
assert.equal(session.status, "completed");
|
||||
assert.equal(vi.mocked(loadSchemaDetails).mock.calls.length, 2);
|
||||
assert.equal(vi.mocked(invalidateObjectMetadataCache).mock.calls.length, 0);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
async function waitForSession(session: { status: string }) {
|
||||
for (let attempt = 0; attempt < 40 && session.status === "running"; attempt += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
@@ -7,6 +7,7 @@ import { filterSchemaDiffTables } from "@/lib/schema/schemaDiffTableFilter";
|
||||
import { compileSchemaDiffTableFilter } from "@/lib/schema/schemaDiffTableFilter";
|
||||
import { filterSchemaDiffFunctions, schemaDiffRoutineKeyFromFunction } from "@/lib/schema/schemaDiffRoutine";
|
||||
import { loadSchemaDetails } from "@/lib/schema/schemaDiffMetadataLoad";
|
||||
import { invalidateObjectMetadataCache } from "@/lib/metadata/objectMetadataCache";
|
||||
import { registerSchemaDiffTask } from "@/lib/schema/schemaDiffCancellation";
|
||||
import type { SchemaDiffTableIdentity, SchemaDiffTableListLoader } from "@/lib/schema/schemaDiffTableList";
|
||||
import { getSchemaDiffNextProgressStep, isSchemaDiffPostgresLike, shouldLoadSchemaDiffExtraObjectPhase, shouldLoadSchemaDiffRoutines, type SchemaDiffProgressPhase } from "@/lib/schema/schemaDiffProgress";
|
||||
@@ -204,6 +205,17 @@ async function runSchemaDiffSession(session: SchemaDiffSession, dependencies: Sc
|
||||
signal.throwIfAborted();
|
||||
({ sourceTables, targetTables } = filterSchemaDiffTables(sourceTableList, targetTableList, tableFilter, sessionOptions, sessionOptions.selectedTables));
|
||||
|
||||
// Column/index/foreign-key reads are served from the persisted object
|
||||
// metadata cache, which has no TTL on the backend. A compare must see the
|
||||
// live structure on both sides, otherwise changes made outside DBX are
|
||||
// hidden behind a stale snapshot. Best effort: a failed deletion only
|
||||
// means the compare falls back to whatever the cache returns. Users can
|
||||
// opt out via `refreshMetadata` to reuse the cache for faster compares.
|
||||
if (sessionOptions.refreshMetadata) {
|
||||
await Promise.all([invalidateObjectMetadataCache({ connectionId: input.sourceConnectionId, database: input.sourceDatabase }), invalidateObjectMetadataCache({ connectionId: input.targetConnectionId, database: input.targetDatabase })]);
|
||||
signal.throwIfAborted();
|
||||
}
|
||||
|
||||
publishProgress(session, { phase: "loading-source-details", current: 0, total: sourceTables.length });
|
||||
sourceDetails = await loadSchemaDetails(
|
||||
sourceTables,
|
||||
|
||||
@@ -9724,6 +9724,7 @@ export default {
|
||||
compareCharset: "Compare character sets and collations",
|
||||
ignoreTableNameCase: "Ignore table name case",
|
||||
ignoreColumnNameCase: "Ignore column name case",
|
||||
refreshMetadata: "Refresh metadata before comparing",
|
||||
detectRenames: "Detect renames",
|
||||
detectTableRenames: "Detect table renames",
|
||||
enableRollback: "Enable rollback",
|
||||
|
||||
@@ -8534,6 +8534,7 @@ export default withEnglishFallback({
|
||||
compareCharset: "Comparar conjuntos de caracteres y collations",
|
||||
ignoreTableNameCase: "Ignorar mayúsculas y minúsculas del nombre de la tabla",
|
||||
ignoreColumnNameCase: "Ignorar mayúsculas y minúsculas del nombre de la columna",
|
||||
refreshMetadata: "Actualizar metadatos antes de comparar",
|
||||
detectRenames: "Detectar renombrados",
|
||||
detectTableRenames: "Detectar renombrados de tabla",
|
||||
enableRollback: "Habilitar reversión",
|
||||
|
||||
@@ -8057,6 +8057,7 @@ export default withEnglishFallback({
|
||||
compareCharset: "Bandingkan character set dan collation",
|
||||
ignoreTableNameCase: "Abaikan huruf besar/kecil pada nama tabel",
|
||||
ignoreColumnNameCase: "Abaikan huruf besar/kecil pada nama kolom",
|
||||
refreshMetadata: "Muat ulang metadata sebelum membandingkan",
|
||||
detectRenames: "Deteksi penggantian nama",
|
||||
detectTableRenames: "Deteksi penggantian nama tabel",
|
||||
enableRollback: "Aktifkan rollback",
|
||||
|
||||
@@ -8323,6 +8323,7 @@ export default withEnglishFallback({
|
||||
batchSection: "Batch",
|
||||
batchPatterns: "Pattern tabella (separati da virgola)",
|
||||
dialectAuto: "auto",
|
||||
refreshMetadata: "Aggiorna i metadati prima del confronto",
|
||||
},
|
||||
},
|
||||
tableMultiSelect: {
|
||||
|
||||
@@ -8343,6 +8343,7 @@ export default withEnglishFallback({
|
||||
batchSection: "バッチ",
|
||||
batchPatterns: "テーブルパターン(カンマ区切り)",
|
||||
dialectAuto: "自動",
|
||||
refreshMetadata: "比較前にメタデータを更新",
|
||||
},
|
||||
},
|
||||
tableMultiSelect: {
|
||||
|
||||
@@ -8447,6 +8447,7 @@ export default withEnglishFallback({
|
||||
batchSection: "일괄",
|
||||
batchPatterns: "테이블 패턴 (쉼표로 구분)",
|
||||
dialectAuto: "자동",
|
||||
refreshMetadata: "비교 전 메타데이터 새로 고침",
|
||||
},
|
||||
},
|
||||
dataCompare: {
|
||||
|
||||
@@ -8120,6 +8120,7 @@ export default withEnglishFallback({
|
||||
compareCharset: "Porównuj zestawy znaków i collations",
|
||||
ignoreTableNameCase: "Ignoruj wielkość liter w nazwach tabel",
|
||||
ignoreColumnNameCase: "Ignoruj wielkość liter w nazwach kolumn",
|
||||
refreshMetadata: "Odśwież metadane przed porównaniem",
|
||||
detectRenames: "Wykrywaj zmiany nazw",
|
||||
detectTableRenames: "Wykrywaj zmiany nazw tabel",
|
||||
enableRollback: "Włącz wycofanie (Rollback)",
|
||||
|
||||
@@ -8322,6 +8322,7 @@ export default withEnglishFallback({
|
||||
batchSection: "Lote",
|
||||
batchPatterns: "Padrões de tabela (separados por vírgula)",
|
||||
dialectAuto: "automático",
|
||||
refreshMetadata: "Atualizar metadados antes da comparação",
|
||||
},
|
||||
},
|
||||
tableMultiSelect: {
|
||||
|
||||
@@ -9898,6 +9898,7 @@ export default withEnglishFallback({
|
||||
batchSection: "Пакетно",
|
||||
batchPatterns: "Шаблоны таблиц (через запятую)",
|
||||
dialectAuto: "авто",
|
||||
refreshMetadata: "Обновлять метаданные перед сравнением",
|
||||
},
|
||||
},
|
||||
tableMultiSelect: {
|
||||
|
||||
@@ -10267,6 +10267,7 @@ export default withEnglishFallback({
|
||||
compareCharset: "เปรียบเทียบชุดอักขระและ Collation",
|
||||
ignoreTableNameCase: "ละเว้นตัวพิมพ์เล็กใหญ่ของชื่อตาราง",
|
||||
ignoreColumnNameCase: "ละเว้นตัวพิมพ์เล็กใหญ่ของชื่อคอลัมน์",
|
||||
refreshMetadata: "รีเฟรชเมทาดาทาก่อนเปรียบเทียบ",
|
||||
detectRenames: "ตรวจหาการเปลี่ยนชื่อ",
|
||||
detectTableRenames: "ตรวจหาการเปลี่ยนชื่อตาราง",
|
||||
enableRollback: "เปิดใช้ Rollback",
|
||||
|
||||
@@ -9691,6 +9691,7 @@ export default withEnglishFallback({
|
||||
compareCharset: "比较字符集和排序规则",
|
||||
ignoreTableNameCase: "忽略表名大小写",
|
||||
ignoreColumnNameCase: "忽略字段名大小写",
|
||||
refreshMetadata: "比较前刷新元数据",
|
||||
detectRenames: "检测重命名",
|
||||
detectTableRenames: "检测表重命名",
|
||||
enableRollback: "启用回滚",
|
||||
|
||||
@@ -7447,6 +7447,7 @@ export default withEnglishFallback({
|
||||
batchSection: "批次模式",
|
||||
batchPatterns: "資料表模式(逗號分隔)",
|
||||
dialectAuto: "自動",
|
||||
refreshMetadata: "比較前重新整理中繼資料",
|
||||
},
|
||||
},
|
||||
tableMultiSelect: {
|
||||
|
||||
@@ -25,6 +25,7 @@ export const POSTGRES_SCHEMA_DIFF_OPTIONS: SchemaDiffOptionItem[] = [
|
||||
{ id: "compareColumnOrder", labelKey: "schemaDiff.options.compareColumnOrder", defaultChecked: false },
|
||||
{ id: "ignoreTableNameCase", labelKey: "schemaDiff.options.ignoreTableNameCase", defaultChecked: false },
|
||||
{ id: "ignoreColumnNameCase", labelKey: "schemaDiff.options.ignoreColumnNameCase", defaultChecked: false },
|
||||
{ id: "refreshMetadata", labelKey: "schemaDiff.options.refreshMetadata", defaultChecked: true },
|
||||
{ id: "detectRenames", labelKey: "schemaDiff.options.detectRenames", defaultChecked: false },
|
||||
{ id: "detectTableRenames", labelKey: "schemaDiff.options.detectTableRenames", defaultChecked: false },
|
||||
{ id: "enableRollback", labelKey: "schemaDiff.options.enableRollback", defaultChecked: false },
|
||||
|
||||
@@ -196,6 +196,12 @@ export interface SchemaDiffCompareOptions {
|
||||
compareCharset: boolean;
|
||||
ignoreTableNameCase: boolean;
|
||||
ignoreColumnNameCase: boolean;
|
||||
/**
|
||||
* Drop the persisted object metadata cache for both endpoints before loading
|
||||
* table details, so the compare reads the live structure. Turn off to reuse
|
||||
* cached metadata (faster, but may hide changes made outside DBX).
|
||||
*/
|
||||
refreshMetadata: boolean;
|
||||
tableIncludePattern: string;
|
||||
tableExcludePattern: string;
|
||||
tableFilterPriority: SchemaDiffTableFilterPriority;
|
||||
@@ -286,6 +292,7 @@ export const DEFAULT_POSTGRES_OPTIONS: SchemaDiffCompareOptions = {
|
||||
compareCharset: true,
|
||||
ignoreTableNameCase: false,
|
||||
ignoreColumnNameCase: false,
|
||||
refreshMetadata: true,
|
||||
tableIncludePattern: "",
|
||||
tableExcludePattern: "",
|
||||
tableFilterPriority: "exclude",
|
||||
@@ -324,6 +331,7 @@ export const DEFAULT_MYSQL_OPTIONS: SchemaDiffCompareOptions = {
|
||||
compareCharset: true,
|
||||
ignoreTableNameCase: false,
|
||||
ignoreColumnNameCase: false,
|
||||
refreshMetadata: true,
|
||||
tableIncludePattern: "",
|
||||
tableExcludePattern: "",
|
||||
tableFilterPriority: "exclude",
|
||||
|
||||
@@ -2415,6 +2415,17 @@ fn compare_mysql_column_charset(options: &SchemaDiffPreparationOptions) -> bool
|
||||
source_dialect == DialectKind::Mysql && target_dialect == DialectKind::Mysql
|
||||
}
|
||||
|
||||
/// MySQL-family engines treat `RESTRICT`, `NO ACTION` and an omitted referential action identically,
|
||||
/// so they are only folded together when both sides of the comparison use the MySQL dialect.
|
||||
/// PostgreSQL, SQL Server, Oracle, SQLite, etc. are deliberately excluded: there `NO ACTION` may be
|
||||
/// deferred to the end of the statement/transaction while `RESTRICT` is checked immediately.
|
||||
fn mysql_foreign_key_actions(options: &SchemaDiffPreparationOptions) -> bool {
|
||||
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();
|
||||
@@ -2719,6 +2730,7 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec<TableDiff> {
|
||||
&normalized_target_fks,
|
||||
options.ignore_table_name_case,
|
||||
options.ignore_column_name_case,
|
||||
mysql_foreign_key_actions(options),
|
||||
);
|
||||
let trigger_diffs = diff_triggers(&source.triggers, &target.triggers);
|
||||
let source_comment = source_table_comments.get(name.as_str()).cloned().unwrap_or(None);
|
||||
@@ -3840,6 +3852,30 @@ fn normalized_foreign_key_action(action: Option<&str>) -> Option<String> {
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
/// Normalizes a referential action for comparison. With `mysql_actions`, InnoDB semantics apply:
|
||||
/// `RESTRICT`, `NO ACTION` and an omitted clause (which MySQL reports as `NO ACTION`) all reject
|
||||
/// the change immediately, so they collapse to one value and never surface as a difference.
|
||||
///
|
||||
/// References (MySQL 8.4 Reference Manual, 15.1.20.5 "FOREIGN KEY Constraints"):
|
||||
/// - "NO ACTION: ... For InnoDB, this is equivalent to RESTRICT".
|
||||
/// - "Specifying RESTRICT (or NO ACTION) is the same as omitting the ON DELETE or ON UPDATE clause."
|
||||
/// - "For an ON DELETE or ON UPDATE that is not specified, the default action is always NO ACTION."
|
||||
/// MariaDB KB "Foreign Keys" likewise documents `NO ACTION` as a "Synonym for RESTRICT".
|
||||
///
|
||||
/// Only the comparison is normalized; generated DDL keeps the source's original action text.
|
||||
/// Known caveat: on MySQL NDB Cluster `NO ACTION` is a deferred check, but schema metadata does
|
||||
/// not carry the storage engine, and NDB foreign keys are rare enough that InnoDB semantics win.
|
||||
fn comparable_foreign_key_action(action: Option<&str>, mysql_actions: bool) -> Option<String> {
|
||||
let normalized = normalized_foreign_key_action(action);
|
||||
if !mysql_actions {
|
||||
return normalized;
|
||||
}
|
||||
match normalized.as_deref() {
|
||||
None | Some("RESTRICT" | "NO ACTION") => Some("NO ACTION".to_string()),
|
||||
_ => normalized,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_self_referencing_fk(
|
||||
fk: &ForeignKeyInfo,
|
||||
own_table_names: &HashSet<&str>,
|
||||
@@ -3885,7 +3921,7 @@ fn normalize_mapped_foreign_key(
|
||||
}
|
||||
|
||||
pub fn diff_foreign_keys(source: &[ForeignKeyInfo], target: &[ForeignKeyInfo]) -> Vec<ForeignKeyDiff> {
|
||||
diff_foreign_keys_with_options(source, target, false, false)
|
||||
diff_foreign_keys_with_options(source, target, false, false, false)
|
||||
}
|
||||
|
||||
fn diff_foreign_keys_with_options(
|
||||
@@ -3893,6 +3929,7 @@ fn diff_foreign_keys_with_options(
|
||||
target: &[ForeignKeyInfo],
|
||||
ignore_table_name_case: bool,
|
||||
ignore_column_name_case: bool,
|
||||
mysql_actions: bool,
|
||||
) -> Vec<ForeignKeyDiff> {
|
||||
let source = group_foreign_keys(source);
|
||||
let target = group_foreign_keys(target);
|
||||
@@ -3912,8 +3949,14 @@ fn diff_foreign_keys_with_options(
|
||||
target.iter().enumerate().find_map(|(index, target_fk)| {
|
||||
(!matched[index]
|
||||
&& target_fk.name.is_empty()
|
||||
&& foreign_key_changes(source_fk, target_fk, ignore_table_name_case, ignore_column_name_case)
|
||||
.is_empty())
|
||||
&& foreign_key_changes(
|
||||
source_fk,
|
||||
target_fk,
|
||||
ignore_table_name_case,
|
||||
ignore_column_name_case,
|
||||
mysql_actions,
|
||||
)
|
||||
.is_empty())
|
||||
.then_some(index)
|
||||
})
|
||||
} else {
|
||||
@@ -3931,7 +3974,8 @@ fn diff_foreign_keys_with_options(
|
||||
};
|
||||
matched[target_index] = true;
|
||||
let target_fk = &target[target_index];
|
||||
let changes = foreign_key_changes(source_fk, target_fk, ignore_table_name_case, ignore_column_name_case);
|
||||
let changes =
|
||||
foreign_key_changes(source_fk, target_fk, ignore_table_name_case, ignore_column_name_case, mysql_actions);
|
||||
if !changes.is_empty() {
|
||||
diffs.push(ForeignKeyDiff {
|
||||
diff_type: "modified".to_string(),
|
||||
@@ -3963,6 +4007,7 @@ fn foreign_key_changes(
|
||||
target_fk: &SchemaForeignKeyInfo,
|
||||
ignore_table_name_case: bool,
|
||||
ignore_column_name_case: bool,
|
||||
mysql_actions: bool,
|
||||
) -> Vec<String> {
|
||||
let mut changes = Vec::new();
|
||||
if source_fk.columns().len() != target_fk.columns().len()
|
||||
@@ -3993,8 +4038,8 @@ fn foreign_key_changes(
|
||||
{
|
||||
changes.push(format!("ref column: {} → {}", target_fk.ref_column, source_fk.ref_column));
|
||||
}
|
||||
let source_on_delete = normalized_foreign_key_action(source_fk.on_delete.as_deref());
|
||||
let target_on_delete = normalized_foreign_key_action(target_fk.on_delete.as_deref());
|
||||
let source_on_delete = comparable_foreign_key_action(source_fk.on_delete.as_deref(), mysql_actions);
|
||||
let target_on_delete = comparable_foreign_key_action(target_fk.on_delete.as_deref(), mysql_actions);
|
||||
if source_on_delete != target_on_delete {
|
||||
changes.push(format!(
|
||||
"delete: {} → {}",
|
||||
@@ -4002,8 +4047,8 @@ fn foreign_key_changes(
|
||||
source_on_delete.as_deref().unwrap_or("")
|
||||
));
|
||||
}
|
||||
let source_on_update = normalized_foreign_key_action(source_fk.on_update.as_deref());
|
||||
let target_on_update = normalized_foreign_key_action(target_fk.on_update.as_deref());
|
||||
let source_on_update = comparable_foreign_key_action(source_fk.on_update.as_deref(), mysql_actions);
|
||||
let target_on_update = comparable_foreign_key_action(target_fk.on_update.as_deref(), mysql_actions);
|
||||
if source_on_update != target_on_update {
|
||||
changes.push(format!(
|
||||
"update: {} → {}",
|
||||
@@ -13692,6 +13737,85 @@ mod tests {
|
||||
assert!(diffs[0].changes.iter().any(|change| change == "update: NO ACTION → CASCADE"));
|
||||
}
|
||||
|
||||
fn mysql_fk_action_options(
|
||||
database_type: DatabaseType,
|
||||
dialect: Option<DialectKind>,
|
||||
source_actions: (Option<&str>, Option<&str>),
|
||||
target_actions: (Option<&str>, Option<&str>),
|
||||
) -> SchemaDiffPreparationOptions {
|
||||
let detail = |(on_update, on_delete): (Option<&str>, Option<&str>)| TableSchemaDetail {
|
||||
name: "purchase_order_amendment".to_string(),
|
||||
columns: vec![column("id_purchase_order", "int unsigned", None)],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: vec![foreign_key(ForeignKeyInfo {
|
||||
name: "purchase_order_amendment_id_purchase_order_foreign".into(),
|
||||
column: "id_purchase_order".into(),
|
||||
ref_schema: None,
|
||||
ref_table: "purchase_order".into(),
|
||||
ref_column: "id".into(),
|
||||
on_update: on_update.map(str::to_string),
|
||||
on_delete: on_delete.map(str::to_string),
|
||||
})],
|
||||
triggers: Vec::new(),
|
||||
ddl: None,
|
||||
};
|
||||
SchemaDiffPreparationOptions {
|
||||
source_tables: vec![table_info("purchase_order_amendment", "TABLE")],
|
||||
target_tables: vec![table_info("purchase_order_amendment", "TABLE")],
|
||||
source_details: vec![detail(source_actions)],
|
||||
target_details: vec![detail(target_actions)],
|
||||
database_type,
|
||||
source_dialect: dialect,
|
||||
target_dialect: dialect,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_fk_restrict_and_no_action_are_equivalent() {
|
||||
let cases = [
|
||||
((Some("NO ACTION"), Some("RESTRICT")), (Some("RESTRICT"), Some("RESTRICT"))),
|
||||
((Some("restrict"), Some("NO ACTION")), (Some("NO ACTION"), Some("RESTRICT"))),
|
||||
((None, None), (Some("RESTRICT"), Some("NO ACTION"))),
|
||||
((Some(""), Some("RESTRICT")), (Some("NO ACTION"), None)),
|
||||
];
|
||||
for (source, target) in cases {
|
||||
let result = prepare_schema_diff(mysql_fk_action_options(
|
||||
DatabaseType::Mysql,
|
||||
Some(DialectKind::Mysql),
|
||||
source,
|
||||
target,
|
||||
));
|
||||
assert!(result.diffs.is_empty(), "{source:?} vs {target:?} reported {:?}", result.diffs);
|
||||
assert!(!result.sync_sql.contains("FOREIGN KEY"), "no FK rebuild expected: {}", result.sync_sql);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_fk_real_action_change_is_still_reported() {
|
||||
let result = prepare_schema_diff(mysql_fk_action_options(
|
||||
DatabaseType::Mysql,
|
||||
Some(DialectKind::Mysql),
|
||||
(Some("CASCADE"), Some("SET NULL")),
|
||||
(Some("NO ACTION"), Some("RESTRICT")),
|
||||
));
|
||||
assert_eq!(result.diffs.len(), 1, "{:?}", result.diffs);
|
||||
let changes = &result.diffs[0].foreign_keys.as_ref().expect("fk diffs")[0].changes;
|
||||
assert!(changes.iter().any(|change| change == "update: NO ACTION → CASCADE"), "{changes:?}");
|
||||
assert!(changes.iter().any(|change| change == "delete: NO ACTION → SET NULL"), "{changes:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_fk_restrict_and_no_action_remain_distinct() {
|
||||
let result = prepare_schema_diff(mysql_fk_action_options(
|
||||
DatabaseType::Postgres,
|
||||
Some(DialectKind::Postgres),
|
||||
(Some("NO ACTION"), Some("RESTRICT")),
|
||||
(Some("RESTRICT"), Some("RESTRICT")),
|
||||
));
|
||||
assert_eq!(result.diffs.len(), 1, "PostgreSQL NO ACTION is deferrable, unlike RESTRICT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modified_foreign_key_sql_preserves_reference_schema_and_actions() {
|
||||
let table_diff = TableDiff {
|
||||
|
||||
Reference in new issue
Block a user