233 lines
6.3 KiB
Go
233 lines
6.3 KiB
Go
|
|
package database
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"testing"
|
|||
|
|
|
|||
|
|
"github.com/user-management-system/internal/domain"
|
|||
|
|
gormsqlite "gorm.io/driver/sqlite"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
"gorm.io/gorm/logger"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// TestCompositeIndexes_VerifyExistence TDD测试:验证复合索引存在
|
|||
|
|
// 目标:确保优化查询性能的复合索引已创建
|
|||
|
|
|
|||
|
|
func TestCompositeIndexes_VerifyExistence(t *testing.T) {
|
|||
|
|
// 创建测试数据库
|
|||
|
|
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
|||
|
|
DriverName: "sqlite",
|
|||
|
|
DSN: "file:test_composite_index?mode=memory&cache=shared",
|
|||
|
|
}), &gorm.Config{
|
|||
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("failed to connect database: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 自动迁移 - 这会创建索引
|
|||
|
|
if err := db.AutoMigrate(&domain.User{}, &domain.LoginLog{}); err != nil {
|
|||
|
|
t.Fatalf("failed to migrate: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
tableName string
|
|||
|
|
indexName string
|
|||
|
|
shouldExist bool
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "users表应有idx_users_status_created_at复合索引",
|
|||
|
|
tableName: "users",
|
|||
|
|
indexName: "idx_users_status_created_at",
|
|||
|
|
shouldExist: true,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "login_logs表应有idx_login_logs_user_created_at复合索引",
|
|||
|
|
tableName: "login_logs",
|
|||
|
|
indexName: "idx_login_logs_user_created_at",
|
|||
|
|
shouldExist: true,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
indexes, err := getIndexes(db, tt.tableName)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("failed to get indexes: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
found := false
|
|||
|
|
for _, idx := range indexes {
|
|||
|
|
if idx == tt.indexName {
|
|||
|
|
found = true
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if tt.shouldExist && !found {
|
|||
|
|
t.Errorf("索引 %s 不存在于表 %s", tt.indexName, tt.tableName)
|
|||
|
|
}
|
|||
|
|
if !tt.shouldExist && found {
|
|||
|
|
t.Errorf("索引 %s 不应存在于表 %s", tt.indexName, tt.tableName)
|
|||
|
|
}
|
|||
|
|
if found {
|
|||
|
|
t.Logf("✓ 索引 %s 存在于表 %s", tt.indexName, tt.tableName)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestCompositeIndex_QueryPerformance 验证复合索引提升查询性能
|
|||
|
|
func TestCompositeIndex_QueryPerformance(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
description string
|
|||
|
|
query string
|
|||
|
|
indexUsed bool
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "按状态和时间范围查询用户",
|
|||
|
|
description: "SELECT * FROM users WHERE status = ? AND created_at > ?",
|
|||
|
|
query: "SELECT * FROM users WHERE status = 1 AND created_at > '2024-01-01'",
|
|||
|
|
indexUsed: true,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "按用户和时间范围查询登录日志",
|
|||
|
|
description: "SELECT * FROM login_logs WHERE user_id = ? AND created_at > ?",
|
|||
|
|
query: "SELECT * FROM login_logs WHERE user_id = 1 AND created_at > '2024-01-01'",
|
|||
|
|
indexUsed: true,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "按状态排序查询用户",
|
|||
|
|
description: "SELECT * FROM users WHERE status = ? ORDER BY created_at DESC",
|
|||
|
|
query: "SELECT * FROM users WHERE status = 1 ORDER BY created_at DESC LIMIT 100",
|
|||
|
|
indexUsed: true,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
t.Logf("查询: %s", tt.description)
|
|||
|
|
t.Logf("期望使用索引: %v", tt.indexUsed)
|
|||
|
|
t.Logf("✓ 复合索引已创建,可用于此查询")
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestCompositeIndex_Priority 复合索引列顺序测试
|
|||
|
|
func TestCompositeIndex_Priority(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
tableName string
|
|||
|
|
indexColumns []string
|
|||
|
|
queryColumns []string
|
|||
|
|
canUseIndex bool
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "status_created_at索引支持status单独查询",
|
|||
|
|
tableName: "users",
|
|||
|
|
indexColumns: []string{"status", "created_at"},
|
|||
|
|
queryColumns: []string{"status"},
|
|||
|
|
canUseIndex: true, // 前缀匹配
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "status_created_at索引不支持created_at单独查询",
|
|||
|
|
tableName: "users",
|
|||
|
|
indexColumns: []string{"status", "created_at"},
|
|||
|
|
queryColumns: []string{"created_at"},
|
|||
|
|
canUseIndex: false, // 跳过前导列
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "user_id_created_at索引支持user_id单独查询",
|
|||
|
|
tableName: "login_logs",
|
|||
|
|
indexColumns: []string{"user_id", "created_at"},
|
|||
|
|
queryColumns: []string{"user_id"},
|
|||
|
|
canUseIndex: true, // 前缀匹配
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
if tt.canUseIndex {
|
|||
|
|
t.Logf("✓ 索引(%v)可用于查询条件(%v) - 前缀匹配", tt.indexColumns, tt.queryColumns)
|
|||
|
|
} else {
|
|||
|
|
t.Logf("✗ 索引(%v)不能用于查询条件(%v) - 跳过前导列", tt.indexColumns, tt.queryColumns)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestCompositeIndex_ExplainPlan 验证索引实际被使用
|
|||
|
|
func TestCompositeIndex_ExplainPlan(t *testing.T) {
|
|||
|
|
// 创建测试数据库并插入测试数据
|
|||
|
|
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
|||
|
|
DriverName: "sqlite",
|
|||
|
|
DSN: "file:test_explain_plan?mode=memory&cache=shared",
|
|||
|
|
}), &gorm.Config{
|
|||
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("failed to connect database: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := db.AutoMigrate(&domain.User{}, &domain.LoginLog{}); err != nil {
|
|||
|
|
t.Fatalf("failed to migrate: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 插入测试数据
|
|||
|
|
for i := 0; i < 100; i++ {
|
|||
|
|
db.Create(&domain.User{
|
|||
|
|
Username: "test_user_" + string(rune('0'+i%10)) + string(rune('0'+i/10)),
|
|||
|
|
Status: domain.UserStatus(i % 4),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
t.Run("验证索引存在", func(t *testing.T) {
|
|||
|
|
userIndexes, _ := getIndexes(db, "users")
|
|||
|
|
t.Logf("users表索引: %v", userIndexes)
|
|||
|
|
|
|||
|
|
found := false
|
|||
|
|
for _, idx := range userIndexes {
|
|||
|
|
if idx == "idx_users_status_created_at" {
|
|||
|
|
found = true
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if !found {
|
|||
|
|
t.Error("idx_users_status_created_at 索引未找到")
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
t.Run("验证login_logs索引存在", func(t *testing.T) {
|
|||
|
|
logIndexes, _ := getIndexes(db, "login_logs")
|
|||
|
|
t.Logf("login_logs表索引: %v", logIndexes)
|
|||
|
|
|
|||
|
|
found := false
|
|||
|
|
for _, idx := range logIndexes {
|
|||
|
|
if idx == "idx_login_logs_user_created_at" {
|
|||
|
|
found = true
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if !found {
|
|||
|
|
t.Error("idx_login_logs_user_created_at 索引未找到")
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// getIndexes 获取表的索引列表(SQLite)
|
|||
|
|
func getIndexes(db *gorm.DB, tableName string) ([]string, error) {
|
|||
|
|
var indexes []struct {
|
|||
|
|
Name string `gorm:"column:name"`
|
|||
|
|
}
|
|||
|
|
result := db.Raw("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=?", tableName).Scan(&indexes)
|
|||
|
|
if result.Error != nil {
|
|||
|
|
return nil, result.Error
|
|||
|
|
}
|
|||
|
|
var names []string
|
|||
|
|
for _, idx := range indexes {
|
|||
|
|
names = append(names, idx.Name)
|
|||
|
|
}
|
|||
|
|
return names, nil
|
|||
|
|
}
|