chore: seed migration objects batch 5
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"hash/fnv"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/model/entity"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -20,14 +21,7 @@ var tenantDbGroups sync.Map
|
||||
|
||||
const registryDbGroup = "imRegistry"
|
||||
|
||||
// RegistryDB returns the tenant registry DB. It falls back to the default DB for
|
||||
// single-database deployments where database.imRegistry is not configured.
|
||||
func RegistryDB(ctx context.Context) gdb.DB {
|
||||
if configs, err := gdb.GetConfigGroup(registryDbGroup); err == nil && len(configs) > 0 {
|
||||
return g.DB(registryDbGroup)
|
||||
}
|
||||
return g.DB()
|
||||
}
|
||||
var registryDbGroupOnce sync.Once
|
||||
|
||||
// DB resolves a tenant registry record and returns the corresponding business DB.
|
||||
func DB(ctx context.Context, tenantId int64) (db gdb.DB, tenant *entity.ImTenant, err error) {
|
||||
@@ -35,7 +29,7 @@ func DB(ctx context.Context, tenantId int64) (db gdb.DB, tenant *entity.ImTenant
|
||||
return nil, nil, gerror.New("租户ID不能为空")
|
||||
}
|
||||
|
||||
if err = RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil {
|
||||
if err = RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil {
|
||||
return nil, nil, gerror.Wrap(err, "获取租户信息失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id <= 0 {
|
||||
@@ -65,9 +59,40 @@ func DB(ctx context.Context, tenantId int64) (db gdb.DB, tenant *entity.ImTenant
|
||||
return db, tenant, nil
|
||||
}
|
||||
|
||||
// RegistryDB returns the dedicated tenant registry connection, or the default
|
||||
// HotGo database when a separate registry group is not configured.
|
||||
func RegistryDB() gdb.DB {
|
||||
registryDbGroupOnce.Do(func() {
|
||||
ensureRegistryDBGroup()
|
||||
})
|
||||
if configs, err := gdb.GetConfigGroup(registryDbGroup); err == nil && len(configs) > 0 {
|
||||
return g.DB(registryDbGroup)
|
||||
}
|
||||
return g.DB()
|
||||
}
|
||||
|
||||
func ensureRegistryDBGroup() {
|
||||
if configs, err := gdb.GetConfigGroup(registryDbGroup); err == nil && len(configs) > 0 {
|
||||
configs[0].Prefix = ""
|
||||
_ = gdb.SetConfigGroup(registryDbGroup, configs)
|
||||
return
|
||||
}
|
||||
|
||||
configs, err := gdb.GetConfigGroup("default")
|
||||
if err != nil || len(configs) == 0 {
|
||||
return
|
||||
}
|
||||
cloned := make(gdb.ConfigGroup, len(configs))
|
||||
copy(cloned, configs)
|
||||
for i := range cloned {
|
||||
cloned[i].Prefix = ""
|
||||
}
|
||||
_ = gdb.SetConfigGroup(registryDbGroup, cloned)
|
||||
}
|
||||
|
||||
func tenantLink(ctx context.Context, tenant *entity.ImTenant) (string, error) {
|
||||
if tenant.DbDsn != "" {
|
||||
return tenant.DbDsn, nil
|
||||
return normalizeMySQLTimeOptions(tenant.DbDsn)
|
||||
}
|
||||
if tenant.DbName == "" {
|
||||
return "", gerror.New("租户业务库未配置")
|
||||
@@ -115,10 +140,31 @@ func replaceMySQLDatabase(link string, dbName string) (string, error) {
|
||||
dbStart += 2
|
||||
queryStart := strings.Index(link[dbStart:], "?")
|
||||
if queryStart < 0 {
|
||||
return link[:dbStart] + dbName, nil
|
||||
return normalizeMySQLTimeOptions(link[:dbStart] + dbName)
|
||||
}
|
||||
queryStart += dbStart
|
||||
return link[:dbStart] + dbName + link[queryStart:], nil
|
||||
return normalizeMySQLTimeOptions(link[:dbStart] + dbName + link[queryStart:])
|
||||
}
|
||||
|
||||
func normalizeMySQLTimeOptions(link string) (string, error) {
|
||||
if !strings.HasPrefix(link, "mysql:") {
|
||||
if !strings.Contains(link, "@tcp(") {
|
||||
return "", gerror.New("租户业务库仅支持 MySQL")
|
||||
}
|
||||
link = "mysql:" + link
|
||||
}
|
||||
parts := strings.SplitN(link, "?", 2)
|
||||
values := make(url.Values)
|
||||
if len(parts) == 2 {
|
||||
var err error
|
||||
values, err = url.ParseQuery(parts[1])
|
||||
if err != nil {
|
||||
return "", gerror.Wrap(err, "租户业务库连接参数格式不正确")
|
||||
}
|
||||
}
|
||||
values.Set("loc", "UTC")
|
||||
values.Set("parseTime", "true")
|
||||
return parts[0] + "?" + values.Encode(), nil
|
||||
}
|
||||
|
||||
func hash(s string) uint64 {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package imtenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeMySQLTimeOptions(t *testing.T) {
|
||||
link, err := normalizeMySQLTimeOptions("user:pass@tcp(127.0.0.1:3306)/tinode?charset=utf8mb4")
|
||||
if err != nil {
|
||||
t.Fatalf("normalize raw mysql dsn: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(link, "mysql:user:pass@tcp(127.0.0.1:3306)/tinode?") {
|
||||
t.Fatalf("unexpected normalized link: %s", link)
|
||||
}
|
||||
if !strings.Contains(link, "loc=UTC") || !strings.Contains(link, "parseTime=true") {
|
||||
t.Fatalf("missing required time options: %s", link)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeMySQLTimeOptionsRejectsUnsupportedFormat(t *testing.T) {
|
||||
if _, err := normalizeMySQLTimeOptions("postgres://user:pass@127.0.0.1:5432/tinode"); err == nil {
|
||||
t.Fatal("expected unsupported format error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceMySQLDatabaseUsesUTC(t *testing.T) {
|
||||
link, err := replaceMySQLDatabase("mysql:user:pass@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=false&charset=utf8mb4", "tinode")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, expected := range []string{"/tinode?", "loc=UTC", "parseTime=true", "charset=utf8mb4"} {
|
||||
if !strings.Contains(link, expected) {
|
||||
t.Fatalf("link %q does not contain %q", link, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextSnowflakeID(t *testing.T) {
|
||||
first, err := NextSnowflakeID(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := NextSnowflakeID(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first <= 0 || second <= first {
|
||||
t.Fatalf("Snowflake IDs are not increasing: %d, %d", first, second)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package imtenant
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNoDirectDefaultDBModelForIMTables(t *testing.T) {
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime caller unavailable")
|
||||
}
|
||||
serverRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", ".."))
|
||||
internalRoot := filepath.Join(serverRoot, "internal")
|
||||
patterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`g\.DB\(\)\.Model\(\s*"im_`),
|
||||
regexp.MustCompile(`g\.DB\(\)\.Model\(\s*` + "`" + `im_`),
|
||||
regexp.MustCompile(`g\.DB\(\)\.(Insert|InsertIgnore|Replace|Save|Update|Delete)\([^,\n]+,\s*"im_`),
|
||||
regexp.MustCompile(`g\.DB\(\)\.(Insert|InsertIgnore|Replace|Save|Update|Delete)\([^,\n]+,\s*` + "`" + `im_`),
|
||||
}
|
||||
|
||||
var violations []string
|
||||
err := filepath.WalkDir(internalRoot, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, pattern := range patterns {
|
||||
if pattern.Match(content) {
|
||||
rel, _ := filepath.Rel(serverRoot, path)
|
||||
violations = append(violations, rel)
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
t.Fatalf("IM public tables must not use default g.DB() model helpers because the default DB has table prefix: %s", strings.Join(violations, ", "))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user