feat: add DM (Dameng) database support via ODBC

Connect to DM8 databases using the odbc-api crate with unixODBC.
Includes schema browsing, query execution, DDL generation,
and full frontend integration with download link to DM ODBC driver.
This commit is contained in:
t8y2
2026-05-07 01:02:56 +08:00
parent 02813f4b9e
commit 4bf4f3b77f
24 changed files with 1111 additions and 122 deletions
@@ -166,7 +166,7 @@ const driverProfiles: Record<
label: "CockroachDB",
icon: "cockroachdb",
},
dm: { type: "postgres", port: 5236, user: "SYSDBA", label: "DM (Dameng)", icon: "dm" },
dm: { type: "dameng", port: 5236, user: "SYSDBA", label: "DM (Dameng)", icon: "dm" },
tdengine: { type: "mysql", port: 6030, user: "root", label: "TDengine", icon: "tdengine" },
custom_mysql: {
type: "mysql",
@@ -322,6 +322,7 @@ const dbOptions = [
{ value: "oracle", label: "Oracle" },
{ value: "elasticsearch", label: "Elasticsearch" },
{ value: "mariadb", label: "MariaDB" },
{ value: "dm", label: "DM (Dameng)" },
];
const mysqlCompat = [
@@ -340,7 +341,6 @@ const pgCompat = [
{ value: "gaussdb", label: "GaussDB" },
{ value: "kingbase", label: "KingBase" },
{ value: "vastbase", label: "Vastbase" },
{ value: "dm", label: "DM (Dameng)" },
{ value: "redshift", label: "Redshift" },
{ value: "cockroachdb", label: "CockroachDB" },
{ value: "custom_postgres", label: "Custom" },
@@ -844,6 +844,13 @@ async function browseDbFilePath() {
<span />
<p class="col-span-3 text-xs text-muted-foreground">
{{ t("connection.dmCompatHint") }}
<a
href="https://eco.dameng.com/download/"
target="_blank"
class="underline text-primary hover:text-primary/80"
>
{{ t("connection.dmDownload") }}
</a>
</p>
</div>
@@ -49,8 +49,8 @@ const props = defineProps<{
focusTableName?: string;
}>();
const SQL_TYPES: DatabaseType[] = ["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"];
const SCHEMA_AWARE_TYPES: DatabaseType[] = ["postgres", "sqlserver", "oracle", "redshift"];
const SQL_TYPES: DatabaseType[] = ["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift", "dameng"];
const SCHEMA_AWARE_TYPES: DatabaseType[] = ["postgres", "sqlserver", "oracle", "redshift", "dameng"];
const CARD_WIDTH = 270;
const COLUMN_ROW_HEIGHT = 24;
const CARD_HEADER_HEIGHT = 44;
+2 -1
View File
@@ -76,7 +76,8 @@ async function resolveSchema(connectionId: string, database: string): Promise<st
config?.db_type === "postgres" ||
config?.db_type === "sqlserver" ||
config?.db_type === "oracle" ||
config?.db_type === "redshift";
config?.db_type === "redshift" ||
config?.db_type === "dameng";
if (needsSchema) {
const schemas = await api.listSchemas(connectionId, database);
return schemas.includes("public") ? "public" : (schemas[0] ?? "");
@@ -54,7 +54,7 @@ type SearchTableTask = {
table: TableInfo;
};
const SCHEMA_AWARE_TYPES = new Set<DatabaseType>(["postgres", "sqlserver", "oracle", "redshift"]);
const SCHEMA_AWARE_TYPES = new Set<DatabaseType>(["postgres", "sqlserver", "oracle", "redshift", "dameng"]);
const SYSTEM_SCHEMAS = new Set([
"information_schema",
"pg_catalog",
+25 -7
View File
@@ -87,7 +87,7 @@ const emit = defineEmits<{
}>();
const sqlFileUnsupportedTypes = new Set(["redis", "mongodb", "elasticsearch"]);
const diagramSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"]);
const diagramSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift", "dameng"]);
const databaseSearchSupportedTypes = new Set([
"mysql",
"postgres",
@@ -97,6 +97,7 @@ const databaseSearchSupportedTypes = new Set([
"redshift",
"duckdb",
"clickhouse",
"dameng",
]);
const tableImportSupportedTypes = new Set([
"mysql",
@@ -109,9 +110,18 @@ const tableImportSupportedTypes = new Set([
"doris",
"starrocks",
"redshift",
"dameng",
]);
const tableStructureSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver"]);
const fieldLineageSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"]);
const fieldLineageSupportedTypes = new Set([
"mysql",
"postgres",
"sqlite",
"sqlserver",
"oracle",
"redshift",
"dameng",
]);
const isExportingDatabase = ref(false);
function currentDatabaseType(): DatabaseType | undefined {
@@ -123,7 +133,7 @@ function quoteIdent(name: string): string {
}
function isSchemaAwareDbType(dbType?: DatabaseType): boolean {
return dbType === "postgres" || dbType === "oracle" || dbType === "sqlserver";
return dbType === "postgres" || dbType === "oracle" || dbType === "sqlserver" || dbType === "dameng";
}
function qualifiedTableName(tableName: string, schema?: string): string {
@@ -277,7 +287,11 @@ async function openData() {
if (!config) throw new Error("Connection config not found");
const qualifiedName =
(config.db_type === "postgres" || config.db_type === "oracle" || config.db_type === "sqlserver") && node.schema
(config.db_type === "postgres" ||
config.db_type === "oracle" ||
config.db_type === "sqlserver" ||
config.db_type === "dameng") &&
node.schema
? `${quoteIdent(node.schema)}.${quoteIdent(node.label)}`
: quoteIdent(node.label);
@@ -286,7 +300,7 @@ async function openData() {
const pks = columns.filter((c) => c.is_primary_key).map((c) => c.name);
const order = pks.length ? ` ORDER BY ${pks.map((pk) => `${quoteIdent(pk)} ASC`).join(", ")}` : "";
let sql: string;
if (config.db_type === "oracle") {
if (config.db_type === "oracle" || config.db_type === "dameng") {
sql = `SELECT * FROM ${qualifiedName}${order} FETCH FIRST 100 ROWS ONLY`;
} else if (config.db_type === "sqlserver") {
sql = `SELECT TOP 100 * FROM ${qualifiedName}${order}`;
@@ -462,7 +476,7 @@ async function confirmDuplicateStructure() {
sql = `CREATE TABLE ${target} (LIKE ${source} INCLUDING ALL);`;
} else if (dbType === "sqlserver") {
sql = `SELECT TOP 0 * INTO ${target} FROM ${source};`;
} else if (dbType === "oracle") {
} else if (dbType === "oracle" || dbType === "dameng") {
sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 1=0`;
} else {
sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 0;`;
@@ -653,7 +667,11 @@ async function exportData(format: "csv" | "json" | "sql") {
try {
await connectionStore.ensureConnected(node.connectionId);
const qualifiedName =
(config.db_type === "postgres" || config.db_type === "oracle" || config.db_type === "sqlserver") && node.schema
(config.db_type === "postgres" ||
config.db_type === "oracle" ||
config.db_type === "sqlserver" ||
config.db_type === "dameng") &&
node.schema
? `${quoteIdent(node.schema)}.${quoteIdent(node.label)}`
: quoteIdent(node.label);
const result = await api.executeQuery(node.connectionId, node.database, `SELECT * FROM ${qualifiedName}`);
+18 -3
View File
@@ -25,7 +25,16 @@ const props = defineProps<{
const store = useConnectionStore();
const SQL_TYPES: DatabaseType[] = ["mysql", "postgres", "sqlite", "sqlserver", "oracle", "clickhouse", "duckdb"];
const SQL_TYPES: DatabaseType[] = [
"mysql",
"postgres",
"sqlite",
"sqlserver",
"oracle",
"clickhouse",
"duckdb",
"dameng",
];
const sqlConnections = computed(() => store.connections.filter((c) => SQL_TYPES.includes(c.db_type)));
@@ -122,7 +131,10 @@ async function loadTables() {
try {
const config = store.getConfig(sourceConnectionId.value);
const needsSchema =
config?.db_type === "postgres" || config?.db_type === "sqlserver" || config?.db_type === "oracle";
config?.db_type === "postgres" ||
config?.db_type === "sqlserver" ||
config?.db_type === "oracle" ||
config?.db_type === "dameng";
if (needsSchema) {
const schemas = await api.listSchemas(sourceConnectionId.value, sourceDatabase.value);
sourceSchema.value = schemas.includes("public") ? "public" : (schemas[0] ?? "");
@@ -210,7 +222,10 @@ async function startTransfer() {
// Auto-detect target schema
const targetConfig = store.getConfig(targetConnectionId.value);
const targetNeedsSchema =
targetConfig?.db_type === "postgres" || targetConfig?.db_type === "sqlserver" || targetConfig?.db_type === "oracle";
targetConfig?.db_type === "postgres" ||
targetConfig?.db_type === "sqlserver" ||
targetConfig?.db_type === "oracle" ||
targetConfig?.db_type === "dameng";
if (targetNeedsSchema && !targetSchema.value) {
try {
const schemas = await api.listSchemas(targetConnectionId.value, targetDatabase.value);
+1 -1
View File
@@ -14,7 +14,7 @@ export function useSchemaOptions() {
function isSchemaAware(connectionId: string): boolean {
const dbType = connectionStore.getConfig(connectionId)?.db_type;
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle";
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle" || dbType === "dameng";
}
async function loadSchemaOptions(connectionId: string, database: string) {
+2 -1
View File
@@ -111,7 +111,8 @@ export default {
sshKeyPassphrasePlaceholder: "Leave empty if key is not encrypted",
sshKeyPathBrowse: "Browse",
sshExposeLan: "Expose tunnel to LAN",
dmCompatHint: "Requires DM PG compatibility mode (set COMPATIBLE_MODE=7 in dm.ini and restart)",
dmCompatHint: "Requires DM8 ODBC driver installed on your system.",
dmDownload: "Download from Dameng",
mongoLegacyHint:
"For older MongoDB servers, enter authSource=admin&authMechanism=SCRAM-SHA-1 here; add directConnection=true only when needed for direct standalone connections.",
compatible: "Compatible",
+2 -1
View File
@@ -110,7 +110,8 @@ export default {
sshKeyPassphrasePlaceholder: "密钥未加密则留空",
sshKeyPathBrowse: "浏览",
sshExposeLan: "允许局域网访问隧道",
dmCompatHint: "需要开启达梦 PG 兼容模式(dm.ini 中设置 COMPATIBLE_MODE=7 并重启服务)",
dmCompatHint: "需要在系统上安装达梦 DM8 ODBC 驱动程序。",
dmDownload: "前往达梦官网下载",
mongoLegacyHint:
"连接旧版 MongoDB 时,可在此填写 authSource=admin&authMechanism=SCRAM-SHA-1;直连单节点时可按需追加 directConnection=true。",
compatible: "兼容",
+8 -2
View File
@@ -22,7 +22,13 @@ export function qualifiedTableName(
options: Pick<BuildTableSelectSqlOptions, "databaseType" | "schema" | "tableName">,
): string {
const { databaseType, schema, tableName } = options;
if ((databaseType === "postgres" || databaseType === "oracle" || databaseType === "sqlserver") && schema) {
if (
(databaseType === "postgres" ||
databaseType === "oracle" ||
databaseType === "sqlserver" ||
databaseType === "dameng") &&
schema
) {
return `${quoteTableIdentifier(databaseType, schema)}.${quoteTableIdentifier(databaseType, tableName)}`;
}
return quoteTableIdentifier(databaseType, tableName);
@@ -47,7 +53,7 @@ export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string
const orderBy = options.orderBy ?? defaultOrderBy;
const order = orderBy ? ` ORDER BY ${orderBy}` : "";
if (databaseType === "oracle") {
if (databaseType === "oracle" || databaseType === "dameng") {
const offset = options.offset ? ` OFFSET ${options.offset} ROWS` : "";
return `SELECT * FROM ${table}${where}${order}${offset} FETCH FIRST ${limit} ROWS ONLY`;
}
+2 -1
View File
@@ -128,6 +128,7 @@ export const useConnectionStore = defineStore("connection", () => {
doris: "Doris",
starrocks: "StarRocks",
redshift: "Redshift",
dameng: "DM (Dameng)",
};
return {
...config,
@@ -652,7 +653,7 @@ export const useConnectionStore = defineStore("connection", () => {
function isSchemaAwareDatabase(connectionId: string): boolean {
const dbType = getConfig(connectionId)?.db_type;
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle";
return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle" || dbType === "dameng";
}
async function listCompletionTables(connectionId: string, database: string): Promise<SqlCompletionTable[]> {
+2 -1
View File
@@ -11,7 +11,8 @@ export type DatabaseType =
| "elasticsearch"
| "doris"
| "starrocks"
| "redshift";
| "redshift"
| "dameng";
export interface ConnectionConfig {
id: string;