From 647768221ebbdc3a60dc5c643e11f4cc7305fe35 Mon Sep 17 00:00:00 2001 From: tianqijiuyun-latiao <69459608+tianqijiuyun-latiao@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:53:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(data-sync):=20=E5=A2=9E=E5=8A=A0=E5=B7=AE?= =?UTF-8?q?=E5=BC=82=20SQL=20=E9=A2=84=E8=A7=88=E8=83=BD=E5=8A=9B=E4=BE=BF?= =?UTF-8?q?=E4=BA=8E=E5=AE=A1=E6=A0=B8=20refs=20#174?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/DataSyncModal.tsx | 166 +++++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/DataSyncModal.tsx b/frontend/src/components/DataSyncModal.tsx index 769885a..cfde4cb 100644 --- a/frontend/src/components/DataSyncModal.tsx +++ b/frontend/src/components/DataSyncModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useMemo, useRef } from 'react'; import { Modal, Form, Select, Button, message, Steps, Transfer, Card, Alert, Divider, Typography, Progress, Checkbox, Table, Drawer, Tabs } from 'antd'; import { useStore } from '../store'; import { DBGetDatabases, DBGetTables, DataSync, DataSyncAnalyze, DataSyncPreview } from '../../wailsjs/go/app/App'; @@ -31,6 +31,118 @@ type TableOps = { selectedDeletePks?: string[]; }; +const quoteSqlIdent = (dbType: string, ident: string): string => { + const raw = String(ident || '').trim(); + if (!raw) return raw; + const t = String(dbType || '').toLowerCase(); + if (t === 'mysql' || t === 'mariadb' || t === 'diros' || t === 'sphinx' || t === 'clickhouse' || t === 'tdengine') { + return `\`${raw.replace(/`/g, '``')}\``; + } + if (t === 'sqlserver') { + return `[${raw.replace(/]/g, ']]')}]`; + } + return `"${raw.replace(/"/g, '""')}"`; +}; + +const quoteSqlTable = (dbType: string, tableName: string): string => { + const raw = String(tableName || '').trim(); + if (!raw) return raw; + if (!raw.includes('.')) return quoteSqlIdent(dbType, raw); + return raw + .split('.') + .map((part) => quoteSqlIdent(dbType, part)) + .join('.'); +}; + +const toSqlLiteral = (value: any, dbType: string): string => { + if (value === null || value === undefined) return 'NULL'; + if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'NULL'; + if (typeof value === 'bigint') return value.toString(); + if (typeof value === 'boolean') { + const t = String(dbType || '').toLowerCase(); + if (t === 'sqlserver') return value ? '1' : '0'; + return value ? 'TRUE' : 'FALSE'; + } + if (value instanceof Date) { + return `'${value.toISOString().replace(/'/g, "''")}'`; + } + if (typeof value === 'object') { + try { + return `'${JSON.stringify(value).replace(/'/g, "''")}'`; + } catch { + return `'${String(value).replace(/'/g, "''")}'`; + } + } + return `'${String(value).replace(/'/g, "''")}'`; +}; + +const buildSqlPreview = ( + previewData: any, + tableName: string, + dbType: string, + ops?: TableOps, +): { sqlText: string; statementCount: number } => { + if (!previewData || !tableName) return { sqlText: '', statementCount: 0 }; + const tableExpr = quoteSqlTable(dbType, tableName); + const pkCol = String(previewData.pkColumn || 'id'); + const statements: string[] = []; + + const insertRows = Array.isArray(previewData.inserts) ? previewData.inserts : []; + const updateRows = Array.isArray(previewData.updates) ? previewData.updates : []; + const deleteRows = Array.isArray(previewData.deletes) ? previewData.deletes : []; + + const selectedInsert = new Set((ops?.selectedInsertPks || []).map((v) => String(v))); + const selectedUpdate = new Set((ops?.selectedUpdatePks || []).map((v) => String(v))); + const selectedDelete = new Set((ops?.selectedDeletePks || []).map((v) => String(v))); + + if (ops?.insert !== false) { + insertRows.forEach((rowWrap: any) => { + const pk = String(rowWrap?.pk ?? ''); + if (selectedInsert.size > 0 && !selectedInsert.has(pk)) return; + const row = rowWrap?.row || {}; + const columns = Object.keys(row); + if (columns.length === 0) return; + const colExpr = columns.map((c) => quoteSqlIdent(dbType, c)).join(', '); + const valExpr = columns.map((c) => toSqlLiteral(row[c], dbType)).join(', '); + statements.push(`INSERT INTO ${tableExpr} (${colExpr}) VALUES (${valExpr});`); + }); + } + + if (ops?.update !== false) { + updateRows.forEach((rowWrap: any) => { + const pk = String(rowWrap?.pk ?? ''); + if (selectedUpdate.size > 0 && !selectedUpdate.has(pk)) return; + const source = rowWrap?.source || {}; + const changedColumns = Array.isArray(rowWrap?.changedColumns) + ? rowWrap.changedColumns + : Object.keys(source).filter((k) => k !== pkCol); + const setCols = changedColumns.filter((c: string) => String(c) !== pkCol); + if (setCols.length === 0) return; + const setExpr = setCols + .map((c: string) => `${quoteSqlIdent(dbType, c)} = ${toSqlLiteral(source[c], dbType)}`) + .join(', '); + statements.push( + `UPDATE ${tableExpr} SET ${setExpr} WHERE ${quoteSqlIdent(dbType, pkCol)} = ${toSqlLiteral(pk, dbType)};`, + ); + }); + } + + if (ops?.delete) { + deleteRows.forEach((rowWrap: any) => { + const pk = String(rowWrap?.pk ?? ''); + if (selectedDelete.size > 0 && !selectedDelete.has(pk)) return; + statements.push( + `DELETE FROM ${tableExpr} WHERE ${quoteSqlIdent(dbType, pkCol)} = ${toSqlLiteral(pk, dbType)};`, + ); + }); + } + + return { + sqlText: statements.join('\n'), + statementCount: statements.length, + }; +}; + const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, onClose }) => { const connections = useStore((state) => state.connections); const [currentStep, setCurrentStep] = useState(0); @@ -402,6 +514,13 @@ const DataSyncModal: React.FC<{ open: boolean; onClose: () => void }> = ({ open, ); }; + const previewSql = useMemo(() => { + if (!previewData || !previewTable) return { sqlText: '', statementCount: 0 }; + const targetType = String(connections.find(c => c.id === targetConnId)?.config?.type || ''); + const ops = tableOptions[previewTable] || { insert: true, update: true, delete: false }; + return buildSqlPreview(previewData, previewTable, targetType, ops); + }, [previewData, previewTable, targetConnId, connections, tableOptions]); + return ( <> void }> = ({ open, /> ) + }, + { + key: 'sql', + label: `SQL(${previewSql.statementCount})`, + children: ( +
+ +
+ 共 {previewSql.statementCount} 条语句(预览数据最多 200 条/类型) + +
+
+                                        {previewSql.sqlText || '-- 当前勾选范围下无 SQL 可预览'}
+                                    
+
+ ) } ]} />