mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 01:03:51 +08:00
✨ feat(sync): 按数据源组合约束迁移能力
- 展示源端到目标端的完整、部分、规划中和不支持状态 - 按能力控制自动建表、补列、索引及继续执行入口 - 支持数据库列表为空时手工输入并补齐六语言提示
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
||||
DBGetTables,
|
||||
DataSync,
|
||||
DataSyncAnalyze,
|
||||
DataSyncCapability,
|
||||
DataSyncPreview,
|
||||
} from "../../wailsjs/go/app/App";
|
||||
import { SavedConnection } from "../types";
|
||||
@@ -76,6 +77,11 @@ import {
|
||||
startDataSyncBackgroundTask,
|
||||
useDataSyncBackgroundTask,
|
||||
} from "./dataSyncBackgroundTask";
|
||||
import {
|
||||
resolveDataSyncCapabilityPresentation,
|
||||
type DataSyncCapabilitySnapshot,
|
||||
} from "./dataSyncCapability";
|
||||
import { resolveDataSyncDatabaseSelection } from "./dataSyncDatabaseSelection";
|
||||
const { Title, Text } = Typography;
|
||||
const { Step } = Steps;
|
||||
const { Option } = Select;
|
||||
@@ -413,6 +419,11 @@ const DataSyncModal: React.FC<{
|
||||
const [targetSchema, setTargetSchema] = useState<string>("");
|
||||
const [targetSchemaLoading, setTargetSchemaLoading] =
|
||||
useState<boolean>(false);
|
||||
const [migrationCapability, setMigrationCapability] =
|
||||
useState<DataSyncCapabilitySnapshot | null>(null);
|
||||
const [migrationCapabilityStatus, setMigrationCapabilityStatus] = useState<
|
||||
"idle" | "loading" | "ready" | "error"
|
||||
>("idle");
|
||||
|
||||
// Step 2: Tables
|
||||
const [allTables, setAllTables] = useState<string[]>([]);
|
||||
@@ -713,17 +724,22 @@ const DataSyncModal: React.FC<{
|
||||
return;
|
||||
}
|
||||
if (workflowType === "migration") {
|
||||
const supportsAutoCreate = migrationCapability?.supportsAutoCreate === true;
|
||||
if (syncMode === "insert_update") {
|
||||
setSyncMode("insert_only");
|
||||
}
|
||||
if (syncContent === "schema") {
|
||||
setSyncContent("both");
|
||||
}
|
||||
if (targetTableStrategy === "existing_only") {
|
||||
if (supportsAutoCreate && targetTableStrategy === "existing_only") {
|
||||
setTargetTableStrategy("smart");
|
||||
} else if (!supportsAutoCreate && targetTableStrategy !== "existing_only") {
|
||||
setTargetTableStrategy("existing_only");
|
||||
}
|
||||
if (!createIndexes) {
|
||||
if (supportsAutoCreate && !createIndexes) {
|
||||
setCreateIndexes(true);
|
||||
} else if (!supportsAutoCreate && createIndexes) {
|
||||
setCreateIndexes(false);
|
||||
}
|
||||
} else {
|
||||
if (targetTableStrategy !== "existing_only") {
|
||||
@@ -742,6 +758,7 @@ const DataSyncModal: React.FC<{
|
||||
syncMode,
|
||||
targetTableStrategy,
|
||||
createIndexes,
|
||||
migrationCapability?.supportsAutoCreate,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -750,6 +767,12 @@ const DataSyncModal: React.FC<{
|
||||
}
|
||||
}, [syncContent, autoAddColumns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (migrationCapability?.supportsAutoAddColumns === false && autoAddColumns) {
|
||||
setAutoAddColumns(false);
|
||||
}
|
||||
}, [migrationCapability?.supportsAutoAddColumns, autoAddColumns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sourceDatasetMode !== "query") return;
|
||||
if (workflowType !== "sync") {
|
||||
@@ -794,18 +817,17 @@ const DataSyncModal: React.FC<{
|
||||
try {
|
||||
const res = await DBGetDatabases(normalizeConnConfig(conn) as any);
|
||||
if (requestSeq !== sourceDatabaseRequestSeqRef.current) return;
|
||||
if (res.success) {
|
||||
const dbRows = Array.isArray(res.data) ? res.data : [];
|
||||
setSourceDbs(
|
||||
dbRows
|
||||
.map((r: any) => r?.Database || r?.database || r?.username)
|
||||
.filter(
|
||||
(name: any) => typeof name === "string" && name.trim() !== "",
|
||||
),
|
||||
);
|
||||
}
|
||||
const selection = resolveDataSyncDatabaseSelection(
|
||||
conn.config,
|
||||
res.success && Array.isArray(res.data) ? res.data : [],
|
||||
);
|
||||
setSourceDbs(selection.options);
|
||||
setSourceDb(selection.preferred);
|
||||
} catch (e: any) {
|
||||
if (requestSeq !== sourceDatabaseRequestSeqRef.current) return;
|
||||
const selection = resolveDataSyncDatabaseSelection(conn.config, []);
|
||||
setSourceDbs(selection.options);
|
||||
setSourceDb(selection.preferred);
|
||||
message.error(
|
||||
tr("data_sync.message.fetch_source_databases_failed_detail", {
|
||||
detail: e?.message || String(e),
|
||||
@@ -836,18 +858,17 @@ const DataSyncModal: React.FC<{
|
||||
try {
|
||||
const res = await DBGetDatabases(normalizeConnConfig(conn) as any);
|
||||
if (requestSeq !== targetDatabaseRequestSeqRef.current) return;
|
||||
if (res.success) {
|
||||
const dbRows = Array.isArray(res.data) ? res.data : [];
|
||||
setTargetDbs(
|
||||
dbRows
|
||||
.map((r: any) => r?.Database || r?.database || r?.username)
|
||||
.filter(
|
||||
(name: any) => typeof name === "string" && name.trim() !== "",
|
||||
),
|
||||
);
|
||||
}
|
||||
const selection = resolveDataSyncDatabaseSelection(
|
||||
conn.config,
|
||||
res.success && Array.isArray(res.data) ? res.data : [],
|
||||
);
|
||||
setTargetDbs(selection.options);
|
||||
setTargetDb(selection.preferred);
|
||||
} catch (e: any) {
|
||||
if (requestSeq !== targetDatabaseRequestSeqRef.current) return;
|
||||
const selection = resolveDataSyncDatabaseSelection(conn.config, []);
|
||||
setTargetDbs(selection.options);
|
||||
setTargetDb(selection.preferred);
|
||||
message.error(
|
||||
tr("data_sync.message.fetch_target_databases_failed_detail", {
|
||||
detail: e?.message || String(e),
|
||||
@@ -871,6 +892,21 @@ const DataSyncModal: React.FC<{
|
||||
|
||||
const nextToTables = async () => {
|
||||
if (!sourceConnId || !targetConnId) return message.error(tr('data_sync.message.select_connections_first'));
|
||||
if (!isSourceQueryMode && migrationCapabilityStatus === "loading") {
|
||||
return message.info(tr("data_sync.capability.loading"));
|
||||
}
|
||||
if (!isSourceQueryMode && migrationCapabilityStatus === "error") {
|
||||
return message.error(tr("data_sync.capability.load_failed"));
|
||||
}
|
||||
if (
|
||||
!isSourceQueryMode &&
|
||||
migrationCapability &&
|
||||
!migrationCapability.canExecute
|
||||
) {
|
||||
return message.error(
|
||||
resolveDataSyncCapabilityPresentation(migrationCapability, tr).message,
|
||||
);
|
||||
}
|
||||
if (!sourceDb) return message.error(tr('data_sync.message.select_source_database'));
|
||||
if (!targetDb) return message.error(tr('data_sync.message.select_target_database'));
|
||||
if (!ensureTargetSchemaSelected()) return;
|
||||
@@ -1320,6 +1356,28 @@ const DataSyncModal: React.FC<{
|
||||
() => connections.find((c) => c.id === targetConnId),
|
||||
[connections, targetConnId],
|
||||
);
|
||||
const capabilityPresentation = useMemo(
|
||||
() =>
|
||||
migrationCapability
|
||||
? resolveDataSyncCapabilityPresentation(migrationCapability, tr)
|
||||
: null,
|
||||
[migrationCapability, i18nLanguage],
|
||||
);
|
||||
const capabilityStatusPresentation = useMemo(() => {
|
||||
if (migrationCapabilityStatus === "loading") {
|
||||
return {
|
||||
alertType: "info" as const,
|
||||
message: tr("data_sync.capability.loading"),
|
||||
};
|
||||
}
|
||||
if (migrationCapabilityStatus === "error") {
|
||||
return {
|
||||
alertType: "error" as const,
|
||||
message: tr("data_sync.capability.load_failed"),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [migrationCapabilityStatus, i18nLanguage]);
|
||||
const targetDialect = useMemo(
|
||||
() =>
|
||||
resolveSqlDialect(
|
||||
@@ -1339,6 +1397,46 @@ const DataSyncModal: React.FC<{
|
||||
isMigrationWorkflow &&
|
||||
((sourceType === "redis" && targetType === "mongodb") ||
|
||||
(sourceType === "mongodb" && targetType === "redis"));
|
||||
|
||||
useEffect(() => {
|
||||
if (!sourceConn || !targetConn) {
|
||||
setMigrationCapability(null);
|
||||
setMigrationCapabilityStatus("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setMigrationCapability(null);
|
||||
setMigrationCapabilityStatus("loading");
|
||||
void DataSyncCapability(
|
||||
{
|
||||
type: String(sourceConn.config?.type || ""),
|
||||
driver: String(sourceConn.config?.driver || ""),
|
||||
oceanBaseProtocol: String(sourceConn.config?.oceanBaseProtocol || ""),
|
||||
} as any,
|
||||
{
|
||||
type: String(targetConn.config?.type || ""),
|
||||
driver: String(targetConn.config?.driver || ""),
|
||||
oceanBaseProtocol: String(targetConn.config?.oceanBaseProtocol || ""),
|
||||
} as any,
|
||||
)
|
||||
.then((capability) => {
|
||||
if (!cancelled) {
|
||||
setMigrationCapability(capability as DataSyncCapabilitySnapshot);
|
||||
setMigrationCapabilityStatus("ready");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setMigrationCapability(null);
|
||||
setMigrationCapabilityStatus("error");
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [sourceConn, targetConn]);
|
||||
const defaultMongoCollectionName = useMemo(() => {
|
||||
if (sourceType === "redis" && targetType === "mongodb") {
|
||||
return `redis_db_${resolveRedisDbIndex(sourceDb || sourceConn?.config?.database)}_keys`;
|
||||
@@ -1713,13 +1811,21 @@ const DataSyncModal: React.FC<{
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={tr("data_sync.field.database")}>
|
||||
<Select value={sourceDb} onChange={setSourceDb} showSearch>
|
||||
{sourceDbs.map((d) => (
|
||||
<Option key={d} value={d}>
|
||||
{d}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
{sourceDbs.length > 0 ? (
|
||||
<Select value={sourceDb} onChange={setSourceDb} showSearch>
|
||||
{sourceDbs.map((d) => (
|
||||
<Option key={d} value={d}>
|
||||
{d}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={sourceDb}
|
||||
onChange={(event) => setSourceDb(event.target.value)}
|
||||
placeholder={tr("data_sync.placeholder.database_manual")}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
@@ -1764,13 +1870,21 @@ const DataSyncModal: React.FC<{
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={tr("data_sync.field.database")}>
|
||||
<Select value={targetDb} onChange={setTargetDb} showSearch>
|
||||
{targetDbs.map((d) => (
|
||||
<Option key={d} value={d}>
|
||||
{d}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
{targetDbs.length > 0 ? (
|
||||
<Select value={targetDb} onChange={setTargetDb} showSearch>
|
||||
{targetDbs.map((d) => (
|
||||
<Option key={d} value={d}>
|
||||
{d}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
value={targetDb}
|
||||
onChange={(event) => setTargetDb(event.target.value)}
|
||||
placeholder={tr("data_sync.placeholder.database_manual")}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
{targetSupportsSchemaSelection && (
|
||||
<Form.Item label={tr("data_sync.field.schema")}>
|
||||
@@ -1946,17 +2060,28 @@ const DataSyncModal: React.FC<{
|
||||
<Select
|
||||
value={targetTableStrategy}
|
||||
onChange={setTargetTableStrategy}
|
||||
disabled={!isMigrationWorkflow || isSourceQueryMode}
|
||||
disabled={
|
||||
!isMigrationWorkflow ||
|
||||
isSourceQueryMode ||
|
||||
migrationCapabilityStatus !== "ready" ||
|
||||
capabilityPresentation?.forceExistingTarget === true
|
||||
}
|
||||
>
|
||||
<Option value="existing_only">
|
||||
{tr("data_sync.option.target_strategy.existing_only")}
|
||||
</Option>
|
||||
<Option value="auto_create_if_missing">
|
||||
<Option
|
||||
value="auto_create_if_missing"
|
||||
disabled={migrationCapability?.supportsAutoCreate !== true}
|
||||
>
|
||||
{tr(
|
||||
"data_sync.option.target_strategy.auto_create_if_missing",
|
||||
)}
|
||||
</Option>
|
||||
<Option value="smart">
|
||||
<Option
|
||||
value="smart"
|
||||
disabled={migrationCapability?.supportsAutoCreate !== true}
|
||||
>
|
||||
{tr("data_sync.option.target_strategy.smart")}
|
||||
</Option>
|
||||
</Select>
|
||||
@@ -1988,7 +2113,11 @@ const DataSyncModal: React.FC<{
|
||||
<Checkbox
|
||||
checked={autoAddColumns}
|
||||
onChange={(e) => setAutoAddColumns(e.target.checked)}
|
||||
disabled={isSourceQueryMode || syncContent === "data"}
|
||||
disabled={
|
||||
isSourceQueryMode ||
|
||||
syncContent === "data" ||
|
||||
migrationCapability?.supportsAutoAddColumns !== true
|
||||
}
|
||||
>
|
||||
{isSchemaCompareEntry
|
||||
? tr("data_sync.compare_entry.option.auto_add_columns")
|
||||
@@ -2004,14 +2133,35 @@ const DataSyncModal: React.FC<{
|
||||
disabled={
|
||||
!isMigrationWorkflow ||
|
||||
targetTableStrategy === "existing_only" ||
|
||||
isSourceQueryMode
|
||||
isSourceQueryMode ||
|
||||
migrationCapability?.supportsAutoCreate !== true
|
||||
}
|
||||
>
|
||||
{tr("data_sync.option.create_indexes")}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
)}
|
||||
{!isSourceQueryMode && capabilityStatusPresentation && (
|
||||
<Alert
|
||||
type={capabilityStatusPresentation.alertType}
|
||||
showIcon
|
||||
message={capabilityStatusPresentation.message}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
)}
|
||||
{!isSourceQueryMode &&
|
||||
capabilityPresentation &&
|
||||
(isMigrationWorkflow ||
|
||||
capabilityPresentation.blocksExecution) && (
|
||||
<Alert
|
||||
type={capabilityPresentation.alertType}
|
||||
showIcon
|
||||
message={capabilityPresentation.message}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
)}
|
||||
{isMigrationWorkflow &&
|
||||
!capabilityPresentation &&
|
||||
targetTableStrategy !== "existing_only" && (
|
||||
<Alert
|
||||
type="info"
|
||||
@@ -2020,14 +2170,16 @@ const DataSyncModal: React.FC<{
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
)}
|
||||
{!isCompareEntry && !isMigrationWorkflow && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={tr("data_sync.alert.existing_target_only")}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
)}
|
||||
{!isCompareEntry &&
|
||||
!isMigrationWorkflow &&
|
||||
!capabilityPresentation?.blocksExecution && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={tr("data_sync.alert.existing_target_only")}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
)}
|
||||
{syncContent !== "schema" && syncMode === "full_overwrite" && (
|
||||
<Alert
|
||||
type="warning"
|
||||
@@ -2493,7 +2645,17 @@ const DataSyncModal: React.FC<{
|
||||
|
||||
<div style={modalFooterBarStyle}>
|
||||
{currentStep === 0 && (
|
||||
<Button type="primary" onClick={nextToTables} loading={loading}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={nextToTables}
|
||||
loading={loading}
|
||||
disabled={
|
||||
!isSourceQueryMode &&
|
||||
(migrationCapabilityStatus === "loading" ||
|
||||
migrationCapabilityStatus === "error" ||
|
||||
capabilityPresentation?.blocksExecution === true)
|
||||
}
|
||||
>
|
||||
{tr("data_sync.action.next")}
|
||||
</Button>
|
||||
)}
|
||||
@@ -2512,6 +2674,8 @@ const DataSyncModal: React.FC<{
|
||||
(isCompareEntry ? false : syncContent === "schema") ||
|
||||
selectedTables.length === 0 ||
|
||||
analyzing ||
|
||||
(!isSourceQueryMode && migrationCapabilityStatus !== "ready") ||
|
||||
(!isSourceQueryMode && capabilityPresentation?.blocksExecution === true) ||
|
||||
(isSourceQueryMode && !sourceQuery.trim())
|
||||
}
|
||||
style={{ marginRight: 8 }}
|
||||
@@ -2533,6 +2697,8 @@ const DataSyncModal: React.FC<{
|
||||
disabled={
|
||||
selectedTables.length === 0 ||
|
||||
(isSourceQueryMode && !sourceQuery.trim()) ||
|
||||
(!isSourceQueryMode && migrationCapabilityStatus !== "ready") ||
|
||||
(!isSourceQueryMode && capabilityPresentation?.blocksExecution === true) ||
|
||||
!executionReadiness.ready
|
||||
}
|
||||
>
|
||||
|
||||
36
frontend/src/components/dataSyncCapability.i18n.test.ts
Normal file
36
frontend/src/components/dataSyncCapability.i18n.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { t } from '../i18n';
|
||||
|
||||
const locales = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP', 'de-DE', 'ru-RU'] as const;
|
||||
|
||||
describe('data sync capability catalog', () => {
|
||||
it.each(locales)('does not expose the obsolete MySQL-to-Kingbase-only scope in %s', (locale) => {
|
||||
const plannerScope = t(
|
||||
'data_sync.alert.auto_create_planner_scope',
|
||||
undefined,
|
||||
locale,
|
||||
).toLowerCase();
|
||||
const autoAddColumns = t(
|
||||
'data_sync.option.auto_add_columns',
|
||||
undefined,
|
||||
locale,
|
||||
).toLowerCase();
|
||||
|
||||
expect(plannerScope.includes('mysql') && plannerScope.includes('kingbase')).toBe(false);
|
||||
expect(autoAddColumns.includes('mysql') && autoAddColumns.includes('kingbase')).toBe(false);
|
||||
});
|
||||
|
||||
it.each(locales)('renders the selected source and target pair in %s', (locale) => {
|
||||
const message = t(
|
||||
'data_sync.capability.full',
|
||||
{ sourceType: 'mysql', targetType: 'postgres' },
|
||||
locale,
|
||||
);
|
||||
|
||||
expect(message).toContain('mysql');
|
||||
expect(message).toContain('postgres');
|
||||
expect(message).not.toContain('{{sourceType}}');
|
||||
expect(message).not.toContain('{{targetType}}');
|
||||
});
|
||||
});
|
||||
80
frontend/src/components/dataSyncCapability.test.ts
Normal file
80
frontend/src/components/dataSyncCapability.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
resolveDataSyncCapabilityPresentation,
|
||||
type DataSyncCapabilitySnapshot,
|
||||
} from './dataSyncCapability';
|
||||
|
||||
const tr = (key: string, vars?: Record<string, unknown>) =>
|
||||
`${key}:${String(vars?.sourceType || '')}->${String(vars?.targetType || '')}`;
|
||||
|
||||
const capability = (
|
||||
overrides: Partial<DataSyncCapabilitySnapshot> = {},
|
||||
): DataSyncCapabilitySnapshot => ({
|
||||
sourceType: 'mysql',
|
||||
targetType: 'postgres',
|
||||
planner: 'mysql-pglike-planner',
|
||||
supportLevel: 'full',
|
||||
canExecute: true,
|
||||
supportsAutoCreate: true,
|
||||
supportsAutoAddColumns: true,
|
||||
requiresExistingTarget: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('resolveDataSyncCapabilityPresentation', () => {
|
||||
it('presents a full planner without the obsolete MySQL-to-Kingbase-only claim', () => {
|
||||
const result = resolveDataSyncCapabilityPresentation(capability(), tr);
|
||||
|
||||
expect(result.alertType).toBe('info');
|
||||
expect(result.message).toBe(
|
||||
'data_sync.capability.full:mysql->postgres',
|
||||
);
|
||||
expect(result.blocksExecution).toBe(false);
|
||||
expect(result.forceExistingTarget).toBe(false);
|
||||
});
|
||||
|
||||
it('makes legacy compatibility mode require an existing target', () => {
|
||||
const result = resolveDataSyncCapabilityPresentation(
|
||||
capability({
|
||||
sourceType: 'oracle',
|
||||
targetType: 'sqlserver',
|
||||
planner: 'generic-legacy-planner',
|
||||
supportLevel: 'partial',
|
||||
supportsAutoCreate: false,
|
||||
supportsAutoAddColumns: false,
|
||||
requiresExistingTarget: true,
|
||||
}),
|
||||
tr,
|
||||
);
|
||||
|
||||
expect(result.alertType).toBe('warning');
|
||||
expect(result.message).toBe(
|
||||
'data_sync.capability.partial:oracle->sqlserver',
|
||||
);
|
||||
expect(result.blocksExecution).toBe(false);
|
||||
expect(result.forceExistingTarget).toBe(true);
|
||||
});
|
||||
|
||||
it.each(['planned', 'unsupported'] as const)(
|
||||
'blocks %s migration pairs before execution',
|
||||
(supportLevel) => {
|
||||
const result = resolveDataSyncCapabilityPresentation(
|
||||
capability({
|
||||
supportLevel,
|
||||
canExecute: false,
|
||||
supportsAutoCreate: false,
|
||||
requiresExistingTarget: true,
|
||||
}),
|
||||
tr,
|
||||
);
|
||||
|
||||
expect(result.alertType).toBe('error');
|
||||
expect(result.message).toBe(
|
||||
`data_sync.capability.${supportLevel}:mysql->postgres`,
|
||||
);
|
||||
expect(result.blocksExecution).toBe(true);
|
||||
expect(result.forceExistingTarget).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
69
frontend/src/components/dataSyncCapability.ts
Normal file
69
frontend/src/components/dataSyncCapability.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { sync } from '../../wailsjs/go/models';
|
||||
|
||||
export type DataSyncCapabilitySupportLevel =
|
||||
| 'full'
|
||||
| 'partial'
|
||||
| 'planned'
|
||||
| 'unsupported';
|
||||
|
||||
export type DataSyncCapabilitySnapshot = Pick<
|
||||
sync.MigrationCapability,
|
||||
| 'sourceType'
|
||||
| 'targetType'
|
||||
| 'planner'
|
||||
| 'canExecute'
|
||||
| 'supportsAutoCreate'
|
||||
| 'supportsAutoAddColumns'
|
||||
| 'requiresExistingTarget'
|
||||
> & {
|
||||
supportLevel: DataSyncCapabilitySupportLevel;
|
||||
};
|
||||
|
||||
type Translate = (
|
||||
key: string,
|
||||
variables?: Record<string, string | number | boolean | null | undefined>,
|
||||
) => string;
|
||||
|
||||
export type DataSyncCapabilityPresentation = {
|
||||
alertType: 'info' | 'warning' | 'error';
|
||||
message: string;
|
||||
blocksExecution: boolean;
|
||||
forceExistingTarget: boolean;
|
||||
};
|
||||
|
||||
export const resolveDataSyncCapabilityPresentation = (
|
||||
capability: DataSyncCapabilitySnapshot,
|
||||
tr: Translate,
|
||||
): DataSyncCapabilityPresentation => {
|
||||
const sourceType = String(capability.sourceType || '').trim() || 'unknown';
|
||||
const targetType = String(capability.targetType || '').trim() || 'unknown';
|
||||
const variables = { sourceType, targetType };
|
||||
|
||||
if (capability.supportLevel === 'full' && capability.canExecute) {
|
||||
return {
|
||||
alertType: 'info',
|
||||
message: tr('data_sync.capability.full', variables),
|
||||
blocksExecution: false,
|
||||
forceExistingTarget: !capability.supportsAutoCreate,
|
||||
};
|
||||
}
|
||||
|
||||
if (capability.supportLevel === 'partial' && capability.canExecute) {
|
||||
return {
|
||||
alertType: 'warning',
|
||||
message: tr('data_sync.capability.partial', variables),
|
||||
blocksExecution: false,
|
||||
forceExistingTarget: true,
|
||||
};
|
||||
}
|
||||
|
||||
const supportLevel = capability.supportLevel === 'planned'
|
||||
? 'planned'
|
||||
: 'unsupported';
|
||||
return {
|
||||
alertType: 'error',
|
||||
message: tr(`data_sync.capability.${supportLevel}`, variables),
|
||||
blocksExecution: true,
|
||||
forceExistingTarget: true,
|
||||
};
|
||||
};
|
||||
58
frontend/src/components/dataSyncDatabaseSelection.test.ts
Normal file
58
frontend/src/components/dataSyncDatabaseSelection.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveDataSyncDatabaseSelection } from './dataSyncDatabaseSelection';
|
||||
|
||||
describe('resolveDataSyncDatabaseSelection', () => {
|
||||
it('deduplicates metadata rows and prefers the configured database', () => {
|
||||
expect(resolveDataSyncDatabaseSelection(
|
||||
{ type: 'mysql', database: 'app' },
|
||||
[{ Database: 'mysql' }, { database: 'APP' }, 'app'],
|
||||
)).toEqual({
|
||||
options: ['mysql', 'APP'],
|
||||
preferred: 'APP',
|
||||
});
|
||||
});
|
||||
|
||||
it('auto-selects the only database returned by the driver', () => {
|
||||
expect(resolveDataSyncDatabaseSelection(
|
||||
{ type: 'trino' },
|
||||
[{ catalog: 'hive' }],
|
||||
)).toEqual({ options: ['hive'], preferred: 'hive' });
|
||||
});
|
||||
|
||||
it('falls back to the configured database when enumeration is empty', () => {
|
||||
expect(resolveDataSyncDatabaseSelection(
|
||||
{ type: 'sqlite', database: 'D:/data/app.db' },
|
||||
[],
|
||||
)).toEqual({
|
||||
options: ['D:/data/app.db'],
|
||||
preferred: 'D:/data/app.db',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the owner for Oracle service-name connections', () => {
|
||||
expect(resolveDataSyncDatabaseSelection(
|
||||
{ type: 'oracle', database: 'ORCL', user: 'APP_OWNER' },
|
||||
[],
|
||||
)).toEqual({ options: ['APP_OWNER'], preferred: 'APP_OWNER' });
|
||||
});
|
||||
|
||||
it('uses the owner for OceanBase Oracle tenants', () => {
|
||||
expect(resolveDataSyncDatabaseSelection(
|
||||
{
|
||||
type: 'oceanbase',
|
||||
database: 'service',
|
||||
user: 'tenant_owner',
|
||||
oceanBaseProtocol: 'oracle',
|
||||
},
|
||||
[],
|
||||
)).toEqual({ options: ['tenant_owner'], preferred: 'tenant_owner' });
|
||||
});
|
||||
|
||||
it('leaves a manual-entry fallback when no database can be inferred', () => {
|
||||
expect(resolveDataSyncDatabaseSelection(
|
||||
{ type: 'custom', driver: 'acme-sql' },
|
||||
[],
|
||||
)).toEqual({ options: [], preferred: '' });
|
||||
});
|
||||
});
|
||||
97
frontend/src/components/dataSyncDatabaseSelection.ts
Normal file
97
frontend/src/components/dataSyncDatabaseSelection.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { resolveOceanBaseProtocolFromConfig } from '../utils/oceanBaseProtocol';
|
||||
|
||||
type DataSyncDatabaseConfig = {
|
||||
type?: unknown;
|
||||
driver?: unknown;
|
||||
database?: unknown;
|
||||
user?: unknown;
|
||||
redisDB?: unknown;
|
||||
oceanBaseProtocol?: unknown;
|
||||
connectionParams?: unknown;
|
||||
uri?: unknown;
|
||||
};
|
||||
|
||||
export type DataSyncDatabaseSelection = {
|
||||
options: string[];
|
||||
preferred: string;
|
||||
};
|
||||
|
||||
const METADATA_NAME_KEYS = ['database', 'username', 'name', 'catalog', 'schema'];
|
||||
|
||||
const normalizeText = (value: unknown): string => String(value ?? '').trim();
|
||||
|
||||
const readMetadataName = (row: unknown): string => {
|
||||
if (typeof row === 'string') {
|
||||
return row.trim();
|
||||
}
|
||||
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const values = new Map<string, unknown>();
|
||||
Object.entries(row as Record<string, unknown>).forEach(([key, value]) => {
|
||||
values.set(key.toLowerCase(), value);
|
||||
});
|
||||
for (const key of METADATA_NAME_KEYS) {
|
||||
const value = normalizeText(values.get(key));
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const resolveConfiguredDatabase = (config: DataSyncDatabaseConfig): string => {
|
||||
const type = normalizeText(config.type || config.driver).toLowerCase();
|
||||
const isOracle = type === 'oracle';
|
||||
let isOceanBaseOracle = false;
|
||||
if (type === 'oceanbase') {
|
||||
try {
|
||||
isOceanBaseOracle = resolveOceanBaseProtocolFromConfig(
|
||||
config as Record<string, unknown>,
|
||||
) === 'oracle';
|
||||
} catch {
|
||||
isOceanBaseOracle = false;
|
||||
}
|
||||
}
|
||||
if (isOracle || isOceanBaseOracle) {
|
||||
return normalizeText(config.user);
|
||||
}
|
||||
if (type === 'redis') {
|
||||
const configuredDatabase = normalizeText(config.database);
|
||||
return configuredDatabase || normalizeText(config.redisDB);
|
||||
}
|
||||
return normalizeText(config.database);
|
||||
};
|
||||
|
||||
export const resolveDataSyncDatabaseSelection = (
|
||||
config: DataSyncDatabaseConfig,
|
||||
rows: unknown[],
|
||||
): DataSyncDatabaseSelection => {
|
||||
const seen = new Set<string>();
|
||||
const options: string[] = [];
|
||||
rows.forEach((row) => {
|
||||
const name = readMetadataName(row);
|
||||
const key = name.toLowerCase();
|
||||
if (!name || seen.has(key)) {
|
||||
return;
|
||||
}
|
||||
seen.add(key);
|
||||
options.push(name);
|
||||
});
|
||||
|
||||
const configured = resolveConfiguredDatabase(config);
|
||||
if (options.length === 0) {
|
||||
return configured
|
||||
? { options: [configured], preferred: configured }
|
||||
: { options: [], preferred: '' };
|
||||
}
|
||||
|
||||
const configuredOption = configured
|
||||
? options.find((option) => option.toLowerCase() === configured.toLowerCase())
|
||||
: undefined;
|
||||
return {
|
||||
options,
|
||||
preferred: configuredOption || (options.length === 1 ? options[0] : ''),
|
||||
};
|
||||
};
|
||||
@@ -433,6 +433,23 @@ if (
|
||||
GetTableColumns: async () => [],
|
||||
DBGetDatabases: async () => ({ success: true, data: ['missav_bot'] }),
|
||||
DBGetTables: async () => ({ success: true, data: cloneBrowserMockValue(mockQueryTables) }),
|
||||
DataSyncCapability: async (sourceConfig: any, targetConfig: any) => {
|
||||
const sourceType = String(sourceConfig?.type || sourceConfig?.driver || '').trim().toLowerCase();
|
||||
const targetType = String(targetConfig?.type || targetConfig?.driver || '').trim().toLowerCase();
|
||||
const canExecute = sourceType !== '' && targetType !== '';
|
||||
return {
|
||||
sourceType,
|
||||
targetType,
|
||||
sourceModel: 'custom',
|
||||
targetModel: 'custom',
|
||||
planner: canExecute ? 'browser-mock-existing-target' : '',
|
||||
supportLevel: canExecute ? 'partial' : 'unsupported',
|
||||
canExecute,
|
||||
supportsAutoCreate: false,
|
||||
supportsAutoAddColumns: false,
|
||||
requiresExistingTarget: true,
|
||||
};
|
||||
},
|
||||
DBGetAllColumns: async () => ({ success: true, data: cloneBrowserMockValue(mockQueryColumns) }),
|
||||
DBGetDatabaseForeignKeys: async () => ({ success: true, data: {} }),
|
||||
DBGetColumns: async (_config: any, _dbName: string, tableName: string) => ({
|
||||
|
||||
@@ -4477,13 +4477,19 @@
|
||||
"data_sync.action.previous": "Zurück",
|
||||
"data_sync.action.start_sync": "Synchronisierung starten",
|
||||
"data_sync.action.view": "Ansehen",
|
||||
"data_sync.alert.auto_create_planner_scope": "Automatisches Erstellen von Tabellen unterstützt derzeit nur MySQL nach Kingbase. Spalten, Primärschlüssel, normale Indizes, eindeutige Indizes und zusammengesetzte Indizes werden migriert; Volltext-, räumliche, Präfix- und Funktionsindizes werden ausdrücklich übersprungen.",
|
||||
"data_sync.alert.auto_create_scope": "Automatisches Erstellen von Tabellen unterstützt derzeit nur MySQL nach Kingbase. Spalten, Primärschlüssel, normale Indizes, eindeutige Indizes und zusammengesetzte Indizes werden migriert; Volltext-, räumliche, Präfix- und Funktionsindizes werden ausdrücklich übersprungen.",
|
||||
"data_sync.alert.auto_create_planner_scope": "Der Migrationsplaner wird anhand der aktuellen Quell-/Zielkombination gewählt. Der Fähigkeitshinweis und die Vorprüfung bestimmen die Unterstützung für automatische Objekte, Felder und Indizes.",
|
||||
"data_sync.alert.auto_create_scope": "Der Migrationsplaner wird anhand der aktuellen Quell-/Zielkombination gewählt. Der Fähigkeitshinweis und die Vorprüfung bestimmen die Unterstützung für automatische Objekte, Felder und Indizes.",
|
||||
"data_sync.alert.existing_target_only": "Die Datensynchronisierung arbeitet standardmäßig mit vorhandenen Zieltabellen. Wechseln Sie zur datenbankübergreifenden Migration, wenn Tabellen erstellt und Daten importiert werden sollen.",
|
||||
"data_sync.alert.full_overwrite": "Vollständiges Überschreiben löscht Daten in den Zieltabellen. Verwenden Sie diese Option vorsichtig.",
|
||||
"data_sync.alert.migration_mode": "Die datenbankübergreifende Migration ist aktiv. Nutzen Sie sie, um Tabellen in eine andere Datenquelle zu übertragen, automatisch zu erstellen und zu importieren.",
|
||||
"data_sync.alert.query_mode": "Die Synchronisierung von SQL-Ergebnismengen unterstützt derzeit benutzerdefiniertes Quell-SQL zu genau einer vorhandenen Zieltabelle. Das Abfrageergebnis muss die Primärschlüsselspalte der Zieltabelle enthalten.",
|
||||
"data_sync.alert.sync_mode": "Die Datensynchronisierung ist aktiv. Nutzen Sie sie für inkrementelle Synchronisierung oder Import mit Überschreiben, wenn Zieltabellen bereits vorhanden sind.",
|
||||
"data_sync.capability.full": "{{sourceType}} → {{targetType}} verwendet einen dedizierten Migrationsplaner mit automatischer Zielerstellung. Felder, Indizes und Einschränkungen werden vor der Ausführung geprüft.",
|
||||
"data_sync.capability.load_failed": "Die Migrationsfähigkeit dieser Quell-/Zielkombination konnte nicht geladen werden; die Ausführung wurde gesperrt. Versuchen Sie es erneut oder prüfen Sie die Anwendungslaufzeit.",
|
||||
"data_sync.capability.loading": "Die Migrationsfähigkeit der aktuellen Quell-/Zielkombination wird geprüft…",
|
||||
"data_sync.capability.partial": "{{sourceType}} → {{targetType}} verwendet den Kompatibilitätsmodus für vorhandene Ziele und kann zur Vorprüfung fortfahren, das Ziel aber nicht automatisch erstellen. Erstellen Sie es zuerst; die tatsächliche Lese-/Schreibfähigkeit wird durch Differenzanalyse und Treibervorprüfung bestimmt.",
|
||||
"data_sync.capability.planned": "{{sourceType}} → {{targetType}} ist an die Migrationsplanung angebunden, kann in dieser Version aber noch nicht ausgeführt werden.",
|
||||
"data_sync.capability.unsupported": "Migration oder Synchronisierung von {{sourceType}} → {{targetType}} wird noch nicht unterstützt.",
|
||||
"data_sync.backend.error.analyze_prepare_secrets_failed": "Zugangsdaten für die Analyse des Datenabgleichs konnten nicht vorbereitet werden: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_failed": "Änderungen konnten nicht angewendet werden: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_unsupported": "Der Zieltreiber unterstützt das Anwenden von Datenänderungen nicht",
|
||||
@@ -4613,7 +4619,7 @@
|
||||
"data_sync.backend.validation.target_table_required": "Zieltabelle ist erforderlich",
|
||||
"data_sync.backend.warning.apply_changes_unsupported": "Der Zieltreiber unterstützt das Anwenden von Datenänderungen nicht.",
|
||||
"data_sync.backend.warning.auto_add_column_sql_generation_failed": "SQL für automatische Ergänzung der Spalte {{column}} konnte nicht erzeugt werden: {{detail}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "Automatische Tabellenerstellung unterstützt derzeit nur MySQL -> Kingbase; aktuelles Paar={{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "Für die aktuelle Kombination gibt es keinen Planer zur automatischen Zielerstellung: {{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_increment_not_preserved_existing_target_add_column": "Spalte {{column}} ist eine Auto-Increment-Spalte; {{feature}} wird beim Ergänzen in einer vorhandenen Zieltabelle nicht automatisch neu erstellt",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_mysql": "Spalte {{column}} Typ {{type}} wurde zu json herabgestuft",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_pg_like": "Spalte {{column}} Typ {{type}} wurde zu jsonb herabgestuft",
|
||||
@@ -4769,7 +4775,7 @@
|
||||
"data_sync.modal.full_overwrite_content": "Vollständiges Überschreiben löscht zuerst die Daten der Zieltabelle und fügt danach Zeilen ein. Bestätigen Sie, dass die Zieldatenbank gesichert wurde.",
|
||||
"data_sync.modal.full_overwrite_ok": "Fortfahren",
|
||||
"data_sync.modal.full_overwrite_title": "Vollständiges Überschreiben bestätigen",
|
||||
"data_sync.option.auto_add_columns": "Fehlende Zielspalten automatisch ergänzen (derzeit für MySQL-Ziele und MySQL nach Kingbase; SQL-Ergebnismengenmodus wird nicht unterstützt)",
|
||||
"data_sync.option.auto_add_columns": "Fehlende Zielspalten automatisch ergänzen (Verfügbarkeit hängt von der aktuellen Quell-/Zielkombination ab; SQL-Ergebnismengenmodus wird nicht unterstützt)",
|
||||
"data_sync.option.content.both": "Schema und Daten synchronisieren",
|
||||
"data_sync.option.content.data": "Nur Daten",
|
||||
"data_sync.option.content.schema": "Nur Schema",
|
||||
@@ -4786,6 +4792,7 @@
|
||||
"data_sync.option.workflow.migration": "Datenbankübergreifende Migration (automatisch erstellen und importieren)",
|
||||
"data_sync.option.workflow.sync": "Datensynchronisierung (Unterschiede mit vorhandenen Zieltabellen synchronisieren)",
|
||||
"data_sync.placeholder.mongo_collection_name": "Mongo-Collection-Namen eingeben",
|
||||
"data_sync.placeholder.database_manual": "Datenbank, Schema oder Katalog eingeben",
|
||||
"data_sync.placeholder.source_query_sql": "Beispiel: SELECT id, name, email FROM users WHERE status = 'active'",
|
||||
"data_sync.placeholder.target_table": "Eine Zieltabelle auswählen",
|
||||
"data_sync.plan.add_missing_columns_before_import": "{{count}} fehlende Felder vor dem Import ergänzen",
|
||||
|
||||
@@ -4477,13 +4477,19 @@
|
||||
"data_sync.action.previous": "Previous",
|
||||
"data_sync.action.start_sync": "Start Sync",
|
||||
"data_sync.action.view": "View",
|
||||
"data_sync.alert.auto_create_planner_scope": "Automatic table creation currently supports only MySQL to Kingbase. It migrates columns, primary keys, regular indexes, unique indexes, and composite indexes, and skips full-text, spatial, prefix, and function indexes explicitly.",
|
||||
"data_sync.alert.auto_create_scope": "Automatic table creation currently supports only MySQL to Kingbase. It migrates columns, primary keys, regular indexes, unique indexes, and composite indexes, and skips full-text, spatial, prefix, and function indexes explicitly.",
|
||||
"data_sync.alert.auto_create_planner_scope": "The migration planner is selected from the current source and target pair. The pair capability notice and preflight results define automatic object, field, and index support.",
|
||||
"data_sync.alert.auto_create_scope": "The migration planner is selected from the current source and target pair. The pair capability notice and preflight results define automatic object, field, and index support.",
|
||||
"data_sync.alert.existing_target_only": "Data sync runs against existing target tables by default. Switch to cross-database migration when you need table creation and import.",
|
||||
"data_sync.alert.full_overwrite": "Full overwrite clears target table data. Use it carefully.",
|
||||
"data_sync.alert.migration_mode": "Cross-database migration is active. Use it to move tables to another data source with automatic table creation and import.",
|
||||
"data_sync.alert.query_mode": "SQL result-set sync currently supports custom source SQL to one existing target table. The query result must include the target table primary-key column.",
|
||||
"data_sync.alert.sync_mode": "Data sync is active. Use it for incremental sync or overwrite import when target tables already exist.",
|
||||
"data_sync.capability.full": "{{sourceType}} → {{targetType}} uses a dedicated migration planner with automatic target creation. Fields, indexes, and degradations are checked before execution.",
|
||||
"data_sync.capability.load_failed": "The migration capability for this source/target pair could not be loaded, so execution is blocked. Retry or check the application runtime.",
|
||||
"data_sync.capability.loading": "Checking migration capability for the current source/target pair…",
|
||||
"data_sync.capability.partial": "{{sourceType}} → {{targetType}} uses existing-target compatibility mode and may proceed to preflight, but cannot create the target automatically. Create it first; actual read/write support is determined by diff analysis and driver preflight.",
|
||||
"data_sync.capability.planned": "{{sourceType}} → {{targetType}} is connected to the migration planning layer, but execution is not available in this version.",
|
||||
"data_sync.capability.unsupported": "Migration or sync from {{sourceType}} → {{targetType}} is not supported yet.",
|
||||
"data_sync.backend.error.analyze_prepare_secrets_failed": "Failed to prepare data sync analysis secrets: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_failed": "Failed to apply changes: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_unsupported": "The target driver does not support applying data changes",
|
||||
@@ -4613,7 +4619,7 @@
|
||||
"data_sync.backend.validation.target_table_required": "Target table is required",
|
||||
"data_sync.backend.warning.apply_changes_unsupported": "The target driver does not support applying data changes.",
|
||||
"data_sync.backend.warning.auto_add_column_sql_generation_failed": "Failed to generate auto-add-column SQL for column {{column}}: {{detail}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "Automatic table creation currently supports only MySQL -> Kingbase; current pair={{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "The current pair has no automatic target-creation planner: {{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_increment_not_preserved_existing_target_add_column": "Column {{column}} is an auto-increment column; {{feature}} will not be recreated automatically when adding it to an existing target table",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_mysql": "Column {{column}} type {{type}} was degraded to json",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_pg_like": "Column {{column}} type {{type}} was degraded to jsonb",
|
||||
@@ -4769,7 +4775,7 @@
|
||||
"data_sync.modal.full_overwrite_content": "Full overwrite clears target table data before inserting rows. Confirm that the target database has been backed up.",
|
||||
"data_sync.modal.full_overwrite_ok": "Continue",
|
||||
"data_sync.modal.full_overwrite_title": "Confirm Full Overwrite",
|
||||
"data_sync.option.auto_add_columns": "Add missing target columns automatically (currently supports MySQL targets and MySQL to Kingbase; SQL result-set mode is not supported)",
|
||||
"data_sync.option.auto_add_columns": "Add missing target columns automatically (availability depends on the current source/target pair; SQL result-set mode is not supported)",
|
||||
"data_sync.option.content.both": "Sync Schema + Data",
|
||||
"data_sync.option.content.data": "Data Only",
|
||||
"data_sync.option.content.schema": "Schema Only",
|
||||
@@ -4786,6 +4792,7 @@
|
||||
"data_sync.option.workflow.migration": "Cross-Database Migration (create tables automatically and import)",
|
||||
"data_sync.option.workflow.sync": "Data Sync (compare and sync against existing target tables)",
|
||||
"data_sync.placeholder.mongo_collection_name": "Enter Mongo collection name",
|
||||
"data_sync.placeholder.database_manual": "Enter a database, schema, or catalog",
|
||||
"data_sync.placeholder.source_query_sql": "Example: SELECT id, name, email FROM users WHERE status = 'active'",
|
||||
"data_sync.placeholder.target_table": "Select one target table",
|
||||
"data_sync.plan.add_missing_columns_before_import": "Add {{count}} missing columns before import",
|
||||
|
||||
@@ -4477,13 +4477,19 @@
|
||||
"data_sync.action.previous": "戻る",
|
||||
"data_sync.action.start_sync": "同期を開始",
|
||||
"data_sync.action.view": "表示",
|
||||
"data_sync.alert.auto_create_planner_scope": "自動テーブル作成は現在 MySQL から Kingbase への移行のみ対応しています。列、主キー、通常インデックス、一意インデックス、複合インデックスを移行し、全文、空間、プレフィックス、関数系インデックスは明示的にスキップします。",
|
||||
"data_sync.alert.auto_create_scope": "自動テーブル作成は現在 MySQL から Kingbase への移行のみ対応しています。列、主キー、通常インデックス、一意インデックス、複合インデックスを移行し、全文、空間、プレフィックス、関数系インデックスは明示的にスキップします。",
|
||||
"data_sync.alert.auto_create_planner_scope": "現在のソースとターゲットの組み合わせに応じて移行プランナーを選択します。オブジェクト、フィールド、インデックスの自動作成可否は、組み合わせの機能表示と事前検証結果に従います。",
|
||||
"data_sync.alert.auto_create_scope": "現在のソースとターゲットの組み合わせに応じて移行プランナーを選択します。オブジェクト、フィールド、インデックスの自動作成可否は、組み合わせの機能表示と事前検証結果に従います。",
|
||||
"data_sync.alert.existing_target_only": "データ同期は既存のターゲットテーブルに対して実行されます。テーブル作成とインポートが必要な場合は、クロスデータベース移行に切り替えてください。",
|
||||
"data_sync.alert.full_overwrite": "全量上書きはターゲットテーブルのデータを消去します。慎重に使用してください。",
|
||||
"data_sync.alert.migration_mode": "クロスデータベース移行が有効です。別のデータソースへテーブルを移し、自動作成とインポートを行う場合に使用します。",
|
||||
"data_sync.alert.query_mode": "SQL 結果セット同期は現在、ソース側のカスタム SQL から単一の既存ターゲットテーブルへの同期に対応しています。クエリ結果にはターゲットテーブルの主キー列が必要です。",
|
||||
"data_sync.alert.sync_mode": "データ同期が有効です。ターゲットテーブルが既に存在する場合の増分同期や上書きインポートに使用します。",
|
||||
"data_sync.capability.full": "{{sourceType}} → {{targetType}} は専用移行プランナーを使用し、ターゲットの自動作成に対応しています。実行前にフィールド、インデックス、縮退項目を検証します。",
|
||||
"data_sync.capability.load_failed": "このソース/ターゲットの移行機能を読み込めなかったため、実行を停止しました。再試行するか、アプリケーションの実行環境を確認してください。",
|
||||
"data_sync.capability.loading": "現在のソース/ターゲットの移行機能を確認しています…",
|
||||
"data_sync.capability.partial": "{{sourceType}} → {{targetType}} は既存ターゲット互換モードで事前検証へ進めますが、自動作成には未対応です。先にターゲットを作成してください。実際の読み書き可否は差分分析とドライバー事前検証で確認されます。",
|
||||
"data_sync.capability.planned": "{{sourceType}} → {{targetType}} は移行計画レイヤーに接続済みですが、このバージョンではまだ実行できません。",
|
||||
"data_sync.capability.unsupported": "{{sourceType}} → {{targetType}} の移行または同期にはまだ対応していません。",
|
||||
"data_sync.backend.error.analyze_prepare_secrets_failed": "データ同期分析のシークレット準備に失敗しました: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_failed": "変更の適用に失敗しました: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_unsupported": "ターゲットドライバーはデータ変更の適用をサポートしていません",
|
||||
@@ -4613,7 +4619,7 @@
|
||||
"data_sync.backend.validation.target_table_required": "ターゲットテーブルは必須です",
|
||||
"data_sync.backend.warning.apply_changes_unsupported": "ターゲットドライバーはデータ変更の適用をサポートしていません。",
|
||||
"data_sync.backend.warning.auto_add_column_sql_generation_failed": "列 {{column}} の自動追加 SQL 生成に失敗しました: {{detail}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "自動テーブル作成は現在 MySQL -> Kingbase のみサポートしています。現在の組み合わせ={{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "現在の組み合わせには自動ターゲット作成プランナーがありません:{{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_increment_not_preserved_existing_target_add_column": "列 {{column}} は自動増分列です。既存ターゲットテーブルに追加する際、{{feature}} は自動再作成されません",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_mysql": "列 {{column}} の型 {{type}} は json に降格されました",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_pg_like": "列 {{column}} の型 {{type}} は jsonb に降格されました",
|
||||
@@ -4769,7 +4775,7 @@
|
||||
"data_sync.modal.full_overwrite_content": "全量上書きはターゲットテーブルのデータを消去してから行を挿入します。ターゲットデータベースのバックアップを確認してください。",
|
||||
"data_sync.modal.full_overwrite_ok": "続行",
|
||||
"data_sync.modal.full_overwrite_title": "全量上書きの確認",
|
||||
"data_sync.option.auto_add_columns": "ターゲットテーブルに不足している列を自動追加(現在は MySQL ターゲットおよび MySQL から Kingbase に対応。SQL 結果セットモードは未対応)",
|
||||
"data_sync.option.auto_add_columns": "ターゲットテーブルに不足している列を自動追加(可否は現在のソース/ターゲットの組み合わせによります。SQL 結果セットモードは未対応)",
|
||||
"data_sync.option.content.both": "スキーマとデータを同期",
|
||||
"data_sync.option.content.data": "データのみ",
|
||||
"data_sync.option.content.schema": "スキーマのみ",
|
||||
@@ -4786,6 +4792,7 @@
|
||||
"data_sync.option.workflow.migration": "クロスデータベース移行(自動作成後にインポート)",
|
||||
"data_sync.option.workflow.sync": "データ同期(既存ターゲットテーブルとの差分を同期)",
|
||||
"data_sync.placeholder.mongo_collection_name": "Mongo コレクション名を入力",
|
||||
"data_sync.placeholder.database_manual": "データベース、スキーマ、またはカタログを入力",
|
||||
"data_sync.placeholder.source_query_sql": "例: SELECT id, name, email FROM users WHERE status = 'active'",
|
||||
"data_sync.placeholder.target_table": "ターゲットテーブルを 1 つ選択",
|
||||
"data_sync.plan.add_missing_columns_before_import": "{{count}} 個の不足フィールドを補完してからインポート",
|
||||
|
||||
@@ -4477,13 +4477,19 @@
|
||||
"data_sync.action.previous": "Назад",
|
||||
"data_sync.action.start_sync": "Начать синхронизацию",
|
||||
"data_sync.action.view": "Просмотреть",
|
||||
"data_sync.alert.auto_create_planner_scope": "Автоматическое создание таблиц сейчас поддерживает только MySQL в Kingbase. Переносятся столбцы, первичные ключи, обычные индексы, уникальные индексы и составные индексы; полнотекстовые, пространственные, префиксные и функциональные индексы явно пропускаются.",
|
||||
"data_sync.alert.auto_create_scope": "Автоматическое создание таблиц сейчас поддерживает только MySQL в Kingbase. Переносятся столбцы, первичные ключи, обычные индексы, уникальные индексы и составные индексы; полнотекстовые, пространственные, префиксные и функциональные индексы явно пропускаются.",
|
||||
"data_sync.alert.auto_create_planner_scope": "Планировщик миграции выбирается по текущей паре источника и цели. Поддержка автоматического создания объектов, полей и индексов определяется сведениями о возможностях пары и результатами предварительной проверки.",
|
||||
"data_sync.alert.auto_create_scope": "Планировщик миграции выбирается по текущей паре источника и цели. Поддержка автоматического создания объектов, полей и индексов определяется сведениями о возможностях пары и результатами предварительной проверки.",
|
||||
"data_sync.alert.existing_target_only": "Синхронизация данных по умолчанию выполняется с существующими целевыми таблицами. Переключитесь на межбазовую миграцию, если нужны создание таблиц и импорт.",
|
||||
"data_sync.alert.full_overwrite": "Полная перезапись очищает данные целевых таблиц. Используйте ее осторожно.",
|
||||
"data_sync.alert.migration_mode": "Активна межбазовая миграция. Используйте ее для переноса таблиц в другой источник данных с автоматическим созданием и импортом.",
|
||||
"data_sync.alert.query_mode": "Синхронизация результата SQL сейчас поддерживает пользовательский SQL источника в одну существующую целевую таблицу. Результат запроса должен содержать столбец первичного ключа целевой таблицы.",
|
||||
"data_sync.alert.sync_mode": "Активна синхронизация данных. Используйте ее для инкрементальной синхронизации или импорта с перезаписью, когда целевые таблицы уже существуют.",
|
||||
"data_sync.capability.full": "Для {{sourceType}} → {{targetType}} используется специализированный планировщик миграции с автоматическим созданием цели. Перед запуском проверяются поля, индексы и ограничения.",
|
||||
"data_sync.capability.load_failed": "Не удалось загрузить возможности миграции для этой пары источника и цели, поэтому выполнение заблокировано. Повторите попытку или проверьте среду приложения.",
|
||||
"data_sync.capability.loading": "Проверяются возможности миграции для текущей пары источника и цели…",
|
||||
"data_sync.capability.partial": "Для {{sourceType}} → {{targetType}} используется режим совместимости с существующей целью: можно перейти к предварительной проверке, но автоматическое создание недоступно. Сначала создайте цель; фактическая поддержка чтения и записи определяется анализом различий и проверкой драйвера.",
|
||||
"data_sync.capability.planned": "Пара {{sourceType}} → {{targetType}} подключена к уровню планирования миграции, но выполнение в этой версии пока недоступно.",
|
||||
"data_sync.capability.unsupported": "Миграция или синхронизация {{sourceType}} → {{targetType}} пока не поддерживается.",
|
||||
"data_sync.backend.error.analyze_prepare_secrets_failed": "Не удалось подготовить секреты анализа синхронизации данных: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_failed": "Не удалось применить изменения: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_unsupported": "Целевой драйвер не поддерживает применение изменений данных",
|
||||
@@ -4613,7 +4619,7 @@
|
||||
"data_sync.backend.validation.target_table_required": "Целевая таблица обязательна",
|
||||
"data_sync.backend.warning.apply_changes_unsupported": "Целевой драйвер не поддерживает применение изменений данных.",
|
||||
"data_sync.backend.warning.auto_add_column_sql_generation_failed": "Не удалось создать SQL для автоматического добавления столбца {{column}}: {{detail}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "Автоматическое создание таблиц сейчас поддерживает только MySQL -> Kingbase; текущая пара={{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "Для текущей пары нет планировщика автоматического создания цели: {{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_increment_not_preserved_existing_target_add_column": "Столбец {{column}} является автоинкрементным; {{feature}} не будет автоматически создан заново при добавлении в существующую целевую таблицу",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_mysql": "Столбец {{column}} типа {{type}} деградирован до json",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_pg_like": "Столбец {{column}} типа {{type}} деградирован до jsonb",
|
||||
@@ -4769,7 +4775,7 @@
|
||||
"data_sync.modal.full_overwrite_content": "Полная перезапись сначала очищает целевую таблицу, а затем вставляет строки. Подтвердите, что целевая база данных сохранена в резервной копии.",
|
||||
"data_sync.modal.full_overwrite_ok": "Продолжить",
|
||||
"data_sync.modal.full_overwrite_title": "Подтвердите полную перезапись",
|
||||
"data_sync.option.auto_add_columns": "Автоматически добавить недостающие целевые столбцы (сейчас поддерживаются цели MySQL и MySQL в Kingbase; режим результата SQL не поддерживается)",
|
||||
"data_sync.option.auto_add_columns": "Автоматически добавить недостающие целевые столбцы (доступность зависит от текущей пары источника и цели; режим результата SQL не поддерживается)",
|
||||
"data_sync.option.content.both": "Синхронизировать схему и данные",
|
||||
"data_sync.option.content.data": "Только данные",
|
||||
"data_sync.option.content.schema": "Только схема",
|
||||
@@ -4786,6 +4792,7 @@
|
||||
"data_sync.option.workflow.migration": "Межбазовая миграция (автоматическое создание и импорт)",
|
||||
"data_sync.option.workflow.sync": "Синхронизация данных (сравнение и синхронизация с существующими целевыми таблицами)",
|
||||
"data_sync.placeholder.mongo_collection_name": "Введите имя коллекции Mongo",
|
||||
"data_sync.placeholder.database_manual": "Введите базу данных, схему или каталог",
|
||||
"data_sync.placeholder.source_query_sql": "Пример: SELECT id, name, email FROM users WHERE status = 'active'",
|
||||
"data_sync.placeholder.target_table": "Выберите одну целевую таблицу",
|
||||
"data_sync.plan.add_missing_columns_before_import": "Дополнить отсутствующие поля перед импортом: {{count}}",
|
||||
|
||||
@@ -4477,13 +4477,19 @@
|
||||
"data_sync.action.previous": "上一步",
|
||||
"data_sync.action.start_sync": "开始同步",
|
||||
"data_sync.action.view": "查看",
|
||||
"data_sync.alert.auto_create_planner_scope": "自动建表当前仅支持 MySQL 到 Kingbase。会迁移字段、主键、普通索引、唯一索引和联合索引,并明确跳过全文、空间、前缀和函数类索引。",
|
||||
"data_sync.alert.auto_create_scope": "自动建表当前仅支持 MySQL 到 Kingbase。会迁移字段、主键、普通索引、唯一索引和联合索引,并明确跳过全文、空间、前缀和函数类索引。",
|
||||
"data_sync.alert.auto_create_planner_scope": "系统会按当前源端和目标端选择迁移规划器;自动建表、字段与索引的实际支持范围以组合能力提示和预检结果为准。",
|
||||
"data_sync.alert.auto_create_scope": "系统会按当前源端和目标端选择迁移规划器;自动建表、字段与索引的实际支持范围以组合能力提示和预检结果为准。",
|
||||
"data_sync.alert.existing_target_only": "数据同步默认使用已有目标表执行。需要跨数据源建表并导入时,请切换到跨库迁移。",
|
||||
"data_sync.alert.full_overwrite": "全量覆盖会清空目标表数据,请谨慎使用。",
|
||||
"data_sync.alert.migration_mode": "当前为跨库迁移。适合将表迁移到另一数据源,并自动建表和导入数据。",
|
||||
"data_sync.alert.query_mode": "SQL 结果集同步当前支持源端自定义 SQL 到单个已存在目标表。查询结果必须包含目标表主键列。",
|
||||
"data_sync.alert.sync_mode": "当前为数据同步。适合目标表已存在时做增量同步或覆盖导入。",
|
||||
"data_sync.capability.full": "{{sourceType}} → {{targetType}} 已接入专用迁移规划器,支持自动创建目标对象;执行前会预检字段、索引和降级项。",
|
||||
"data_sync.capability.load_failed": "无法读取当前源/目标组合的迁移能力,已阻止继续执行;请重试或检查应用运行时。",
|
||||
"data_sync.capability.loading": "正在验证当前源/目标组合的迁移能力…",
|
||||
"data_sync.capability.partial": "{{sourceType}} → {{targetType}} 使用已有目标对象兼容模式,可进入预检但暂不支持自动创建;请先创建目标对象,实际读写能力以差异分析和驱动预检为准。",
|
||||
"data_sync.capability.planned": "{{sourceType}} → {{targetType}} 已进入迁移规划阶段,但当前版本尚不可执行。",
|
||||
"data_sync.capability.unsupported": "当前尚不支持 {{sourceType}} → {{targetType}} 的迁移或同步。",
|
||||
"data_sync.backend.error.analyze_prepare_secrets_failed": "准备数据同步分析密钥失败: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_failed": "应用变更失败: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_unsupported": "目标驱动不支持应用数据变更",
|
||||
@@ -4613,7 +4619,7 @@
|
||||
"data_sync.backend.validation.target_table_required": "目标表不能为空",
|
||||
"data_sync.backend.warning.apply_changes_unsupported": "目标驱动不支持应用数据变更。",
|
||||
"data_sync.backend.warning.auto_add_column_sql_generation_failed": "字段 {{column}} 自动补齐 SQL 生成失败: {{detail}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "自动建表目前仅支持 MySQL -> Kingbase;当前组合={{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "当前组合未接入自动建表规划器:{{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_increment_not_preserved_existing_target_add_column": "字段 {{column}} 为自增列,补齐到已有目标表时不会自动补建 {{feature}}",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_mysql": "字段 {{column}} 类型 {{type}} 已降级为 json",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_pg_like": "字段 {{column}} 类型 {{type}} 已降级为 jsonb",
|
||||
@@ -4769,7 +4775,7 @@
|
||||
"data_sync.modal.full_overwrite_content": "全量覆盖会先清空目标表数据再插入,请确认目标库已完成备份。",
|
||||
"data_sync.modal.full_overwrite_ok": "继续执行",
|
||||
"data_sync.modal.full_overwrite_title": "确认全量覆盖",
|
||||
"data_sync.option.auto_add_columns": "自动补齐目标表缺失字段(当前支持 MySQL 目标及 MySQL 到 Kingbase;SQL 结果集模式暂不支持)",
|
||||
"data_sync.option.auto_add_columns": "自动补齐目标表缺失字段(是否可用由当前源/目标组合决定;SQL 结果集模式暂不支持)",
|
||||
"data_sync.option.content.both": "同步结构和数据",
|
||||
"data_sync.option.content.data": "仅同步数据",
|
||||
"data_sync.option.content.schema": "仅同步结构",
|
||||
@@ -4786,6 +4792,7 @@
|
||||
"data_sync.option.workflow.migration": "跨库迁移(自动建表后导入)",
|
||||
"data_sync.option.workflow.sync": "数据同步(基于已有目标表做差异同步)",
|
||||
"data_sync.placeholder.mongo_collection_name": "请输入 Mongo 集合名",
|
||||
"data_sync.placeholder.database_manual": "请输入数据库、Schema 或 Catalog",
|
||||
"data_sync.placeholder.source_query_sql": "例如:SELECT id, name, email FROM users WHERE status = 'active'",
|
||||
"data_sync.placeholder.target_table": "请选择一个目标表",
|
||||
"data_sync.plan.add_missing_columns_before_import": "补齐 {{count}} 个缺失字段后导入",
|
||||
|
||||
@@ -4477,13 +4477,19 @@
|
||||
"data_sync.action.previous": "上一步",
|
||||
"data_sync.action.start_sync": "開始同步",
|
||||
"data_sync.action.view": "查看",
|
||||
"data_sync.alert.auto_create_planner_scope": "自動建表目前僅支援 MySQL 到 Kingbase。會遷移欄位、主鍵、一般索引、唯一索引和聯合索引,並明確跳過全文、空間、前綴和函數類索引。",
|
||||
"data_sync.alert.auto_create_scope": "自動建表目前僅支援 MySQL 到 Kingbase。會遷移欄位、主鍵、一般索引、唯一索引和聯合索引,並明確跳過全文、空間、前綴和函數類索引。",
|
||||
"data_sync.alert.auto_create_planner_scope": "系統會依目前來源端與目標端選擇遷移規劃器;自動建表、欄位與索引的實際支援範圍以組合能力提示和預檢結果為準。",
|
||||
"data_sync.alert.auto_create_scope": "系統會依目前來源端與目標端選擇遷移規劃器;自動建表、欄位與索引的實際支援範圍以組合能力提示和預檢結果為準。",
|
||||
"data_sync.alert.existing_target_only": "資料同步預設使用已有目標表執行。需要跨資料來源建表並導入時,請切換到跨庫遷移。",
|
||||
"data_sync.alert.full_overwrite": "全量覆寫會清空目標表資料,請謹慎使用。",
|
||||
"data_sync.alert.migration_mode": "目前為跨庫遷移。適合將表遷移到另一資料來源,並自動建表和導入資料。",
|
||||
"data_sync.alert.query_mode": "SQL 結果集同步目前支援來源端自定義 SQL 到單個已存在目標表。查詢結果必須包含目標表主鍵列。",
|
||||
"data_sync.alert.sync_mode": "目前為資料同步。適合目標表已存在時做增量同步或覆寫導入。",
|
||||
"data_sync.capability.full": "{{sourceType}} → {{targetType}} 已接入專用遷移規劃器,支援自動建立目標物件;執行前會預檢欄位、索引與降級項目。",
|
||||
"data_sync.capability.load_failed": "無法讀取目前來源/目標組合的遷移能力,已阻止繼續執行;請重試或檢查應用程式執行環境。",
|
||||
"data_sync.capability.loading": "正在驗證目前來源/目標組合的遷移能力…",
|
||||
"data_sync.capability.partial": "{{sourceType}} → {{targetType}} 使用既有目標物件相容模式,可進入預檢但暫不支援自動建立;請先建立目標物件,實際讀寫能力以差異分析與驅動預檢為準。",
|
||||
"data_sync.capability.planned": "{{sourceType}} → {{targetType}} 已進入遷移規劃階段,但目前版本尚不可執行。",
|
||||
"data_sync.capability.unsupported": "目前尚不支援 {{sourceType}} → {{targetType}} 的遷移或同步。",
|
||||
"data_sync.backend.error.analyze_prepare_secrets_failed": "準備資料同步分析金鑰失敗: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_failed": "套用變更失敗: {{detail}}",
|
||||
"data_sync.backend.error.apply_changes_unsupported": "目標驅動不支援套用資料變更",
|
||||
@@ -4613,7 +4619,7 @@
|
||||
"data_sync.backend.validation.target_table_required": "目標表不能為空",
|
||||
"data_sync.backend.warning.apply_changes_unsupported": "目標驅動不支援套用資料變更。",
|
||||
"data_sync.backend.warning.auto_add_column_sql_generation_failed": "欄位 {{column}} 自動補齊 SQL 產生失敗: {{detail}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "自動建表目前僅支援 MySQL -> Kingbase;目前組合={{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_create_pair_unsupported": "目前組合尚未接入自動建表規劃器:{{sourceType}} -> {{targetType}}",
|
||||
"data_sync.backend.warning.auto_increment_not_preserved_existing_target_add_column": "欄位 {{column}} 為自增列,補齊到已有目標表時不會自動補建 {{feature}}",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_mysql": "欄位 {{column}} 型別 {{type}} 已降級為 json",
|
||||
"data_sync.backend.warning.clickhouse_complex_type_degraded_pg_like": "欄位 {{column}} 型別 {{type}} 已降級為 jsonb",
|
||||
@@ -4769,7 +4775,7 @@
|
||||
"data_sync.modal.full_overwrite_content": "全量覆寫會先清空目標表資料再插入,請確認目標庫已完成備份。",
|
||||
"data_sync.modal.full_overwrite_ok": "繼續執行",
|
||||
"data_sync.modal.full_overwrite_title": "確認全量覆寫",
|
||||
"data_sync.option.auto_add_columns": "自動補齊目標表缺失欄位(目前支援 MySQL 目標及 MySQL 到 Kingbase;SQL 結果集模式暫不支援)",
|
||||
"data_sync.option.auto_add_columns": "自動補齊目標表缺失欄位(是否可用由目前來源/目標組合決定;SQL 結果集模式暫不支援)",
|
||||
"data_sync.option.content.both": "同步結構和資料",
|
||||
"data_sync.option.content.data": "僅同步資料",
|
||||
"data_sync.option.content.schema": "僅同步結構",
|
||||
@@ -4786,6 +4792,7 @@
|
||||
"data_sync.option.workflow.migration": "跨庫遷移(自動建表後導入)",
|
||||
"data_sync.option.workflow.sync": "資料同步(基於已有目標表做差異同步)",
|
||||
"data_sync.placeholder.mongo_collection_name": "請輸入 Mongo 集合名",
|
||||
"data_sync.placeholder.database_manual": "請輸入資料庫、Schema 或 Catalog",
|
||||
"data_sync.placeholder.source_query_sql": "例如:SELECT id, name, email FROM users WHERE status = 'active'",
|
||||
"data_sync.placeholder.target_table": "請選擇一個目標表",
|
||||
"data_sync.plan.add_missing_columns_before_import": "補齊 {{count}} 個缺失欄位後導入",
|
||||
|
||||
Reference in New Issue
Block a user