dump_sqlite.go 5.3 KB

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