2026-01-22 04:36:52 +08:00
|
|
|
|
package db
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-04 01:34:09 +08:00
|
|
|
|
"bytes"
|
2026-01-22 04:36:52 +08:00
|
|
|
|
"code.hoteas.com/golang/hotime/cache"
|
|
|
|
|
|
. "code.hoteas.com/golang/hotime/common"
|
2026-04-13 00:38:50 +08:00
|
|
|
|
"code.hoteas.com/golang/hotime/log"
|
2026-01-22 04:36:52 +08:00
|
|
|
|
"database/sql"
|
2026-01-22 09:32:01 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
"sync"
|
2026-04-13 00:38:50 +08:00
|
|
|
|
"sync/atomic"
|
2026-01-22 09:32:01 +08:00
|
|
|
|
|
2026-03-20 10:46:51 +08:00
|
|
|
|
_ "gitee.com/chunanyong/dm"
|
2026-01-22 04:36:52 +08:00
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-13 00:38:50 +08:00
|
|
|
|
// DBError 最后一次 SQL 操作的结构化快照
|
|
|
|
|
|
// 嵌入框架 Error 类型,与 Obj 等保持一致的错误 API(HasError/GetError/Unwrap)
|
|
|
|
|
|
type DBError struct {
|
|
|
|
|
|
Error
|
|
|
|
|
|
Query string
|
|
|
|
|
|
Data []interface{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 04:36:52 +08:00
|
|
|
|
// HoTimeDB 数据库操作核心结构体
|
|
|
|
|
|
type HoTimeDB struct {
|
|
|
|
|
|
*sql.DB
|
|
|
|
|
|
ContextBase
|
|
|
|
|
|
DBName string
|
|
|
|
|
|
*cache.HoTimeCache
|
2026-04-13 00:38:50 +08:00
|
|
|
|
Log *log.Logger
|
|
|
|
|
|
Type string // 数据库类型: mysql, sqlite3, postgres
|
|
|
|
|
|
Prefix string
|
|
|
|
|
|
ConnectFunc func() (*sql.DB, *sql.DB)
|
|
|
|
|
|
lastError atomic.Pointer[DBError] // 无锁读写,始终记录最后一次 SQL 操作
|
|
|
|
|
|
limit Slice
|
|
|
|
|
|
*sql.Tx //事务对象
|
|
|
|
|
|
SlaveDB *sql.DB // 从数据库
|
|
|
|
|
|
limitMu sync.Mutex
|
|
|
|
|
|
Dialect Dialect // 数据库方言适配器
|
|
|
|
|
|
testTx *sql.Tx // 测试事务:设置后所有操作都在此事务内,测试结束回滚
|
|
|
|
|
|
testMu *sync.Mutex // 保护 testTx 单连接不被并发访问
|
2026-04-13 06:45:16 +08:00
|
|
|
|
testLogBuf *bytes.Buffer // 测试模式:SQL 日志写入此 buffer 而非直接打印,失败时才输出
|
|
|
|
|
|
testLogBufMu sync.Mutex // 保护 testLogBuf 并发写入
|
|
|
|
|
|
testSQLErrCount int32 // 测试模式下 SQL 错误计数(atomic)
|
|
|
|
|
|
txFailed *bool // 事务内是否有SQL出错,出错则事务必须回滚(指针确保按值传递 HoTimeDB 时状态共享)
|
2026-04-13 00:38:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetLast 返回最后一次 SQL 操作的完整快照(Query + Data + Error)
|
|
|
|
|
|
func (that *HoTimeDB) GetLast() *DBError {
|
|
|
|
|
|
return that.lastError.Load()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetLastError 返回最后一次 SQL 的错误(nil = 成功)
|
|
|
|
|
|
func (that *HoTimeDB) GetLastError() error {
|
|
|
|
|
|
if e := that.lastError.Load(); e != nil {
|
|
|
|
|
|
return e.GetError()
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetLastQuery 返回最后一次执行的 SQL 语句
|
|
|
|
|
|
func (that *HoTimeDB) GetLastQuery() string {
|
|
|
|
|
|
if e := that.lastError.Load(); e != nil {
|
|
|
|
|
|
return e.Query
|
|
|
|
|
|
}
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetLastData 返回最后一次 SQL 的参数
|
|
|
|
|
|
func (that *HoTimeDB) GetLastData() []interface{} {
|
|
|
|
|
|
if e := that.lastError.Load(); e != nil {
|
|
|
|
|
|
return e.Data
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
2026-01-22 04:36:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SetConnect 设置数据库配置连接
|
2026-04-13 00:38:50 +08:00
|
|
|
|
func (that *HoTimeDB) SetConnect(connect func() (master, slave *sql.DB)) {
|
2026-01-22 04:36:52 +08:00
|
|
|
|
that.ConnectFunc = connect
|
2026-04-13 00:38:50 +08:00
|
|
|
|
that.InitDb()
|
2026-01-22 04:36:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// InitDb 初始化数据库连接
|
2026-04-13 00:38:50 +08:00
|
|
|
|
func (that *HoTimeDB) InitDb() {
|
|
|
|
|
|
that.DB, that.SlaveDB = that.ConnectFunc()
|
2026-01-22 04:36:52 +08:00
|
|
|
|
if that.DB == nil {
|
2026-04-13 00:38:50 +08:00
|
|
|
|
return
|
2026-01-22 04:36:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
e := that.DB.Ping()
|
2026-04-13 00:38:50 +08:00
|
|
|
|
if e != nil {
|
|
|
|
|
|
dbErr := &DBError{}
|
|
|
|
|
|
dbErr.SetError(e)
|
|
|
|
|
|
that.lastError.Store(dbErr)
|
|
|
|
|
|
if that.Log != nil {
|
|
|
|
|
|
that.Log.Error().Err(e).Msg("数据库连接失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-22 04:36:52 +08:00
|
|
|
|
|
|
|
|
|
|
if that.SlaveDB != nil {
|
|
|
|
|
|
e := that.SlaveDB.Ping()
|
2026-04-13 00:38:50 +08:00
|
|
|
|
if e != nil {
|
|
|
|
|
|
dbErr := &DBError{}
|
|
|
|
|
|
dbErr.SetError(e)
|
|
|
|
|
|
that.lastError.Store(dbErr)
|
|
|
|
|
|
if that.Log != nil {
|
|
|
|
|
|
that.Log.Error().Err(e).Msg("从数据库连接失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-22 04:36:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if that.Dialect == nil {
|
|
|
|
|
|
that.initDialect()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// initDialect 根据数据库类型初始化方言
|
|
|
|
|
|
func (that *HoTimeDB) initDialect() {
|
|
|
|
|
|
switch that.Type {
|
|
|
|
|
|
case "postgres", "postgresql":
|
|
|
|
|
|
that.Dialect = &PostgreSQLDialect{}
|
|
|
|
|
|
case "sqlite3", "sqlite":
|
|
|
|
|
|
that.Dialect = &SQLiteDialect{}
|
2026-03-20 10:46:51 +08:00
|
|
|
|
case "dm", "dameng":
|
|
|
|
|
|
that.Dialect = &DMDialect{}
|
2026-01-22 04:36:52 +08:00
|
|
|
|
default:
|
|
|
|
|
|
that.Dialect = &MySQLDialect{}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetDialect 获取当前方言适配器
|
|
|
|
|
|
func (that *HoTimeDB) GetDialect() Dialect {
|
|
|
|
|
|
if that.Dialect == nil {
|
|
|
|
|
|
that.initDialect()
|
|
|
|
|
|
}
|
|
|
|
|
|
return that.Dialect
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SetDialect 设置方言适配器
|
|
|
|
|
|
func (that *HoTimeDB) SetDialect(dialect Dialect) {
|
|
|
|
|
|
that.Dialect = dialect
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetType 获取数据库类型
|
|
|
|
|
|
func (that *HoTimeDB) GetType() string {
|
|
|
|
|
|
return that.Type
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetPrefix 获取表前缀
|
|
|
|
|
|
func (that *HoTimeDB) GetPrefix() string {
|
|
|
|
|
|
return that.Prefix
|
|
|
|
|
|
}
|
2026-01-22 09:32:01 +08:00
|
|
|
|
|
|
|
|
|
|
// GetProcessor 获取标识符处理器
|
|
|
|
|
|
// 用于处理表名、字段名的前缀添加和引号转换
|
|
|
|
|
|
func (that *HoTimeDB) GetProcessor() *IdentifierProcessor {
|
|
|
|
|
|
return NewIdentifierProcessor(that.GetDialect(), that.Prefix)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// T 辅助方法:获取带前缀和引号的表名
|
|
|
|
|
|
// 用于手动构建 SQL 时使用
|
|
|
|
|
|
// 示例: db.T("order") 返回 "`app_order`" (MySQL) 或 "\"app_order\"" (PostgreSQL)
|
|
|
|
|
|
func (that *HoTimeDB) T(table string) string {
|
|
|
|
|
|
return that.GetProcessor().ProcessTableName(table)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// C 辅助方法:获取带前缀和引号的 table.column
|
|
|
|
|
|
// 支持两种调用方式:
|
|
|
|
|
|
// - db.C("order", "name") 返回 "`app_order`.`name`"
|
|
|
|
|
|
// - db.C("order.name") 返回 "`app_order`.`name`"
|
|
|
|
|
|
func (that *HoTimeDB) C(args ...string) string {
|
|
|
|
|
|
if len(args) == 0 {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(args) == 1 {
|
|
|
|
|
|
return that.GetProcessor().ProcessColumn(args[0])
|
|
|
|
|
|
}
|
|
|
|
|
|
// 两个参数: table, column
|
|
|
|
|
|
dialect := that.GetDialect()
|
|
|
|
|
|
table := args[0]
|
|
|
|
|
|
column := args[1]
|
|
|
|
|
|
// 去除已有引号
|
|
|
|
|
|
table = trimQuotes(table)
|
|
|
|
|
|
column = trimQuotes(column)
|
|
|
|
|
|
return dialect.QuoteIdentifier(that.Prefix+table) + "." + dialect.QuoteIdentifier(column)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// trimQuotes 去除字符串两端的引号
|
|
|
|
|
|
func trimQuotes(s string) string {
|
|
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
|
|
if len(s) >= 2 {
|
|
|
|
|
|
if (s[0] == '`' && s[len(s)-1] == '`') || (s[0] == '"' && s[len(s)-1] == '"') {
|
|
|
|
|
|
return s[1 : len(s)-1]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return s
|
|
|
|
|
|
}
|
2026-03-14 10:19:57 +08:00
|
|
|
|
|
2026-04-04 01:34:09 +08:00
|
|
|
|
// SetTestLogBuffer 设置测试 SQL 日志缓冲区,非 nil 时 SQL 日志写入 buffer 而非直接打印
|
|
|
|
|
|
func (that *HoTimeDB) SetTestLogBuffer(buf *bytes.Buffer) {
|
|
|
|
|
|
that.testLogBufMu.Lock()
|
|
|
|
|
|
that.testLogBuf = buf
|
|
|
|
|
|
that.testLogBufMu.Unlock()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FlushTestLog 取出并清空缓冲区中的 SQL 日志,返回累积的日志文本
|
|
|
|
|
|
func (that *HoTimeDB) FlushTestLog() string {
|
|
|
|
|
|
that.testLogBufMu.Lock()
|
|
|
|
|
|
defer that.testLogBufMu.Unlock()
|
|
|
|
|
|
if that.testLogBuf == nil {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
s := that.testLogBuf.String()
|
|
|
|
|
|
that.testLogBuf.Reset()
|
|
|
|
|
|
return s
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 06:45:16 +08:00
|
|
|
|
// TestSQLErrorCount 返回测试模式下的 SQL 错误计数
|
|
|
|
|
|
func (that *HoTimeDB) TestSQLErrorCount() int32 {
|
|
|
|
|
|
return atomic.LoadInt32(&that.testSQLErrCount)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ResetTestSQLErrorCount 重置测试 SQL 错误计数
|
|
|
|
|
|
func (that *HoTimeDB) ResetTestSQLErrorCount() {
|
|
|
|
|
|
atomic.StoreInt32(&that.testSQLErrCount, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 10:19:57 +08:00
|
|
|
|
// BeginTestTx 开启测试事务,设置后所有 Query/Exec/Action 都在此事务内执行
|
|
|
|
|
|
func (that *HoTimeDB) BeginTestTx() error {
|
|
|
|
|
|
tx, err := that.DB.Begin()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
that.testTx = tx
|
2026-03-14 13:06:32 +08:00
|
|
|
|
that.testMu = &sync.Mutex{}
|
2026-03-14 10:19:57 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RollbackTestTx 回滚测试事务,清除 testTx 状态
|
|
|
|
|
|
func (that *HoTimeDB) RollbackTestTx() error {
|
|
|
|
|
|
if that.testTx != nil {
|
|
|
|
|
|
err := that.testTx.Rollback()
|
|
|
|
|
|
that.testTx = nil
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|