fix(schema): use sync sql for modified schema diff

This commit is contained in:
t8y2 2026-06-22 17:21:08 +08:00
parent dac97b83a6
commit 7e5fd88e63
4 changed files with 185 additions and 50 deletions

View File

@ -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<string, string> = {
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() {

View File

@ -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<string, string> = {
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;

View File

@ -143,6 +143,8 @@ pub struct TableDiff {
pub source_table_comment: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_table_comment: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sync_sql: Option<String>,
}
#[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<TableDiff> {
triggers: None,
source_table_comment: None,
target_table_comment: None,
sync_sql: None,
});
}
@ -300,6 +318,7 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec<TableDiff> {
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<TableDiff> {
target_ddl: None,
source_table_comment: None,
target_table_comment: None,
sync_sql: None,
});
}
@ -334,6 +354,7 @@ fn diff_schema(options: &SchemaDiffPreparationOptions) -> Vec<TableDiff> {
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<TableDiff> {
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!(

View File

@ -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");
});