| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- package database
- import (
- "database/sql"
- "fmt"
- "os"
- "strconv"
- "strings"
- "unicode/utf8"
- "gorm.io/driver/sqlite"
- "gorm.io/gorm"
- "gorm.io/gorm/logger"
- )
- // DumpSQLite writes a portable SQL text dump of the SQLite database at srcPath
- // to outPath. The output mirrors the `sqlite3 .dump` format (schema + data +
- // indexes wrapped in a transaction), so it can be rebuilt with RestoreSQLite or
- // loaded by the sqlite3 CLI. The source database is opened read-only in effect
- // and left untouched.
- func DumpSQLite(srcPath, outPath string) error {
- data, err := DumpSQLiteToBytes(srcPath)
- if err != nil {
- return err
- }
- return os.WriteFile(outPath, data, 0o644)
- }
- // DumpSQLiteToBytes builds the same `sqlite3 .dump`-style SQL text as DumpSQLite
- // but returns it in memory, which the panel uses to stream a migration download.
- func DumpSQLiteToBytes(srcPath string) ([]byte, error) {
- if _, err := os.Stat(srcPath); err != nil {
- return nil, fmt.Errorf("source sqlite not found at %s: %w", srcPath, err)
- }
- gdb, err := gorm.Open(sqlite.Open(srcPath), &gorm.Config{Logger: logger.Discard})
- if err != nil {
- return nil, err
- }
- sqlDB, err := gdb.DB()
- if err != nil {
- return nil, err
- }
- defer sqlDB.Close()
- var b strings.Builder
- b.WriteString("PRAGMA foreign_keys=OFF;\n")
- b.WriteString("BEGIN TRANSACTION;\n")
- // Tables in creation order, each followed by its data.
- type object struct{ name, ddl string }
- var tables []object
- 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`)
- if err != nil {
- return nil, err
- }
- for rows.Next() {
- var o object
- if err := rows.Scan(&o.name, &o.ddl); err != nil {
- rows.Close()
- return nil, err
- }
- tables = append(tables, o)
- }
- if err := rows.Err(); err != nil {
- rows.Close()
- return nil, err
- }
- rows.Close()
- for _, t := range tables {
- b.WriteString(t.ddl)
- b.WriteString(";\n")
- if err := dumpTableData(sqlDB, t.name, &b); err != nil {
- return nil, err
- }
- }
- // AUTOINCREMENT bookkeeping, restored verbatim like the sqlite3 CLI does.
- if sqliteTableExists(sqlDB, "sqlite_sequence") {
- b.WriteString("DELETE FROM sqlite_sequence;\n")
- if err := dumpTableData(sqlDB, "sqlite_sequence", &b); err != nil {
- return nil, err
- }
- }
- // Indexes, triggers and views after the data is in place.
- rows2, err := sqlDB.Query(`SELECT sql FROM sqlite_master WHERE type IN ('index','trigger','view') AND sql IS NOT NULL ORDER BY rowid`)
- if err != nil {
- return nil, err
- }
- for rows2.Next() {
- var ddl string
- if err := rows2.Scan(&ddl); err != nil {
- rows2.Close()
- return nil, err
- }
- b.WriteString(ddl)
- b.WriteString(";\n")
- }
- if err := rows2.Err(); err != nil {
- rows2.Close()
- return nil, err
- }
- rows2.Close()
- b.WriteString("COMMIT;\n")
- return []byte(b.String()), nil
- }
- // RestoreSQLite rebuilds a SQLite database at dstPath from a SQL text dump
- // produced by DumpSQLite (or `sqlite3 .dump`). dstPath must not already exist so
- // an existing database is never clobbered silently.
- func RestoreSQLite(dumpPath, dstPath string) error {
- script, err := os.ReadFile(dumpPath)
- if err != nil {
- return err
- }
- if _, err := os.Stat(dstPath); err == nil {
- return fmt.Errorf("destination already exists: %s", dstPath)
- }
- gdb, err := gorm.Open(sqlite.Open(dstPath), &gorm.Config{Logger: logger.Discard})
- if err != nil {
- return err
- }
- sqlDB, err := gdb.DB()
- if err != nil {
- return err
- }
- // mattn/go-sqlite3 executes every statement in a multi-statement string.
- if _, err := sqlDB.Exec(string(script)); err != nil {
- sqlDB.Close()
- os.Remove(dstPath)
- return fmt.Errorf("restore failed: %w", err)
- }
- return sqlDB.Close()
- }
- // dumpTableData appends one INSERT statement per row of table to b.
- func dumpTableData(db *sql.DB, table string, b *strings.Builder) error {
- rows, err := db.Query(`SELECT * FROM "` + table + `"`)
- if err != nil {
- return err
- }
- defer rows.Close()
- cols, err := rows.Columns()
- if err != nil {
- return err
- }
- n := len(cols)
- prefix := `INSERT INTO "` + table + `" VALUES(`
- for rows.Next() {
- vals := make([]any, n)
- ptrs := make([]any, n)
- for i := range vals {
- ptrs[i] = &vals[i]
- }
- if err := rows.Scan(ptrs...); err != nil {
- return err
- }
- b.WriteString(prefix)
- for i, v := range vals {
- if i > 0 {
- b.WriteByte(',')
- }
- b.WriteString(sqliteLiteral(v))
- }
- b.WriteString(");\n")
- }
- return rows.Err()
- }
- // sqliteLiteral renders a scanned column value as a SQLite SQL literal.
- func sqliteLiteral(v any) string {
- switch x := v.(type) {
- case nil:
- return "NULL"
- case int64:
- return strconv.FormatInt(x, 10)
- case float64:
- return strconv.FormatFloat(x, 'g', -1, 64)
- case bool:
- if x {
- return "1"
- }
- return "0"
- case string:
- return quoteSQLiteText(x)
- case []byte:
- if utf8.Valid(x) {
- return quoteSQLiteText(string(x))
- }
- var sb strings.Builder
- sb.WriteString("X'")
- for _, c := range x {
- fmt.Fprintf(&sb, "%02x", c)
- }
- sb.WriteByte('\'')
- return sb.String()
- default:
- return quoteSQLiteText(fmt.Sprintf("%v", x))
- }
- }
- func quoteSQLiteText(s string) string {
- return "'" + strings.ReplaceAll(s, "'", "''") + "'"
- }
- func sqliteTableExists(db *sql.DB, name string) bool {
- var found string
- err := db.QueryRow(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, name).Scan(&found)
- return err == nil
- }
|