feat(db): 添加对达梦数据库的支持

- 在应用程序中新增对达梦数据库(DM)的配置和连接支持
- 实现 SetDmDB 函数以配置达梦数据库连接
- 更新数据库操作逻辑,支持达梦特有的 SQL 语法和功能
- 在相关文件中添加达梦数据库的处理逻辑,包括表创建、数据插入和查询
- 更新 go.mod 和 go.sum 文件以引入达梦数据库驱动
- 增强文档,详细说明达梦数据库的配置和使用方法
This commit is contained in:
2026-03-20 10:46:51 +08:00
parent 1546967918
commit 7f7b585ffb
194 changed files with 211502 additions and 2328 deletions
+64 -6
View File
@@ -14,13 +14,18 @@ func (that *HoTimeDB) backupSave(path string, tt string, code int) {
fd, _ := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
defer fd.Close()
q := "`"
if that.Dialect != nil {
q = that.Dialect.QuoteChar()
}
str := "\r\n"
if code == 0 || code == 2 {
str += that.backupDdl(tt)
}
if code == 0 || code == 1 {
str += "insert into `" + tt + "`\r\n\r\n("
str += "insert into " + q + tt + q + "\r\n\r\n("
str += that.backupCol(tt)
}
@@ -29,16 +34,69 @@ func (that *HoTimeDB) backupSave(path string, tt string, code int) {
// backupDdl 备份表结构(DDL
func (that *HoTimeDB) backupDdl(tt string) string {
data := that.Query("show create table " + tt)
if len(data) == 0 {
switch that.Type {
case "dm", "dameng":
return that.backupDdlDM(tt)
default:
data := that.Query("show create table " + tt)
if len(data) == 0 {
return ""
}
return ObjToStr(data[0]["Create Table"]) + ";\r\n\r\n"
}
}
// backupDdlDM 达梦数据库 DDL 导出
func (that *HoTimeDB) backupDdlDM(tt string) string {
cols := that.Query(`SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, DATA_DEFAULT FROM USER_TAB_COLUMNS WHERE TABLE_NAME='` + tt + `' ORDER BY COLUMN_ID`)
if len(cols) == 0 {
return ""
}
return ObjToStr(data[0]["Create Table"]) + ";\r\n\r\n"
ddl := "CREATE TABLE \"" + tt + "\" (\r\n"
for i, col := range cols {
name := col.GetString("COLUMN_NAME")
dtype := col.GetString("DATA_TYPE")
nullable := col.GetString("NULLABLE")
defVal := col.GetString("DATA_DEFAULT")
colDef := " \"" + name + "\" " + dtype
prec := col.GetInt("DATA_PRECISION")
scale := col.GetInt("DATA_SCALE")
length := col.GetInt("DATA_LENGTH")
if prec > 0 {
colDef += "(" + ObjToStr(prec)
if scale > 0 {
colDef += "," + ObjToStr(scale)
}
colDef += ")"
} else if length > 0 && (strings.Contains(strings.ToUpper(dtype), "CHAR") || strings.Contains(strings.ToUpper(dtype), "BINARY")) {
colDef += "(" + ObjToStr(length) + ")"
}
if nullable == "N" {
colDef += " NOT NULL"
}
if defVal != "" {
colDef += " DEFAULT " + strings.TrimSpace(defVal)
}
if i < len(cols)-1 {
colDef += ","
}
ddl += colDef + "\r\n"
}
ddl += ");\r\n\r\n"
return ddl
}
// backupCol 备份表数据
func (that *HoTimeDB) backupCol(tt string) string {
q := "`"
if that.Dialect != nil {
q = that.Dialect.QuoteChar()
}
str := ""
data := that.Select(tt, "*")
@@ -54,9 +112,9 @@ func (that *HoTimeDB) backupCol(tt string) string {
for k := range data[0] {
if tempLthData == lthCol-1 {
str += "`" + k + "`) "
str += q + k + q + ") "
} else {
str += "`" + k + "`,"
str += q + k + q + ","
}
col[tempLthData] = k
tempLthData++
+62 -6
View File
@@ -285,13 +285,21 @@ func (that *HoTimeDB) Insert(table string, data map[string]interface{}) int64 {
query := "INSERT INTO " + processor.ProcessTableName(table) + " " + queryString + "VALUES" + valueString
res, err := that.Exec(query, values...)
id := int64(0)
if err.GetError() == nil && res != nil {
id1, err := res.LastInsertId()
that.LastErr.SetError(err)
id = id1
if that.Dialect != nil && !that.Dialect.SupportsLastInsertId() {
returningClause := that.Dialect.ReturningClause("id")
query = strings.TrimSuffix(query, ";") + returningClause
rows := that.Query(query, values...)
if len(rows) > 0 {
id = rows[0].GetInt64("id")
}
} else {
res, err := that.Exec(query, values...)
if err.GetError() == nil && res != nil {
id1, e := res.LastInsertId()
that.LastErr.SetError(e)
id = id1
}
}
// 如果插入成功,删除缓存
@@ -475,6 +483,8 @@ func (that *HoTimeDB) Upsert(table string, data Map, uniqueKeys Slice, updateCol
query = that.buildPostgresUpsert(table, columns, uniqueKeyStrs, updateColumnStrs, rawValues)
case "sqlite3", "sqlite":
query = that.buildSQLiteUpsert(table, columns, uniqueKeyStrs, updateColumnStrs, rawValues)
case "dm", "dameng":
query = that.buildDMUpsert(table, columns, uniqueKeyStrs, updateColumnStrs, rawValues)
default: // mysql
query = that.buildMySQLUpsert(table, columns, uniqueKeyStrs, updateColumnStrs, rawValues)
}
@@ -608,6 +618,52 @@ func (that *HoTimeDB) buildSQLiteUpsert(table string, columns []string, uniqueKe
") DO UPDATE SET " + strings.Join(updateParts, ", ")
}
// buildDMUpsert 构建达梦的 Upsert 语句(MERGE INTO
func (that *HoTimeDB) buildDMUpsert(table string, columns []string, uniqueKeys []string, updateColumns []string, rawValues map[string]string) string {
processor := that.GetProcessor()
dialect := that.GetDialect()
quotedTable := processor.ProcessTableName(table)
srcParts := make([]string, len(columns))
for i, col := range columns {
if raw, ok := rawValues[col]; ok {
srcParts[i] = raw + " AS " + dialect.QuoteIdentifier(col)
} else {
srcParts[i] = "? AS " + dialect.QuoteIdentifier(col)
}
}
onParts := make([]string, len(uniqueKeys))
for i, key := range uniqueKeys {
qk := dialect.QuoteIdentifier(key)
onParts[i] = quotedTable + "." + qk + " = src." + qk
}
updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns {
qc := dialect.QuoteIdentifier(col)
if raw, ok := rawValues[col]; ok {
updateParts[i] = qc + " = " + raw
} else {
updateParts[i] = qc + " = src." + qc
}
}
insertCols := make([]string, len(columns))
insertVals := make([]string, len(columns))
for i, col := range columns {
qc := dialect.QuoteIdentifier(col)
insertCols[i] = qc
insertVals[i] = "src." + qc
}
return "MERGE INTO " + quotedTable + " USING (SELECT " + strings.Join(srcParts, ", ") +
") src ON (" + strings.Join(onParts, " AND ") +
") WHEN MATCHED THEN UPDATE SET " + strings.Join(updateParts, ", ") +
" WHEN NOT MATCHED THEN INSERT (" + strings.Join(insertCols, ", ") +
") VALUES (" + strings.Join(insertVals, ", ") + ")"
}
// Update 更新数据
func (that *HoTimeDB) Update(table string, data Map, where Map) int64 {
processor := that.GetProcessor()
+3
View File
@@ -7,6 +7,7 @@ import (
"strings"
"sync"
_ "gitee.com/chunanyong/dm"
_ "github.com/go-sql-driver/mysql"
_ "github.com/mattn/go-sqlite3"
"github.com/sirupsen/logrus"
@@ -75,6 +76,8 @@ func (that *HoTimeDB) initDialect() {
that.Dialect = &PostgreSQLDialect{}
case "sqlite3", "sqlite":
that.Dialect = &SQLiteDialect{}
case "dm", "dameng":
that.Dialect = &DMDialect{}
default:
that.Dialect = &MySQLDialect{}
}
+94
View File
@@ -206,6 +206,100 @@ func (d *PostgreSQLDialect) UpsertSQL(table string, columns []string, uniqueKeys
strings.Join(updateParts, ", "))
}
// DMDialect 达梦数据库方言实现
type DMDialect struct{}
func (d *DMDialect) GetName() string {
return "dm"
}
func (d *DMDialect) Quote(name string) string {
if strings.Contains(name, ".") || strings.Contains(name, " ") {
return name
}
return "\"" + name + "\""
}
func (d *DMDialect) QuoteIdentifier(name string) string {
name = strings.Trim(name, "`\"")
return "\"" + name + "\""
}
func (d *DMDialect) QuoteChar() string {
return "\""
}
func (d *DMDialect) Placeholder(index int) string {
return "?"
}
func (d *DMDialect) Placeholders(count int, startIndex int) string {
if count <= 0 {
return ""
}
placeholders := make([]string, count)
for i := 0; i < count; i++ {
placeholders[i] = "?"
}
return strings.Join(placeholders, ",")
}
func (d *DMDialect) SupportsLastInsertId() bool {
return true // DM Go 驱动原生支持 IDENTITY 列的 LastInsertId
}
func (d *DMDialect) ReturningClause(column string) string {
return " RETURNING " + d.Quote(column)
}
func (d *DMDialect) UpsertSQL(table string, columns []string, uniqueKeys []string, updateColumns []string) string {
// MERGE INTO table USING (SELECT ? AS col1, ? AS col2) src
// ON (table.uk = src.uk) WHEN MATCHED THEN UPDATE SET col1 = src.col1
// WHEN NOT MATCHED THEN INSERT (col1, col2) VALUES (src.col1, src.col2)
quotedTable := d.Quote(table)
srcParts := make([]string, len(columns))
for i, col := range columns {
if strings.HasSuffix(col, "[#]") {
srcParts[i] = col
} else {
srcParts[i] = "? AS " + d.Quote(col)
}
}
onParts := make([]string, len(uniqueKeys))
for i, key := range uniqueKeys {
qk := d.Quote(key)
onParts[i] = quotedTable + "." + qk + " = src." + qk
}
updateParts := make([]string, len(updateColumns))
for i, col := range updateColumns {
if strings.HasSuffix(col, "[#]") {
updateParts[i] = col
} else {
qc := d.Quote(col)
updateParts[i] = qc + " = src." + qc
}
}
insertCols := make([]string, len(columns))
insertVals := make([]string, len(columns))
for i, col := range columns {
qc := d.Quote(col)
insertCols[i] = qc
insertVals[i] = "src." + qc
}
return fmt.Sprintf("MERGE INTO %s USING (SELECT %s) src ON (%s) WHEN MATCHED THEN UPDATE SET %s WHEN NOT MATCHED THEN INSERT (%s) VALUES (%s)",
quotedTable,
strings.Join(srcParts, ", "),
strings.Join(onParts, " AND "),
strings.Join(updateParts, ", "),
strings.Join(insertCols, ", "),
strings.Join(insertVals, ", "))
}
// SQLiteDialect SQLite 方言实现
type SQLiteDialect struct{}
+10 -26
View File
@@ -83,41 +83,33 @@ func (p *IdentifierProcessor) ProcessTableNameNoPrefix(name string) string {
// ProcessColumn 处理 table.column 格式
// 输入: "name" 或 "order.name" 或 "`order`.name" 或 "`order`.`name`"
// 输出: "`name`" 或 "app_order.name"
// 注意: 单独的列名加引号(避免关键字冲突),table.column 格式不加引号
// 输出: "`name`" 或 "`app_order`.`name`" (MySQL) / "\"app_order\".\"name\"" (DM/PG)
func (p *IdentifierProcessor) ProcessColumn(name string) string {
// 检查是否包含点号
if !strings.Contains(name, ".") {
// 单独的列名,需要加引号(避免关键字冲突)
return p.dialect.QuoteIdentifier(p.stripQuotes(name))
}
// 处理 table.column 格式,不加引号,只添加前缀
parts := p.splitTableColumn(name)
if len(parts) == 2 {
tableName := p.stripQuotes(parts[0])
columnName := p.stripQuotes(parts[1])
// table.column 格式不加反引号
return p.prefix + tableName + "." + columnName
return p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(columnName)
}
// 无法解析,返回原样但转换引号
return p.convertQuotes(name)
}
// ProcessColumnNoPrefix 处理 table.column 格式(不添加前缀)
func (p *IdentifierProcessor) ProcessColumnNoPrefix(name string) string {
if !strings.Contains(name, ".") {
// 单独的列名,需要加引号(避免关键字冲突)
return p.dialect.QuoteIdentifier(p.stripQuotes(name))
}
// table.column 格式不加引号
parts := p.splitTableColumn(name)
if len(parts) == 2 {
tableName := p.stripQuotes(parts[0])
columnName := p.stripQuotes(parts[1])
return tableName + "." + columnName
return p.dialect.QuoteIdentifier(tableName) + "." + p.dialect.QuoteIdentifier(columnName)
}
return p.convertQuotes(name)
@@ -125,9 +117,8 @@ func (p *IdentifierProcessor) ProcessColumnNoPrefix(name string) string {
// ProcessConditionString 智能解析条件字符串(如 ON 条件)
// 输入: "user.id = order.user_id AND order.status = 1"
// 输出: "app_user.id = app_order.user_id AND app_order.status = 1"
// 注意: table.column 格式不加反引号,因为 MySQL/SQLite/PostgreSQL 都能正确解析
// 这样可以保持返回的列名与原始列名一致,便于聚合函数等场景读取结果
// 输出: "`app_user`.`id` = `app_order`.`user_id` AND `app_order`.`status` = 1" (MySQL)
// 输出: "\"app_user\".\"id\" = \"app_order\".\"user_id\" AND \"app_order\".\"status\" = 1" (DM/PG)
func (p *IdentifierProcessor) ProcessConditionString(condition string) string {
if condition == "" {
return condition
@@ -136,28 +127,24 @@ func (p *IdentifierProcessor) ProcessConditionString(condition string) string {
result := condition
// 首先处理已有完整引号的情况 `table`.`column` 或 "table"."column"
// 去除引号,只添加前缀
fullyQuotedPattern := regexp.MustCompile("[`\"]([a-zA-Z_][a-zA-Z0-9_]*)[`\"]\\.[`\"]([a-zA-Z_][a-zA-Z0-9_]*)[`\"]")
result = fullyQuotedPattern.ReplaceAllStringFunc(result, func(match string) string {
parts := fullyQuotedPattern.FindStringSubmatch(match)
if len(parts) == 3 {
tableName := parts[1]
colName := parts[2]
// table.column 格式不加反引号,只添加前缀
return p.prefix + tableName + "." + colName
return p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(colName)
}
return match
})
// 然后处理部分引号的情况 `table`.column 或 "table".column
// 去除引号,只添加前缀
quotedTablePattern := regexp.MustCompile("[`\"]([a-zA-Z_][a-zA-Z0-9_]*)[`\"]\\.([a-zA-Z_][a-zA-Z0-9_]*)(?:[^`\"]|$)")
result = quotedTablePattern.ReplaceAllStringFunc(result, func(match string) string {
parts := quotedTablePattern.FindStringSubmatch(match)
if len(parts) >= 3 {
tableName := parts[1]
colName := parts[2]
// 保留末尾字符(如果有)
suffix := ""
if len(match) > len(parts[0])-1 {
lastChar := match[len(match)-1]
@@ -165,24 +152,21 @@ func (p *IdentifierProcessor) ProcessConditionString(condition string) string {
suffix = string(lastChar)
}
}
// table.column 格式不加反引号,只添加前缀
return p.prefix + tableName + "." + colName + suffix
return p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(colName) + suffix
}
return match
})
// 最后处理无引号的情况 table.column
// 使用更精确的正则,确保不匹配已处理的内容
unquotedPattern := regexp.MustCompile(`([^` + "`" + `"\w]|^)([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)([^` + "`" + `"\w(]|$)`)
result = unquotedPattern.ReplaceAllStringFunc(result, func(match string) string {
parts := unquotedPattern.FindStringSubmatch(match)
if len(parts) >= 5 {
prefix := parts[1] // 前面的边界字符
prefix := parts[1]
tableName := parts[2]
colName := parts[3]
suffix := parts[4] // 后面的边界字符
// table.column 格式不加反引号,只添加前缀
return prefix + p.prefix + tableName + "." + colName + suffix
suffix := parts[4]
return prefix + p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(colName) + suffix
}
return match
})
+1 -1
View File
@@ -399,7 +399,7 @@ func classifyDBType(typeName string) int {
"SERIAL", "BIGSERIAL", "SMALLSERIAL",
"YEAR", "BOOL", "BOOLEAN", "OID":
return dbTypeInteger
case "DECIMAL", "NUMERIC", "NEWDECIMAL":
case "DECIMAL", "NUMERIC", "NEWDECIMAL", "NUMBER":
return dbTypeDecimal
case "FLOAT", "DOUBLE", "REAL", "FLOAT4", "FLOAT8", "DOUBLE PRECISION":
return dbTypeFloat
+27 -27
View File
@@ -244,39 +244,12 @@ func (that *HoTimeDB) varCond(k string, v interface{}) (string, []interface{}) {
k = strings.Replace(k, "[#]", "", -1)
k = processor.ProcessColumn(k) + " "
where += " " + k + "=" + ObjToStr(v) + " "
case "[##]": // 直接添加value到sql,需要考虑防注入
where += " " + ObjToStr(v)
case "[#!]":
k = strings.Replace(k, "[#!]", "", -1)
k = processor.ProcessColumn(k) + " "
where += " " + k + "!=" + ObjToStr(v) + " "
case "[!#]":
k = strings.Replace(k, "[!#]", "", -1)
k = processor.ProcessColumn(k) + " "
where += " " + k + "!=" + ObjToStr(v) + " "
case "[~]":
k = strings.Replace(k, "[~]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
v = "%" + ObjToStr(v) + "%"
res = append(res, v)
case "[!~]": // 左边任意
k = strings.Replace(k, "[!~]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
v = "%" + ObjToStr(v) + ""
res = append(res, v)
case "[~!]": // 右边任意
k = strings.Replace(k, "[~!]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
v = ObjToStr(v) + "%"
res = append(res, v)
case "[~~]": // 手动任意
k = strings.Replace(k, "[~~]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
res = append(res, v)
default:
def = true
}
@@ -307,6 +280,33 @@ func (that *HoTimeDB) varCond(k string, v interface{}) (string, []interface{}) {
vs := ObjToSlice(v)
res = append(res, vs[0])
res = append(res, vs[1])
case "[##]":
where += " " + ObjToStr(v)
case "[#!]":
k = strings.Replace(k, "[#!]", "", -1)
k = processor.ProcessColumn(k) + " "
where += " " + k + "!=" + ObjToStr(v) + " "
case "[!#]":
k = strings.Replace(k, "[!#]", "", -1)
k = processor.ProcessColumn(k) + " "
where += " " + k + "!=" + ObjToStr(v) + " "
case "[!~]": // 左边任意
k = strings.Replace(k, "[!~]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
v = "%" + ObjToStr(v) + ""
res = append(res, v)
case "[~!]": // 右边任意
k = strings.Replace(k, "[~!]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
v = ObjToStr(v) + "%"
res = append(res, v)
case "[~~]": // 手动任意
k = strings.Replace(k, "[~~]", "", -1)
k = processor.ProcessColumn(k) + " "
where += k + " LIKE ? "
res = append(res, v)
default:
where, res = that.handleDefaultCondition(k, v, where, res)
}