dump_sqlite.go 5.4 KB

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