mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-11-09 00:22:38 -06:00
SQLite and MySQL dumps store bools as 0/1. Since switching to pgx, restoring such a dump into PostgreSQL fails with "unable to encode 0 into binary format for bool".
207 lines
5.6 KiB
Go
207 lines
5.6 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package db
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.vikunja.io/api/pkg/log"
|
|
|
|
"xorm.io/xorm/schemas"
|
|
)
|
|
|
|
var validTableName = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
|
|
|
// Formats a dumped time value may be in, depending on the database and driver the dump was created with.
|
|
var dumpTimeFormats = []string{
|
|
time.RFC3339Nano,
|
|
"2006-01-02 15:04:05.999999999Z07:00",
|
|
"2006-01-02 15:04:05.999999999",
|
|
"2006-01-02T15:04:05.999999999",
|
|
"2006-01-02",
|
|
}
|
|
|
|
// parseDumpTime parses a time string from a dump. Values without a timezone are
|
|
// interpreted as UTC since that's what dumps are written in.
|
|
func parseDumpTime(value string) (t time.Time, err error) {
|
|
for _, format := range dumpTimeFormats {
|
|
t, err = time.ParseInLocation(format, value, time.UTC)
|
|
if err == nil {
|
|
return t, nil
|
|
}
|
|
}
|
|
return t, err
|
|
}
|
|
|
|
func validateTableName(table string) error {
|
|
if !validTableName.MatchString(table) {
|
|
return fmt.Errorf("invalid table name: %q", table)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Dump dumps all Vikunja database tables
|
|
func Dump() (data map[string][]byte, err error) {
|
|
tableNames := RegisteredTableNames()
|
|
|
|
data = make(map[string][]byte, len(tableNames))
|
|
for _, name := range tableNames {
|
|
entries := []map[string]interface{}{}
|
|
err := x.Table(name).Find(&entries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data[name], err = json.Marshal(entries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Restore restores a table with all its entries
|
|
func Restore(table string, contents []map[string]interface{}) (err error) {
|
|
if err := validateTableName(table); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := x.IsTableExist(table); err != nil {
|
|
return err
|
|
}
|
|
|
|
meta, err := x.DBMetas()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var metaForCurrentTable *schemas.Table
|
|
for _, m := range meta {
|
|
if m.Name == table {
|
|
metaForCurrentTable = m
|
|
break
|
|
}
|
|
}
|
|
|
|
if metaForCurrentTable == nil {
|
|
log.Fatalf("Could not find table definition for table %s", table)
|
|
}
|
|
|
|
for _, content := range contents {
|
|
for colName, value := range content {
|
|
col := metaForCurrentTable.GetColumn(colName)
|
|
if col == nil {
|
|
log.Warningf("Column %s does not exist in table %s, dropping it from the restored data", colName, table)
|
|
delete(content, colName)
|
|
continue
|
|
}
|
|
|
|
// SQLite and MySQL dump bools as 0/1, which pgx refuses to bind to a bool column.
|
|
if col.SQLType.IsBool() {
|
|
if num, is := value.(float64); is {
|
|
content[colName] = num != 0
|
|
}
|
|
continue
|
|
}
|
|
|
|
strVal, is := value.(string)
|
|
if !is || !col.SQLType.IsTime() {
|
|
continue
|
|
}
|
|
|
|
// Date fields might get restored as 0001-01-01 from null dates. This can have unintended side-effects like
|
|
// users being scheduled for deletion after a restore.
|
|
// To avoid this, we set these dates to nil so that they'll end up as null in the db.
|
|
if strVal == "" || strings.HasPrefix(strVal, "0001-") {
|
|
content[colName] = nil
|
|
continue
|
|
}
|
|
|
|
// Dumps contain dates as RFC3339 strings, which MySQL and MariaDB reject ("Incorrect
|
|
// datetime value"). Convert to time.Time and let the driver format it for the target db.
|
|
t, err := parseDumpTime(strVal)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse time value %q for column %s in table %s: %w", strVal, colName, table, err)
|
|
}
|
|
content[colName] = t
|
|
}
|
|
|
|
if _, err := x.Table(table).Insert(content); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if Type() == schemas.POSTGRES {
|
|
idSequence := table + "_id_seq"
|
|
_, err = x.Query(`SELECT setval('"` + idSequence + `"', COALESCE((SELECT MAX(id) FROM "` + table + `"), 1))`)
|
|
if err != nil {
|
|
log.Warningf("Could not reset id sequence for %s: %s", idSequence, err)
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// RestoreAndTruncate removes all content from the table before restoring it from the contents map
|
|
func RestoreAndTruncate(table string, contents []map[string]interface{}) (err error) {
|
|
if err := validateTableName(table); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := x.IsTableExist(table); err != nil {
|
|
return err
|
|
}
|
|
|
|
if x.Dialect().URI().DBType == schemas.SQLITE {
|
|
if _, err := x.Query(`DELETE FROM "` + table + `"`); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if _, err := x.Query("TRUNCATE TABLE " + x.Quote(table)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return Restore(table, contents)
|
|
}
|
|
|
|
// TruncateAllTables deletes all data from every registered Vikunja table.
|
|
// Used by e2e tests to ensure a clean database state before each test.
|
|
func TruncateAllTables() error {
|
|
for _, name := range RegisteredTableNames() {
|
|
if err := validateTableName(name); err != nil {
|
|
return err
|
|
}
|
|
|
|
if x.Dialect().URI().DBType == schemas.SQLITE {
|
|
if _, err := x.Query(`DELETE FROM "` + name + `"`); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if _, err := x.Query("TRUNCATE TABLE " + x.Quote(name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|