1
0

dump_sqlite.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. package database
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "os"
  7. "strconv"
  8. "strings"
  9. "unicode/utf8"
  10. "github.com/mattn/go-sqlite3"
  11. "gorm.io/driver/sqlite"
  12. "gorm.io/gorm"
  13. "gorm.io/gorm/logger"
  14. )
  15. // DumpSQLite writes a portable SQL text dump of the SQLite database at srcPath
  16. // to outPath. The output mirrors the `sqlite3 .dump` format (schema + data +
  17. // indexes wrapped in a transaction), so it can be rebuilt with RestoreSQLite or
  18. // loaded by the sqlite3 CLI. The source database is opened read-only in effect
  19. // and left untouched.
  20. func DumpSQLite(srcPath, outPath string) error {
  21. data, err := DumpSQLiteToBytes(srcPath)
  22. if err != nil {
  23. return err
  24. }
  25. return os.WriteFile(outPath, data, 0o600)
  26. }
  27. // DumpSQLiteToBytes builds the same `sqlite3 .dump`-style SQL text as DumpSQLite
  28. // but returns it in memory, which the panel uses to stream a migration download.
  29. func DumpSQLiteToBytes(srcPath string) ([]byte, error) {
  30. if _, err := os.Stat(srcPath); err != nil {
  31. return nil, fmt.Errorf("source sqlite not found at %s: %w", srcPath, err)
  32. }
  33. gdb, err := gorm.Open(sqlite.Open(srcPath), &gorm.Config{Logger: logger.Discard})
  34. if err != nil {
  35. return nil, err
  36. }
  37. sqlDB, err := gdb.DB()
  38. if err != nil {
  39. return nil, err
  40. }
  41. defer sqlDB.Close()
  42. var b strings.Builder
  43. b.WriteString("PRAGMA foreign_keys=OFF;\n")
  44. b.WriteString("BEGIN TRANSACTION;\n")
  45. // Tables in creation order, each followed by its data.
  46. type object struct{ name, ddl string }
  47. var tables []object
  48. rows, err := sqlDB.QueryContext(context.Background(), `SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND sql IS NOT NULL ORDER BY rowid`)
  49. if err != nil {
  50. return nil, err
  51. }
  52. defer rows.Close()
  53. for rows.Next() {
  54. var o object
  55. if err := rows.Scan(&o.name, &o.ddl); err != nil {
  56. return nil, err
  57. }
  58. tables = append(tables, o)
  59. }
  60. if err := rows.Err(); err != nil {
  61. return nil, err
  62. }
  63. for _, t := range tables {
  64. b.WriteString(t.ddl)
  65. b.WriteString(";\n")
  66. if err := dumpTableData(sqlDB, t.name, &b); err != nil {
  67. return nil, err
  68. }
  69. }
  70. // AUTOINCREMENT bookkeeping, restored verbatim like the sqlite3 CLI does.
  71. if sqliteTableExists(sqlDB, "sqlite_sequence") {
  72. b.WriteString("DELETE FROM sqlite_sequence;\n")
  73. if err := dumpTableData(sqlDB, "sqlite_sequence", &b); err != nil {
  74. return nil, err
  75. }
  76. }
  77. // Indexes, triggers and views after the data is in place.
  78. rows2, err := sqlDB.QueryContext(context.Background(), `SELECT sql FROM sqlite_master WHERE type IN ('index','trigger','view') AND sql IS NOT NULL ORDER BY rowid`)
  79. if err != nil {
  80. return nil, err
  81. }
  82. defer rows2.Close()
  83. for rows2.Next() {
  84. var ddl string
  85. if err := rows2.Scan(&ddl); err != nil {
  86. return nil, err
  87. }
  88. b.WriteString(ddl)
  89. b.WriteString(";\n")
  90. }
  91. if err := rows2.Err(); err != nil {
  92. return nil, err
  93. }
  94. b.WriteString("COMMIT;\n")
  95. return []byte(b.String()), nil
  96. }
  97. // RestoreSQLite rebuilds a SQLite database at dstPath from a SQL text dump
  98. // produced by DumpSQLite (or `sqlite3 .dump`). dstPath must not already exist so
  99. // an existing database is never clobbered silently.
  100. func RestoreSQLite(dumpPath, dstPath string) error {
  101. script, err := os.ReadFile(dumpPath)
  102. if err != nil {
  103. return err
  104. }
  105. if _, err := os.Stat(dstPath); err == nil {
  106. return fmt.Errorf("destination already exists: %s", dstPath)
  107. }
  108. gdb, err := gorm.Open(sqlite.Open(dstPath), &gorm.Config{Logger: logger.Discard})
  109. if err != nil {
  110. return err
  111. }
  112. sqlDB, err := gdb.DB()
  113. if err != nil {
  114. return err
  115. }
  116. if err := replayDump(sqlDB, string(script)); err != nil {
  117. sqlDB.Close()
  118. os.Remove(dstPath)
  119. return fmt.Errorf("restore failed: %w", err)
  120. }
  121. return sqlDB.Close()
  122. }
  123. // replayDump runs the script on one connection that cannot open a second database
  124. // file: ATTACH and VACUUM INTO both attach, and a dump only rebuilds its own tables.
  125. func replayDump(sqlDB *sql.DB, script string) error {
  126. ctx := context.Background()
  127. conn, err := sqlDB.Conn(ctx)
  128. if err != nil {
  129. return err
  130. }
  131. defer conn.Close()
  132. err = conn.Raw(func(driverConn any) error {
  133. sc, ok := driverConn.(*sqlite3.SQLiteConn)
  134. if !ok {
  135. return fmt.Errorf("unexpected sqlite driver connection %T", driverConn)
  136. }
  137. sc.SetLimit(sqlite3.SQLITE_LIMIT_ATTACHED, 0)
  138. return nil
  139. })
  140. if err != nil {
  141. return err
  142. }
  143. // mattn/go-sqlite3 executes every statement in a multi-statement string.
  144. _, err = conn.ExecContext(ctx, script)
  145. return err
  146. }
  147. // dumpTableData appends one INSERT statement per row of table to b.
  148. func dumpTableData(db *sql.DB, table string, b *strings.Builder) error {
  149. rows, err := db.QueryContext(context.Background(), `SELECT * FROM "`+table+`"`)
  150. if err != nil {
  151. return err
  152. }
  153. defer rows.Close()
  154. cols, err := rows.Columns()
  155. if err != nil {
  156. return err
  157. }
  158. n := len(cols)
  159. prefix := `INSERT INTO "` + table + `" VALUES(`
  160. for rows.Next() {
  161. vals := make([]any, n)
  162. ptrs := make([]any, n)
  163. for i := range vals {
  164. ptrs[i] = &vals[i]
  165. }
  166. if err := rows.Scan(ptrs...); err != nil {
  167. return err
  168. }
  169. b.WriteString(prefix)
  170. for i, v := range vals {
  171. if i > 0 {
  172. b.WriteByte(',')
  173. }
  174. b.WriteString(sqliteLiteral(v))
  175. }
  176. b.WriteString(");\n")
  177. }
  178. return rows.Err()
  179. }
  180. // sqliteLiteral renders a scanned column value as a SQLite SQL literal.
  181. func sqliteLiteral(v any) string {
  182. switch x := v.(type) {
  183. case nil:
  184. return "NULL"
  185. case int64:
  186. return strconv.FormatInt(x, 10)
  187. case float64:
  188. return strconv.FormatFloat(x, 'g', -1, 64)
  189. case bool:
  190. if x {
  191. return "1"
  192. }
  193. return "0"
  194. case string:
  195. return quoteSQLiteText(x)
  196. case []byte:
  197. if utf8.Valid(x) {
  198. return quoteSQLiteText(string(x))
  199. }
  200. var sb strings.Builder
  201. sb.WriteString("X'")
  202. for _, c := range x {
  203. fmt.Fprintf(&sb, "%02x", c)
  204. }
  205. sb.WriteByte('\'')
  206. return sb.String()
  207. default:
  208. return quoteSQLiteText(fmt.Sprintf("%v", x))
  209. }
  210. }
  211. func quoteSQLiteText(s string) string {
  212. return "'" + strings.ReplaceAll(s, "'", "''") + "'"
  213. }
  214. func sqliteTableExists(db *sql.DB, name string) bool {
  215. var found string
  216. err := db.QueryRowContext(context.Background(), `SELECT name FROM sqlite_master WHERE type='table' AND name=?`, name).Scan(&found)
  217. return err == nil
  218. }