feat(sync): 按数据源组合约束迁移能力

- 展示源端到目标端的完整、部分、规划中和不支持状态
- 按能力控制自动建表、补列、索引及继续执行入口
- 支持数据库列表为空时手工输入并补齐六语言提示
This commit is contained in:
Syngnat
2026-08-08 19:34:36 +08:00
parent f15b630e70
commit 4df879ccde
13 changed files with 639 additions and 74 deletions

View File

@@ -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
}
>

View 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}}');
});
});

View 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);
},
);
});

View 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,
};
};

View 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: '' });
});
});

View 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] : ''),
};
};

View File

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