From 7e5fd88e63f4e0f6e0ae54c46978ed91ee3b9535 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 22 Jun 2026 17:21:08 +0800 Subject: [PATCH] fix(schema): use sync sql for modified schema diff --- .../src/components/diff/SchemaDiffDialog.vue | 51 +----------- apps/desktop/src/lib/schemaDiff.ts | 57 +++++++++++++ crates/dbx-core/src/schema_diff.rs | 81 ++++++++++++++++++- packages/app-tests/schemaDiff.test.ts | 46 +++++++++++ 4 files changed, 185 insertions(+), 50 deletions(-) create mode 100644 packages/app-tests/schemaDiff.test.ts diff --git a/apps/desktop/src/components/diff/SchemaDiffDialog.vue b/apps/desktop/src/components/diff/SchemaDiffDialog.vue index a689465d3..773feec89 100644 --- a/apps/desktop/src/components/diff/SchemaDiffDialog.vue +++ b/apps/desktop/src/components/diff/SchemaDiffDialog.vue @@ -18,7 +18,7 @@ import { getSchemaDiffOptionsForDbType } from "@/lib/schemaDiffOptions"; import { getDefaultOptionsForDbType } from "@/types/schemaDiff"; import type { SchemaDiffCompareOptions, SchemaDiffConfig } from "@/types/schemaDiff"; import type { ObjectSourceKind } from "@/types/database"; -import { convertToSchemaDiffObjects, groupDiffObjects, type OperationGroup, type SchemaDiffObject, type DiffOperationType, type DiffObjectKind, type SchemaDiffPreparation } from "@/lib/schemaDiff"; +import { buildDeploySqlForObjects, convertToSchemaDiffObjects, groupDiffObjects, type OperationGroup, type SchemaDiffObject, type DiffOperationType, type DiffObjectKind, type SchemaDiffPreparation } from "@/lib/schemaDiff"; import { Splitpanes, Pane } from "splitpanes"; import "splitpanes/dist/splitpanes.css"; @@ -459,54 +459,7 @@ function handleToggleObjectSelection(objectId: string, selected: boolean) { } function regenerateDeploySql() { - // Only select top-level objects (exclude children) - const selected = diffObjects.value.filter((o) => { - const isTopLevel = !o.id.startsWith("col-") && !o.id.startsWith("idx-") && !o.id.startsWith("fk-") && !o.id.startsWith("trg-"); - return o.selected && o.operationType !== "none" && isTopLevel; - }); - - if (selected.length === 0) { - deploySql.value = "-- No objects selected"; - return; - } - - const lines: string[] = []; - - for (const obj of selected) { - if (obj.operationType === "create") { - if (obj.sourceDdl) { - lines.push(`-- Create ${obj.objectKind}: ${obj.name}`); - lines.push(obj.sourceDdl); - lines.push(""); - } - } else if (obj.operationType === "delete") { - lines.push(`-- Drop ${obj.objectKind}: ${obj.name}`); - const dropSql = generateDropSql(obj); - lines.push(dropSql); - lines.push(""); - } else if (obj.operationType === "modify") { - if (obj.sourceDdl) { - lines.push(`-- Modify ${obj.objectKind}: ${obj.name}`); - lines.push(obj.sourceDdl); - lines.push(""); - } - } - } - - deploySql.value = lines.join("\n") || "-- No DDL available for selected objects"; -} - -function generateDropSql(obj: SchemaDiffObject): string { - const typeMap: Record = { - table: "TABLE", - view: "VIEW", - function: "FUNCTION", - sequence: "SEQUENCE", - rule: "RULE", - owner: "OWNED BY", - }; - const sqlType = typeMap[obj.objectKind] || obj.objectKind.toUpperCase(); - return `DROP ${sqlType} IF EXISTS ${obj.name};`; + deploySql.value = buildDeploySqlForObjects(diffObjects.value); } async function handleExecuteScript() { diff --git a/apps/desktop/src/lib/schemaDiff.ts b/apps/desktop/src/lib/schemaDiff.ts index 7327de1e6..eeecc8233 100644 --- a/apps/desktop/src/lib/schemaDiff.ts +++ b/apps/desktop/src/lib/schemaDiff.ts @@ -76,6 +76,7 @@ export interface TableDiff { targetDdl?: string; sourceTableComment?: string | null; targetTableComment?: string | null; + syncSql?: string; } export interface TableSchemaDetail { @@ -202,6 +203,7 @@ export function convertToSchemaDiffObjects(tableDiffs: TableDiff[], functionDiff selected: opType !== "none", sourceDdl: diff.ddl, targetDdl: diff.targetDdl, + deploySql: diff.syncSql, changes: diff.columns?.flatMap((c) => c.changes || []), children: [ ...(diff.columns?.map((c) => ({ @@ -310,6 +312,61 @@ export function convertToSchemaDiffObjects(tableDiffs: TableDiff[], functionDiff return objects; } +export function buildDeploySqlForObjects(objects: SchemaDiffObject[]): string { + const selected = objects.filter((o) => { + const isTopLevel = !o.id.startsWith("col-") && !o.id.startsWith("idx-") && !o.id.startsWith("fk-") && !o.id.startsWith("trg-"); + return o.selected && o.operationType !== "none" && isTopLevel; + }); + + if (selected.length === 0) { + return "-- No objects selected"; + } + + const lines: string[] = []; + + for (const obj of selected) { + if (obj.deploySql?.trim()) { + lines.push(obj.deploySql.trim()); + lines.push(""); + continue; + } + + if (obj.operationType === "create") { + if (obj.sourceDdl) { + lines.push(`-- Create ${obj.objectKind}: ${obj.name}`); + lines.push(obj.sourceDdl); + lines.push(""); + } + } else if (obj.operationType === "delete") { + lines.push(`-- Drop ${obj.objectKind}: ${obj.name}`); + const dropSql = generateDropSql(obj); + lines.push(dropSql); + lines.push(""); + } else if (obj.operationType === "modify") { + if (obj.sourceDdl) { + lines.push(`-- Modify ${obj.objectKind}: ${obj.name}`); + lines.push(obj.sourceDdl); + lines.push(""); + } + } + } + + return lines.join("\n") || "-- No DDL available for selected objects"; +} + +function generateDropSql(obj: SchemaDiffObject): string { + const typeMap: Record = { + table: "TABLE", + view: "VIEW", + function: "FUNCTION", + sequence: "SEQUENCE", + rule: "RULE", + owner: "OWNED BY", + }; + const sqlType = typeMap[obj.objectKind] || obj.objectKind.toUpperCase(); + return `DROP ${sqlType} IF EXISTS ${obj.name};`; +} + export interface ObjectTypeGroup { kind: DiffObjectKind; label: string; diff --git a/crates/dbx-core/src/schema_diff.rs b/crates/dbx-core/src/schema_diff.rs index c0807c864..107aa3834 100644 --- a/crates/dbx-core/src/schema_diff.rs +++ b/crates/dbx-core/src/schema_diff.rs @@ -143,6 +143,8 @@ pub struct TableDiff { pub source_table_comment: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub target_table_comment: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sync_sql: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -213,11 +215,26 @@ pub struct SchemaDiffPreparation { } pub fn prepare_schema_diff(options: SchemaDiffPreparationOptions) -> SchemaDiffPreparation { - let diffs = diff_schema(&options); + let mut diffs = diff_schema(&options); let function_diffs = diff_functions(&options.source_functions, &options.target_functions); let sequence_diffs = diff_sequences(&options.source_sequences, &options.target_sequences); let rule_diffs = diff_rules(&options.source_rules, &options.target_rules); let owner_diffs = diff_owners(&options.source_owners, &options.target_owners); + for diff in &mut diffs { + let sync_sql = generate_schema_sync_sql( + std::slice::from_ref(diff), + &[], + &[], + &[], + &[], + options.database_type, + options.target_schema.as_deref(), + options.cascade_delete, + ); + if !sync_sql.is_empty() { + diff.sync_sql = Some(sync_sql); + } + } let sync_sql = generate_schema_sync_sql( &diffs, &function_diffs, @@ -283,6 +300,7 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec { triggers: None, source_table_comment: None, target_table_comment: None, + sync_sql: None, }); } @@ -300,6 +318,7 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec { target_ddl: target_details.get(name_clone.as_str()).and_then(|detail| detail.ddl.clone()), source_table_comment: None, target_table_comment: None, + sync_sql: None, }); } @@ -317,6 +336,7 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec { target_ddl: None, source_table_comment: None, target_table_comment: None, + sync_sql: None, }); } @@ -334,6 +354,7 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec { target_ddl: target_details.get(name_clone.as_str()).and_then(|detail| detail.ddl.clone()), source_table_comment: None, target_table_comment: None, + sync_sql: None, }); } @@ -368,6 +389,7 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec { target_ddl: target_details.get(name_clone.as_str()).and_then(|detail| detail.ddl.clone()), source_table_comment: if has_diff { comment_changed.then_some(source_comment) } else { None }, target_table_comment: if has_diff { comment_changed.then_some(target_comment) } else { None }, + sync_sql: None, }); } @@ -1536,6 +1558,7 @@ mod tests { target_ddl: None, source_table_comment: None, target_table_comment: None, + sync_sql: None, }]; assert_eq!( @@ -1570,6 +1593,7 @@ mod tests { target_ddl: None, source_table_comment: Some(Some("用户表".to_string())), target_table_comment: Some(Some("Users".to_string())), + sync_sql: None, }]; assert_eq!( @@ -1637,6 +1661,60 @@ mod tests { assert!(result.sync_sql.is_empty()); } + #[test] + fn prepare_schema_diff_attaches_per_table_sync_sql() { + let options = SchemaDiffPreparationOptions { + source_tables: vec![TableInfo { + name: "users".to_string(), + table_type: "BASE TABLE".to_string(), + comment: None, + parent_schema: None, + parent_name: None, + }], + target_tables: vec![TableInfo { + name: "users".to_string(), + table_type: "BASE TABLE".to_string(), + comment: None, + parent_schema: None, + parent_name: None, + }], + source_details: vec![TableSchemaDetail { + name: "users".to_string(), + columns: vec![column("name", "varchar(128)", None)], + indexes: Vec::new(), + foreign_keys: Vec::new(), + triggers: Vec::new(), + ddl: Some("CREATE TABLE `users` (`name` varchar(128));".to_string()), + }], + target_details: vec![TableSchemaDetail { + name: "users".to_string(), + columns: vec![column("name", "varchar(64)", None)], + indexes: Vec::new(), + foreign_keys: Vec::new(), + triggers: Vec::new(), + ddl: Some("CREATE TABLE `users` (`name` varchar(64));".to_string()), + }], + source_functions: Vec::new(), + target_functions: Vec::new(), + source_sequences: Vec::new(), + target_sequences: Vec::new(), + source_rules: Vec::new(), + target_rules: Vec::new(), + source_owners: Vec::new(), + target_owners: Vec::new(), + database_type: DatabaseType::Mysql, + target_schema: None, + ignore_comments: false, + cascade_delete: false, + }; + + let result = prepare_schema_diff(options); + let table_sync_sql = result.diffs[0].sync_sql.as_deref().unwrap_or_default(); + + assert!(table_sync_sql.contains("ALTER TABLE `users`")); + assert!(!table_sync_sql.contains("CREATE TABLE")); + } + #[test] fn qualifies_generated_schema_sync_sql_with_target_schema() { let diffs = vec![TableDiff { @@ -1683,6 +1761,7 @@ mod tests { target_ddl: None, source_table_comment: None, target_table_comment: None, + sync_sql: None, }]; assert_eq!( diff --git a/packages/app-tests/schemaDiff.test.ts b/packages/app-tests/schemaDiff.test.ts new file mode 100644 index 000000000..de037a9f3 --- /dev/null +++ b/packages/app-tests/schemaDiff.test.ts @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import { test } from "vitest"; +import { buildDeploySqlForObjects, convertToSchemaDiffObjects, type TableDiff } from "../../apps/desktop/src/lib/schemaDiff.ts"; + +test("uses generated sync SQL for modified table deployment", () => { + const tableDiffs: TableDiff[] = [ + { + type: "modified", + objectType: "table", + name: "users", + ddl: "CREATE TABLE `users` (`name` varchar(64));", + syncSql: "-- Alter table: users\nALTER TABLE `users`\n MODIFY COLUMN `name` varchar(128) NOT NULL;", + columns: [ + { + type: "modified", + name: "name", + changes: ["type: varchar(64) -> varchar(128)"], + }, + ], + }, + ]; + + const objects = convertToSchemaDiffObjects(tableDiffs); + const deploySql = buildDeploySqlForObjects(objects); + + assert.equal( + deploySql, + "-- Alter table: users\nALTER TABLE `users`\n MODIFY COLUMN `name` varchar(128) NOT NULL;\n", + ); + assert.equal(deploySql.includes("CREATE TABLE"), false); +}); + +test("falls back to source DDL when object sync SQL is unavailable", () => { + const tableDiffs: TableDiff[] = [ + { + type: "added", + objectType: "table", + name: "users", + ddl: "CREATE TABLE `users` (`id` int);", + }, + ]; + + const objects = convertToSchemaDiffObjects(tableDiffs); + + assert.equal(buildDeploySqlForObjects(objects), "-- Create table: users\nCREATE TABLE `users` (`id` int);\n"); +});