diff --git a/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue b/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue index 23bb3dae99..6946109c2d 100644 --- a/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue +++ b/apps/desktop/src/components/diff/SchemaDiffConfigStep.vue @@ -105,6 +105,15 @@ function handleUpdateSelectedTables(value: string[]) { if (restrictTables.value) emit("update:selectedTables", [...value]); } +function clearUnavailableTableSelection() { + sourceTableList.value = []; + targetTableList.value = []; + if (props.selectedTables === undefined && !restrictTables.value && localSelectedTables.value.length === 0) return; + restrictTables.value = false; + localSelectedTables.value = []; + emit("update:selectedTables", undefined); +} + function getTableIdentity(side: SchemaDiffTableSide): SchemaDiffTableIdentity { return side === "source" ? { connectionId: props.sourceConnectionId, database: props.sourceDatabase, schema: props.sourceSchema } : { connectionId: props.targetConnectionId, database: props.targetDatabase, schema: props.targetSchema }; } @@ -277,13 +286,17 @@ watch( selectedTables: props.selectedTables, }), (current, previous) => { + if (!isTableIdentityReady("source")) { + clearUnavailableTableSelection(); + return; + } + if (!previous) return; if (current.source.every((value, index) => value === previous.source[index])) return; if (current.selectedTables !== previous.selectedTables) return; if (current.selectedTables === undefined && !restrictTables.value) return; - restrictTables.value = false; - localSelectedTables.value = []; - emit("update:selectedTables", undefined); + clearUnavailableTableSelection(); }, + { immediate: true }, ); watch( @@ -446,7 +459,15 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
{{ t("diff.tableSelectionUnrestricted") }}
- + @@ -535,7 +556,7 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st -
+
{{ t("diff.autoMatchHint") }}
{{ t("diff.matchedTables", { matched: matchResult.matched.length, total: localSelectedTables.length }) }} diff --git a/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue b/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue index c719e7e69f..16645b7cd8 100644 --- a/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue +++ b/apps/desktop/src/components/diff/SchemaDiffDdlPanel.vue @@ -8,6 +8,8 @@ import { useSettingsStore } from "@/stores/settingsStore"; import { DEFAULT_CUSTOM_THEME_DDL_COLORS } from "@/stores/settingsStore"; import { useDiffScrollSync } from "@/composables/useDiffScrollSync"; import { buildHunks, type DiffLine } from "@/components/diff/DiffHunkBuilder"; +import { buildSchemaDiffHighlightSegments, type SchemaDiffHighlightSegment } from "@/lib/schema/schemaDiffHighlight"; +import { findSchemaDiffDdlLineNumber } from "@/lib/schema/schemaDiffDdlLocate"; import DiffSvgConnector from "@/components/diff/DiffSvgConnector.vue"; import { FileCode, ScrollText, Copy, Play, FileDiff } from "@lucide/vue"; import { Splitpanes, Pane } from "splitpanes"; @@ -34,6 +36,7 @@ function toRgba(hex: string, alpha: number): string { const props = defineProps<{ selectedObject: SchemaDiffObject | null; + focusedObject?: SchemaDiffObject | null; deploySql: string; deploySqlAll: string; compatibilityWarnings?: CompatibilityWarning[]; @@ -57,6 +60,8 @@ const leftPaneRef = ref(); const rightPaneRef = ref(); const containerSize = ref({ width: 0, height: 0 }); const connectorKey = ref(0); +const focusedSourceLineNumber = ref(null); +const focusedTargetLineNumber = ref(null); const rollbackDiffContainerRef = ref(); const rollbackLeftPaneRef = ref(); @@ -89,7 +94,7 @@ const { syncScroll: rollbackSyncScroll, measureHunks: rollbackMeasureHunks } = u }); function collectModifySegments(diffHunks: ReturnType) { - const map = new Map(); + const map = new Map(); for (const hunk of diffHunks) { for (let i = 0; i < hunk.leftLines.length; i++) { const left = hunk.leftLines[i]; @@ -99,13 +104,13 @@ function collectModifySegments(diffHunks: ReturnType) { map.set(key, renderModifyLine(left, right)); } else if (left.type === "modify" && !left.isPadding && left.comparisonContent !== undefined) { map.set(key, { - leftSegments: renderModifyContent(left.content, left.comparisonContent).sourceSegments, + leftSegments: buildSchemaDiffHighlightSegments(left.content, left.comparisonContent).sourceSegments, rightSegments: [], }); } else if (right.type === "modify" && !right.isPadding && right.comparisonContent !== undefined) { map.set(key, { leftSegments: [], - rightSegments: renderModifyContent(right.comparisonContent, right.content).targetSegments, + rightSegments: buildSchemaDiffHighlightSegments(right.comparisonContent, right.content).targetSegments, }); } } @@ -177,14 +182,46 @@ function rollbackUpdateContainerSize() { rollbackContainerSize.value = { width: rect.width, height: rect.height }; } -watch( - () => props.selectedObject?.id, - async () => { - await nextTick(); - updateContainerSize(); - requestMeasure(); - }, -); +watch([() => props.selectedObject?.id, () => props.focusedObject?.id, hunks], async () => { + if (props.focusedObject?.parentId) activeTab.value = "ddl"; + await nextTick(); + updateContainerSize(); + requestMeasure(); + locateFocusedObject(); +}); + +function scrollPaneToLine(pane: HTMLDivElement | undefined, lineNumber: number | null) { + if (!pane || lineNumber === null) return; + const row = pane.querySelector(`[data-ddl-line-number="${lineNumber}"]`); + if (!row) return; + const paneRect = pane.getBoundingClientRect(); + const rowRect = row.getBoundingClientRect(); + pane.scrollTo({ + top: Math.max(0, pane.scrollTop + rowRect.top - paneRect.top - pane.clientHeight / 2 + rowRect.height / 2), + behavior: "smooth", + }); +} + +function locateFocusedObject() { + const selectedObject = props.selectedObject; + const focusedObject = props.focusedObject; + if (!selectedObject || !focusedObject || focusedObject.id === selectedObject.id) { + focusedSourceLineNumber.value = null; + focusedTargetLineNumber.value = null; + return; + } + + focusedSourceLineNumber.value = findSchemaDiffDdlLineNumber(selectedObject.sourceDdl ?? "", focusedObject, "source"); + focusedTargetLineNumber.value = findSchemaDiffDdlLineNumber(selectedObject.targetDdl ?? "", focusedObject, "target"); + scrollPaneToLine(leftPaneRef.value, focusedSourceLineNumber.value); + scrollPaneToLine(rightPaneRef.value, focusedTargetLineNumber.value); +} + +function focusedLineClass(line: DiffLine, side: "source" | "target"): string { + const focusedLineNumber = side === "source" ? focusedSourceLineNumber.value : focusedTargetLineNumber.value; + if (line.isPadding || focusedLineNumber === null) return ""; + return line.lineNumber === focusedLineNumber ? "outline outline-1 -outline-offset-1 outline-primary/70" : ""; +} function updateContainerSize() { const el = diffContainerRef.value; @@ -225,86 +262,11 @@ function lineTextClass(line: DiffLine): string { return ""; } -function computeCharDiffs(source: string, target: string): { source: string; target: string }[] { - const result: { source: string; target: string }[] = []; - let sIdx = 0; - let tIdx = 0; - while (sIdx < source.length || tIdx < target.length) { - if (sIdx >= source.length) { - result.push({ source: "", target: target.substring(tIdx) }); - break; - } - if (tIdx >= target.length) { - result.push({ source: source.substring(sIdx), target: "" }); - break; - } - if (source[sIdx] === target[tIdx]) { - let matchLen = 0; - while (sIdx + matchLen < source.length && tIdx + matchLen < target.length && source[sIdx + matchLen] === target[tIdx + matchLen]) { - matchLen++; - } - result.push({ - source: source.substring(sIdx, sIdx + matchLen), - target: target.substring(tIdx, tIdx + matchLen), - }); - sIdx += matchLen; - tIdx += matchLen; - } else { - let sMatch = -1; - let tMatch = -1; - for (let i = 0; i < Math.min(10, source.length - sIdx, target.length - tIdx); i++) { - if (source[sIdx + i] === target[tIdx]) { - sMatch = i; - tMatch = 0; - break; - } - if (source[sIdx] === target[tIdx + i]) { - sMatch = 0; - tMatch = i; - break; - } - } - if (sMatch === -1) { - sMatch = Math.min(1, source.length - sIdx); - tMatch = Math.min(1, target.length - tIdx); - } - result.push({ - source: source.substring(sIdx, sIdx + (sMatch > 0 ? sMatch : 1)), - target: target.substring(tIdx, tIdx + (tMatch > 0 ? tMatch : 1)), - }); - sIdx += sMatch > 0 ? sMatch : 1; - tIdx += tMatch > 0 ? tMatch : 1; - } - } - return result; -} - -function renderModifyLine(leftLine: DiffLine, rightLine: DiffLine): { leftSegments: Segment[]; rightSegments: Segment[] } { - const { sourceSegments, targetSegments } = renderModifyContent(leftLine.content, rightLine.content); +function renderModifyLine(leftLine: DiffLine, rightLine: DiffLine): { leftSegments: SchemaDiffHighlightSegment[]; rightSegments: SchemaDiffHighlightSegment[] } { + const { sourceSegments, targetSegments } = buildSchemaDiffHighlightSegments(leftLine.content, rightLine.content); return { leftSegments: sourceSegments, rightSegments: targetSegments }; } -function renderModifyContent(source: string, target: string): { sourceSegments: Segment[]; targetSegments: Segment[] } { - const charDiffs = computeCharDiffs(source, target); - const leftSegments: Segment[] = []; - const rightSegments: Segment[] = []; - for (const cd of charDiffs) { - if (cd.source === cd.target) { - leftSegments.push({ text: cd.source, changed: false }); - rightSegments.push({ text: cd.target, changed: false }); - } else { - if (cd.source) leftSegments.push({ text: cd.source, changed: true }); - if (cd.target) rightSegments.push({ text: cd.target, changed: true }); - } - } - return { sourceSegments: leftSegments, targetSegments: rightSegments }; -} - -interface Segment { - text: string; - changed: boolean; -} - function copyDeploySql() { copyToClipboard(props.deploySql); toast(t("diff.copied"), 2000); @@ -386,19 +348,11 @@ function copyDeploySqlAll() {
-
+
{{ t("diff.sourceDdl") }}
-
+
{{ line.lineNumber ?? "" }} @@ -418,19 +372,11 @@ function copyDeploySqlAll() {
-
+
{{ t("diff.targetDdl") }}
-
+
{{ line.lineNumber ?? "" }} @@ -462,19 +408,11 @@ function copyDeploySqlAll() {
-
+
{{ t("rollbackComparison.forwardSql") }}
-
+
{{ line.lineNumber ?? "" }} @@ -492,19 +430,11 @@ function copyDeploySqlAll() {
-
+
{{ t("rollbackComparison.rollbackSql") }}
-
+
{{ line.lineNumber ?? "" }} diff --git a/apps/desktop/src/components/diff/SchemaDiffDeployStep.vue b/apps/desktop/src/components/diff/SchemaDiffDeployStep.vue index 69cdd7635c..fee4af85f4 100644 --- a/apps/desktop/src/components/diff/SchemaDiffDeployStep.vue +++ b/apps/desktop/src/components/diff/SchemaDiffDeployStep.vue @@ -9,7 +9,20 @@ import { useTheme } from "@/composables/useTheme"; import { loadEditorTheme, editorFontTheme } from "@/lib/editor/editorThemes"; import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect"; import { Splitpanes, Pane } from "splitpanes"; -import { schemaDiffReviewAlert, selectedSchemaDiffObjects, summarizeSchemaDiffOperations, type SchemaDiffObject, type DiffOperationType, type DiffObjectKind, type CompatibilityWarning, type RenameCandidate, type MissingRollbackObject, type RollbackCompleteness } from "@/lib/schema/schemaDiff"; +import { + schemaDiffObjectSelectionState, + schemaDiffReviewAlert, + schemaDiffSelectionTargets, + selectedSchemaDiffObjects, + summarizeSchemaDiffOperations, + type SchemaDiffObject, + type DiffOperationType, + type DiffObjectKind, + type CompatibilityWarning, + type RenameCandidate, + type MissingRollbackObject, + type RollbackCompleteness, +} from "@/lib/schema/schemaDiff"; import ImpactReportPanel from "@/components/diff/ImpactReportPanel.vue"; import type { ImpactReport } from "@/types/governance"; import { ArrowLeft, Copy, Download, Play, Loader2, PlusCircle, XCircle, ArrowRightLeft, Table, Eye, FunctionSquare, ListOrdered, ScrollText, UserCog, Columns3, ListTree, Link2, Zap, AlertTriangle, ShieldCheck } from "@lucide/vue"; @@ -54,13 +67,13 @@ const objectPositions = computed(() => { const positions = new Map(); const sql = props.deploySql; for (const obj of topLevelObjects.value) { - const patterns = [`-- Create ${obj.objectKind}: ${obj.name}`, `-- Modify ${obj.objectKind}: ${obj.name}`, `-- Drop ${obj.objectKind}: ${obj.name}`]; + const patterns = [`-- Create ${obj.objectKind}: ${obj.name}`, `-- Alter ${obj.objectKind}: ${obj.name}`, `-- Modify ${obj.objectKind}: ${obj.name}`, `-- Drop ${obj.objectKind}: ${obj.name}`]; for (const pattern of patterns) { const index = sql.indexOf(pattern); if (index !== -1) { let endPos = sql.length; const remaining = sql.slice(index + pattern.length); - const nextMatch = remaining.match(/--\s*(Create|Modify|Drop)\s+\w+:/); + const nextMatch = remaining.match(/--\s*(Create|Alter|Modify|Drop)\s+\w+:/); if (nextMatch && nextMatch.index !== undefined) { endPos = index + pattern.length + nextMatch.index; } @@ -90,8 +103,8 @@ const topLevelObjects = computed(() => { const operationOrder: Record = { create: 0, modify: 1, delete: 2, none: 3 }; return props.selectedObjects .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; + const state = schemaDiffObjectSelectionState(o); + return o.operationType !== "none" && (state.checked || state.indeterminate); }) .sort((a, b) => operationOrder[a.operationType] - operationOrder[b.operationType]); }); @@ -338,6 +351,7 @@ function getObjectIconColor(kind: DiffObjectKind): string { {{ obj.name }} + {{ schemaDiffSelectionTargets(obj).filter((child) => child.selected).length }}/{{ schemaDiffSelectionTargets(obj).length }} {{ getOperationLabel(obj.operationType) }} diff --git a/apps/desktop/src/components/diff/SchemaDiffDialog.vue b/apps/desktop/src/components/diff/SchemaDiffDialog.vue index dbdf8fd75a..9088456ee5 100644 --- a/apps/desktop/src/components/diff/SchemaDiffDialog.vue +++ b/apps/desktop/src/components/diff/SchemaDiffDialog.vue @@ -22,24 +22,26 @@ import { createConcurrencyLimiter, mapWithConcurrency, schemaDiffMetadataConcurr import { createSchemaDiffTableListLoader, type SchemaDiffTableIdentity } from "@/lib/schema/schemaDiffTableList"; import { normalizeSchemaDiffCompareOptions } from "@/types/schemaDiff"; import type { SchemaDiffCompareOptions, SchemaDiffConfig, FieldMappingEntry } from "@/types/schemaDiff"; -import type { ObjectSourceKind, TableInfo } from "@/types/database"; +import type { DatabaseType, ObjectSourceKind, TableInfo } from "@/types/database"; import { - buildDeploySqlForObjects, convertToSchemaDiffObjects, detectDestructiveSchemaDiffStatements, groupDiffObjects, injectColumnRenameSql, schemaDiffDeployTargetSchema, - schemaDiffSelectionOwnerId, + findSchemaDiffObject, + flattenSchemaDiffObjects, + schemaDiffSelectionTargets, + selectSchemaDiffInput, selectedSchemaDiffObjects, setSchemaDiffObjectSelected, + setSchemaDiffObjectSelectedWithDependencies, summarizeSchemaDiffOperations, databaseTypeToDialectKind, normalizeDialectKind, type OperationGroup, type SchemaDiffObject, type DiffOperationType, - type DiffObjectKind, type SchemaDiffPreparation, type MissingRollbackObject, type RollbackCompleteness, @@ -122,6 +124,7 @@ const renameCandidates = ref([]); const compatibilityWarnings = ref([]); const permissionDiffs = ref([]); const dependencyGraph = ref(null); +let deploySqlGeneration = 0; // Rename candidates panel const showRenamePanel = ref(true); @@ -250,25 +253,54 @@ const { configs, activeConfigId, activeConfig, recentConfigs, ensureDefaultConfi const schemaDiffPanelOptions = computed(() => normalizeSchemaDiffCompareOptions(activeConfig.value?.options, getDbType())); const selectedObject = computed(() => { + const object = selectedTreeObject.value; + if (!object) return null; + return object.parentId ? (findSchemaDiffObject(diffObjects.value, object.parentId) ?? object) : object; +}); + +const selectedTreeObject = computed(() => { if (!selectedObjectId.value) return null; for (const group of diffGroups.value) { for (const typeGroup of group.typeGroups) { - const obj = typeGroup.objects.find((o) => o.id === selectedObjectId.value); - if (obj) return obj; + const object = flattenSchemaDiffObjects(typeGroup.objects).find((candidate) => candidate.id === selectedObjectId.value); + if (object) return object; } } return null; }); const canDeploy = computed(() => { - return diffObjects.value.some((o) => o.selected && o.operationType !== "none"); + return selectedSchemaDiffObjects(diffObjects.value).length > 0; }); +function resetComparisonResultState() { + deploySqlGeneration++; + step.value = "config"; + diffObjects.value = []; + diffGroups.value = []; + selectedObjectId.value = null; + deploySql.value = ""; + deploySqlAll.value = ""; + lastDiffResult.value = null; + rollbackSql.value = ""; + rollbackCompleteness.value = "complete"; + missingRollbackObjects.value = []; + renameCandidates.value = []; + compatibilityWarnings.value = []; + permissionDiffs.value = []; + dependencyGraph.value = null; + deploySqlMode.value = "forward"; + showConfirmDialog.value = false; + showResultDialog.value = false; + deployResult.value = null; +} + // Watch for prefilled values watch( () => open.value, (isOpen) => { if (isOpen) { + resetComparisonResultState(); ensureDefaultConfig(); if (props.prefillConnectionId) { sourceConnectionId.value = props.prefillConnectionId; @@ -308,7 +340,7 @@ watch( }, ); -function getDbType(): string { +function getDbType(): DatabaseType { const targetConfig = store.getConfig(targetConnectionId.value); return targetConfig?.db_type || "postgres"; } @@ -528,7 +560,7 @@ async function handleCompare() { } } deploySqlMode.value = "forward"; - regenerateDeploySql(); + await regenerateDeploySql(); step.value = "result"; } catch (e: any) { @@ -543,39 +575,29 @@ function handleToggleGroup(operationType: DiffOperationType) { diffGroups.value = diffGroups.value.map((g) => (g.operationType === operationType ? { ...g, expanded: !g.expanded } : g)); } -function handleToggleTypeGroup(operationType: DiffOperationType, kind: DiffObjectKind) { - diffGroups.value = diffGroups.value.map((g) => { - if (g.operationType !== operationType) return g; - return { - ...g, - typeGroups: g.typeGroups.map((tg) => (tg.kind === kind ? { ...tg, expanded: !tg.expanded } : tg)), - }; - }); -} - function handleToggleGroupSelection(operationType: DiffOperationType, selected: boolean) { const group = diffGroups.value.find((candidate) => candidate.operationType === operationType); for (const object of group?.typeGroups.flatMap((typeGroup) => typeGroup.objects) ?? []) { - setSchemaDiffObjectSelected(diffObjects.value, schemaDiffSelectionOwnerId(object), selected); + for (const target of schemaDiffSelectionTargets(object)) { + updateObjectSelection(target.id, selected); + } } rebuildDiffGroups(); - regenerateDeploySql(); + void regenerateDeploySql(); } -function handleToggleTypeSelection(operationType: DiffOperationType, kind: DiffObjectKind, selected: boolean) { - const typeGroup = diffGroups.value.find((group) => group.operationType === operationType)?.typeGroups.find((candidate) => candidate.kind === kind); - for (const object of typeGroup?.objects ?? []) { - setSchemaDiffObjectSelected(diffObjects.value, schemaDiffSelectionOwnerId(object), selected); +function handleToggleObjectSelection(object: SchemaDiffObject, selected: boolean) { + let changed = false; + for (const target of schemaDiffSelectionTargets(object)) { + changed = updateObjectSelection(target.id, selected) || changed; } + if (!changed) return; rebuildDiffGroups(); - regenerateDeploySql(); + void regenerateDeploySql(); } -function handleToggleObjectSelection(objectId: string, selected: boolean) { - const reviewObject = diffGroups.value.flatMap((group) => group.typeGroups.flatMap((typeGroup) => typeGroup.objects)).find((object) => object.id === objectId); - if (!setSchemaDiffObjectSelected(diffObjects.value, reviewObject ? schemaDiffSelectionOwnerId(reviewObject) : objectId, selected)) return; - rebuildDiffGroups(); - regenerateDeploySql(); +function updateObjectSelection(objectId: string, selected: boolean): boolean { + return lastDiffResult.value ? setSchemaDiffObjectSelectedWithDependencies(diffObjects.value, lastDiffResult.value, objectId, selected) : setSchemaDiffObjectSelected(diffObjects.value, objectId, selected); } function rebuildDiffGroups() { @@ -591,8 +613,43 @@ function rebuildDiffGroups() { })); } -function regenerateDeploySql() { - deploySql.value = buildDeploySqlForObjects(diffObjects.value); +async function regenerateDeploySql() { + const result = lastDiffResult.value; + if (!result) { + deploySql.value = "-- No objects selected"; + rollbackSql.value = ""; + return; + } + + const generation = ++deploySqlGeneration; + const options = normalizeSchemaDiffCompareOptions(activeConfig.value?.options, getDbType()); + const input = selectSchemaDiffInput(result, diffObjects.value); + let plan; + try { + plan = await api.generateSchemaSyncPlan(input, { + databaseType: getDbType(), + targetSchema: schemaDiffDeployTargetSchema(getDbType(), targetDatabase.value, targetSchema.value), + cascadeDelete: options.cascadeDelete, + sourceDialect: options.sourceDialect ? normalizeDialectKind(options.sourceDialect) : sourceDbType.value ? databaseTypeToDialectKind(sourceDbType.value) : undefined, + fieldMappings: options.fieldMappings, + enableRollback: options.enableRollback, + }); + } catch (error: any) { + if (generation === deploySqlGeneration) toast(error?.message || String(error), 5000); + return; + } + if (generation !== deploySqlGeneration) return; + + let forwardSql = plan.syncSql || "-- No objects selected"; + let nextRollbackSql = plan.rollbackSyncSql ?? ""; + rollbackCompleteness.value = plan.rollbackCompleteness ?? "complete"; + missingRollbackObjects.value = plan.missingRollbackObjects ?? []; + if (options.detectRenames && options.renameThreshold) { + forwardSql = injectColumnRenameSql(forwardSql, input.diffs, options.renameThreshold); + if (nextRollbackSql) nextRollbackSql = injectColumnRenameSql(nextRollbackSql, input.diffs, options.renameThreshold, true); + } + rollbackSql.value = nextRollbackSql; + deploySql.value = deploySqlMode.value === "rollback" && nextRollbackSql ? nextRollbackSql : forwardSql; } function switchDeploySqlMode(mode: "forward" | "rollback") { @@ -604,7 +661,7 @@ function switchDeploySqlMode(mode: "forward" | "rollback") { if (mode === "rollback" && rollbackSql.value) { deploySql.value = rollbackSql.value; } else { - regenerateDeploySql(); + void regenerateDeploySql(); } } @@ -643,7 +700,8 @@ function applyRename(rc: RenameCandidate) { } } if (found) { - regenerateDeploySql(); + rebuildDiffGroups(); + void regenerateDeploySql(); toast(t("diff.renameApplied"), 2000); } } @@ -659,11 +717,12 @@ function ignoreRename(index: number) { } } renameCandidates.value.splice(index, 1); - regenerateDeploySql(); + rebuildDiffGroups(); + void regenerateDeploySql(); } async function handleExecuteScript() { - if (!deploySql.value || deploySql.value.startsWith("-- ")) { + if (!deploySql.value.trim() || deploySql.value.trim() === "-- No objects selected") { toast(t("diff.noObjectsSelected"), 3000); return; } @@ -706,8 +765,9 @@ function showDeployTxResult(txLog: any) { deployResult.value = buildDeployTxResult(txLog, t); showResultDialog.value = true; } -async function handleSelectObject(obj: SchemaDiffObject) { - selectedObjectId.value = obj.id; +async function handleSelectObject(reviewObject: SchemaDiffObject) { + selectedObjectId.value = reviewObject.id; + const obj = reviewObject.parentId ? (findSchemaDiffObject(diffObjects.value, reviewObject.parentId) ?? reviewObject) : reviewObject; // Dynamically fetch DDL for objects that don't have pre-generated DDL // (views need runtime retrieval; functions should already have definition) @@ -806,7 +866,7 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st } function handleDeployReview() { - const selectedObjects = diffObjects.value.filter((o) => o.selected && o.operationType !== "none"); + const selectedObjects = selectedSchemaDiffObjects(diffObjects.value); if (selectedObjects.length === 0) { toast(t("diff.noObjectsSelected"), 3000); return; @@ -843,6 +903,13 @@ const deployStats = computed(() => { }; }); +const selectedCompatibilityWarnings = computed(() => { + if (!lastDiffResult.value) return []; + const input = selectSchemaDiffInput(lastDiffResult.value, diffObjects.value); + const selectedColumns = new Set(input.diffs.flatMap((diff) => (diff.columns ?? []).map((column) => `${diff.name}\u0000${column.name}`))); + return compatibilityWarnings.value.filter((warning) => selectedColumns.has(`${warning.table}\u0000${warning.column}`)); +}); + const targetConnectionInfo = computed(() => { const config = store.getConfig(targetConnectionId.value); if (!config) return null; @@ -959,24 +1026,16 @@ const targetConnectionInfo = computed(() => {
- +
{ :executing="executing" :rollback-sql="rollbackSql" :deploy-sql-mode="deploySqlMode" - :compatibility-warnings="compatibilityWarnings" + :compatibility-warnings="selectedCompatibilityWarnings" :rename-candidates="renameCandidates" :rollback-completeness="rollbackCompleteness" :missing-rollback-objects="missingRollbackObjects" diff --git a/apps/desktop/src/components/diff/SchemaDiffObjectTree.vue b/apps/desktop/src/components/diff/SchemaDiffObjectTree.vue index b19ed4443a..6f68f4d04f 100644 --- a/apps/desktop/src/components/diff/SchemaDiffObjectTree.vue +++ b/apps/desktop/src/components/diff/SchemaDiffObjectTree.vue @@ -1,25 +1,11 @@