feat: 实现 SQLite 迁移备份、完整表结构与简单写入队列
This commit is contained in:
+103
-9
@@ -10,29 +10,105 @@ import (
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// OpenWriter 按 DEVELOPMENT 7.7 打开写连接(带 _txlock=immediate)。
|
||||
const dbFileName = "nixmsg.db"
|
||||
|
||||
// DB 持有读写连接与写入队列。
|
||||
type DB struct {
|
||||
Write *sql.DB
|
||||
Read *sql.DB
|
||||
Queue *Queue
|
||||
}
|
||||
|
||||
// Open 打开数据目录下的库:写连接、读连接池、迁移,并启动简单写入队列。
|
||||
// 若 nixmsg.db 尚不存在则跳过迁移前备份。
|
||||
func Open(dataDir, synchronous string) (*DB, error) {
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("mkdir data_dir: %w", err)
|
||||
}
|
||||
dbPath := filepath.Join(dataDir, dbFileName)
|
||||
existed, err := fileExists(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
write, err := OpenWriter(dataDir, synchronous)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if migErr := Migrate(write, dataDir, existed); migErr != nil {
|
||||
_ = write.Close()
|
||||
return nil, migErr
|
||||
}
|
||||
read, err := OpenReader(dataDir, synchronous)
|
||||
if err != nil {
|
||||
_ = write.Close()
|
||||
return nil, err
|
||||
}
|
||||
q := NewQueue(write)
|
||||
return &DB{Write: write, Read: read, Queue: q}, nil
|
||||
}
|
||||
|
||||
// Close 停止写入队列并关闭连接。
|
||||
func (d *DB) Close() error {
|
||||
var first error
|
||||
if d.Queue != nil {
|
||||
if err := d.Queue.Close(); err != nil {
|
||||
first = err
|
||||
}
|
||||
}
|
||||
if d.Read != nil {
|
||||
if err := d.Read.Close(); err != nil && first == nil {
|
||||
first = err
|
||||
}
|
||||
}
|
||||
if d.Write != nil {
|
||||
if err := d.Write.Close(); err != nil && first == nil {
|
||||
first = err
|
||||
}
|
||||
}
|
||||
return first
|
||||
}
|
||||
|
||||
// OpenWriter 按 DEVELOPMENT 7.7 打开写连接(带 _txlock=immediate,MaxOpenConns=1)。
|
||||
func OpenWriter(dataDir, synchronous string) (*sql.DB, error) {
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("mkdir data_dir: %w", err)
|
||||
}
|
||||
dsn, err := writeDSN(dataDir, synchronous)
|
||||
dsn, err := buildDSN(dataDir, synchronous, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open sqlite: %w", err)
|
||||
return nil, fmt.Errorf("open sqlite writer: %w", err)
|
||||
}
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
if err := db.Ping(); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, fmt.Errorf("ping sqlite: %w", err)
|
||||
return nil, fmt.Errorf("ping sqlite writer: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func writeDSN(dataDir, synchronous string) (string, error) {
|
||||
// OpenReader 打开读连接池(DSN 不含 _txlock=immediate)。
|
||||
func OpenReader(dataDir, synchronous string) (*sql.DB, error) {
|
||||
dsn, err := buildDSN(dataDir, synchronous, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open sqlite reader: %w", err)
|
||||
}
|
||||
if err := db.Ping(); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, fmt.Errorf("ping sqlite reader: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func buildDSN(dataDir, synchronous string, writer bool) (string, error) {
|
||||
sync := strings.ToUpper(strings.TrimSpace(synchronous))
|
||||
if sync == "" {
|
||||
sync = "FULL"
|
||||
@@ -40,10 +116,28 @@ func writeDSN(dataDir, synchronous string) (string, error) {
|
||||
if sync != "FULL" && sync != "NORMAL" {
|
||||
return "", fmt.Errorf("invalid sqlite_synchronous: %s", synchronous)
|
||||
}
|
||||
dbPath := filepath.ToSlash(filepath.Join(dataDir, "nixmsg.db"))
|
||||
return fmt.Sprintf(
|
||||
"file:%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=synchronous(%s)&_pragma=foreign_keys(ON)&_pragma=secure_delete(ON)&_txlock=immediate",
|
||||
dbPath := filepath.ToSlash(filepath.Join(dataDir, dbFileName))
|
||||
dsn := fmt.Sprintf(
|
||||
"file:%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=synchronous(%s)&_pragma=foreign_keys(ON)&_pragma=secure_delete(ON)",
|
||||
dbPath,
|
||||
sync,
|
||||
), nil
|
||||
)
|
||||
if writer {
|
||||
dsn += "&_txlock=immediate"
|
||||
}
|
||||
return dsn, nil
|
||||
}
|
||||
|
||||
func fileExists(path string) (bool, error) {
|
||||
st, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if st.IsDir() {
|
||||
return false, fmt.Errorf("db path is a directory: %s", path)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user