diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 3e58839..82e494f 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -261,7 +261,7 @@ jobs: TARGET_PLATFORM="${{ matrix.platform }}" GOOS="${TARGET_PLATFORM%%/*}" GOARCH="${TARGET_PLATFORM##*/}" - DRIVERS=(mariadb doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase mongodb tdengine clickhouse) + DRIVERS=(mariadb oceanbase doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse) OUTDIR="drivers/${{ matrix.os_name }}" mkdir -p "$OUTDIR" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f97c3cd..98f8290 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -252,7 +252,7 @@ jobs: TARGET_PLATFORM="${{ matrix.platform }}" GOOS="${TARGET_PLATFORM%%/*}" GOARCH="${TARGET_PLATFORM##*/}" - DRIVERS=(mariadb doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase mongodb tdengine clickhouse) + DRIVERS=(mariadb oceanbase doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse) OUTDIR="drivers/${{ matrix.os_name }}" mkdir -p "$OUTDIR" diff --git a/build-driver-agents.sh b/build-driver-agents.sh index 455f72d..567e2a8 100755 --- a/build-driver-agents.sh +++ b/build-driver-agents.sh @@ -5,7 +5,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" -DEFAULT_DRIVERS=(mariadb doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase mongodb tdengine clickhouse) +DEFAULT_DRIVERS=(mariadb oceanbase doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse) +DEFAULT_PLATFORMS=(darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64 linux/arm64) usage() { cat <<'EOF' @@ -14,8 +15,8 @@ usage() { 选项: --drivers <列表> 指定驱动列表(逗号分隔),例如:kingbase,mongodb - --platform - 目标平台,默认使用当前 Go 环境(go env GOOS/GOARCH) + --platform <目标> 目标平台:current、all、GOOS/GOARCH,或逗号分隔列表 + 默认 current(当前 Go 环境) --out-dir <目录> 输出目录根路径,默认:dist/driver-agents --bundle-name <文件名> 驱动总包 zip 名称,默认:GoNavi-DriverAgents.zip --strict 任一驱动构建失败即中断(默认失败后继续,最后汇总) @@ -25,6 +26,8 @@ usage() { ./build-driver-agents.sh ./build-driver-agents.sh --drivers kingbase ./build-driver-agents.sh --platform windows/amd64 --drivers kingbase,mongodb + ./build-driver-agents.sh --platform all + ./build-driver-agents.sh --platform darwin/arm64,windows/amd64,linux/amd64 EOF } @@ -33,7 +36,8 @@ normalize_driver() { name="$(echo "${1:-}" | tr '[:upper:]' '[:lower:]' | xargs)" case "$name" in doris|diros) echo "doris" ;; - mariadb|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|mongodb|tdengine|clickhouse) + open_gauss|open-gauss) echo "opengauss" ;; + mariadb|oceanbase|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|mongodb|tdengine|clickhouse) echo "$name" ;; *) @@ -58,6 +62,83 @@ platform_dir_name() { esac } +current_platform() { + echo "$(go env GOOS)/$(go env GOARCH)" +} + +append_platform() { + local candidate + candidate="$1" + if [[ "$platform_seen" == *"|$candidate|"* ]]; then + return 0 + fi + platforms+=("$candidate") + platform_seen="${platform_seen}${candidate}|" +} + +normalize_platform() { + local value goos goarch platform_dir + value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" + case "$value" in + current|"") + current_platform + ;; + */*) + goos="${value%%/*}" + goarch="${value##*/}" + platform_dir="$(platform_dir_name "$goos")" + if [[ -z "$goos" || -z "$goarch" || "$platform_dir" == "Unknown" ]]; then + return 1 + fi + echo "$goos/$goarch" + ;; + *) + return 1 + ;; + esac +} + +zip_bundle() { + local bundle_zip_path="$1" + local bundle_stage_dir="$2" + local -a bundle_dirs=() + local dir + + for dir in "$bundle_stage_dir"/*; do + [[ -d "$dir" ]] || continue + bundle_dirs+=("$(basename "$dir")") + done + + if [[ ${#bundle_dirs[@]} -eq 0 ]]; then + echo "❌ 驱动总包 staging 目录为空。" + exit 1 + fi + + rm -f "$bundle_zip_path" + if command -v zip >/dev/null 2>&1; then + ( + cd "$bundle_stage_dir" + zip -qry "$bundle_zip_path" "${bundle_dirs[@]}" + ) + elif command -v python3 >/dev/null 2>&1; then + BUNDLE_STAGE_DIR="$bundle_stage_dir" BUNDLE_ZIP_PATH="$bundle_zip_path" python3 - <<'PY' +import os +import zipfile +from pathlib import Path + +stage = Path(os.environ["BUNDLE_STAGE_DIR"]) +target = Path(os.environ["BUNDLE_ZIP_PATH"]) +with zipfile.ZipFile(target, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for path in stage.rglob("*"): + if path.is_file(): + zf.write(path, path.relative_to(stage).as_posix()) +PY + else + echo "❌ 未找到 zip 或 python3,无法生成驱动总包 zip。" + exit 1 + fi +} + driver_csv="" target_platform="" out_root="dist/driver-agents" @@ -103,20 +184,6 @@ if ! command -v go >/dev/null 2>&1; then exit 1 fi -if [[ -z "$target_platform" ]]; then - target_platform="$(go env GOOS)/$(go env GOARCH)" -fi - -if [[ "$target_platform" != */* ]]; then - echo "❌ --platform 参数格式错误,应为 GOOS/GOARCH,例如 darwin/arm64" - exit 1 -fi - -goos="${target_platform%%/*}" -goarch="${target_platform##*/}" -platform_key="${goos}-${goarch}" -platform_dir="$(platform_dir_name "$goos")" - declare -a drivers=() if [[ -n "$driver_csv" ]]; then IFS=',' read -r -a raw_drivers <<<"$driver_csv" @@ -131,68 +198,114 @@ else drivers=("${DEFAULT_DRIVERS[@]}") fi -output_dir="${out_root%/}/${platform_key}" +declare -a platforms=() +platform_seen="|" +if [[ -z "$target_platform" ]]; then + target_platform="current" +fi +IFS=',' read -r -a raw_platforms <<<"$target_platform" +for item in "${raw_platforms[@]}"; do + normalized_platform="$(printf '%s' "$item" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" + if [[ "$normalized_platform" == "all" ]]; then + for default_platform in "${DEFAULT_PLATFORMS[@]}"; do + append_platform "$default_platform" + done + continue + fi + normalized_platform="$(normalize_platform "$item")" || { + echo "❌ --platform 参数格式错误,应为 current、all、GOOS/GOARCH 或逗号分隔列表,例如 darwin/arm64,windows/amd64" + exit 1 + } + append_platform "$normalized_platform" +done + +if [[ ${#platforms[@]} -eq 0 ]]; then + echo "❌ 未指定有效目标平台。" + exit 1 +fi + +mkdir -p "$out_root" +out_root_abs="$(cd "$out_root" && pwd)" bundle_stage_dir="$(mktemp -d "${TMPDIR:-/tmp}/gonavi-driver-bundle.XXXXXX")" -bundle_platform_dir="$bundle_stage_dir/$platform_dir" cleanup() { rm -rf "$bundle_stage_dir" } trap cleanup EXIT -mkdir -p "$output_dir" "$bundle_platform_dir" -output_dir_abs="$(cd "$output_dir" && pwd)" -bundle_zip_path="$output_dir_abs/$bundle_name" +if [[ ${#platforms[@]} -eq 1 ]]; then + single_platform="${platforms[0]}" + single_platform_key="${single_platform/\//-}" + single_output_dir="${out_root%/}/$single_platform_key" + mkdir -p "$single_output_dir" + bundle_zip_path="$(cd "$single_output_dir" && pwd)/$bundle_name" +else + bundle_zip_path="$out_root_abs/$bundle_name" +fi declare -a built_assets=() declare -a failed_drivers=() declare -a skipped_drivers=() echo "🚀 开始构建 optional-driver-agent" -echo " 平台:$goos/$goarch" -echo " 输出目录:$output_dir_abs" +echo " 平台:${platforms[*]}" +echo " 输出根目录:$out_root_abs" echo " 驱动列表:${drivers[*]}" -echo "🧭 生成 driver-agent revision 指纹" -"$SCRIPT_DIR/tools/generate-driver-agent-revisions.sh" --platform "$target_platform" -for driver in "${drivers[@]}"; do - if [[ "$driver" == "duckdb" && "$goos" == "windows" && "$goarch" != "amd64" ]]; then - echo "⚠️ 跳过 duckdb(仅支持 windows/amd64)" - skipped_drivers+=("$driver") - continue - fi +for platform in "${platforms[@]}"; do + goos="${platform%%/*}" + goarch="${platform##*/}" + platform_key="${goos}-${goarch}" + platform_dir="$(platform_dir_name "$goos")" + output_dir="${out_root%/}/${platform_key}" + bundle_platform_dir="$bundle_stage_dir/$platform_dir" - build_driver="$(build_driver_name "$driver")" - tag="gonavi_${build_driver}_driver" - asset_name="${driver}-driver-agent-${goos}-${goarch}" - if [[ "$goos" == "windows" ]]; then - asset_name="${asset_name}.exe" - fi - output_path="$output_dir_abs/$asset_name" + mkdir -p "$output_dir" "$bundle_platform_dir" + output_dir_abs="$(cd "$output_dir" && pwd)" - cgo_enabled=0 - if [[ "$driver" == "duckdb" ]]; then - cgo_enabled=1 - fi + echo "" + echo "🧭 生成 driver-agent revision 指纹:$platform" + "$SCRIPT_DIR/tools/generate-driver-agent-revisions.sh" --platform "$platform" - echo "🔧 构建 $driver -> $asset_name (tag=$tag, CGO_ENABLED=$cgo_enabled)" - set +e - CGO_ENABLED="$cgo_enabled" GOOS="$goos" GOARCH="$goarch" GOTOOLCHAIN=auto \ - go build -tags "$tag" -trimpath -ldflags "-s -w" -o "$output_path" ./cmd/optional-driver-agent - build_exit=$? - set -e - - if [[ $build_exit -ne 0 ]]; then - echo "❌ 构建失败:$driver" - failed_drivers+=("$driver") - if [[ "$strict_mode" == "true" ]]; then - exit $build_exit + for driver in "${drivers[@]}"; do + if [[ "$driver" == "duckdb" && "$goos" == "windows" && "$goarch" != "amd64" ]]; then + echo "⚠️ 跳过 duckdb($platform 仅支持 windows/amd64)" + skipped_drivers+=("duckdb($platform)") + continue fi - continue - fi - cp "$output_path" "$bundle_platform_dir/$asset_name" - built_assets+=("$asset_name") + build_driver="$(build_driver_name "$driver")" + tag="gonavi_${build_driver}_driver" + asset_name="${driver}-driver-agent-${goos}-${goarch}" + if [[ "$goos" == "windows" ]]; then + asset_name="${asset_name}.exe" + fi + output_path="$output_dir_abs/$asset_name" + + cgo_enabled=0 + if [[ "$driver" == "duckdb" ]]; then + cgo_enabled=1 + fi + + echo "🔧 构建 $driver -> $asset_name (platform=$platform, tag=$tag, CGO_ENABLED=$cgo_enabled)" + set +e + CGO_ENABLED="$cgo_enabled" GOOS="$goos" GOARCH="$goarch" GOTOOLCHAIN=auto \ + go build -tags "$tag" -trimpath -ldflags "-s -w" -o "$output_path" ./cmd/optional-driver-agent + build_exit=$? + set -e + + if [[ $build_exit -ne 0 ]]; then + echo "❌ 构建失败:$driver ($platform)" + failed_drivers+=("$driver($platform)") + if [[ "$strict_mode" == "true" ]]; then + exit $build_exit + fi + continue + fi + + cp "$output_path" "$bundle_platform_dir/$asset_name" + built_assets+=("$platform_dir/$asset_name") + done done if [[ ${#built_assets[@]} -eq 0 ]]; then @@ -200,25 +313,11 @@ if [[ ${#built_assets[@]} -eq 0 ]]; then exit 1 fi -rm -f "$bundle_zip_path" -if command -v zip >/dev/null 2>&1; then - ( - cd "$bundle_stage_dir" - zip -qry "$bundle_zip_path" "$platform_dir" - ) -elif command -v ditto >/dev/null 2>&1; then - ( - cd "$bundle_stage_dir" - ditto -c -k --sequesterRsrc --keepParent "$platform_dir" "$bundle_zip_path" - ) -else - echo "❌ 未找到 zip/ditto,无法生成驱动总包 zip。" - exit 1 -fi +zip_bundle "$bundle_zip_path" "$bundle_stage_dir" echo "" echo "✅ 构建完成" -echo " 单文件输出目录:$output_dir_abs" +echo " 单文件输出根目录:$out_root_abs" echo " 驱动总包:$bundle_zip_path" echo " 已构建:${built_assets[*]}" if [[ ${#skipped_drivers[@]} -gt 0 ]]; then diff --git a/build-release.sh b/build-release.sh index a022e7e..13cd9bb 100755 --- a/build-release.sh +++ b/build-release.sh @@ -46,6 +46,13 @@ RED='\033[0;31m' YELLOW='\033[1;33m' NC='\033[0m' +BUILD_FAILURES=() + +record_build_failure() { + local target="$1" + BUILD_FAILURES+=("$target") +} + get_file_size_bytes() { local target="$1" if [ ! -f "$target" ]; then @@ -159,6 +166,7 @@ package_macos_release() { wails build -platform "darwin/${platform}" -clean -ldflags "$LDFLAGS" if [ $? -ne 0 ]; then echo -e "${RED} ❌ macOS ${platform} 构建失败。${NC}" + record_build_failure "macOS ${platform}" return fi @@ -213,6 +221,7 @@ if command -v x86_64-w64-mingw32-gcc &> /dev/null; then echo " ✅ 已生成 ${APP_NAME}-${VERSION}-windows-amd64.exe" else echo -e "${RED} ❌ Windows amd64 构建失败。${NC}" + record_build_failure "Windows amd64" fi else echo -e "${YELLOW} ⚠️ 未找到 MinGW 工具 (x86_64-w64-mingw32-gcc),跳过 Windows amd64 构建。${NC}" @@ -230,6 +239,7 @@ if command -v aarch64-w64-mingw32-gcc &> /dev/null; then echo " ✅ 已生成 ${APP_NAME}-${VERSION}-windows-arm64.exe" else echo -e "${RED} ❌ Windows arm64 构建失败。${NC}" + record_build_failure "Windows arm64" fi else echo -e "${YELLOW} ⚠️ 未找到 MinGW ARM64 工具 (aarch64-w64-mingw32-gcc),跳过 Windows arm64 构建。${NC}" @@ -259,6 +269,7 @@ if [ "$CURRENT_OS" = "Linux" ] && [ "$CURRENT_ARCH" = "x86_64" ]; then echo " ✅ 已生成 ${APP_NAME}-${VERSION}-linux-amd64.tar.gz" else echo -e "${RED} ❌ Linux amd64 构建失败。${NC}" + record_build_failure "Linux amd64" fi elif command -v x86_64-linux-gnu-gcc &> /dev/null; then # macOS 或其他系统,尝试交叉编译 @@ -279,6 +290,7 @@ elif command -v x86_64-linux-gnu-gcc &> /dev/null; then echo " ✅ 已生成 ${APP_NAME}-${VERSION}-linux-amd64.tar.gz" else echo -e "${RED} ❌ Linux amd64 交叉编译失败。${NC}" + record_build_failure "Linux amd64" fi unset CC CXX CGO_ENABLED else @@ -304,6 +316,7 @@ if [ "$CURRENT_OS" = "Linux" ] && [ "$CURRENT_ARCH" = "aarch64" ]; then echo " ✅ 已生成 ${APP_NAME}-${VERSION}-linux-arm64.tar.gz" else echo -e "${RED} ❌ Linux arm64 构建失败。${NC}" + record_build_failure "Linux arm64" fi elif command -v aarch64-linux-gnu-gcc &> /dev/null; then # 交叉编译 @@ -324,6 +337,7 @@ elif command -v aarch64-linux-gnu-gcc &> /dev/null; then echo " ✅ 已生成 ${APP_NAME}-${VERSION}-linux-arm64.tar.gz" else echo -e "${RED} ❌ Linux arm64 交叉编译失败。${NC}" + record_build_failure "Linux arm64" fi unset CC CXX CGO_ENABLED else @@ -357,12 +371,21 @@ else fi echo "" -echo -e "${GREEN}🎉 所有任务完成!构建产物在 'dist/' 目录下:${NC}" +if [ "${#BUILD_FAILURES[@]}" -gt 0 ]; then + echo -e "${RED}❌ 构建未完全成功,失败平台:${BUILD_FAILURES[*]}${NC}" + echo -e "${YELLOW}📦 已成功生成的产物在 'dist/' 目录下:${NC}" +else + echo -e "${GREEN}🎉 所有任务完成!构建产物在 'dist/' 目录下:${NC}" +fi ls -lh "$DIST_DIR" echo "" echo -e "${GREEN}📋 支持的平台:${NC}" -echo " • macOS (Intel/Apple Silicon): .dmg" +echo " • macOS (Intel/Apple Silicon): .zip" echo " • Windows (x64/ARM64): .exe" echo " • Linux (x64/ARM64): .tar.gz" echo "" echo -e "${YELLOW}💡 提示:Linux AppImage 包请使用 GitHub Actions CI/CD 构建。${NC}" + +if [ "${#BUILD_FAILURES[@]}" -gt 0 ]; then + exit 1 +fi diff --git a/cmd/optional-driver-agent/provider_oceanbase.go b/cmd/optional-driver-agent/provider_oceanbase.go new file mode 100644 index 0000000..11f9b23 --- /dev/null +++ b/cmd/optional-driver-agent/provider_oceanbase.go @@ -0,0 +1,12 @@ +//go:build gonavi_oceanbase_driver + +package main + +import "GoNavi-Wails/internal/db" + +func init() { + agentDriverType = "oceanbase" + agentDatabaseFactory = func() db.Database { + return &db.OceanBaseDB{} + } +} diff --git a/cmd/optional-driver-agent/provider_opengauss.go b/cmd/optional-driver-agent/provider_opengauss.go new file mode 100644 index 0000000..ebeec52 --- /dev/null +++ b/cmd/optional-driver-agent/provider_opengauss.go @@ -0,0 +1,12 @@ +//go:build gonavi_opengauss_driver + +package main + +import "GoNavi-Wails/internal/db" + +func init() { + agentDriverType = "opengauss" + agentDatabaseFactory = func() db.Database { + return &db.OpenGaussDB{} + } +} diff --git a/docs/driver-manifest.json b/docs/driver-manifest.json index d04fba3..7dd4802 100644 --- a/docs/driver-manifest.json +++ b/docs/driver-manifest.json @@ -7,6 +7,12 @@ "checksumPolicy": "off", "downloadUrl": "builtin://activate/mariadb" }, + "oceanbase": { + "engine": "go", + "version": "1.9.3", + "checksumPolicy": "off", + "downloadUrl": "builtin://activate/oceanbase" + }, "doris": { "engine": "go", "version": "1.9.3", @@ -61,6 +67,12 @@ "checksumPolicy": "off", "downloadUrl": "builtin://activate/vastbase" }, + "opengauss": { + "engine": "go", + "version": "1.11.1", + "checksumPolicy": "off", + "downloadUrl": "builtin://activate/opengauss" + }, "mongodb": { "engine": "go", "version": "2.5.0", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 7396e24..bed8925 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -d0464f9da25e9356e61652e638c99ffe \ No newline at end of file +0295a42fd931778d85157816d79d29e5 \ No newline at end of file diff --git a/frontend/src/components/ConnectionModal.tsx b/frontend/src/components/ConnectionModal.tsx index 77341ce..f813319 100644 --- a/frontend/src/components/ConnectionModal.tsx +++ b/frontend/src/components/ConnectionModal.tsx @@ -154,6 +154,8 @@ const getDefaultPortByType = (type: string) => { return 9010; case "mysql": return 3306; + case "oceanbase": + return 2881; case "doris": case "diros": return 9030; @@ -162,6 +164,7 @@ const getDefaultPortByType = (type: string) => { case "clickhouse": return 9000; case "postgres": + case "opengauss": return 5432; case "redis": return 6379; @@ -194,6 +197,7 @@ const getDefaultPortByType = (type: string) => { const singleHostUriSchemesByType: Record = { postgres: ["postgresql", "postgres"], + opengauss: ["opengauss", "jdbc:opengauss", "postgresql", "postgres"], clickhouse: ["clickhouse"], oracle: ["oracle"], sqlserver: ["sqlserver"], @@ -208,6 +212,7 @@ const singleHostUriSchemesByType: Record = { const sslSupportedTypes = new Set([ "mysql", "mariadb", + "oceanbase", "doris", "diros", "sphinx", @@ -219,6 +224,7 @@ const sslSupportedTypes = new Set([ "kingbase", "highgo", "vastbase", + "opengauss", "mongodb", "redis", "tdengine", @@ -237,6 +243,7 @@ const isFileDatabaseType = (type: string) => const isMySQLCompatibleType = (type: string) => type === "mysql" || type === "mariadb" || + type === "oceanbase" || type === "doris" || type === "diros" || type === "sphinx"; @@ -247,6 +254,7 @@ const supportsConnectionParamsForType = (type: string) => type === "kingbase" || type === "highgo" || type === "vastbase" || + type === "opengauss" || type === "oracle" || type === "sqlserver" || type === "clickhouse" || @@ -271,6 +279,12 @@ const normalizeDriverType = (value: string): string => { .toLowerCase(); if (normalized === "postgresql") return "postgres"; if (normalized === "doris") return "diros"; + if ( + normalized === "open_gauss" || + normalized === "open-gauss" || + normalized === "opengauss" + ) + return "opengauss"; return normalized; }; @@ -1276,6 +1290,8 @@ const ConnectionModal: React.FC<{ const parsed = parseMultiHostUri(trimmedUri, "mysql") || parseMultiHostUri(trimmedUri, "jdbc:mysql") || + parseMultiHostUri(trimmedUri, "oceanbase") || + parseMultiHostUri(trimmedUri, "jdbc:oceanbase") || parseMultiHostUri(trimmedUri, "diros") || parseMultiHostUri(trimmedUri, "doris"); if (!parsed) { @@ -1524,7 +1540,8 @@ const ConnectionModal: React.FC<{ type === "postgres" || type === "kingbase" || type === "highgo" || - type === "vastbase" + type === "vastbase" || + type === "opengauss" ) { const sslMode = String(parsed.params.get("sslmode") || "") .trim() @@ -1681,7 +1698,8 @@ const ConnectionModal: React.FC<{ const getUriPlaceholder = () => { if (isMySQLCompatibleType(dbType)) { const defaultPort = getDefaultPortByType(dbType); - const scheme = dbType === "diros" ? "doris" : "mysql"; + const scheme = + dbType === "diros" ? "doris" : dbType === "oceanbase" ? "oceanbase" : "mysql"; return `${scheme}://user:pass@127.0.0.1:${defaultPort},127.0.0.2:${defaultPort}/db_name?topology=replica`; } if (isFileDatabaseType(dbType)) { @@ -1701,6 +1719,9 @@ const ConnectionModal: React.FC<{ if (dbType === "oracle") { return "oracle://user:pass@127.0.0.1:1521/ORCLPDB1"; } + if (dbType === "opengauss") { + return "opengauss://user:pass@127.0.0.1:5432/db_name"; + } return "例如: postgres://user:pass@127.0.0.1:5432/db_name"; }; @@ -1713,6 +1734,7 @@ const ConnectionModal: React.FC<{ case "kingbase": case "highgo": case "vastbase": + case "opengauss": return "application_name=GoNavi&statement_timeout=30000"; case "oracle": return "PREFETCH_ROWS=5000&TRACE FILE=/tmp/go-ora.trc"; @@ -1775,7 +1797,8 @@ const ConnectionModal: React.FC<{ mergeConnectionParams(params, values.connectionParams); const dbPath = database ? `/${encodeURIComponent(database)}` : "/"; const query = params.toString(); - const scheme = type === "diros" ? "doris" : "mysql"; + const scheme = + type === "diros" ? "doris" : type === "oceanbase" ? "oceanbase" : "mysql"; return `${scheme}://${encodedAuth}${hosts.join(",")}${dbPath}${query ? `?${query}` : ""}`; } @@ -1903,7 +1926,8 @@ const ConnectionModal: React.FC<{ type === "postgres" || type === "kingbase" || type === "highgo" || - type === "vastbase" + type === "vastbase" || + type === "opengauss" ) { params.set("sslmode", "require"); } else if (type === "sqlserver") { @@ -1942,7 +1966,8 @@ const ConnectionModal: React.FC<{ type === "postgres" || type === "kingbase" || type === "highgo" || - type === "vastbase" + type === "vastbase" || + type === "opengauss" ) { params.set("sslmode", "disable"); } else if (type === "sqlserver") { @@ -2131,6 +2156,7 @@ const ConnectionModal: React.FC<{ const mysqlReplicaHosts = configType === "mysql" || configType === "mariadb" || + configType === "oceanbase" || configType === "diros" || configType === "sphinx" ? normalizedHosts.slice(1) @@ -3528,6 +3554,11 @@ const ConnectionModal: React.FC<{ { label: "国产数据库", items: [ + { + key: "oceanbase", + name: "OceanBase", + icon: getDbIcon("oceanbase", undefined, 36), + }, { key: "dameng", name: "Dameng (达梦)", @@ -3548,6 +3579,11 @@ const ConnectionModal: React.FC<{ name: "Vastbase (海量)", icon: getDbIcon("vastbase", undefined, 36), }, + { + key: "opengauss", + name: "OpenGauss", + icon: getDbIcon("opengauss", undefined, 36), + }, ], }, { @@ -4598,7 +4634,8 @@ const ConnectionModal: React.FC<{ {(dbType === "postgres" || dbType === "kingbase" || dbType === "highgo" || - dbType === "vastbase") && + dbType === "vastbase" || + dbType === "opengauss") && renderConfigSectionCard({ sectionKey: "service", icon: , diff --git a/frontend/src/components/DataSyncModal.tsx b/frontend/src/components/DataSyncModal.tsx index 7726f92..c3a650b 100644 --- a/frontend/src/components/DataSyncModal.tsx +++ b/frontend/src/components/DataSyncModal.tsx @@ -50,7 +50,7 @@ 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') { + if (t === 'mysql' || t === 'mariadb' || t === 'oceanbase' || t === 'diros' || t === 'sphinx' || t === 'clickhouse' || t === 'tdengine') { return `\`${raw.replace(/`/g, '``')}\``; } if (t === 'sqlserver') { diff --git a/frontend/src/components/DataViewer.tsx b/frontend/src/components/DataViewer.tsx index b417309..5fb7fde 100644 --- a/frontend/src/components/DataViewer.tsx +++ b/frontend/src/components/DataViewer.tsx @@ -457,7 +457,7 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAct const dbType = resolveDataSourceType(config); const dbTypeLower = String(dbType || '').trim().toLowerCase(); - const isMySQLFamily = dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'diros'; + const isMySQLFamily = dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'oceanbase' || dbTypeLower === 'diros'; const normalizedQuickWhereCondition = normalizeQuickWhereCondition(quickWhereCondition); const quickWhereValidation = validateQuickWhereCondition(normalizedQuickWhereCondition); if (!quickWhereValidation.ok) { diff --git a/frontend/src/components/DatabaseIcons.tsx b/frontend/src/components/DatabaseIcons.tsx index 1dbf45f..6ba4329 100644 --- a/frontend/src/components/DatabaseIcons.tsx +++ b/frontend/src/components/DatabaseIcons.tsx @@ -12,6 +12,7 @@ export interface DbIconProps { const DB_DEFAULT_COLORS: Record = { mysql: '#00758F', mariadb: '#003545', + oceanbase: '#0052CC', postgres: '#336791', redis: '#DC382D', mongodb: '#47A248', @@ -24,6 +25,7 @@ const DB_DEFAULT_COLORS: Record = { sqlite: '#003B57', duckdb: '#FFC107', vastbase: '#0066CC', + opengauss: '#2446A8', highgo: '#00A86B', tdengine: '#2962FF', diros: '#0050B3', @@ -90,6 +92,9 @@ const MySQLIcon: React.FC = ({ size = 16, color }) => ( const MariaDBIcon: React.FC = ({ size = 16, color }) => ( ); +const OceanBaseIcon: React.FC = ({ size = 16, color }) => ( + +); const PostgresIcon: React.FC = ({ size = 16, color }) => ( ); @@ -131,6 +136,9 @@ const DamengIcon: React.FC = ({ size = 16, color }) => ( const VastBaseIcon: React.FC = ({ size = 16, color }) => ( ); +const OpenGaussIcon: React.FC = ({ size = 16, color }) => ( + +); const HighGoIcon: React.FC = ({ size = 16, color }) => ( ); @@ -165,6 +173,7 @@ const SphinxIconFallback: React.FC = ({ size = 16, color }) => ( const DB_ICON_MAP: Record> = { mysql: MySQLIcon, mariadb: MariaDBIcon, + oceanbase: OceanBaseIcon, diros: DorisIcon, sphinx: SphinxIcon, postgres: PostgresIcon, @@ -179,6 +188,7 @@ const DB_ICON_MAP: Record> = { sqlite: SQLiteIcon, duckdb: DuckDBIcon, vastbase: VastBaseIcon, + opengauss: OpenGaussIcon, highgo: HighGoIcon, tdengine: TDengineIcon, custom: CustomIcon, @@ -186,9 +196,9 @@ const DB_ICON_MAP: Record> = { /** 可选图标类型列表(用于图标选择器 UI) */ export const DB_ICON_TYPES: string[] = [ - 'mysql', 'mariadb', 'postgres', 'redis', 'mongodb', 'jvm', + 'mysql', 'mariadb', 'oceanbase', 'postgres', 'redis', 'mongodb', 'jvm', 'oracle', 'sqlserver', 'sqlite', 'duckdb', 'clickhouse', - 'kingbase', 'dameng', 'vastbase', 'highgo', 'tdengine', 'custom', + 'kingbase', 'dameng', 'vastbase', 'opengauss', 'highgo', 'tdengine', 'custom', ]; /** 该类型是否有品牌 SVG 文件 */ @@ -204,12 +214,12 @@ export const getDbIcon = (type: string, color?: string, size?: number): React.Re /** 获取数据库图标显示名称(中文) */ export const getDbIconLabel = (type: string): string => { const labels: Record = { - mysql: 'MySQL', mariadb: 'MariaDB', postgres: 'PostgreSQL', + mysql: 'MySQL', mariadb: 'MariaDB', oceanbase: 'OceanBase', postgres: 'PostgreSQL', redis: 'Redis', mongodb: 'MongoDB', jvm: 'JVM', oracle: 'Oracle', sqlserver: 'SQL Server', clickhouse: 'ClickHouse', sqlite: 'SQLite', duckdb: 'DuckDB', kingbase: '金仓', dameng: '达梦', - vastbase: 'VastBase', highgo: '瀚高', tdengine: 'TDengine', + vastbase: 'VastBase', opengauss: 'OpenGauss', highgo: '瀚高', tdengine: 'TDengine', custom: '自定义', }; return labels[type?.toLowerCase()] || type; diff --git a/frontend/src/components/DefinitionViewer.tsx b/frontend/src/components/DefinitionViewer.tsx index 19e482d..ac83d20 100644 --- a/frontend/src/components/DefinitionViewer.tsx +++ b/frontend/src/components/DefinitionViewer.tsx @@ -43,9 +43,11 @@ const DefinitionViewer: React.FC = ({ tab }) => { if (type === 'custom') { const driver = String(conn?.config?.driver || '').trim().toLowerCase(); if (driver === 'diros' || driver === 'doris') return 'mysql'; + if (driver === 'oceanbase') return 'mysql'; + if (driver === 'opengauss' || driver === 'open_gauss' || driver === 'open-gauss') return 'opengauss'; return driver; } - if (type === 'mariadb' || type === 'diros' || type === 'sphinx') return 'mysql'; + if (type === 'mariadb' || type === 'oceanbase' || type === 'diros' || type === 'sphinx') return 'mysql'; if (type === 'dameng') return 'dm'; return type; }; @@ -133,7 +135,8 @@ const DefinitionViewer: React.FC = ({ tab }) => { case 'postgres': case 'kingbase': case 'highgo': - case 'vastbase': { + case 'vastbase': + case 'opengauss': { const schemaRef = schema || 'public'; return [`SELECT pg_get_viewdef('${escapeSQLLiteral(schemaRef)}.${safeName}'::regclass, true) AS view_definition`]; } @@ -179,7 +182,8 @@ const DefinitionViewer: React.FC = ({ tab }) => { case 'postgres': case 'kingbase': case 'highgo': - case 'vastbase': { + case 'vastbase': + case 'opengauss': { const schemaRef = schema || 'public'; return [`SELECT pg_get_functiondef(p.oid) AS routine_definition FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = '${escapeSQLLiteral(schemaRef)}' AND p.proname = '${safeName}' LIMIT 1`]; } diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index df52733..ab95acd 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -494,12 +494,14 @@ describe('QueryEditor external SQL save', () => { it.each([ 'mysql', 'mariadb', + 'oceanbase', 'diros', 'sphinx', 'postgres', 'kingbase', 'highgo', 'vastbase', + 'opengauss', 'sqlserver', 'sqlite', 'duckdb', diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 6e3eac4..47c8965 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -97,6 +97,11 @@ const normalizeDriverType = (value: string): string => { const normalized = String(value || '').trim().toLowerCase(); if (normalized === 'postgresql') return 'postgres'; if (normalized === 'doris') return 'diros'; + if ( + normalized === 'open_gauss' || + normalized === 'open-gauss' || + normalized === 'opengauss' + ) return 'opengauss'; return normalized; }; @@ -525,6 +530,9 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> 'kingbase', 'highgo', 'vastbase', + 'opengauss', + 'open_gauss', + 'open-gauss', 'sqlserver', 'oracle', 'dameng', @@ -535,6 +543,9 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> 'kingbase', 'highgo', 'vastbase', + 'opengauss', + 'open_gauss', + 'open-gauss', 'sqlserver', 'oracle', 'dm', @@ -563,9 +574,11 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> if (type === 'custom') { const driver = String(conn?.config?.driver || '').trim().toLowerCase(); if (driver === 'diros' || driver === 'doris') return 'mysql'; + if (driver === 'oceanbase') return 'mysql'; + if (driver === 'opengauss' || driver === 'open_gauss' || driver === 'open-gauss') return 'opengauss'; return driver; } - if (type === 'mariadb' || type === 'diros' || type === 'sphinx') return 'mysql'; + if (type === 'mariadb' || type === 'oceanbase' || type === 'diros' || type === 'sphinx') return 'mysql'; if (type === 'dameng') return 'dm'; return type; }; @@ -730,6 +743,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> case 'kingbase': case 'highgo': case 'vastbase': + case 'opengauss': return [{ sql: `SELECT schemaname AS schema_name, viewname AS view_name FROM pg_catalog.pg_views WHERE schemaname != 'information_schema' AND schemaname NOT LIKE 'pg_%' ORDER BY schemaname, viewname` }]; case 'sqlserver': { const safeDb = quoteSqlServerIdentifier(dbName || 'master'); @@ -774,6 +788,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> case 'kingbase': case 'highgo': case 'vastbase': + case 'opengauss': return [{ sql: `SELECT DISTINCT event_object_schema AS schema_name, event_object_table AS table_name, trigger_name FROM information_schema.triggers WHERE trigger_schema NOT IN ('pg_catalog', 'information_schema') AND trigger_schema NOT LIKE 'pg_%' ORDER BY event_object_schema, event_object_table, trigger_name` }]; case 'sqlserver': { const safeDb = quoteSqlServerIdentifier(dbName || 'master'); @@ -821,6 +836,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> case 'kingbase': case 'highgo': case 'vastbase': + case 'opengauss': return normalizeMetadataQuerySpecs([ { // PostgreSQL 11+ / 部分 PG-like:通过 prokind 区分 FUNCTION/PROCEDURE @@ -2921,7 +2937,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> case 'mysql': query = `SHOW CREATE VIEW \`${viewName.replace(/`/g, '``')}\``; break; - case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': { + case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': { const parts = viewName.split('.'); const schema = parts.length > 1 ? parts[0] : 'public'; const name = parts.length > 1 ? parts[1] : viewName; @@ -2977,7 +2993,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> case 'mysql': template = `CREATE VIEW \`view_name\` AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;`; break; - case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': + case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': template = `CREATE OR REPLACE VIEW view_name AS\nSELECT column1, column2\nFROM table_name\nWHERE condition;`; break; case 'sqlserver': @@ -3088,7 +3104,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> case 'mysql': query = `SHOW CREATE ${routineType} \`${name.replace(/`/g, '``')}\``; break; - case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': { + case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': { const schemaRef = schema || 'public'; query = `SELECT pg_get_functiondef(p.oid) AS routine_definition FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = '${escapeSQLLiteral(schemaRef)}' AND p.proname = '${escapeSQLLiteral(name)}' LIMIT 1`; break; @@ -3158,7 +3174,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> ? `DELIMITER $$\nCREATE PROCEDURE proc_name(IN param1 INT)\nBEGIN\n SELECT * FROM table_name WHERE id = param1;\nEND$$\nDELIMITER ;` : `DELIMITER $$\nCREATE FUNCTION func_name(param1 INT)\nRETURNS INT\nDETERMINISTIC\nBEGIN\n RETURN param1 * 2;\nEND$$\nDELIMITER ;`; break; - case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': + case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': case 'opengauss': template = isProc ? `CREATE OR REPLACE PROCEDURE proc_name(param1 integer)\nLANGUAGE plpgsql\nAS $$\nBEGIN\n -- procedure body\nEND;\n$$;` : `CREATE OR REPLACE FUNCTION func_name(param1 integer)\nRETURNS integer\nLANGUAGE plpgsql\nAS $$\nBEGIN\n RETURN param1 * 2;\nEND;\n$$;`; diff --git a/frontend/src/components/TableDesigner.tsx b/frontend/src/components/TableDesigner.tsx index 5550f1d..8c6f0d3 100644 --- a/frontend/src/components/TableDesigner.tsx +++ b/frontend/src/components/TableDesigner.tsx @@ -836,6 +836,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => { if (normalized === 'postgresql' || normalized === 'pg') return 'postgres'; if (normalized === 'mssql' || normalized === 'sql_server' || normalized === 'sql-server') return 'sqlserver'; if (normalized === 'doris') return 'diros'; + if (normalized === 'open_gauss' || normalized === 'open-gauss') return 'opengauss'; return normalized; }; @@ -871,6 +872,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => { switch (dbType) { case 'mysql': case 'mariadb': + case 'oceanbase': case 'diros': return `CREATE TRIGGER trigger_name BEFORE INSERT ON \`${tblName}\` @@ -882,6 +884,7 @@ END;`; case 'kingbase': case 'highgo': case 'vastbase': + case 'opengauss': return `CREATE OR REPLACE FUNCTION trigger_function_name() RETURNS TRIGGER AS $$ BEGIN @@ -931,12 +934,14 @@ END;`; switch (dbType) { case 'mysql': case 'mariadb': + case 'oceanbase': case 'diros': return `DROP TRIGGER IF EXISTS \`${triggerName}\``; case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': + case 'opengauss': return `DROP TRIGGER IF EXISTS "${triggerName}" ON "${tblName}"`; case 'sqlserver': return `DROP TRIGGER IF EXISTS [${triggerName}]`; diff --git a/frontend/src/components/TableOverview.tsx b/frontend/src/components/TableOverview.tsx index 63c7cf1..10e52bc 100644 --- a/frontend/src/components/TableOverview.tsx +++ b/frontend/src/components/TableOverview.tsx @@ -57,9 +57,11 @@ const getMetadataDialect = (connType: string, driver?: string): string => { if (type === 'custom') { const d = (driver || '').trim().toLowerCase(); if (d === 'diros' || d === 'doris') return 'mysql'; + if (d === 'oceanbase') return 'mysql'; + if (d === 'opengauss' || d === 'open_gauss' || d === 'open-gauss') return 'opengauss'; return d; } - if (type === 'mariadb' || type === 'diros' || type === 'sphinx') return 'mysql'; + if (type === 'mariadb' || type === 'oceanbase' || type === 'diros' || type === 'sphinx') return 'mysql'; if (type === 'dameng') return 'dm'; return type; }; @@ -85,7 +87,8 @@ ORDER BY table_name`; case 'postgres': case 'kingbase': case 'vastbase': - case 'highgo': { + case 'highgo': + case 'opengauss': { const schema = schemaName || 'public'; return ` SELECT diff --git a/frontend/src/components/TriggerViewer.tsx b/frontend/src/components/TriggerViewer.tsx index b380f62..85c9baf 100644 --- a/frontend/src/components/TriggerViewer.tsx +++ b/frontend/src/components/TriggerViewer.tsx @@ -29,9 +29,11 @@ const TriggerViewer: React.FC = ({ tab }) => { if (type === 'custom') { const driver = String(conn?.config?.driver || '').trim().toLowerCase(); if (driver === 'diros' || driver === 'doris') return 'mysql'; + if (driver === 'oceanbase') return 'mysql'; + if (driver === 'opengauss' || driver === 'open_gauss' || driver === 'open-gauss') return 'opengauss'; return driver; } - if (type === 'mariadb' || type === 'diros' || type === 'sphinx') return 'mysql'; + if (type === 'mariadb' || type === 'oceanbase' || type === 'diros' || type === 'sphinx') return 'mysql'; if (type === 'dameng') return 'dm'; return type; }; @@ -62,6 +64,7 @@ const TriggerViewer: React.FC = ({ tab }) => { case 'kingbase': case 'highgo': case 'vastbase': + case 'opengauss': return [`SELECT pg_get_triggerdef(t.oid, true) AS trigger_definition FROM pg_trigger t JOIN pg_class c ON t.tgrelid = c.oid @@ -179,7 +182,8 @@ LIMIT 1`]; case 'postgres': case 'kingbase': case 'highgo': - case 'vastbase': { + case 'vastbase': + case 'opengauss': { return row.trigger_definition || row.TRIGGER_DEFINITION || Object.values(row)[0] || ''; } case 'sqlserver': { diff --git a/frontend/src/components/tableDataDangerActions.test.ts b/frontend/src/components/tableDataDangerActions.test.ts index ea4fdf2..65f5579 100644 --- a/frontend/src/components/tableDataDangerActions.test.ts +++ b/frontend/src/components/tableDataDangerActions.test.ts @@ -5,7 +5,9 @@ import { supportsTableTruncateAction } from './tableDataDangerActions'; describe('tableDataDangerActions', () => { it('supports native truncate for known relational dialects', () => { expect(supportsTableTruncateAction('mysql')).toBe(true); + expect(supportsTableTruncateAction('oceanbase')).toBe(true); expect(supportsTableTruncateAction('postgres')).toBe(true); + expect(supportsTableTruncateAction('opengauss')).toBe(true); expect(supportsTableTruncateAction('custom', 'postgresql')).toBe(true); expect(supportsTableTruncateAction('custom', 'kingbase8')).toBe(true); }); diff --git a/frontend/src/components/tableDataDangerActions.ts b/frontend/src/components/tableDataDangerActions.ts index 2868f26..72cebed 100644 --- a/frontend/src/components/tableDataDangerActions.ts +++ b/frontend/src/components/tableDataDangerActions.ts @@ -9,6 +9,10 @@ const resolveCustomDriverDialect = (driver: string): string => { case 'pq': case 'pgx': return 'postgres'; + case 'opengauss': + case 'open_gauss': + case 'open-gauss': + return 'opengauss'; case 'dm': case 'dameng': case 'dm8': @@ -21,6 +25,8 @@ const resolveCustomDriverDialect = (driver: string): string => { case 'diros': case 'doris': return 'diros'; + case 'oceanbase': + return 'oceanbase'; case 'kingbase': case 'kingbase8': case 'kingbasees': @@ -34,7 +40,9 @@ const resolveCustomDriverDialect = (driver: string): string => { break; } + if (normalized.includes('opengauss') || normalized.includes('open_gauss') || normalized.includes('open-gauss')) return 'opengauss'; if (normalized.includes('postgres')) return 'postgres'; + if (normalized.includes('oceanbase')) return 'oceanbase'; if (normalized.includes('kingbase')) return 'kingbase'; if (normalized.includes('highgo')) return 'highgo'; if (normalized.includes('vastbase')) return 'vastbase'; @@ -56,10 +64,12 @@ export const supportsTableTruncateAction = (type: string, driver?: string): bool switch (resolveTableDataActionDBType(type, driver)) { case 'mysql': case 'mariadb': + case 'oceanbase': case 'postgres': case 'kingbase': case 'highgo': case 'vastbase': + case 'opengauss': case 'sqlserver': case 'oracle': case 'dameng': diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 33b9aec..857c111 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -76,6 +76,7 @@ const DEFAULT_GLOBAL_PROXY: GlobalProxyConfig = { const SUPPORTED_CONNECTION_TYPES = new Set([ "mysql", "mariadb", + "oceanbase", "doris", "diros", "sphinx", @@ -90,6 +91,7 @@ const SUPPORTED_CONNECTION_TYPES = new Set([ "mongodb", "highgo", "vastbase", + "opengauss", "jvm", "sqlite", "duckdb", @@ -98,6 +100,7 @@ const SUPPORTED_CONNECTION_TYPES = new Set([ const SSL_SUPPORTED_CONNECTION_TYPES = new Set([ "mysql", "mariadb", + "oceanbase", "diros", "sphinx", "dameng", @@ -108,6 +111,7 @@ const SSL_SUPPORTED_CONNECTION_TYPES = new Set([ "kingbase", "highgo", "vastbase", + "opengauss", "mongodb", "redis", "tdengine", @@ -120,6 +124,8 @@ const getDefaultPortByType = (type: string): number => { case "mysql": case "mariadb": return 3306; + case "oceanbase": + return 2881; case "doris": case "diros": return 9030; @@ -131,6 +137,7 @@ const getDefaultPortByType = (type: string): number => { return 9000; case "postgres": case "vastbase": + case "opengauss": return 5432; case "redis": return 6379; @@ -270,6 +277,13 @@ const normalizeConnectionType = (value: unknown): string => { if (type === "doris") { return "diros"; } + if ( + type === "open_gauss" || + type === "open-gauss" || + type === "opengauss" + ) { + return "opengauss"; + } return SUPPORTED_CONNECTION_TYPES.has(type) ? type : DEFAULT_CONNECTION_TYPE; }; diff --git a/frontend/src/utils/connectionModalPresentation.test.ts b/frontend/src/utils/connectionModalPresentation.test.ts index 7ec77c0..a7e8867 100644 --- a/frontend/src/utils/connectionModalPresentation.test.ts +++ b/frontend/src/utils/connectionModalPresentation.test.ts @@ -68,6 +68,7 @@ describe('connectionModalPresentation', () => { const allTypes = [ 'mysql', 'mariadb', + 'oceanbase', 'doris', 'diros', 'sphinx', @@ -81,6 +82,7 @@ describe('connectionModalPresentation', () => { 'kingbase', 'highgo', 'vastbase', + 'opengauss', 'mongodb', 'redis', 'tdengine', diff --git a/frontend/src/utils/connectionModalPresentation.ts b/frontend/src/utils/connectionModalPresentation.ts index f4eb3b7..a8abb18 100644 --- a/frontend/src/utils/connectionModalPresentation.ts +++ b/frontend/src/utils/connectionModalPresentation.ts @@ -55,6 +55,7 @@ type ConnectionConfigSectionCopy = { const mysqlCompatibleTypes = new Set([ 'mysql', 'mariadb', + 'oceanbase', 'doris', 'diros', 'sphinx', @@ -64,6 +65,7 @@ const postgresCompatibleTypes = new Set([ 'kingbase', 'highgo', 'vastbase', + 'opengauss', ]); const fileDatabaseTypes = new Set(['sqlite', 'duckdb']); diff --git a/frontend/src/utils/dataSourceCapabilities.ts b/frontend/src/utils/dataSourceCapabilities.ts index 56331f4..a45d7d2 100644 --- a/frontend/src/utils/dataSourceCapabilities.ts +++ b/frontend/src/utils/dataSourceCapabilities.ts @@ -9,6 +9,10 @@ const normalizeDataSourceToken = (raw: string): string => { return 'diros'; case 'postgresql': return 'postgres'; + case 'opengauss': + case 'open_gauss': + case 'open-gauss': + return 'opengauss'; case 'dm': return 'dameng'; default: @@ -29,12 +33,14 @@ export const resolveDataSourceType = (config: ConnectionLike): string => { const SQL_QUERY_EXPORT_TYPES = new Set([ 'mysql', 'mariadb', + 'oceanbase', 'diros', 'sphinx', 'postgres', 'kingbase', 'highgo', 'vastbase', + 'opengauss', 'sqlserver', 'sqlite', 'duckdb', @@ -47,12 +53,14 @@ const SQL_QUERY_EXPORT_TYPES = new Set([ const COPY_INSERT_TYPES = new Set([ 'mysql', 'mariadb', + 'oceanbase', 'diros', 'sphinx', 'postgres', 'kingbase', 'highgo', 'vastbase', + 'opengauss', 'sqlserver', 'sqlite', 'duckdb', diff --git a/frontend/src/utils/driverImportGuidance.test.ts b/frontend/src/utils/driverImportGuidance.test.ts index 3403509..d382324 100644 --- a/frontend/src/utils/driverImportGuidance.test.ts +++ b/frontend/src/utils/driverImportGuidance.test.ts @@ -17,6 +17,8 @@ describe('driver import guidance', () => { it('documents custom driver aliases for kingbase and related fallbacks', () => { expect(CUSTOM_CONNECTION_DRIVER_HELP).toContain('kingbase8'); expect(CUSTOM_CONNECTION_DRIVER_HELP).toContain('pgx'); + expect(CUSTOM_CONNECTION_DRIVER_HELP).toContain('open_gauss'); + expect(CUSTOM_CONNECTION_DRIVER_HELP).toContain('oceanbase'); expect(CUSTOM_CONNECTION_DRIVER_HELP).toContain('JDBC Jar'); }); }); diff --git a/frontend/src/utils/driverImportGuidance.ts b/frontend/src/utils/driverImportGuidance.ts index 2980dfc..77889f5 100644 --- a/frontend/src/utils/driverImportGuidance.ts +++ b/frontend/src/utils/driverImportGuidance.ts @@ -7,4 +7,4 @@ export const DRIVER_LOCAL_IMPORT_SINGLE_FILE_HELP = '行内“导入驱动包”仅用于单个驱动文件/总包(如 `mariadb-driver-agent`、`mariadb-driver-agent.exe`、`GoNavi-DriverAgents.zip`),不支持直接导入 JDBC Jar;批量导入请使用上方“导入驱动目录”。'; export const CUSTOM_CONNECTION_DRIVER_HELP = - '已支持: mysql, postgres, sqlite, oracle, dm, kingbase;别名支持 postgresql/pgx、dm8、kingbase8/kingbasees/kingbasev8。当前不支持通过 JDBC Jar 扩展驱动。'; + '已支持: mysql, oceanbase, postgres, opengauss, sqlite, oracle, dm, kingbase;别名支持 postgresql/pgx、open_gauss/open-gauss、dm8、kingbase8/kingbasees/kingbasev8。当前不支持通过 JDBC Jar 扩展驱动。'; diff --git a/frontend/src/utils/queryAutoLimit.test.ts b/frontend/src/utils/queryAutoLimit.test.ts index bb4ea13..52df494 100644 --- a/frontend/src/utils/queryAutoLimit.test.ts +++ b/frontend/src/utils/queryAutoLimit.test.ts @@ -6,6 +6,7 @@ describe('applyQueryAutoLimit', () => { const limitDialects = [ 'mysql', 'mariadb', + 'oceanbase', 'diros', 'doris', 'sphinx', @@ -15,6 +16,7 @@ describe('applyQueryAutoLimit', () => { 'kingbase8', 'highgo', 'vastbase', + 'opengauss', 'sqlite', 'sqlite3', 'duckdb', diff --git a/frontend/src/utils/sidebarMetadata.ts b/frontend/src/utils/sidebarMetadata.ts index 2b09cd6..7db92b9 100644 --- a/frontend/src/utils/sidebarMetadata.ts +++ b/frontend/src/utils/sidebarMetadata.ts @@ -16,10 +16,14 @@ const normalizeSidebarConnectionDialect = (type: string, driver: string): string if (normalizedType === 'custom') { const normalizedDriver = String(driver || '').trim().toLowerCase(); if (normalizedDriver === 'postgresql' || normalizedDriver === 'postgres' || normalizedDriver === 'pg') return 'postgres'; + if (normalizedDriver === 'opengauss' || normalizedDriver === 'open_gauss' || normalizedDriver === 'open-gauss') return 'opengauss'; if (normalizedDriver === 'dameng' || normalizedDriver === 'dm' || normalizedDriver === 'dm8') return 'dm'; + if (normalizedDriver === 'oceanbase') return 'mysql'; if (normalizedDriver.includes('oracle')) return 'oracle'; return normalizedDriver; } + if (normalizedType === 'oceanbase') return 'mysql'; + if (normalizedType === 'open_gauss' || normalizedType === 'open-gauss') return 'opengauss'; if (normalizedType === 'dameng') return 'dm'; return normalizedType; }; diff --git a/frontend/src/utils/sql.ts b/frontend/src/utils/sql.ts index c7b251a..bf4ea25 100644 --- a/frontend/src/utils/sql.ts +++ b/frontend/src/utils/sql.ts @@ -37,12 +37,12 @@ export const quoteIdentPart = (dbType: string, ident: string) => { if (!raw) return raw; const dbTypeLower = (dbType || '').toLowerCase(); - if (dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'diros' || dbTypeLower === 'sphinx' || dbTypeLower === 'tdengine' || dbTypeLower === 'clickhouse') { + if (dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'oceanbase' || dbTypeLower === 'diros' || dbTypeLower === 'sphinx' || dbTypeLower === 'tdengine' || dbTypeLower === 'clickhouse') { return `\`${raw.replace(/`/g, '``')}\``; } // 对于 KingBase/PostgreSQL,只在必要时加引号 - if (dbTypeLower === 'kingbase' || dbTypeLower === 'postgres') { + if (dbTypeLower === 'kingbase' || dbTypeLower === 'postgres' || dbTypeLower === 'opengauss') { if (needsQuote(raw)) { return `"${raw.replace(/"/g, '""')}"`; } @@ -153,7 +153,7 @@ export const buildOrderBySQL = ( // MySQL/MariaDB 大表在无显式排序需求时强制 ORDER BY(即使按主键)可能触发 filesort, // 导致 `Error 1038 (HY001): Out of sort memory`。 // 因此仅在用户主动点击排序时下发 ORDER BY,默认分页查询不加兜底排序。 - if (dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'diros') { + if (dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'oceanbase' || dbTypeLower === 'diros') { return ''; } diff --git a/frontend/src/utils/sqlDialect.test.ts b/frontend/src/utils/sqlDialect.test.ts index 5baee91..eb4bfea 100644 --- a/frontend/src/utils/sqlDialect.test.ts +++ b/frontend/src/utils/sqlDialect.test.ts @@ -14,12 +14,16 @@ const names = (items: Array<{ name: string }>) => items.map((item) => item.name) describe('sqlDialect', () => { it('normalizes datasource aliases without collapsing all dialects to mysql', () => { expect(resolveSqlDialect('postgresql')).toBe('postgres'); + expect(resolveSqlDialect('OpenGauss')).toBe('opengauss'); + expect(resolveSqlDialect('OceanBase')).toBe('oceanbase'); expect(resolveSqlDialect('doris')).toBe('diros'); expect(resolveSqlDialect('dameng')).toBe('dameng'); expect(resolveSqlDialect('custom', 'kingbase8')).toBe('kingbase'); expect(resolveSqlDialect('custom', 'dm8')).toBe('dameng'); expect(resolveSqlDialect('custom', 'mariadb')).toBe('mariadb'); + expect(resolveSqlDialect('custom', 'open_gauss')).toBe('opengauss'); expect(isMysqlFamilyDialect('mariadb')).toBe(true); + expect(isMysqlFamilyDialect('oceanbase')).toBe(true); expect(isMysqlFamilyDialect('oracle')).toBe(false); }); @@ -28,6 +32,8 @@ describe('sqlDialect', () => { expect(values(resolveColumnTypeOptions('oracle'))).not.toContain('tinyint(1)'); expect(values(resolveColumnTypeOptions('dameng'))).toContain('VARCHAR2(255)'); expect(values(resolveColumnTypeOptions('kingbase'))).toContain('integer'); + expect(values(resolveColumnTypeOptions('opengauss'))).toContain('integer'); + expect(values(resolveColumnTypeOptions('oceanbase'))).toContain('varchar(255)'); expect(values(resolveColumnTypeOptions('kingbase'))).not.toContain('tinyint(1)'); expect(values(resolveColumnTypeOptions('diros'))).toContain('LARGEINT'); expect(values(resolveColumnTypeOptions('sphinx'))).toContain('text'); diff --git a/frontend/src/utils/sqlDialect.ts b/frontend/src/utils/sqlDialect.ts index 54321df..c8c5a80 100644 --- a/frontend/src/utils/sqlDialect.ts +++ b/frontend/src/utils/sqlDialect.ts @@ -8,12 +8,14 @@ export type SqlFunctionCompletion = { export type SqlDialect = | 'mysql' | 'mariadb' + | 'oceanbase' | 'diros' | 'sphinx' | 'postgres' | 'kingbase' | 'highgo' | 'vastbase' + | 'opengauss' | 'oracle' | 'dameng' | 'sqlserver' @@ -46,6 +48,10 @@ export const resolveSqlDialect = (rawType: string, rawDriver = ''): SqlDialect = case 'pq': case 'pgx': return 'postgres'; + case 'opengauss': + case 'open_gauss': + case 'open-gauss': + return 'opengauss'; case 'mssql': case 'sql_server': case 'sql-server': @@ -67,6 +73,7 @@ export const resolveSqlDialect = (rawType: string, rawDriver = ''): SqlDialect = case 'kingbasev8': return 'kingbase'; case 'mariadb': + case 'oceanbase': case 'mysql': case 'sphinx': case 'kingbase': @@ -83,7 +90,9 @@ export const resolveSqlDialect = (rawType: string, rawDriver = ''): SqlDialect = break; } + if (source.includes('opengauss') || source.includes('open_gauss') || source.includes('open-gauss')) return 'opengauss'; if (source.includes('postgres')) return 'postgres'; + if (source.includes('oceanbase')) return 'oceanbase'; if (source.includes('mariadb')) return 'mariadb'; if (source.includes('mysql')) return 'mysql'; if (source.includes('doris') || source.includes('diros')) return 'diros'; @@ -103,11 +112,11 @@ export const resolveSqlDialect = (rawType: string, rawDriver = ''): SqlDialect = }; export const isMysqlFamilyDialect = (dbType: string): boolean => ( - ['mysql', 'mariadb', 'diros', 'sphinx', 'tidb', 'oceanbase', 'starrocks'].includes(resolveSqlDialect(dbType)) + ['mysql', 'mariadb', 'oceanbase', 'diros', 'sphinx', 'tidb', 'starrocks'].includes(resolveSqlDialect(dbType)) ); export const isPgLikeDialect = (dbType: string): boolean => ( - ['postgres', 'kingbase', 'highgo', 'vastbase'].includes(resolveSqlDialect(dbType)) + ['postgres', 'kingbase', 'highgo', 'vastbase', 'opengauss'].includes(resolveSqlDialect(dbType)) ); export const isOracleLikeDialect = (dbType: string): boolean => ( @@ -423,9 +432,9 @@ const COMMON_TYPES = optionValues(['int', 'varchar(255)', 'text', 'datetime', 'd export const resolveColumnTypeOptions = (dbType: string): ColumnTypeOption[] => { const dialect = resolveSqlDialect(dbType); - if (dialect === 'mariadb' || dialect === 'mysql') return MYSQL_TYPES; if (dialect === 'diros') return DORIS_TYPES; if (dialect === 'sphinx') return SPHINX_TYPES; + if (isMysqlFamilyDialect(dialect)) return MYSQL_TYPES; if (isPgLikeDialect(dialect)) return PG_TYPES; if (dialect === 'oracle') return ORACLE_TYPES; if (dialect === 'dameng') return DAMENG_TYPES; diff --git a/internal/app/db_context.go b/internal/app/db_context.go index 009e405..22e5bf1 100644 --- a/internal/app/db_context.go +++ b/internal/app/db_context.go @@ -15,7 +15,7 @@ func normalizeRunConfig(config connection.ConnectionConfig, dbName string) conne } switch strings.ToLower(strings.TrimSpace(config.Type)) { - case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "sqlserver", "mongodb", "tdengine", "clickhouse": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver", "mongodb", "tdengine", "clickhouse": // 这些类型的 dbName 表示"数据库",需要写入连接配置以选择目标库。 runConfig.Database = name case "dameng": @@ -62,7 +62,7 @@ func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string, } switch dbType { - case "postgres", "kingbase", "highgo", "vastbase": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": // PG/金仓/瀚高/海量:dbName 在 UI 里是"数据库",schema 需从 tableName 或使用默认 public。 return "public", rawTable default: diff --git a/internal/app/db_proxy.go b/internal/app/db_proxy.go index 14af069..3faf8ca 100644 --- a/internal/app/db_proxy.go +++ b/internal/app/db_proxy.go @@ -203,11 +203,13 @@ func defaultPortByType(driverType string) int { switch strings.ToLower(strings.TrimSpace(driverType)) { case "mysql", "mariadb": return 3306 + case "oceanbase": + return 2881 case "diros": return 9030 case "sphinx": return 9306 - case "postgres", "vastbase": + case "postgres", "vastbase", "opengauss": return 5432 case "redis": return 6379 diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index 3f78584..3eae8d4 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -117,14 +117,14 @@ func (a *App) CreateDatabase(config connection.ConnectionConfig, dbName string) escapedDbName := strings.ReplaceAll(dbName, "`", "``") query := fmt.Sprintf("CREATE DATABASE `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci", escapedDbName) dbType := strings.ToLower(strings.TrimSpace(runConfig.Type)) - if dbType == "postgres" || dbType == "kingbase" || dbType == "highgo" || dbType == "vastbase" { + if dbType == "postgres" || dbType == "kingbase" || dbType == "highgo" || dbType == "vastbase" || dbType == "opengauss" { escapedDbName = strings.ReplaceAll(dbName, `"`, `""`) query = fmt.Sprintf("CREATE DATABASE \"%s\"", escapedDbName) } else if dbType == "tdengine" { query = fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", quoteIdentByType(dbType, dbName)) } else if dbType == "clickhouse" { query = fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", quoteIdentByType(dbType, dbName)) - } else if dbType == "mariadb" || dbType == "diros" { + } else if dbType == "mariadb" || dbType == "diros" || dbType == "oceanbase" { // MariaDB uses same syntax as MySQL } else if dbType == "sphinx" { return connection.QueryResult{Success: false, Message: "Sphinx 暂不支持创建数据库"} @@ -150,6 +150,8 @@ func resolveDDLDBType(config connection.ConnectionConfig) string { switch driver { case "postgresql", "postgres", "pg", "pq", "pgx": return "postgres" + case "opengauss", "open_gauss", "open-gauss": + return "opengauss" case "dm", "dameng", "dm8": return "dameng" case "sqlite3", "sqlite": @@ -164,9 +166,13 @@ func resolveDDLDBType(config connection.ConnectionConfig) string { return "highgo" case "vastbase": return "vastbase" + case "oceanbase": + return "oceanbase" } switch { + case strings.Contains(driver, "opengauss"), strings.Contains(driver, "open_gauss"), strings.Contains(driver, "open-gauss"): + return "opengauss" case strings.Contains(driver, "postgres"): return "postgres" case strings.Contains(driver, "kingbase"): @@ -181,6 +187,8 @@ func resolveDDLDBType(config connection.ConnectionConfig) string { return "sphinx" case strings.Contains(driver, "diros"), strings.Contains(driver, "doris"): return "diros" + case strings.Contains(driver, "oceanbase"): + return "oceanbase" default: return driver } @@ -203,7 +211,7 @@ func normalizeSchemaAndTableByType(dbType string, dbName string, tableName strin } } - if dbType == "postgres" || dbType == "highgo" || dbType == "vastbase" { + if dbType == "postgres" || dbType == "highgo" || dbType == "vastbase" || dbType == "opengauss" { schema, table := db.SplitSQLQualifiedName(rawTable) if schema != "" && table != "" { return schema, table @@ -222,7 +230,7 @@ func normalizeSchemaAndTableByType(dbType string, dbName string, tableName strin } switch dbType { - case "postgres", "kingbase", "highgo", "vastbase": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": return "public", rawTable default: return rawDB, rawTable @@ -243,7 +251,7 @@ func buildRunConfigForDDL(config connection.ConnectionConfig, dbType string, dbN if strings.EqualFold(strings.TrimSpace(config.Type), "custom") { // custom 连接的 dbName 语义依赖 driver,尽量在常见驱动上对齐内置类型行为。 switch dbType { - case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "dameng", "clickhouse": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "dameng", "clickhouse": if strings.TrimSpace(dbName) != "" { runConfig.Database = strings.TrimSpace(dbName) } @@ -264,9 +272,9 @@ func (a *App) RenameDatabase(config connection.ConnectionConfig, oldName string, dbType := resolveDDLDBType(config) switch dbType { - case "mysql", "mariadb", "diros", "sphinx": - return connection.QueryResult{Success: false, Message: "MySQL/MariaDB/Doris/Sphinx 不支持直接重命名数据库,请新建库后迁移数据"} - case "postgres", "kingbase", "highgo", "vastbase": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx": + return connection.QueryResult{Success: false, Message: "MySQL/MariaDB/OceanBase/Doris/Sphinx 不支持直接重命名数据库,请新建库后迁移数据"} + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": if strings.EqualFold(strings.TrimSpace(config.Database), oldName) { return connection.QueryResult{Success: false, Message: "当前连接正在使用目标数据库,请先连接到其他数据库后再重命名"} } @@ -297,11 +305,11 @@ func (a *App) DropDatabase(config connection.ConnectionConfig, dbName string) co sql string ) switch dbType { - case "mysql", "mariadb", "diros", "tdengine", "clickhouse": + case "mysql", "mariadb", "oceanbase", "diros", "tdengine", "clickhouse": runConfig = config runConfig.Database = "" sql = fmt.Sprintf("DROP DATABASE %s", quoteIdentByType(dbType, dbName)) - case "postgres", "kingbase", "highgo", "vastbase": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": if strings.EqualFold(strings.TrimSpace(config.Database), dbName) { return connection.QueryResult{Success: false, Message: "当前连接正在使用目标数据库,请先连接到其他数据库后再删除"} } @@ -336,7 +344,7 @@ func (a *App) RenameTable(config connection.ConnectionConfig, dbName string, old dbType := resolveDDLDBType(config) switch dbType { - case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "sqlserver", "clickhouse": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "clickhouse": default: return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持重命名表", dbType)} } @@ -350,7 +358,7 @@ func (a *App) RenameTable(config connection.ConnectionConfig, dbName string, old var sql string switch dbType { - case "mysql", "mariadb", "diros", "sphinx", "clickhouse": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "clickhouse": newQualifiedTable := quoteTableIdentByType(dbType, schemaName, newTableName) sql = fmt.Sprintf("RENAME TABLE %s TO %s", oldQualifiedTable, newQualifiedTable) case "sqlserver": @@ -382,7 +390,7 @@ func (a *App) DropTable(config connection.ConnectionConfig, dbName string, table dbType := resolveDDLDBType(config) switch dbType { - case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "sqlserver", "tdengine", "clickhouse": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "tdengine", "clickhouse": default: return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除表", dbType)} } @@ -990,7 +998,7 @@ func resolveCreateStatementWithFallback(dbInst db.Database, config connection.Co func supportsCreateStatementFallback(dbType string) bool { switch dbType { - case "postgres", "kingbase", "highgo", "vastbase": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": return true default: return false @@ -999,7 +1007,7 @@ func supportsCreateStatementFallback(dbType string) bool { func supportsViewCreateStatementLookup(dbType string) bool { switch dbType { - case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "sqlserver", "oracle", "dameng", "sqlite", "duckdb", "clickhouse": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver", "oracle", "dameng", "sqlite", "duckdb", "clickhouse": return true default: return false @@ -1180,7 +1188,7 @@ func (a *App) DropView(config connection.ConnectionConfig, dbName string, viewNa dbType := resolveDDLDBType(config) switch dbType { - case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "sqlserver", "clickhouse": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "sqlite", "duckdb", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "clickhouse": default: return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除视图", dbType)} } @@ -1215,7 +1223,7 @@ func (a *App) DropFunction(config connection.ConnectionConfig, dbName string, ro dbType := resolveDDLDBType(config) switch dbType { - case "mysql", "mariadb", "diros", "sphinx", "postgres", "kingbase", "oracle", "dameng", "highgo", "vastbase", "sqlserver", "duckdb": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "postgres", "kingbase", "oracle", "dameng", "highgo", "vastbase", "opengauss", "sqlserver", "duckdb": default: return connection.QueryResult{Success: false, Message: fmt.Sprintf("当前数据源(%s)暂不支持删除函数/存储过程", dbType)} } @@ -1269,10 +1277,10 @@ func (a *App) RenameView(config connection.ConnectionConfig, dbName string, oldN var sql string switch dbType { - case "mysql", "mariadb", "diros", "sphinx", "clickhouse": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "clickhouse": newQualified := quoteTableIdentByType(dbType, schemaName, newName) sql = fmt.Sprintf("RENAME TABLE %s TO %s", oldQualified, newQualified) - case "postgres", "kingbase", "highgo", "vastbase": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": sql = fmt.Sprintf("ALTER VIEW %s RENAME TO %s", oldQualified, newQuoted) case "sqlserver": oldFullName := schemaName + "." + pureOldName diff --git a/internal/app/methods_driver.go b/internal/app/methods_driver.go index 1be8e20..0a37cbc 100644 --- a/internal/app/methods_driver.go +++ b/internal/app/methods_driver.go @@ -310,6 +310,7 @@ const builtinDriverManifestJSON = `{ "drivers": { "mysql": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off" }, "mariadb": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/mariadb" }, + "oceanbase": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/oceanbase" }, "doris": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/doris" }, "sphinx": { "engine": "go", "version": "1.9.3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sphinx" }, "sqlserver": { "engine": "go", "version": "1.9.6", "checksumPolicy": "off", "downloadUrl": "builtin://activate/sqlserver" }, @@ -319,6 +320,7 @@ const builtinDriverManifestJSON = `{ "kingbase": { "engine": "go", "version": "0.0.0-20201021123113-29bd62a876c3", "checksumPolicy": "off", "downloadUrl": "builtin://activate/kingbase" }, "highgo": { "engine": "go", "version": "0.0.0-local", "checksumPolicy": "off", "downloadUrl": "builtin://activate/highgo" }, "vastbase": { "engine": "go", "version": "1.11.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/vastbase" }, + "opengauss": { "engine": "go", "version": "1.11.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/opengauss" }, "mongodb": { "engine": "go", "version": "2.5.0", "checksumPolicy": "off", "downloadUrl": "builtin://activate/mongodb" }, "tdengine": { "engine": "go", "version": "3.7.8", "checksumPolicy": "off", "downloadUrl": "builtin://activate/tdengine" }, "clickhouse": { "engine": "go", "version": "2.43.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/clickhouse" } @@ -363,6 +365,7 @@ var pinnedDriverPackageMap = map[string]pinnedDriverPackage{ var latestDriverVersionMap = map[string]string{ "mysql": "1.9.3", "mariadb": "1.9.3", + "oceanbase": "1.9.3", "diros": "1.9.3", "sphinx": "1.9.3", "sqlserver": "1.9.6", @@ -372,6 +375,7 @@ var latestDriverVersionMap = map[string]string{ "kingbase": "0.0.0-20201021123113-29bd62a876c3", "highgo": "0.0.0-local", "vastbase": "1.11.2", + "opengauss": "1.11.1", "mongodb": "2.5.0", "tdengine": "3.7.8", "clickhouse": "2.43.1", @@ -382,6 +386,7 @@ var latestDriverVersionMap = map[string]string{ var driverGoModulePathMap = map[string]string{ "mariadb": "github.com/go-sql-driver/mysql", + "oceanbase": "github.com/go-sql-driver/mysql", "diros": "github.com/go-sql-driver/mysql", "sphinx": "github.com/go-sql-driver/mysql", "sqlserver": "github.com/microsoft/go-mssqldb", @@ -391,6 +396,7 @@ var driverGoModulePathMap = map[string]string{ "kingbase": "gitea.com/kingbase/gokb", "highgo": "github.com/highgo/pq-sm3", "vastbase": "github.com/lib/pq", + "opengauss": "github.com/lib/pq", "mongodb": "go.mongodb.org/mongo-driver/v2", "tdengine": "github.com/taosdata/driver-go/v3", "clickhouse": "github.com/ClickHouse/clickhouse-go/v2", @@ -1366,6 +1372,8 @@ func normalizeDriverType(driverType string) string { return "diros" case "postgresql": return "postgres" + case "opengauss", "open_gauss", "open-gauss": + return "opengauss" default: return normalized } @@ -1435,6 +1443,7 @@ func allDriverDefinitionsWithPackages(packages map[string]pinnedDriverPackage) [ // 其他数据源需要先在驱动管理中“安装启用”。 buildOptionalGoDriverDefinition("mariadb", "MariaDB", packages), + buildOptionalGoDriverDefinition("oceanbase", "OceanBase", packages), buildOptionalGoDriverDefinition("diros", "Doris", packages), buildOptionalGoDriverDefinition("sphinx", "Sphinx", packages), buildOptionalGoDriverDefinition("sqlserver", "SQL Server", packages), @@ -1444,6 +1453,7 @@ func allDriverDefinitionsWithPackages(packages map[string]pinnedDriverPackage) [ buildOptionalGoDriverDefinition("kingbase", "Kingbase", packages), buildOptionalGoDriverDefinition("highgo", "HighGo", packages), buildOptionalGoDriverDefinition("vastbase", "Vastbase", packages), + buildOptionalGoDriverDefinition("opengauss", "OpenGauss", packages), buildOptionalGoDriverDefinition("mongodb", "MongoDB", packages), buildOptionalGoDriverDefinition("tdengine", "TDengine", packages), buildOptionalGoDriverDefinition("clickhouse", "ClickHouse", packages), @@ -3589,6 +3599,8 @@ func optionalDriverBuildTag(driverType string, selectedVersion string) (string, return "gonavi_mysql_driver", nil case "mariadb": return "gonavi_mariadb_driver", nil + case "oceanbase": + return "gonavi_oceanbase_driver", nil case "diros": return "gonavi_diros_driver", nil case "sphinx": @@ -3607,6 +3619,8 @@ func optionalDriverBuildTag(driverType string, selectedVersion string) (string, return "gonavi_highgo_driver", nil case "vastbase": return "gonavi_vastbase_driver", nil + case "opengauss": + return "gonavi_opengauss_driver", nil case "mongodb": if resolveMongoDriverMajorFromVersion(selectedVersion) == 1 { return "gonavi_mongodb_driver_v1", nil diff --git a/internal/app/methods_file.go b/internal/app/methods_file.go index 2d1d0b4..c5e33fb 100644 --- a/internal/app/methods_file.go +++ b/internal/app/methods_file.go @@ -1219,7 +1219,7 @@ const ( func supportsTruncateTableForDBType(dbType string) bool { switch strings.ToLower(strings.TrimSpace(dbType)) { - case "mysql", "mariadb", "postgres", "kingbase", "highgo", "vastbase", "sqlserver", "oracle", "dameng", "clickhouse", "duckdb": + case "mysql", "mariadb", "oceanbase", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver", "oracle", "dameng", "clickhouse", "duckdb": return true default: return false @@ -1353,7 +1353,7 @@ func quoteIdentByType(dbType string, ident string) string { } switch dbType { - case "mysql", "mariadb", "diros", "sphinx", "tdengine", "clickhouse": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "tdengine", "clickhouse": return "`" + strings.ReplaceAll(ident, "`", "``") + "`" case "kingbase": cleaned := db.NormalizeKingbaseIdentifier(ident) @@ -1578,7 +1578,7 @@ func buildListViewQueries(config connection.ConnectionConfig, dbName string) []s dbType := resolveDDLDBType(config) escapedDbName := escapeSQLLiteral(dbName) switch dbType { - case "mysql", "mariadb", "diros", "sphinx": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx": queries := []string{ fmt.Sprintf(`SELECT TABLE_SCHEMA AS schema_name, TABLE_NAME AS object_name, TABLE_TYPE AS table_type FROM information_schema.tables WHERE TABLE_TYPE='VIEW' AND TABLE_SCHEMA='%s' ORDER BY TABLE_NAME`, escapedDbName), } @@ -1586,7 +1586,7 @@ func buildListViewQueries(config connection.ConnectionConfig, dbName string) []s queries = append(queries, fmt.Sprintf("SHOW FULL TABLES FROM %s WHERE Table_type = 'VIEW'", quoteIdentByType("mysql", dbName))) } return queries - case "postgres", "kingbase", "highgo", "vastbase": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": return []string{ `SELECT table_schema AS schema_name, table_name AS object_name FROM information_schema.views WHERE table_schema NOT IN ('pg_catalog', 'information_schema') ORDER BY table_schema, table_name`, } @@ -1681,7 +1681,7 @@ func buildViewCreateQueries(config connection.ConnectionConfig, dbName, schemaNa escapedDB := escapeSQLLiteral(dbName) switch dbType { - case "mysql", "mariadb", "diros", "sphinx": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx": if safeSchema == "" { safeSchema = strings.TrimSpace(dbName) } @@ -1693,7 +1693,7 @@ func buildViewCreateQueries(config connection.ConnectionConfig, dbName, schemaNa return []string{ fmt.Sprintf("SHOW CREATE VIEW %s", quoteIdentByType("mysql", safeView)), } - case "postgres", "kingbase", "highgo", "vastbase": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": if safeSchema == "" { safeSchema = "public" } @@ -1960,7 +1960,8 @@ func formatSQLValue(dbType string, v interface{}) string { case time.Time: return "'" + val.Format("2006-01-02 15:04:05") + "'" case string: - if (strings.ToLower(strings.TrimSpace(dbType)) == "mysql" || strings.ToLower(strings.TrimSpace(dbType)) == "diros") && isMySQLHexLiteral(val) { + normalizedType := strings.ToLower(strings.TrimSpace(dbType)) + if (normalizedType == "mysql" || normalizedType == "oceanbase" || normalizedType == "diros") && isMySQLHexLiteral(val) { return val } escaped := strings.ReplaceAll(val, "'", "''") diff --git a/internal/app/sql_sanitize.go b/internal/app/sql_sanitize.go index 2990bcc..1aae3c7 100644 --- a/internal/app/sql_sanitize.go +++ b/internal/app/sql_sanitize.go @@ -67,7 +67,7 @@ func isReadOnlySQLQuery(dbType string, query string) bool { func sanitizeSQLForPgLike(dbType string, query string) string { switch strings.ToLower(strings.TrimSpace(dbType)) { - case "postgres", "kingbase", "highgo", "vastbase": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": // 有些情况下会出现多层重复引用(例如 """"schema"""" 或 ""schema"""),单次修复不一定收敛。 // 这里做有限次数的迭代,直到输出不再变化。 out := query diff --git a/internal/db/database.go b/internal/db/database.go index 429f7d7..ec701af 100644 --- a/internal/db/database.go +++ b/internal/db/database.go @@ -120,6 +120,8 @@ func normalizeDatabaseType(dbType string) string { return "diros" case "postgresql": return "postgres" + case "opengauss", "open_gauss", "open-gauss": + return "opengauss" default: return normalized } diff --git a/internal/db/database_optional_factories_full.go b/internal/db/database_optional_factories_full.go index 3de3d1b..2cad11c 100644 --- a/internal/db/database_optional_factories_full.go +++ b/internal/db/database_optional_factories_full.go @@ -4,6 +4,7 @@ package db func registerOptionalDatabaseFactories() { registerDatabaseFactory(newOptionalDriverAgentDatabase("mariadb"), "mariadb") + registerDatabaseFactory(newOptionalDriverAgentDatabase("oceanbase"), "oceanbase") registerDatabaseFactory(newOptionalDriverAgentDatabase("diros"), "diros", "doris") registerDatabaseFactory(newOptionalDriverAgentDatabase("sphinx"), "sphinx") registerDatabaseFactory(newOptionalDriverAgentDatabase("sqlserver"), "sqlserver") @@ -13,6 +14,7 @@ func registerOptionalDatabaseFactories() { registerDatabaseFactory(newOptionalDriverAgentDatabase("kingbase"), "kingbase") registerDatabaseFactory(newOptionalDriverAgentDatabase("highgo"), "highgo") registerDatabaseFactory(newOptionalDriverAgentDatabase("vastbase"), "vastbase") + registerDatabaseFactory(newOptionalDriverAgentDatabase("opengauss"), "opengauss", "open_gauss", "open-gauss") registerDatabaseFactory(newOptionalDriverAgentDatabase("mongodb"), "mongodb") registerDatabaseFactory(newOptionalDriverAgentDatabase("tdengine"), "tdengine") registerDatabaseFactory(newOptionalDriverAgentDatabase("clickhouse"), "clickhouse") diff --git a/internal/db/database_optional_factories_lite.go b/internal/db/database_optional_factories_lite.go index 3078709..778cb34 100644 --- a/internal/db/database_optional_factories_lite.go +++ b/internal/db/database_optional_factories_lite.go @@ -4,6 +4,7 @@ package db func registerOptionalDatabaseFactories() { registerDatabaseFactory(newOptionalDriverAgentDatabase("mariadb"), "mariadb") + registerDatabaseFactory(newOptionalDriverAgentDatabase("oceanbase"), "oceanbase") registerDatabaseFactory(newOptionalDriverAgentDatabase("diros"), "diros", "doris") registerDatabaseFactory(newOptionalDriverAgentDatabase("sphinx"), "sphinx") registerDatabaseFactory(newOptionalDriverAgentDatabase("sqlserver"), "sqlserver") @@ -13,6 +14,7 @@ func registerOptionalDatabaseFactories() { registerDatabaseFactory(newOptionalDriverAgentDatabase("kingbase"), "kingbase") registerDatabaseFactory(newOptionalDriverAgentDatabase("highgo"), "highgo") registerDatabaseFactory(newOptionalDriverAgentDatabase("vastbase"), "vastbase") + registerDatabaseFactory(newOptionalDriverAgentDatabase("opengauss"), "opengauss", "open_gauss", "open-gauss") registerDatabaseFactory(newOptionalDriverAgentDatabase("mongodb"), "mongodb") registerDatabaseFactory(newOptionalDriverAgentDatabase("tdengine"), "tdengine") registerDatabaseFactory(newOptionalDriverAgentDatabase("clickhouse"), "clickhouse") diff --git a/internal/db/driver_agent_revisions_gen.go b/internal/db/driver_agent_revisions_gen.go index 3db335b..a2c4ae2 100644 --- a/internal/db/driver_agent_revisions_gen.go +++ b/internal/db/driver_agent_revisions_gen.go @@ -4,18 +4,20 @@ package db func init() { optionalDriverAgentRevisions = map[string]string{ - "mariadb": "src-d6c5c6717338834c", - "diros": "src-ed4f0f64ed28d3fa", - "sphinx": "src-f52324f0a812d7c8", - "sqlserver": "src-ec165f18de9cd8b3", - "sqlite": "src-9dea6c76bc931114", - "duckdb": "src-14027ac1de3c50c7", - "dameng": "src-1a08880ff5bbcf31", - "kingbase": "src-28eed0e4d942b724", - "highgo": "src-76146bf97f07f25c", - "vastbase": "src-555b60c4863542b6", - "mongodb": "src-2540a7350c4243aa", - "tdengine": "src-ce3e4a9c46f6b92d", - "clickhouse": "src-78e5ada4da56704d", + "mariadb": "src-99785848cecbb326", + "oceanbase": "src-3e0a7e0e0ab0b619", + "diros": "src-3d09d05982abf8cf", + "sphinx": "src-85601aaa25456cf1", + "sqlserver": "src-703a625726deff98", + "sqlite": "src-7d841dda22330f67", + "duckdb": "src-6d3b9eefc9802162", + "dameng": "src-8a758078ab1fd981", + "kingbase": "src-0086ac3cb15dd34d", + "highgo": "src-147aec3df7ef7461", + "vastbase": "src-cb0a4f1e6633f810", + "opengauss": "src-f9924897702b760d", + "mongodb": "src-a4eab8b91194dc18", + "tdengine": "src-f43c25ea8b55d81f", + "clickhouse": "src-c24f610c0f65228f", } } diff --git a/internal/db/driver_support.go b/internal/db/driver_support.go index e8559cd..cb86866 100644 --- a/internal/db/driver_support.go +++ b/internal/db/driver_support.go @@ -22,6 +22,7 @@ var coreBuiltinDrivers = map[string]struct{}{ // 注意:这是一种运行时门控(installed.json 标记),并不减少主二进制体积。 var optionalGoDrivers = map[string]struct{}{ "mariadb": {}, + "oceanbase": {}, "diros": {}, "sphinx": {}, "sqlserver": {}, @@ -31,6 +32,7 @@ var optionalGoDrivers = map[string]struct{}{ "kingbase": {}, "highgo": {}, "vastbase": {}, + "opengauss": {}, "mongodb": {}, "tdengine": {}, "clickhouse": {}, @@ -53,6 +55,8 @@ func normalizeRuntimeDriverType(driverType string) string { return "diros" case "postgresql": return "postgres" + case "opengauss", "open_gauss", "open-gauss": + return "opengauss" default: return normalized } @@ -68,6 +72,8 @@ func driverDisplayName(driverType string) string { return "Redis" case "mariadb": return "MariaDB" + case "oceanbase": + return "OceanBase" case "diros": return "Doris" case "sphinx": @@ -88,6 +94,8 @@ func driverDisplayName(driverType string) string { return "HighGo" case "vastbase": return "Vastbase" + case "opengauss": + return "OpenGauss" case "mongodb": return "MongoDB" case "tdengine": diff --git a/internal/db/driver_support_test.go b/internal/db/driver_support_test.go index a4efb46..bd86fae 100644 --- a/internal/db/driver_support_test.go +++ b/internal/db/driver_support_test.go @@ -100,6 +100,24 @@ func TestManagedDriverRequiresInstallMarker(t *testing.T) { } } +func TestNewCompatibleDriversAreOptionalAgentDrivers(t *testing.T) { + tmpDir := t.TempDir() + SetExternalDriverDownloadDirectory(tmpDir) + + for _, driverType := range []string{"oceanbase", "opengauss", "open_gauss"} { + if IsBuiltinDriver(driverType) { + t.Fatalf("%s 不应是免安装内置驱动", driverType) + } + if !IsOptionalGoDriver(driverType) { + t.Fatalf("%s 应走可选 driver-agent 链路", driverType) + } + supported, _ := DriverRuntimeSupportStatus(driverType) + if supported { + t.Fatalf("%s 未安装 agent 时不应可用", driverType) + } + } +} + func TestMySQLBuiltinRuntimeSupportAvailable(t *testing.T) { tmpDir := t.TempDir() SetExternalDriverDownloadDirectory(tmpDir) diff --git a/internal/db/mysql_impl.go b/internal/db/mysql_impl.go index 952a663..4bebbb0 100644 --- a/internal/db/mysql_impl.go +++ b/internal/db/mysql_impl.go @@ -194,7 +194,7 @@ func buildMySQLCompatibleDSN(config connection.ConnectionConfig, protocol, addre params.Set("timeout", fmt.Sprintf("%ds", timeout)) params.Set("tls", tlsMode) params.Set("multiStatements", "true") - if parsed, ok := parseMySQLCompatibleURI(config.URI, "mysql", "doris", "diros"); ok { + if parsed, ok := parseMySQLCompatibleURI(config.URI, "mysql", "doris", "diros", "oceanbase"); ok { mergeMySQLConnectionParams(params, parsed.Query()) } mergeMySQLConnectionParams(params, mysqlConnectionParamsFromText(config.ConnectionParams)) diff --git a/internal/db/oceanbase_impl.go b/internal/db/oceanbase_impl.go new file mode 100644 index 0000000..bb571af --- /dev/null +++ b/internal/db/oceanbase_impl.go @@ -0,0 +1,191 @@ +//go:build gonavi_full_drivers || gonavi_oceanbase_driver + +package db + +import ( + "database/sql" + "fmt" + "strings" + + "GoNavi-Wails/internal/connection" + "GoNavi-Wails/internal/ssh" + "GoNavi-Wails/internal/utils" + + mysqlDriver "github.com/go-sql-driver/mysql" +) + +const ( + oceanbaseDriverName = "oceanbase" + defaultOceanBasePort = 2881 +) + +// OceanBaseDB 使用独立 driver 名称接入,底层协议兼容 MySQL。 +type OceanBaseDB struct { + MySQLDB +} + +func init() { + for _, name := range sql.Drivers() { + if name == oceanbaseDriverName { + return + } + } + sql.Register(oceanbaseDriverName, &mysqlDriver.MySQLDriver{}) +} + +func applyOceanBaseURI(config connection.ConnectionConfig) connection.ConnectionConfig { + uriText := strings.TrimSpace(config.URI) + if uriText == "" { + return config + } + parsed, ok := parseMySQLCompatibleURI(uriText, "oceanbase", "mysql") + if !ok { + return config + } + + if parsed.User != nil { + if config.User == "" { + config.User = parsed.User.Username() + } + if pass, ok := parsed.User.Password(); ok && config.Password == "" { + config.Password = pass + } + } + + if dbName := strings.TrimPrefix(parsed.Path, "/"); dbName != "" && config.Database == "" { + config.Database = dbName + } + + defaultPort := config.Port + if defaultPort <= 0 { + defaultPort = defaultOceanBasePort + } + + hostsFromURI := make([]string, 0, 4) + hostText := strings.TrimSpace(parsed.Host) + if hostText != "" { + for _, entry := range strings.Split(hostText, ",") { + host, port, ok := parseHostPortWithDefault(entry, defaultPort) + if !ok { + continue + } + hostsFromURI = append(hostsFromURI, normalizeMySQLAddress(host, port)) + } + } + + if len(config.Hosts) == 0 && len(hostsFromURI) > 0 { + config.Hosts = hostsFromURI + } + if strings.TrimSpace(config.Host) == "" && len(hostsFromURI) > 0 { + host, port, ok := parseHostPortWithDefault(hostsFromURI[0], defaultPort) + if ok { + config.Host = host + config.Port = port + } + } + + if config.Topology == "" { + topology := strings.TrimSpace(parsed.Query().Get("topology")) + if topology != "" { + config.Topology = strings.ToLower(topology) + } + } + + return config +} + +func collectOceanBaseAddresses(config connection.ConnectionConfig) []string { + defaultPort := config.Port + if defaultPort <= 0 { + defaultPort = defaultOceanBasePort + } + + candidates := make([]string, 0, len(config.Hosts)+1) + if len(config.Hosts) > 0 { + candidates = append(candidates, config.Hosts...) + } else { + candidates = append(candidates, normalizeMySQLAddress(config.Host, defaultPort)) + } + + result := make([]string, 0, len(candidates)) + seen := make(map[string]struct{}, len(candidates)) + for _, entry := range candidates { + host, port, ok := parseHostPortWithDefault(entry, defaultPort) + if !ok { + continue + } + normalized := normalizeMySQLAddress(host, port) + if _, exists := seen[normalized]; exists { + continue + } + seen[normalized] = struct{}{} + result = append(result, normalized) + } + return result +} + +func (o *OceanBaseDB) getDSN(config connection.ConnectionConfig) (string, error) { + database := config.Database + protocol := "tcp" + address := normalizeMySQLAddress(config.Host, config.Port) + + if config.UseSSH { + netName, err := ssh.RegisterSSHNetwork(config.SSH) + if err != nil { + return "", fmt.Errorf("创建 SSH 隧道失败:%w", err) + } + protocol = netName + } + + return buildMySQLCompatibleDSN(config, protocol, address, database), nil +} + +func (o *OceanBaseDB) Connect(config connection.ConnectionConfig) error { + runConfig := applyOceanBaseURI(config) + addresses := collectOceanBaseAddresses(runConfig) + if len(addresses) == 0 { + return fmt.Errorf("连接建立后验证失败:未找到可用的 OceanBase 地址") + } + + var errorDetails []string + for index, address := range addresses { + candidateConfig := runConfig + host, port, ok := parseHostPortWithDefault(address, defaultOceanBasePort) + if !ok { + continue + } + candidateConfig.Host = host + candidateConfig.Port = port + candidateConfig.User, candidateConfig.Password = resolveMySQLCredential(runConfig, index) + + dsn, err := o.getDSN(candidateConfig) + if err != nil { + errorDetails = append(errorDetails, fmt.Sprintf("%s 生成连接串失败: %v", address, err)) + continue + } + db, err := sql.Open(oceanbaseDriverName, dsn) + if err != nil { + errorDetails = append(errorDetails, fmt.Sprintf("%s 打开失败: %v", address, err)) + continue + } + + timeout := getConnectTimeout(candidateConfig) + ctx, cancel := utils.ContextWithTimeout(timeout) + pingErr := db.PingContext(ctx) + cancel() + if pingErr != nil { + _ = db.Close() + errorDetails = append(errorDetails, fmt.Sprintf("%s 验证失败: %v", address, pingErr)) + continue + } + + o.conn = db + o.pingTimeout = timeout + return nil + } + + if len(errorDetails) == 0 { + return fmt.Errorf("连接建立后验证失败:未找到可用的 OceanBase 地址") + } + return fmt.Errorf("连接建立后验证失败:%s", strings.Join(errorDetails, ";")) +} diff --git a/internal/db/opengauss_impl.go b/internal/db/opengauss_impl.go new file mode 100644 index 0000000..d8f4b56 --- /dev/null +++ b/internal/db/opengauss_impl.go @@ -0,0 +1,83 @@ +//go:build gonavi_full_drivers || gonavi_opengauss_driver + +package db + +import ( + "net" + "strconv" + "strings" + + "GoNavi-Wails/internal/connection" +) + +const defaultOpenGaussPort = 5432 + +// OpenGaussDB 使用 PostgreSQL wire protocol 兼容链路,通过独立 agent 类型暴露。 +type OpenGaussDB struct { + PostgresDB +} + +func applyOpenGaussURI(config connection.ConnectionConfig) connection.ConnectionConfig { + uriText := strings.TrimSpace(config.URI) + if uriText == "" { + return config + } + parsed, ok := parseConnectionURI(uriText, "opengauss", "postgres", "postgresql") + if !ok { + return config + } + + if parsed.User != nil { + if config.User == "" { + config.User = parsed.User.Username() + } + if pass, ok := parsed.User.Password(); ok && config.Password == "" { + config.Password = pass + } + } + + if dbName := strings.TrimPrefix(parsed.Path, "/"); dbName != "" && config.Database == "" { + config.Database = dbName + } + + defaultPort := config.Port + if defaultPort <= 0 { + defaultPort = defaultOpenGaussPort + } + if strings.TrimSpace(config.Host) == "" && strings.TrimSpace(parsed.Host) != "" { + host, port, ok := parseHostPortWithDefault(parsed.Host, defaultPort) + if ok { + config.Host = host + config.Port = port + } + } + if config.Port <= 0 { + config.Port = defaultOpenGaussPort + } + + return config +} + +func (o *OpenGaussDB) getDSN(config connection.ConnectionConfig) string { + runConfig := applyOpenGaussURI(config) + if runConfig.Port <= 0 { + runConfig.Port = defaultOpenGaussPort + } + if strings.TrimSpace(runConfig.Host) != "" { + if host, port, err := net.SplitHostPort(runConfig.Host); err == nil { + runConfig.Host = host + if p, convErr := strconv.Atoi(port); convErr == nil && p > 0 { + runConfig.Port = p + } + } + } + return o.PostgresDB.getDSN(runConfig) +} + +func (o *OpenGaussDB) Connect(config connection.ConnectionConfig) error { + runConfig := applyOpenGaussURI(config) + if runConfig.Port <= 0 { + runConfig.Port = defaultOpenGaussPort + } + return o.PostgresDB.Connect(runConfig) +} diff --git a/internal/db/optional_driver_agent_impl.go b/internal/db/optional_driver_agent_impl.go index 3cba3ba..1cf4fa5 100644 --- a/internal/db/optional_driver_agent_impl.go +++ b/internal/db/optional_driver_agent_impl.go @@ -37,6 +37,7 @@ const ( optionalAgentMethodGetTriggers = "getTriggers" optionalAgentMethodApplyChanges = "applyChanges" optionalAgentDefaultScannerMaxBytes = 8 << 20 + optionalAgentMetadataProbeTimeout = 5 * time.Second ) type optionalAgentRequest struct { @@ -86,7 +87,7 @@ func ProbeOptionalDriverAgentMetadata(driverType string, executablePath string) }() var metadata OptionalDriverAgentMetadata - if err := client.call(optionalAgentRequest{Method: optionalAgentMethodMetadata}, &metadata, nil, nil); err != nil { + if err := client.callWithTimeout(optionalAgentRequest{Method: optionalAgentMethodMetadata}, &metadata, nil, nil, optionalAgentMetadataProbeTimeout); err != nil { return OptionalDriverAgentMetadata{}, err } metadata.DriverType = normalizeRuntimeDriverType(metadata.DriverType) @@ -243,6 +244,37 @@ func (c *optionalDriverAgentClient) call(req optionalAgentRequest, out interface return nil } +func (c *optionalDriverAgentClient) callWithTimeout(req optionalAgentRequest, out interface{}, fields *[]string, rowsAffected *int64, timeout time.Duration) error { + if timeout <= 0 { + return c.call(req, out, fields, rowsAffected) + } + + errCh := make(chan error, 1) + go func() { + errCh <- c.call(req, out, fields, rowsAffected) + }() + + timer := time.NewTimer(timeout) + defer timer.Stop() + + select { + case err := <-errCh: + return err + case <-timer.C: + c.forceTerminate() + return fmt.Errorf("%s 驱动代理 metadata 探测超时(%s),请确认导入的是正确的 driver-agent 可执行文件", driverDisplayName(c.driver), timeout) + } +} + +func (c *optionalDriverAgentClient) forceTerminate() { + if c.stdin != nil { + _ = c.stdin.Close() + } + if c.cmd != nil && c.cmd.Process != nil { + _ = c.cmd.Process.Kill() + } +} + func (c *optionalDriverAgentClient) close() error { c.mu.Lock() defer c.mu.Unlock() diff --git a/internal/db/postgres_impl.go b/internal/db/postgres_impl.go index 911676e..fd7d346 100644 --- a/internal/db/postgres_impl.go +++ b/internal/db/postgres_impl.go @@ -64,7 +64,7 @@ func (p *PostgresDB) getDSN(config connection.ConnectionConfig) string { q := url.Values{} q.Set("sslmode", resolvePostgresSSLMode(config)) q.Set("connect_timeout", strconv.Itoa(getConnectTimeoutSeconds(config))) - mergeConnectionParamsFromConfig(q, config, "postgres", "postgresql") + mergeConnectionParamsFromConfig(q, config, "postgres", "postgresql", "opengauss") u.RawQuery = q.Encode() return u.String() diff --git a/internal/sync/migration_kernel_router.go b/internal/sync/migration_kernel_router.go index aa88df2..1e6769e 100644 --- a/internal/sync/migration_kernel_router.go +++ b/internal/sync/migration_kernel_router.go @@ -107,7 +107,7 @@ func isMySQLLikeType(dbType string) bool { func classifyMigrationDataModel(dbType string) MigrationDataModel { switch normalizeMigrationDBType(dbType) { - case "mysql", "mariadb", "postgres", "kingbase", "highgo", "vastbase", "oracle", "sqlserver", "dameng", "sqlite", "duckdb": + case "mysql", "mariadb", "oceanbase", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "oracle", "sqlserver", "dameng", "sqlite", "duckdb": return MigrationDataModelRelational case "mongodb": return MigrationDataModelDocument diff --git a/internal/sync/migration_type_resolver.go b/internal/sync/migration_type_resolver.go index 937e2d7..e303970 100644 --- a/internal/sync/migration_type_resolver.go +++ b/internal/sync/migration_type_resolver.go @@ -12,6 +12,8 @@ func normalizeMigrationDBType(dbType string) string { return "diros" case "postgresql": return "postgres" + case "opengauss", "open_gauss", "open-gauss": + return "opengauss" case "dm", "dm8": return "dameng" case "sqlite3": @@ -31,6 +33,8 @@ func resolveMigrationDBType(config connection.ConnectionConfig) string { switch driver { case "postgresql", "postgres", "pg", "pq", "pgx": return "postgres" + case "opengauss", "open_gauss", "open-gauss": + return "opengauss" case "dm", "dameng", "dm8": return "dameng" case "sqlite3", "sqlite": @@ -45,6 +49,8 @@ func resolveMigrationDBType(config connection.ConnectionConfig) string { return "highgo" case "vastbase": return "vastbase" + case "oceanbase": + return "oceanbase" case "mysql", "mysql2": return "mysql" case "mariadb": @@ -52,6 +58,8 @@ func resolveMigrationDBType(config connection.ConnectionConfig) string { } switch { + case strings.Contains(driver, "opengauss"), strings.Contains(driver, "open_gauss"), strings.Contains(driver, "open-gauss"): + return "opengauss" case strings.Contains(driver, "postgres"): return "postgres" case strings.Contains(driver, "kingbase"): @@ -68,6 +76,8 @@ func resolveMigrationDBType(config connection.ConnectionConfig) string { return "diros" case strings.Contains(driver, "maria"): return "mariadb" + case strings.Contains(driver, "oceanbase"): + return "oceanbase" case strings.Contains(driver, "mysql"): return "mysql" case strings.Contains(driver, "dameng"), strings.Contains(driver, "dm"): @@ -79,7 +89,7 @@ func resolveMigrationDBType(config connection.ConnectionConfig) string { func isMySQLCoreType(dbType string) bool { switch normalizeMigrationDBType(dbType) { - case "mysql", "mariadb", "diros": + case "mysql", "mariadb", "oceanbase", "diros": return true default: return false diff --git a/internal/sync/schema_migration.go b/internal/sync/schema_migration.go index 0506a59..39a6acd 100644 --- a/internal/sync/schema_migration.go +++ b/internal/sync/schema_migration.go @@ -561,7 +561,7 @@ func intFromAny(v interface{}) int { func isPGLikeSource(dbType string) bool { switch normalizeMigrationDBType(dbType) { - case "postgres", "kingbase", "highgo", "vastbase", "duckdb": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss", "duckdb": return true default: return false @@ -852,7 +852,7 @@ func mapPGLikeDefaultToMySQL(col connection.ColumnDefinition, targetType string) func isPGLikeTarget(dbType string) bool { switch normalizeMigrationDBType(dbType) { - case "postgres", "kingbase", "highgo", "vastbase", "duckdb": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss", "duckdb": return true default: return false diff --git a/internal/sync/source_query_sync.go b/internal/sync/source_query_sync.go index 571aa75..d299080 100644 --- a/internal/sync/source_query_sync.go +++ b/internal/sync/source_query_sync.go @@ -277,16 +277,16 @@ func (s *SyncEngine) previewSourceQuery(config SyncConfig, limit int) (TableDiff inserts, updates, deletes, _ := diffRowsByPK(ctx.PKColumn, ctx.SourceRows, ctx.TargetRows) out := TableDiffPreview{ - Table: ctx.TableName, - PKColumn: ctx.PKColumn, - ColumnTypes: make(map[string]string, len(ctx.TargetCols)), + Table: ctx.TableName, + PKColumn: ctx.PKColumn, + ColumnTypes: make(map[string]string, len(ctx.TargetCols)), SchemaSummary: "SQL 结果集同步预览", - TotalInserts: len(inserts), - TotalUpdates: len(updates), - TotalDeletes: len(deletes), - Inserts: make([]PreviewRow, 0, minInt(limit, len(inserts))), - Updates: make([]PreviewUpdateRow, 0, minInt(limit, len(updates))), - Deletes: make([]PreviewRow, 0, minInt(limit, len(deletes))), + TotalInserts: len(inserts), + TotalUpdates: len(updates), + TotalDeletes: len(deletes), + Inserts: make([]PreviewRow, 0, minInt(limit, len(inserts))), + Updates: make([]PreviewUpdateRow, 0, minInt(limit, len(updates))), + Deletes: make([]PreviewRow, 0, minInt(limit, len(deletes))), } for _, col := range ctx.TargetCols { name := strings.ToLower(strings.TrimSpace(col.Name)) @@ -433,7 +433,7 @@ func (s *SyncEngine) runSourceQuerySync(config SyncConfig) SyncResult { applyTableName := ctx.TargetTable switch ctx.TargetType { - case "postgres", "kingbase", "highgo", "vastbase", "sqlserver": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver": applyTableName = ctx.TargetQueryTable } applier, ok := targetDB.(db.BatchApplier) diff --git a/internal/sync/sql_helpers.go b/internal/sync/sql_helpers.go index af647b9..ee21151 100644 --- a/internal/sync/sql_helpers.go +++ b/internal/sync/sql_helpers.go @@ -22,7 +22,7 @@ func quoteIdentByType(dbType string, ident string) string { } switch dbType { - case "mysql", "mariadb", "diros", "sphinx", "clickhouse", "tdengine": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "clickhouse", "tdengine": return "`" + strings.ReplaceAll(ident, "`", "``") + "`" case "sqlserver": escaped := strings.ReplaceAll(ident, "]", "]]") @@ -74,7 +74,7 @@ func normalizeSchemaAndTable(dbType string, dbName string, tableName string) (st } switch strings.ToLower(strings.TrimSpace(dbType)) { - case "postgres", "kingbase", "highgo", "vastbase": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": return "public", rawTable case "duckdb": return "main", rawTable @@ -93,7 +93,7 @@ func qualifiedNameForQuery(dbType string, schema string, table string, original } switch strings.ToLower(strings.TrimSpace(dbType)) { - case "postgres", "kingbase", "highgo", "vastbase": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss": s := strings.TrimSpace(schema) if s == "" { s = "public" @@ -111,7 +111,7 @@ func qualifiedNameForQuery(dbType string, schema string, table string, original return raw } return s + "." + table - case "mysql", "mariadb", "diros", "sphinx", "clickhouse", "tdengine": + case "mysql", "mariadb", "oceanbase", "diros", "sphinx", "clickhouse", "tdengine": s := strings.TrimSpace(schema) if s == "" || table == "" { return table diff --git a/internal/sync/sync_engine.go b/internal/sync/sync_engine.go index fa9152f..e945ec4 100644 --- a/internal/sync/sync_engine.go +++ b/internal/sync/sync_engine.go @@ -206,7 +206,7 @@ func (s *SyncEngine) RunSync(config SyncConfig) SyncResult { sourceQueryTable, targetQueryTable := plan.SourceQueryTable, plan.TargetQueryTable applyTableName := targetTable switch targetType { - case "postgres", "kingbase", "highgo", "vastbase", "sqlserver": + case "postgres", "kingbase", "highgo", "vastbase", "opengauss", "sqlserver": applyTableName = targetQueryTable } diff --git a/tools/generate-driver-agent-revisions.sh b/tools/generate-driver-agent-revisions.sh index d1bafd3..b6c9c4c 100755 --- a/tools/generate-driver-agent-revisions.sh +++ b/tools/generate-driver-agent-revisions.sh @@ -5,7 +5,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$SCRIPT_DIR" -DEFAULT_DRIVERS=(mariadb diros sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase mongodb tdengine clickhouse) +DEFAULT_DRIVERS=(mariadb oceanbase diros sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse) OUTPUT_FILE="internal/db/driver_agent_revisions_gen.go" usage() { @@ -25,6 +25,8 @@ normalize_driver() { value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" case "$value" in doris|diros) echo "diros" ;; + oceanbase) echo "oceanbase" ;; + opengauss|open_gauss|open-gauss) echo "opengauss" ;; mariadb|diros|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|mongodb|tdengine|clickhouse) echo "$value" ;; @@ -79,6 +81,8 @@ internal/db/timeout.go) case "$driver:$identity" in mariadb:internal/db/mariadb_impl.go|\ +oceanbase:internal/db/oceanbase_impl.go|\ +oceanbase:internal/db/mysql_impl.go|\ diros:internal/db/diros_impl.go|\ diros:internal/db/mysql_impl.go|\ sphinx:internal/db/sphinx_impl.go|\ @@ -95,6 +99,8 @@ kingbase:internal/db/kingbase_impl.go|\ kingbase:internal/db/kingbase_identifier_utils.go|\ highgo:internal/db/highgo_impl.go|\ vastbase:internal/db/vastbase_impl.go|\ +opengauss:internal/db/opengauss_impl.go|\ +opengauss:internal/db/postgres_impl.go|\ mongodb:internal/db/mongodb_impl.go|\ mongodb:internal/db/mongodb_impl_v1.go|\ tdengine:internal/db/tdengine_impl.go|\