clearlove2.1
1package migrate
2
3import (
4 "database/sql"
5 "os"
6 "strconv"
7 "strings"
8
9 _ "modernc.org/sqlite"
10)
11
12// Row 一行数据:列名 -> 字符串值(NULL 统一映射为空串)
13type Row map[string]string
14
15// Source 旧版数据源。支持两种来源:
16// - SQLite 数据库文件(旧版 data.db)
17// - MySQL 导出文件(mysqldump / 宝塔导出的 .sql)
18type Source interface {
19 // HasTable 数据源中是否存在指定表
20 HasTable(table string) bool
21 // Rows 读取整表,fields 为候选列名(数据源中不存在的列不会出现在结果行里)
22 Rows(table string, fields ...string) ([]Row, error)
23 // Kind 数据源类型(sqlite / mysqldump)
24 Kind() string
25 // Close 释放资源
26 Close() error
27}
28
29// field 从行中按候选列名取第一个非空值
30func field(row Row, names ...string) string {
31 for _, n := range names {
32 if v, ok := row[n]; ok && v != "" {
33 return v
34 }
35 }
36 return ""
37}
38
39// fieldInt 取整数值(缺失或非法时返回 0)
40func fieldInt(row Row, names ...string) int64 {
41 n, _ := strconv.ParseInt(strings.TrimSpace(field(row, names...)), 10, 64)
42 return n
43}
44
45// ---------- SQLite 数据源 ----------
46
47type sqliteSource struct {
48 db *sql.DB
49 has map[string]bool
50 cols map[string]map[string]bool
51}
52
53// openSQLiteSource 打开旧版 SQLite 数据库文件
54func openSQLiteSource(path string) (*sqliteSource, error) {
55 db, err := sql.Open("sqlite", path)
56 if err != nil {
57 return nil, err
58 }
59 if err := db.Ping(); err != nil {
60 db.Close()
61 return nil, err
62 }
63 s := &sqliteSource{db: db, has: map[string]bool{}, cols: map[string]map[string]bool{}}
64 rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table'")
65 if err != nil {
66 db.Close()
67 return nil, err
68 }
69 var tables []string
70 for rows.Next() {
71 var name sql.NullString
72 if rows.Scan(&name) == nil && name.Valid {
73 tables = append(tables, name.String)
74 }
75 }
76 rows.Close()
77 for _, t := range tables {
78 s.has[t] = true
79 s.cols[t] = map[string]bool{}
80 cr, err := db.Query("PRAGMA table_info(" + quoteIdent(t) + ")")
81 if err != nil {
82 continue
83 }
84 for cr.Next() {
85 var cid int
86 var name, ctype string
87 var notnull, pk int
88 var dflt sql.NullString
89 if cr.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk) == nil {
90 s.cols[t][name] = true
91 }
92 }
93 cr.Close()
94 }
95 return s, nil
96}
97
98func (s *sqliteSource) HasTable(table string) bool { return s.has[table] }
99
100func (s *sqliteSource) Kind() string { return "sqlite" }
101
102func (s *sqliteSource) Close() error { return s.db.Close() }
103
104func (s *sqliteSource) Rows(table string, fields ...string) ([]Row, error) {
105 if !s.has[table] {
106 return nil, nil
107 }
108 var exprs []string
109 for _, f := range fields {
110 if s.cols[table][f] {
111 exprs = append(exprs, quoteIdent(f)+" AS "+quoteIdent(f))
112 }
113 }
114 if len(exprs) == 0 {
115 return nil, nil
116 }
117 rows, err := s.db.Query("SELECT " + strings.Join(exprs, ",") + " FROM " + quoteIdent(table))
118 if err != nil {
119 return nil, err
120 }
121 defer rows.Close()
122 names, err := rows.Columns()
123 if err != nil {
124 return nil, err
125 }
126 var out []Row
127 for rows.Next() {
128 vals := make([]sql.NullString, len(names))
129 ptrs := make([]any, len(names))
130 for i := range vals {
131 ptrs[i] = &vals[i]
132 }
133 if err := rows.Scan(ptrs...); err != nil {
134 continue
135 }
136 row := Row{}
137 for i, c := range names {
138 if vals[i].Valid {
139 row[c] = vals[i].String
140 }
141 }
142 out = append(out, row)
143 }
144 return out, nil
145}
146
147// quoteIdent 用双引号包裹标识符(SQLite 兼容)
148func quoteIdent(s string) string {
149 return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
150}
151
152// ---------- 数据源探测 ----------
153
154// OpenSource 按文件内容自动识别数据源类型:SQLite 数据库文件或 MySQL 导出文件
155func OpenSource(path string) (Source, error) {
156 f, err := os.Open(path)
157 if err != nil {
158 return nil, err
159 }
160 head := make([]byte, 16)
161 n, _ := f.Read(head)
162 f.Close()
163 // SQLite 文件头:"SQLite format 3\x00"
164 if n >= 16 && string(head[:15]) == "SQLite format 3" {
165 return openSQLiteSource(path)
166 }
167 data, err := os.ReadFile(path)
168 if err != nil {
169 return nil, err
170 }
171 if len(data) == 0 {
172 return nil, errEmptyFile
173 }
174 return parseDump(data)
175}