245 lines
6.9 KiB
Go
245 lines
6.9 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// touchFile 先建空文件(零字节即合法 SQLite 库),绕开存在性检查。
|
|
func touchFile(t *testing.T, path string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte{}, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func newTestSession(driver, database string) *Session {
|
|
return &Session{
|
|
ID: "test-pool",
|
|
Conn: ConnReq{Driver: driver, Database: database},
|
|
sqlPool: map[string]*sql.DB{},
|
|
poolUsed: map[string]time.Time{},
|
|
LastUsed: time.Now(),
|
|
}
|
|
}
|
|
|
|
// 池化:同 database 复用同一 *sql.DB 句柄。
|
|
func TestSessionSqlDBReuse(t *testing.T) {
|
|
dbFile := filepath.Join(t.TempDir(), "pool.db")
|
|
touchFile(t, dbFile)
|
|
s := newTestSession("sqlite", dbFile)
|
|
defer s.closeAll()
|
|
|
|
a, err := s.sqlDB("")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := s.sqlDB("")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a != b {
|
|
t.Fatal("同 database 应复用同一 *sql.DB 句柄")
|
|
}
|
|
// sqlite 下 database 参数不覆盖文件路径
|
|
c, err := s.sqlDB("main")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c != a {
|
|
t.Fatal("sqlite 会话应忽略 database 复用同一句柄")
|
|
}
|
|
}
|
|
|
|
// 池化上限:超过 8 个句柄时关闭最久未用(LRU 轮换)。
|
|
func TestSessionSqlDBLRU(t *testing.T) {
|
|
// mysql 驱动 sql.Open 不立即建连,可离线验证池管理逻辑
|
|
s := newTestSession("mysql", "")
|
|
defer s.closeAll()
|
|
|
|
first, err := s.sqlDB("db0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 1; i <= maxPoolHandles; i++ {
|
|
time.Sleep(2 * time.Millisecond) // 保证 LRU 时间序确定
|
|
if _, err := s.sqlDB(string(rune('a' + i))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
s.mu.Lock()
|
|
n := len(s.sqlPool)
|
|
_, firstAlive := s.sqlPool["db0"]
|
|
s.mu.Unlock()
|
|
if n > maxPoolHandles {
|
|
t.Fatalf("池句柄数 %d 超上限 %d", n, maxPoolHandles)
|
|
}
|
|
if firstAlive {
|
|
t.Fatal("最久未用句柄应被淘汰")
|
|
}
|
|
_ = first
|
|
}
|
|
|
|
// 会话路径端到端:ExecSQL 走池化,两次执行共享句柄且结果正确。
|
|
func TestExecSQLSessionPooling(t *testing.T) {
|
|
dbFile := filepath.Join(t.TempDir(), "exec.db")
|
|
touchFile(t, dbFile)
|
|
id, err := OpenSession(ConnReq{Driver: "sqlite", Database: dbFile})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer CloseSession(id)
|
|
|
|
mk := func(sqlText string) ConnReq {
|
|
c, err := ResolveConn(ConnReq{SessionID: id, SQL: sqlText})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return c
|
|
}
|
|
if r := ExecSQL(context.Background(), mk("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)")); r.Error != "" {
|
|
t.Fatal(r.Error)
|
|
}
|
|
if r := ExecSQL(context.Background(), mk("INSERT INTO t (v) VALUES ('a'),('b'),('c')")); r.Error != "" {
|
|
t.Fatal(r.Error)
|
|
}
|
|
r := ExecSQL(context.Background(), mk("SELECT * FROM t ORDER BY id"))
|
|
if r.Error != "" || r.RowCount != 3 {
|
|
t.Fatalf("查询结果错误: %#v", r)
|
|
}
|
|
|
|
s, err := GetSession(id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.mu.Lock()
|
|
poolSize := len(s.sqlPool)
|
|
s.mu.Unlock()
|
|
if poolSize != 1 {
|
|
t.Fatalf("会话池应仅 1 个句柄,实际 %d", poolSize)
|
|
}
|
|
|
|
// offset 分页:跳过前 1 行取 1 行
|
|
p := mk("SELECT * FROM t ORDER BY id")
|
|
p.Offset = 1
|
|
p.Limit = 1
|
|
r2 := ExecSQL(context.Background(), p)
|
|
if r2.Error != "" || r2.RowCount != 1 {
|
|
t.Fatalf("offset 分页错误: %#v", r2)
|
|
}
|
|
if r2.Rows[0]["v"] != "b" {
|
|
t.Fatalf("offset=1 应返回第 2 行,实际 %#v", r2.Rows[0])
|
|
}
|
|
if r2.Offset != 1 || !r2.Truncated {
|
|
t.Fatalf("分页元数据错误: offset=%d truncated=%v", r2.Offset, r2.Truncated)
|
|
}
|
|
|
|
// 会话路径危险确认:ConfirmDanger 必须经 ConnFor 透传
|
|
d := mk("DROP TABLE t")
|
|
if r := ExecSQL(context.Background(), d); !r.NeedConfirm {
|
|
t.Fatalf("会话路径未确认应拦截: %#v", r)
|
|
}
|
|
d.ConfirmDanger = true
|
|
if r := ExecSQL(context.Background(), d); r.Error != "" || r.NeedConfirm {
|
|
t.Fatalf("会话路径确认后应放行: %#v", r)
|
|
}
|
|
|
|
// 逐请求字段透传:RequestID/TimeoutMs 丢失会让取消/超时在前端(全走会话路径)静默失效
|
|
s2, err := GetSession(id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c2 := s2.ConnFor(ConnReq{Driver: "sqlite", SessionID: id, SQL: "SELECT 1", RequestID: "req-1", TimeoutMs: 5000})
|
|
if c2.RequestID != "req-1" || c2.TimeoutMs != 5000 {
|
|
t.Fatalf("ConnFor 应透传 RequestID/TimeoutMs: %#v", c2)
|
|
}
|
|
}
|
|
|
|
// 危险语句二段流:未确认拦截,确认后放行。
|
|
func TestExecSQLDangerConfirm(t *testing.T) {
|
|
dbFile := filepath.Join(t.TempDir(), "danger.db")
|
|
touchFile(t, dbFile)
|
|
c := ConnReq{Driver: "sqlite", Database: dbFile}
|
|
|
|
r := ExecSQL(context.Background(), ConnReq{Driver: "sqlite", Database: dbFile, SQL: "CREATE TABLE t (id int)"})
|
|
if r.Error != "" {
|
|
t.Fatal(r.Error)
|
|
}
|
|
// 未确认:DROP 不执行
|
|
r = ExecSQL(context.Background(), ConnReq{Driver: "sqlite", Database: dbFile, SQL: "DROP TABLE t"})
|
|
if !r.NeedConfirm {
|
|
t.Fatalf("应 needConfirm: %#v", r)
|
|
}
|
|
if r.Error == "" {
|
|
t.Fatal("应返回命中原因")
|
|
}
|
|
// 表仍在
|
|
r = ExecSQL(context.Background(), ConnReq{Driver: "sqlite", Database: dbFile, SQL: "SELECT count(*) AS n FROM t"})
|
|
if r.Error != "" {
|
|
t.Fatalf("DROP 不应已执行: %s", r.Error)
|
|
}
|
|
// 确认后放行
|
|
c.SQL = "DROP TABLE t"
|
|
c.ConfirmDanger = true
|
|
r = ExecSQL(context.Background(), c)
|
|
if r.Error != "" || r.NeedConfirm {
|
|
t.Fatalf("确认后应放行: %#v", r)
|
|
}
|
|
// 表已删
|
|
r = ExecSQL(context.Background(), ConnReq{Driver: "sqlite", Database: dbFile, SQL: "SELECT count(*) FROM t"})
|
|
if r.Error == "" {
|
|
t.Fatal("确认后 DROP 应已生效")
|
|
}
|
|
}
|
|
|
|
// bigint 精度:超 2^53 的 int64 以字符串无损返回。
|
|
func TestExecSQLBigint(t *testing.T) {
|
|
dbFile := filepath.Join(t.TempDir(), "bigint.db")
|
|
touchFile(t, dbFile)
|
|
mk := func(sqlText string) ConnReq {
|
|
return ConnReq{Driver: "sqlite", Database: dbFile, SQL: sqlText}
|
|
}
|
|
if r := ExecSQL(context.Background(), mk("CREATE TABLE b (id INTEGER PRIMARY KEY)")); r.Error != "" {
|
|
t.Fatal(r.Error)
|
|
}
|
|
if r := ExecSQL(context.Background(), mk("INSERT INTO b (id) VALUES (9223372036854775807)")); r.Error != "" {
|
|
t.Fatal(r.Error)
|
|
}
|
|
r := ExecSQL(context.Background(), mk("SELECT id FROM b"))
|
|
if r.Error != "" {
|
|
t.Fatal(r.Error)
|
|
}
|
|
if r.Rows[0]["id"] != "9223372036854775807" {
|
|
t.Fatalf("bigint 应字符串保真: %#v", r.Rows[0]["id"])
|
|
}
|
|
}
|
|
|
|
// EXPLAIN:sqlite 行列计划非空。
|
|
func TestExplainSQLite(t *testing.T) {
|
|
dbFile := filepath.Join(t.TempDir(), "explain.db")
|
|
touchFile(t, dbFile)
|
|
c := ConnReq{Driver: "sqlite", Database: dbFile, SQL: "CREATE TABLE e (id int)"}
|
|
if r := ExecSQL(context.Background(), c); r.Error != "" {
|
|
t.Fatal(r.Error)
|
|
}
|
|
r := ExplainSQL(context.Background(), ConnReq{Driver: "sqlite", Database: dbFile, SQL: "SELECT * FROM e"})
|
|
if r.Error != "" {
|
|
t.Fatal(r.Error)
|
|
}
|
|
if len(r.Rows) == 0 {
|
|
t.Fatal("sqlite EXPLAIN QUERY PLAN 应返回行列计划")
|
|
}
|
|
}
|
|
|
|
// SQLite 打开不存在文件应报错而非静默建空库。
|
|
func TestSQLiteMissingFile(t *testing.T) {
|
|
_, err := Open(ConnReq{Driver: "sqlite", Database: filepath.Join(t.TempDir(), "not-exist", "x.db")})
|
|
if err == nil {
|
|
t.Fatal("不存在的 sqlite 路径应报错")
|
|
}
|
|
}
|