// Copyright (C) 2020 The Syncthing Authors. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. package main import ( "database/sql" "database/sql/driver" "encoding/json" "errors" "log" "strings" "github.com/lib/pq" "github.com/syncthing/syncthing/lib/ur/contract" ) func migrate(db *sql.DB) error { var count uint64 log.Println("Checking old table row count, this might take a while...") if err := db.QueryRow(`SELECT COUNT(1) FROM Reports`).Scan(&count); err != nil || count == 0 { // err != nil most likely means table does not exist. return nil } log.Printf("Found %d records, will perform migration.", count) tx, err := db.Begin() if err != nil { log.Println("sql:", err) return err } defer tx.Rollback() // These must be lower case, because we don't quote them when creating, so postgres creates them lower case. // Yet pg.CopyIn quotes them, which makes them case sensitive. stmt, err := tx.Prepare(pq.CopyIn("reportsjson", "received", "report")) if err != nil { log.Println("sql:", err) return err } // Custom types used in the old struct. var rep contract.Report var rescanIntvs pq.Int64Array var fsWatcherDelay pq.Int64Array pullOrder := make(IntMap) fileSystemType := make(IntMap) themes := make(IntMap) transportStats := make(IntMap) rows, err := db.Query(`SELECT ` + strings.Join(rep.FieldNames(), ", ") + `, FolderFsWatcherDelays, RescanIntvs, FolderPullOrder, FolderFilesystemType, GUITheme, Transport FROM Reports`) if err != nil { log.Println("sql:", err) return err } defer rows.Close() var done uint64 pct := count / 100 for rows.Next() { err := rows.Scan(append(rep.FieldPointers(), &fsWatcherDelay, &rescanIntvs, &pullOrder, &fileSystemType, &themes, &transportStats)...) if err != nil { log.Println("sql scan:", err) return err } // Patch up parts that used to use custom types rep.RescanIntvs = make([]int, len(rescanIntvs)) for i := range rescanIntvs { rep.RescanIntvs[i] = int(rescanIntvs[i]) } rep.FolderUsesV3.FsWatcherDelays = make([]int, len(fsWatcherDelay)) for i := range fsWatcherDelay { rep.FolderUsesV3.FsWatcherDelays[i] = int(fsWatcherDelay[i]) } rep.FolderUsesV3.PullOrder = pullOrder rep.FolderUsesV3.FilesystemType = fileSystemType rep.GUIStats.Theme = themes rep.TransportStats = transportStats _, err = stmt.Exec(rep.Received, rep) if err != nil { log.Println("sql insert:", err) return err } done++ if done%pct == 0 { log.Printf("Migration progress %d/%d (%d%%)", done, count, (100*done)/count) } } // Tell the driver bulk copy is finished _, err = stmt.Exec() if err != nil { log.Println("sql stmt exec:", err) return err } err = stmt.Close() if err != nil { log.Println("sql stmt close:", err) return err } _, err = tx.Exec("DROP TABLE Reports") if err != nil { log.Println("sql drop:", err) return err } err = tx.Commit() if err != nil { log.Println("sql commit:", err) return err } return nil } type IntMap map[string]int func (p IntMap) Value() (driver.Value, error) { return json.Marshal(p) } func (p *IntMap) Scan(src interface{}) error { source, ok := src.([]byte) if !ok { return errors.New("Type assertion .([]byte) failed.") } var i map[string]int err := json.Unmarshal(source, &i) if err != nil { return err } *p = i return nil }