diff --git a/packages/sqle/src/locale/en-US/dataSourceComparison.ts b/packages/sqle/src/locale/en-US/dataSourceComparison.ts
new file mode 100644
index 000000000..a347f8370
--- /dev/null
+++ b/packages/sqle/src/locale/en-US/dataSourceComparison.ts
@@ -0,0 +1,18 @@
+// Only the keys that participate in cross-locale switching are translated
+// here; other dataSourceComparison keys fall back to zh-CN via i18next's
+// per-key fallback (fallbackLng = zh-CN). The drop-create warning banner
+// MUST display in the user's selected language because it surfaces data
+// loss risk; relying on zh-CN fallback would defeat the i18n contract.
+// eslint-disable-next-line import/no-anonymous-default-export
+export default {
+ entry: {
+ modifiedSqlDrawer: {
+ dropCreateWarningBanner:
+ 'Modify SQL contains destructive operations. Please review before execution.',
+ dropCreateWarningTable:
+ 'Data loss risk; table will be dropped and recreated.',
+ dropCreateWarningView:
+ 'View will be recreated; downstream queries depending on this view may be affected.'
+ }
+ }
+};
diff --git a/packages/sqle/src/locale/en-US/index.ts b/packages/sqle/src/locale/en-US/index.ts
index cbd01cc81..2e356e3b9 100644
--- a/packages/sqle/src/locale/en-US/index.ts
+++ b/packages/sqle/src/locale/en-US/index.ts
@@ -28,6 +28,7 @@ import pipelineConfiguration from './pipelineConfiguration';
import versionManagement from './versionManagement';
import sqlInsights from './sqlInsights';
import globalDashboard from './globalDashboard';
+import dataSourceComparison from './dataSourceComparison';
// eslint-disable-next-line import/no-anonymous-default-export
export default {
@@ -61,6 +62,7 @@ export default {
pipelineConfiguration,
versionManagement,
sqlInsights,
- globalDashboard
+ globalDashboard,
+ dataSourceComparison
}
};
diff --git a/packages/sqle/src/locale/zh-CN/dataSourceComparison.ts b/packages/sqle/src/locale/zh-CN/dataSourceComparison.ts
index 68d50c8a1..cf522ae31 100644
--- a/packages/sqle/src/locale/zh-CN/dataSourceComparison.ts
+++ b/packages/sqle/src/locale/zh-CN/dataSourceComparison.ts
@@ -53,7 +53,11 @@ export default {
}
},
modifiedSqlDrawer: {
- title: '变更SQL语句信息'
+ title: '变更SQL语句信息',
+ dropCreateWarningBanner:
+ '变更SQL中含数据破坏性操作,请人工确认后再执行。',
+ dropCreateWarningTable: '数据将丢失;表将被删除并重建。',
+ dropCreateWarningView: '视图将被重建;依赖该视图的下游查询可能受影响。'
},
modifiedSqlAuditResult: {
cardTitle: '变更语句'
diff --git a/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/__tests__/ModifiedSqlDrawer.test.tsx b/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/__tests__/ModifiedSqlDrawer.test.tsx
index 414610913..1c1d77deb 100644
--- a/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/__tests__/ModifiedSqlDrawer.test.tsx
+++ b/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/__tests__/ModifiedSqlDrawer.test.tsx
@@ -8,6 +8,10 @@ import { mockUsePermission } from '@actiontech/shared/lib/testUtil/mockHook/mock
import { getBySelector } from '@actiontech/shared/lib/testUtil/customQuery';
import { mockProjectInfo } from '@actiontech/shared/lib/testUtil/mockHook/data';
import { compressToEncodedURIComponent } from 'lz-string';
+import { IDatabaseDiffModifySQL } from '@actiontech/shared/lib/api/sqle/service/common';
+import i18n from 'i18next';
+import enUSDataSourceComparison from '../../../../locale/en-US/dataSourceComparison';
+import zhCNDataSourceComparison from '../../../../locale/zh-CN/dataSourceComparison';
describe('ModifiedSqlDrawer', () => {
beforeEach(() => {
@@ -119,4 +123,152 @@ describe('ModifiedSqlDrawer', () => {
customRender(true);
expect(screen.queryByText('生成变更工单')).not.toBeInTheDocument();
});
+
+ // ---------------------------------------------------------------------------
+ // UI-3 (compat-RISK-6): WARNING banner + per-line highlight coverage.
+ //
+ // map-case fixtures: every scenario stuffs `databaseDiffModifiedSqlInfos`
+ // with a hand-crafted `modify_sqls` list so the production code's WARNING
+ // detection (`anySqlHasWarning` + `sqlHasWarningLine`) walks the same input
+ // path it would in real life. We never assert against translated strings to
+ // avoid coupling to the locale wording — the data-testid contract is the
+ // stable surface for both banner and highlight.
+ // ---------------------------------------------------------------------------
+ const buildDiffInfos = (
+ sqls: string[],
+ schemaName = 'dms'
+ ): IDatabaseDiffModifySQL[] => [
+ {
+ schema_name: schemaName,
+ modify_sqls: sqls.map((sql) => ({ sql_statement: sql }))
+ }
+ ];
+
+ const renderWithSqls = (sqls: string[]) =>
+ sqleSuperRender(
+
+ );
+
+ it('should render the drop-create warning banner when a SQL statement contains -- WARNING comment', () => {
+ renderWithSqls([
+ `-- WARNING: data loss risk; table will be dropped and recreated.\nDROP TABLE IF EXISTS audit_files;\nCREATE TABLE audit_files (id BIGINT);`
+ ]);
+
+ expect(
+ screen.getByTestId('modified-sql-drop-create-warning-banner')
+ ).toBeInTheDocument();
+ });
+
+ it('should add a warning-highlight-line class to every -- WARNING line inside the SQL preview', () => {
+ renderWithSqls([
+ `-- WARNING: data loss risk; table will be dropped and recreated.\nDROP TABLE IF EXISTS audit_files;\nCREATE TABLE audit_files (id BIGINT);`
+ ]);
+
+ const highlightContainer = screen.getByTestId('warning-highlighted-sql');
+ expect(highlightContainer).toBeInTheDocument();
+ const warningLineNodes = highlightContainer.querySelectorAll(
+ '.warning-highlight-line'
+ );
+ expect(warningLineNodes.length).toBe(1);
+ expect(warningLineNodes[0].textContent).toMatch(/-- WARNING:/);
+ });
+
+ it('should not render the banner nor the warning highlight container when no SQL contains a -- WARNING comment', () => {
+ renderWithSqls([
+ `ALTER TABLE audit_files ADD COLUMN status VARCHAR(32);`,
+ `ALTER TABLE audit_files ADD COLUMN created_at DATETIME;`
+ ]);
+
+ expect(
+ screen.queryByTestId('modified-sql-drop-create-warning-banner')
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId('warning-highlighted-sql')
+ ).not.toBeInTheDocument();
+ // Sanity: SQL lines still render but never carry the highlight class.
+ expect(document.querySelectorAll('.warning-highlight-line').length).toBe(0);
+ });
+
+ it('should render the drop-create warning banner only once even when multiple modify SQLs carry -- WARNING comments', () => {
+ renderWithSqls([
+ `-- WARNING: view will be recreated; downstream queries may be affected.\nDROP VIEW IF EXISTS v_audit_summary;\nCREATE VIEW v_audit_summary AS SELECT id FROM audit_files;`,
+ `-- WARNING: data loss risk; table will be dropped and recreated.\nDROP TABLE IF EXISTS audit_files;\nCREATE TABLE audit_files (id BIGINT, status VARCHAR(32));`
+ ]);
+
+ expect(
+ screen.queryAllByTestId('modified-sql-drop-create-warning-banner').length
+ ).toBe(1);
+ // Both SQL fragments must keep their own highlight container — banner
+ // dedup happens at the drawer level, not by collapsing per-fragment
+ // visual cues.
+ expect(screen.queryAllByTestId('warning-highlighted-sql').length).toBe(2);
+ expect(document.querySelectorAll('.warning-highlight-line').length).toBe(2);
+ });
+
+ // Case 4 — i18n switching: banner copy MUST follow the active language.
+ // jest-setup only seeds zh-CN; we add the en-US bundle on the fly so the
+ // test can flip to English and assert the literal differs from Chinese.
+ describe('locale switching for the WARNING banner copy', () => {
+ beforeAll(() => {
+ i18n.addResourceBundle(
+ 'en-US',
+ 'translation',
+ { dataSourceComparison: enUSDataSourceComparison },
+ true,
+ true
+ );
+ });
+
+ afterEach(() => {
+ // restore default zh-CN so the rest of the test file keeps using the
+ // language jest-setup expects
+ i18n.changeLanguage('zh-CN');
+ });
+
+ it('should render the WARNING banner copy according to the active language (zh-CN vs en-US)', async () => {
+ const sqls = [
+ `-- WARNING: data loss risk; table will be dropped and recreated.\nDROP TABLE IF EXISTS audit_files;\nCREATE TABLE audit_files (id BIGINT);`
+ ];
+
+ // ----- zh-CN -----
+ await i18n.changeLanguage('zh-CN');
+ const zhView = renderWithSqls(sqls);
+ const zhBanner = zhView.getByTestId(
+ 'modified-sql-drop-create-warning-banner'
+ );
+ const zhCopy =
+ zhCNDataSourceComparison.entry.modifiedSqlDrawer
+ .dropCreateWarningBanner;
+ expect(zhBanner).toHaveTextContent(zhCopy);
+ zhView.unmount();
+
+ // ----- en-US -----
+ await i18n.changeLanguage('en-US');
+ const enView = renderWithSqls(sqls);
+ const enBanner = enView.getByTestId(
+ 'modified-sql-drop-create-warning-banner'
+ );
+ const enCopy =
+ enUSDataSourceComparison.entry.modifiedSqlDrawer
+ .dropCreateWarningBanner;
+ expect(enBanner).toHaveTextContent(enCopy);
+
+ // sanity — the two locales must actually produce different copy or the
+ // test would always pass on either branch.
+ expect(zhCopy).not.toEqual(enCopy);
+ });
+ });
});
diff --git a/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlAuditResult/List.tsx b/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlAuditResult/List.tsx
index a4bdd86fb..d9f82e930 100644
--- a/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlAuditResult/List.tsx
+++ b/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlAuditResult/List.tsx
@@ -5,9 +5,14 @@ import { SqlAuditResultCollapseStyleWrapper } from '../ComparisonTreeNode/Compar
import AuditResult from '../SqlAuditResult';
import {
ModifiedSqlAuditResultInfoStyleWrapper,
- ModifiedSqlAuditResultTitleStyleWrapper
+ ModifiedSqlAuditResultTitleStyleWrapper,
+ WarningHighlightedSqlStyleWrapper
} from './style';
import { useTranslation } from 'react-i18next';
+import {
+ isWarningLine,
+ sqlHasWarningLine
+} from '../ModifiedSqlDrawer/warningSql';
type Props = {
dataSource?: ISQLStatementWithAuditResult[];
@@ -17,6 +22,35 @@ type Props = {
auditError?: string;
};
+// Renders a SQL string verbatim, line by line, while attaching a
+// `warning-highlight-line` className to every `-- WARNING:` line. Used in
+// place of SQLRenderer when WARNING annotations are present so the warning
+// rows visibly stand out without depending on SQLRenderer's internal HTML.
+const SqlWithWarningHighlight: React.FC<{ sql: string }> = ({ sql }) => {
+ const lines = sql.split(/\r?\n/);
+ return (
+
+ {lines.map((line, lineIndex) => (
+
+ {line === '' ? ' ' : line}
+
+ ))}
+
+ );
+};
+
const ModifiedSqlAuditResultList: React.FC = ({
dataSource,
instanceType,
@@ -34,6 +68,8 @@ const ModifiedSqlAuditResultList: React.FC = ({
split={false}
dataSource={dataSource}
renderItem={(item, index) => {
+ const sqlText = item.sql_statement ?? '';
+ const containsWarning = sqlHasWarningLine(sqlText);
return (
@@ -45,7 +81,11 @@ const ModifiedSqlAuditResultList: React.FC = ({
-
+ {containsWarning ? (
+
+ ) : (
+
+ )}
diff --git a/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlAuditResult/style.ts b/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlAuditResult/style.ts
index 12f09518a..f656fd2a6 100644
--- a/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlAuditResult/style.ts
+++ b/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlAuditResult/style.ts
@@ -20,3 +20,30 @@ export const ModifiedSqlAuditResultTitleStyleWrapper = styled('div')`
color: ${({ theme }) => theme.sharedTheme.uiToken.colorPrimary};
}
`;
+
+export const WarningHighlightedSqlStyleWrapper = styled('pre')`
+ margin: 0;
+ padding: 8px 12px;
+ background: ${({ theme }) => theme.sharedTheme.uiToken.colorBgBase};
+ border: 1px solid ${({ theme }) => theme.sharedTheme.uiToken.colorBorder};
+ border-radius: 4px;
+ font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
+ font-size: 12px;
+ line-height: 20px;
+ white-space: pre;
+ overflow-x: auto;
+
+ .code-line {
+ display: block;
+ min-height: 20px;
+ }
+
+ .warning-highlight-line {
+ background: ${({ theme }) => theme.sharedTheme.uiToken.colorWarningBgHover};
+ color: ${({ theme }) => theme.sharedTheme.uiToken.colorWarning};
+ border-left: 3px solid
+ ${({ theme }) => theme.sharedTheme.uiToken.colorWarning};
+ padding-left: 6px;
+ font-weight: 500;
+ }
+`;
diff --git a/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlDrawer/index.tsx b/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlDrawer/index.tsx
index 60356f438..9e69e14ee 100644
--- a/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlDrawer/index.tsx
+++ b/packages/sqle/src/page/DataSourceComparison/ComparisonEntry/component/ModifiedSqlDrawer/index.tsx
@@ -1,13 +1,14 @@
import { BasicDrawer } from '@actiontech/dms-kit';
import { IDatabaseDiffModifySQL } from '@actiontech/shared/lib/api/sqle/service/common';
-import { Spin } from 'antd';
+import { Alert, Spin } from 'antd';
import { useTranslation } from 'react-i18next';
import { SelectedInstanceInfo } from '../../index.type';
import { useCurrentProject } from '@actiontech/shared/lib/features';
import { CreateWorkflowForModifiedSqlAction } from '../../actions';
import { IGenDatabaseDiffModifySQLsV1Params } from '@actiontech/shared/lib/api/sqle/service/database_comparison/index.d';
import ModifiedSqlAuditResult from '../ModifiedSqlAuditResult';
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
+import { anySqlHasWarning } from './warningSql';
type Props = {
open: boolean;
onClose: () => void;
@@ -36,6 +37,18 @@ const ModifiedSqlDrawer: React.FC = ({
onClose();
setAuditResultCollapseActiveKeys([]);
};
+ // Surface the drawer-level warning banner exactly once when ANY modify SQL
+ // across ANY schema contains a `-- WARNING:` line. The banner is independent
+ // of dbType: drivers emit WARNING lines whenever a non-destructive ALTER is
+ // not possible (e.g. Hive DROP+CREATE fallback, VIEW recreation).
+ const hasAnyWarningSql = useMemo(() => {
+ if (!databaseDiffModifiedSqlInfos) {
+ return false;
+ }
+ return databaseDiffModifiedSqlInfos.some((schemaInfo) =>
+ anySqlHasWarning(schemaInfo.modify_sqls)
+ );
+ }, [databaseDiffModifiedSqlInfos]);
return (
= ({
})}
>
+ {hasAnyWarningSql && (
+
+ )}
{
+ if (!sql) {
+ return false;
+ }
+ return sql.split(/\r?\n/).some((line) => isWarningLine(line));
+};
+
+/**
+ * Returns true when the trimmed line starts with the WARNING marker.
+ */
+export const isWarningLine = (line: string): boolean => {
+ return line.trimStart().startsWith(WARNING_LINE_PREFIX);
+};
+
+/**
+ * Aggregates whether any SQL statement in the given iterable contains a
+ * WARNING marker. Used by ModifiedSqlDrawer to decide whether to show the
+ * top banner exactly once.
+ */
+export const anySqlHasWarning = (
+ sqls: Array<{ sql_statement?: string } | undefined> | undefined
+): boolean => {
+ if (!sqls || sqls.length === 0) {
+ return false;
+ }
+ return sqls.some((item) => sqlHasWarningLine(item?.sql_statement));
+};