fork github.com/doug-martin

This commit is contained in:
2025-03-22 23:02:05 +08:00
commit f14642a736
131 changed files with 34555 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
package engine
import (
"database/sql"
"fmt"
"github.com/lib/pq"
_ "github.com/denisenkom/go-mssqldb"
_ "github.com/go-sql-driver/mysql"
_ "github.com/mattn/go-sqlite3"
_ "git.fsdpf.net/go/db/v2/dialect/mysql"
_ "git.fsdpf.net/go/db/v2/dialect/postgres"
_ "git.fsdpf.net/go/db/v2/dialect/sqlite3"
_ "git.fsdpf.net/go/db/v2/dialect/sqlserver"
"git.fsdpf.net/go/db/v2"
"git.fsdpf.net/go/db/v2/internal/errors"
)
type Engine struct {
configs map[string]DBConfig
dbs map[string]*sql.DB
}
var _engine *Engine
func init() {
_engine = &Engine{
configs: make(map[string]DBConfig),
dbs: make(map[string]*sql.DB),
}
}
func (e Engine) Connection(name string) *db.Database {
cfg, ok := e.configs[name]
if !ok {
panic(errors.New(fmt.Sprintf("Database connection %s not configured.", name)))
}
_db, ok := e.dbs[name]
if !ok {
_db = e.MakeConnection(cfg)
e.dbs[name] = _db
}
return db.New(cfg.Driver, _db)
}
func (e Engine) MakeConnection(cfg DBConfig) *sql.DB {
dsn := cfg.ToDSN()
switch cfg.Driver {
case "mysql":
case "sqlite3":
case "sqlserver":
case "postgres":
if url, err := pq.ParseURL(dsn); err == nil {
dsn = url
} else {
panic(err)
}
default:
panic(fmt.Sprintf("Unsupported driver: %s", cfg.Driver))
}
db, err := sql.Open(cfg.Driver, dsn)
if err != nil {
panic(err)
}
if err := db.Ping(); err != nil {
panic(err)
}
return db
}
func Open(cfgs map[string]DBConfig) *Engine {
for n, cfg := range cfgs {
_engine.configs[n] = cfg
}
return _engine
}
+399
View File
@@ -0,0 +1,399 @@
package engine
import (
"fmt"
"net/url"
"time"
)
// DBConfig 数据库配置结构体
type DBConfig struct {
Driver string
Host string
Port string
Database string
Username string
Password string
Charset string
Prefix string
ConnMaxLifetime time.Duration
ConnMaxIdleTime time.Duration
MaxIdleConns int
MaxOpenConns int
ParseTime bool
EnableLog bool
ReadHosts []string
WriteHosts []string
MySQL struct {
Dsn string
Collation string
UnixSocket string
MultiStatements bool
}
PostgreSQL struct {
Sslmode string
TLS string
SearchPath string
ConnectTimeout int
ApplicationName string
}
SQLite struct {
File string
Journal string
Locking string
Mode string
Synchronous int
Cache string
BusyTimeout int
}
SQLServer struct {
Instance string
Encrypt string
TrustServerCert bool
AppName string
FailoverPartner string
PacketSize int
WorkstationID string
ConnectionTimeout int
KeepAlive int
Dsn string
}
}
// Option 配置选项类型
type Option func(*DBConfig)
// ToDSN 生成对应驱动的 DSN
func (c *DBConfig) ToDSN() string {
switch c.Driver {
case "mysql":
return c.toMySQLDSN()
case "pgsql":
return c.toPostgreSQLDSN()
case "sqlite3":
return c.toSQLiteDSN()
case "sqlserver":
return c.toSQLServerDSN()
default:
panic(fmt.Sprintf("Unsupported driver for DSN generation: %s", c.Driver))
}
}
// toMySQLDSN 生成 MySQL DSN
func (c *DBConfig) toMySQLDSN() string {
if c.MySQL.Dsn != "" {
return c.MySQL.Dsn
}
var dsn string
if c.Username != "" {
if c.Password != "" {
dsn = fmt.Sprintf("%s:%s@", c.Username, c.Password)
} else {
dsn = fmt.Sprintf("%s@", c.Username)
}
}
if c.MySQL.UnixSocket != "" {
dsn += fmt.Sprintf("unix(%s)", c.MySQL.UnixSocket)
} else {
host := c.Host
if host == "" && len(c.WriteHosts) > 0 {
host = c.WriteHosts[0]
}
if host == "" && len(c.ReadHosts) > 0 {
host = c.ReadHosts[0]
}
if c.Port == "" {
c.Port = "3306"
}
dsn += fmt.Sprintf("tcp(%s:%s)", host, c.Port)
}
dsn += fmt.Sprintf("/%s", c.Database)
params := url.Values{}
if c.Charset != "" {
params.Add("charset", c.Charset)
}
if c.ParseTime {
params.Add("parseTime", "true")
}
if c.MySQL.Collation != "" {
params.Add("collation", c.MySQL.Collation)
}
if c.MySQL.MultiStatements {
params.Add("multiStatements", "true")
}
if len(params) > 0 {
dsn += "?" + params.Encode()
}
return dsn
}
// toPostgreSQLDSN 生成 PostgreSQL DSN
func (c *DBConfig) toPostgreSQLDSN() string {
params := url.Values{}
if c.Host != "" {
params.Add("host", c.Host)
} else if len(c.WriteHosts) > 0 {
params.Add("host", c.WriteHosts[0])
} else if len(c.ReadHosts) > 0 {
params.Add("host", c.ReadHosts[0])
}
if c.Port == "" {
c.Port = "5432" // 默认 PostgreSQL 端口
}
params.Add("port", c.Port)
params.Add("dbname", c.Database)
params.Add("user", c.Username)
if c.Password != "" {
params.Add("password", c.Password)
}
if c.PostgreSQL.Sslmode != "" {
params.Add("sslmode", c.PostgreSQL.Sslmode)
}
if c.PostgreSQL.ConnectTimeout > 0 {
params.Add("connect_timeout", fmt.Sprintf("%d", c.PostgreSQL.ConnectTimeout))
}
if c.PostgreSQL.ApplicationName != "" {
params.Add("application_name", c.PostgreSQL.ApplicationName)
}
return "postgres://" + c.Username + ":" + url.QueryEscape(c.Password) + "@" + params.Get("host") + ":" + c.Port + "/" + c.Database + "?" + params.Encode()
}
// toSQLiteDSN 生成 SQLite DSN
func (c *DBConfig) toSQLiteDSN() string {
if c.SQLite.File == "" {
return ""
}
dsn := c.SQLite.File
params := url.Values{}
if c.SQLite.Journal != "" {
params.Add("_journal", c.SQLite.Journal)
}
if c.SQLite.Locking != "" {
params.Add("_locking", c.SQLite.Locking)
}
if c.SQLite.Mode != "" {
params.Add("_mode", c.SQLite.Mode)
}
if c.SQLite.Synchronous > 0 {
params.Add("_synchronous", fmt.Sprintf("%d", c.SQLite.Synchronous))
}
if c.SQLite.Cache != "" {
params.Add("_cache", c.SQLite.Cache)
}
if c.SQLite.BusyTimeout > 0 {
params.Add("_busy_timeout", fmt.Sprintf("%d", c.SQLite.BusyTimeout))
}
if len(params) > 0 {
dsn += "?" + params.Encode()
}
return dsn
}
// toSQLServerDSN 生成 SQL Server DSN
func (c *DBConfig) toSQLServerDSN() string {
if c.SQLServer.Dsn != "" {
return c.SQLServer.Dsn
}
host := c.Host
if host == "" && len(c.WriteHosts) > 0 {
host = c.WriteHosts[0]
}
if host == "" && len(c.ReadHosts) > 0 {
host = c.ReadHosts[0]
}
if c.Port == "" {
c.Port = "1433" // 默认 SQL Server 端口
}
if c.SQLServer.Instance != "" {
host += "\\" + c.SQLServer.Instance
}
params := url.Values{}
params.Add("server", host)
params.Add("database", c.Database)
if c.SQLServer.Encrypt != "" {
params.Add("encrypt", c.SQLServer.Encrypt)
}
if c.SQLServer.TrustServerCert {
params.Add("TrustServerCertificate", "true")
}
if c.SQLServer.AppName != "" {
params.Add("app name", c.SQLServer.AppName)
}
if c.SQLServer.ConnectionTimeout > 0 {
params.Add("connection timeout", fmt.Sprintf("%d", c.SQLServer.ConnectionTimeout))
}
return "sqlserver://" + c.Username + ":" + url.QueryEscape(c.Password) + "@" + host + ":" + c.Port + "?" + params.Encode()
}
// NewDBConfig 创建新的 DBConfig 实例,检查关键参数
func NewDBConfig(driver string, options ...Option) DBConfig {
if driver == "" {
panic("Driver is required and cannot be empty")
}
// 支持的驱动类型
validDrivers := map[string]bool{
"mysql": true,
"pgsql": true,
"sqlite3": true,
"sqlserver": true,
}
if !validDrivers[driver] {
panic(fmt.Sprintf("Unsupported driver: %s", driver))
}
// 初始化配置
config := &DBConfig{
Driver: driver,
Charset: "utf8mb4",
ConnMaxLifetime: 2 * time.Hour,
ConnMaxIdleTime: 30 * time.Minute,
MaxIdleConns: 10,
MaxOpenConns: 100,
}
// 应用所有选项
for _, opt := range options {
opt(config)
}
// 检查关键参数
switch config.Driver {
case "mysql", "pgsql", "sqlserver":
if config.Host == "" && len(config.ReadHosts) == 0 && len(config.WriteHosts) == 0 {
panic(fmt.Sprintf("Host, ReadHosts, or WriteHosts is required for %s driver", config.Driver))
}
if config.Database == "" {
panic(fmt.Sprintf("Database is required for %s driver", config.Driver))
}
if config.Username == "" {
panic(fmt.Sprintf("Username is required for %s driver", config.Driver))
}
if config.Password == "" {
panic(fmt.Sprintf("Password is required for %s driver", config.Driver))
}
case "sqlite3":
if config.SQLite.File == "" {
panic("File is required for sqlite3 driver")
}
}
return *config
}
// 通用的 WithOption 配置函数
func WithHost(host string) Option {
return func(c *DBConfig) {
c.Host = host
}
}
func WithPort(port string) Option {
return func(c *DBConfig) {
c.Port = port
}
}
func WithDatabase(database string) Option {
return func(c *DBConfig) {
c.Database = database
}
}
func WithUsername(username string) Option {
return func(c *DBConfig) {
c.Username = username
}
}
func WithPassword(password string) Option {
return func(c *DBConfig) {
c.Password = password
}
}
func WithParseTime(parseTime bool) Option {
return func(c *DBConfig) {
c.ParseTime = parseTime
}
}
func WithCharset(charset string) Option {
return func(c *DBConfig) {
c.Charset = charset
}
}
func WithReadHosts(hosts []string) Option {
return func(c *DBConfig) {
c.ReadHosts = hosts
}
}
func WithWriteHosts(hosts []string) Option {
return func(c *DBConfig) {
c.WriteHosts = hosts
}
}
// MySQL 专用选项
func WithMySQLCollation(collation string) Option {
return func(c *DBConfig) {
if c.Driver != "mysql" {
panic("WithMySQLCollation is only valid for mysql driver")
}
c.MySQL.Collation = collation
}
}
func WithMySQLUnixSocket(socket string) Option {
return func(c *DBConfig) {
if c.Driver != "mysql" {
panic("WithMySQLUnixSocket is only valid for mysql driver")
}
c.MySQL.UnixSocket = socket
}
}
// PostgreSQL 专用选项
func WithPgSslmode(sslmode string) Option {
return func(c *DBConfig) {
if c.Driver != "pgsql" {
panic("WithPgSslmode is only valid for pgsql driver")
}
c.PostgreSQL.Sslmode = sslmode
}
}
// SQLite 专用选项
func WithSQLiteFile(file string) Option {
return func(c *DBConfig) {
if c.Driver != "sqlite3" {
panic("WithSQLiteFile is only valid for sqlite3 driver")
}
c.SQLite.File = file
}
}
func WithSQLiteJournal(journal string) Option {
return func(c *DBConfig) {
if c.Driver != "sqlite3" {
panic("WithSQLiteJournal is only valid for sqlite3 driver")
}
c.SQLite.Journal = journal
}
}
// SQL Server 专用选项
func WithSQLServerInstance(instance string) Option {
return func(c *DBConfig) {
if c.Driver != "sqlserver" {
panic("WithSQLServerInstance is only valid for sqlserver driver")
}
c.SQLServer.Instance = instance
}
}
+383
View File
@@ -0,0 +1,383 @@
package engine_test
import (
"testing"
"time"
"git.fsdpf.net/go/db/v2/engine"
)
// TestMySQLConfig 测试 MySQL 驱动的配置
func TestMySQLConfig(t *testing.T) {
tests := []struct {
name string
options []engine.Option
wantErr bool
errMsg string
}{
{
name: "Valid Config",
options: []engine.Option{
engine.WithHost("localhost"),
engine.WithDatabase("testdb"),
engine.WithUsername("root"),
engine.WithPassword("password"),
engine.WithMySQLCollation("utf8mb4_unicode_ci"),
},
wantErr: false,
},
{
name: "Missing Host",
options: []engine.Option{
engine.WithDatabase("testdb"),
engine.WithUsername("root"),
engine.WithPassword("password"),
},
wantErr: true,
errMsg: "Host, ReadHosts, or WriteHosts is required for mysql driver",
},
{
name: "Missing Database",
options: []engine.Option{
engine.WithHost("localhost"),
engine.WithUsername("root"),
engine.WithPassword("password"),
},
wantErr: true,
errMsg: "Database is required for mysql driver",
},
{
name: "engine.engine.With SQL Server engine.Option",
options: []engine.Option{
engine.WithHost("localhost"),
engine.WithDatabase("testdb"),
engine.WithUsername("root"),
engine.WithPassword("password"),
engine.WithSQLServerInstance("SQLEXPRESS"),
},
wantErr: true,
errMsg: "engine.WithSQLServerInstance is only valid for sqlserver driver",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
if !tt.wantErr {
t.Errorf("engine.NewDBConfig() panicked unexpectedly: %v", r)
} else if tt.errMsg != "" && r != tt.errMsg {
t.Errorf("engine.NewDBConfig() panic = %v, want %v", r, tt.errMsg)
}
} else if tt.wantErr {
t.Errorf("engine.NewDBConfig() did not panic, expected panic with: %v", tt.errMsg)
}
}()
config := engine.NewDBConfig("mysql", tt.options...)
if !tt.wantErr && (config.Driver != "mysql" || config.MySQL.Collation != "utf8mb4_unicode_ci") {
t.Errorf("MySQL config not set correctly: %+v", config)
}
})
}
}
// TestPostgreSQLConfig 测试 PostgreSQL 驱动的配置
func TestPostgreSQLConfig(t *testing.T) {
tests := []struct {
name string
options []engine.Option
wantErr bool
errMsg string
}{
{
name: "Valid Config",
options: []engine.Option{
engine.WithHost("localhost"),
engine.WithDatabase("testdb"),
engine.WithUsername("postgres"),
engine.WithPassword("password"),
engine.WithPgSslmode("disable"),
},
wantErr: false,
},
{
name: "Missing Username",
options: []engine.Option{
engine.WithHost("localhost"),
engine.WithDatabase("testdb"),
engine.WithPassword("password"),
},
wantErr: true,
errMsg: "Username is required for pgsql driver",
},
{
name: "engine.engine.With MySQL engine.Option",
options: []engine.Option{
engine.WithHost("localhost"),
engine.WithDatabase("testdb"),
engine.WithUsername("postgres"),
engine.WithPassword("password"),
engine.WithMySQLCollation("utf8mb4_unicode_ci"),
},
wantErr: true,
errMsg: "engine.WithMySQLCollation is only valid for mysql driver",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
if !tt.wantErr {
t.Errorf("engine.NewDBConfig() panicked unexpectedly: %v", r)
} else if tt.errMsg != "" && r != tt.errMsg {
t.Errorf("engine.NewDBConfig() panic = %v, want %v", r, tt.errMsg)
}
} else if tt.wantErr {
t.Errorf("engine.NewDBConfig() did not panic, expected panic with: %v", tt.errMsg)
}
}()
config := engine.NewDBConfig("pgsql", tt.options...)
if !tt.wantErr && (config.Driver != "pgsql" || config.PostgreSQL.Sslmode != "disable") {
t.Errorf("PostgreSQL config not set correctly: %+v", config)
}
})
}
}
// TestSQLiteConfig 测试 SQLite 驱动的配置
func TestSQLiteConfig(t *testing.T) {
tests := []struct {
name string
options []engine.Option
wantErr bool
errMsg string
}{
{
name: "Valid Config",
options: []engine.Option{
engine.WithSQLiteFile("test.db"),
engine.WithSQLiteJournal("WAL"),
},
wantErr: false,
},
{
name: "Missing File",
options: []engine.Option{},
wantErr: true,
errMsg: "File is required for sqlite3 driver",
},
{
name: "engine.engine.With PostgreSQL engine.Option",
options: []engine.Option{
engine.WithSQLiteFile("test.db"),
engine.WithPgSslmode("disable"),
},
wantErr: true,
errMsg: "engine.WithPgSslmode is only valid for pgsql driver",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
if !tt.wantErr {
t.Errorf("engine.NewDBConfig() panicked unexpectedly: %v", r)
} else if tt.errMsg != "" && r != tt.errMsg {
t.Errorf("engine.NewDBConfig() panic = %v, want %v", r, tt.errMsg)
}
} else if tt.wantErr {
t.Errorf("engine.NewDBConfig() did not panic, expected panic with: %v", tt.errMsg)
}
}()
config := engine.NewDBConfig("sqlite3", tt.options...)
if !tt.wantErr && (config.Driver != "sqlite3" || config.SQLite.File != "test.db") {
t.Errorf("SQLite config not set correctly: %+v", config)
}
})
}
}
// TestSQLServerConfig 测试 SQL Server 驱动的配置
func TestSQLServerConfig(t *testing.T) {
tests := []struct {
name string
options []engine.Option
wantErr bool
errMsg string
}{
{
name: "Valid Config",
options: []engine.Option{
engine.WithHost("sqlserver.example.com"),
engine.WithDatabase("master"),
engine.WithUsername("sa"),
engine.WithPassword("password"),
engine.WithSQLServerInstance("SQLEXPRESS"),
},
wantErr: false,
},
{
name: "Missing Password",
options: []engine.Option{
engine.WithHost("sqlserver.example.com"),
engine.WithDatabase("master"),
engine.WithUsername("sa"),
},
wantErr: true,
errMsg: "Password is required for sqlserver driver",
},
{
name: "engine.engine.With SQLite engine.Option",
options: []engine.Option{
engine.WithHost("sqlserver.example.com"),
engine.WithDatabase("master"),
engine.WithUsername("sa"),
engine.WithPassword("password"),
engine.WithSQLiteFile("test.db"),
},
wantErr: true,
errMsg: "engine.WithSQLiteFile is only valid for sqlite3 driver",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
if !tt.wantErr {
t.Errorf("engine.NewDBConfig() panicked unexpectedly: %v", r)
} else if tt.errMsg != "" && r != tt.errMsg {
t.Errorf("engine.NewDBConfig() panic = %v, want %v", r, tt.errMsg)
}
} else if tt.wantErr {
t.Errorf("engine.NewDBConfig() did not panic, expected panic with: %v", tt.errMsg)
}
}()
config := engine.NewDBConfig("sqlserver", tt.options...)
if !tt.wantErr && (config.Driver != "sqlserver" || config.SQLServer.Instance != "SQLEXPRESS") {
t.Errorf("SQL Server config not set correctly: %+v", config)
}
})
}
}
// TestInvalidDriver 测试不支持的驱动
func TestInvalidDriver(t *testing.T) {
defer func() {
if r := recover(); r != nil {
if r != "Unsupported driver: oracle" {
t.Errorf("engine.NewDBConfig() panic = %v, want 'Unsupported driver: oracle'", r)
}
} else {
t.Errorf("engine.NewDBConfig() did not panic for invalid driver")
}
}()
engine.NewDBConfig("oracle")
}
// TestDefaultValues 测试默认值
func TestDefaultValues(t *testing.T) {
config := engine.NewDBConfig("mysql",
engine.WithHost("localhost"),
engine.WithDatabase("testdb"),
engine.WithUsername("root"),
engine.WithPassword("password"),
)
if config.Charset != "utf8mb4" {
t.Errorf("Default Charset = %v, want utf8mb4", config.Charset)
}
if config.ConnMaxLifetime != 2*time.Hour {
t.Errorf("Default ConnMaxLifetime = %v, want 2h", config.ConnMaxLifetime)
}
if config.MaxOpenConns != 100 {
t.Errorf("Default MaxOpenConns = %v, want 100", config.MaxOpenConns)
}
}
func TestToDSN(t *testing.T) {
tests := []struct {
name string
config engine.DBConfig
wantDSN string
wantErr bool
errMsg string
}{
{
name: "MySQL DSN",
config: engine.NewDBConfig("mysql",
engine.WithHost("localhost"),
engine.WithPort("3306"),
engine.WithDatabase("testdb"),
engine.WithUsername("root"),
engine.WithPassword("password"),
engine.WithMySQLCollation("utf8mb4_unicode_ci"),
),
wantDSN: "root:password@tcp(localhost:3306)/testdb?charset=utf8mb4&collation=utf8mb4_unicode_ci",
wantErr: false,
},
{
name: "PostgreSQL DSN",
config: engine.NewDBConfig("pgsql",
engine.WithHost("localhost"),
engine.WithPort("5432"),
engine.WithDatabase("testdb"),
engine.WithUsername("postgres"),
engine.WithPassword("password"),
engine.WithPgSslmode("disable"),
),
wantDSN: "postgres://postgres:password@localhost:5432/testdb?host=localhost&port=5432&dbname=testdb&user=postgres&password=password&sslmode=disable",
wantErr: false,
},
{
name: "SQLite DSN",
config: engine.NewDBConfig("sqlite3",
engine.WithSQLiteFile("test.db"),
engine.WithSQLiteJournal("WAL"),
),
wantDSN: "test.db?_journal=WAL",
wantErr: false,
},
{
name: "SQL Server DSN",
config: engine.NewDBConfig("sqlserver",
engine.WithHost("sqlserver.example.com"),
engine.WithPort("1433"),
engine.WithDatabase("master"),
engine.WithUsername("sa"),
engine.WithPassword("password"),
engine.WithSQLServerInstance("SQLEXPRESS"),
),
wantDSN: "sqlserver://sa:password@sqlserver.example.com\\SQLEXPRESS:1433?server=sqlserver.example.com\\SQLEXPRESS&database=master",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
if !tt.wantErr {
t.Errorf("ToDSN() panicked unexpectedly: %v", r)
} else if tt.errMsg != "" && r != tt.errMsg {
t.Errorf("ToDSN() panic = %v, want %v", r, tt.errMsg)
}
} else if tt.wantErr {
t.Errorf("ToDSN() did not panic, expected panic with: %v", tt.errMsg)
}
}()
dsn := tt.config.ToDSN()
if !tt.wantErr && dsn != tt.wantDSN {
t.Errorf("ToDSN() = %v, want %v", dsn, tt.wantDSN)
}
})
}
}