Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command to recreate tables #12407

Merged
merged 35 commits into from
Sep 6, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f1e15e3
Add command to recreate tables
zeripath Aug 1, 2020
05d551c
placate lint
zeripath Aug 2, 2020
bb7f07e
ensure that current engine is closed
zeripath Aug 2, 2020
129e4ce
Quote the table name
zeripath Aug 2, 2020
3cfbd35
Apply suggestions from code review
zeripath Aug 2, 2020
60479f8
Let's see if mssql will tolerate this...
zeripath Aug 2, 2020
007afa9
turns out the issue is the double underscore
zeripath Aug 2, 2020
82cf751
Update models/migrations/migrations.go
zeripath Aug 3, 2020
3e79034
let us go overboard with quotes
zeripath Aug 3, 2020
0b041ca
Seriously MSSQL?
zeripath Aug 3, 2020
214ed20
Merge remote-tracking branch 'origin/master' into doctor-recreate-tables
zeripath Aug 3, 2020
d708602
Add some logging
zeripath Aug 3, 2020
8ab1573
Finally fix MSSQL
zeripath Aug 3, 2020
d75cd7e
Merge branch 'master' into doctor-recreate-tables
zeripath Aug 5, 2020
b78d35e
Merge remote-tracking branch 'origin/master' into doctor-recreate-tables
zeripath Aug 7, 2020
6ae198f
as per @silverwind
zeripath Aug 7, 2020
2c5a3d2
Merge remote-tracking branch 'origin/master' into doctor-recreate-tables
zeripath Aug 10, 2020
aaa2131
Ensure that the indexes are recreated too
zeripath Aug 10, 2020
2a39249
shorter prefix
zeripath Aug 11, 2020
69c8ff9
use the innodb storeengine
zeripath Aug 11, 2020
9e9f2c6
Merge branch 'master' into doctor-recreate-tables
zeripath Aug 29, 2020
fb015ca
remove extra ensureuptodate
zeripath Aug 29, 2020
25d886c
Add debug flag
zeripath Aug 29, 2020
9e6f5d2
drop and recreate indices in sqlite and postgres
zeripath Sep 2, 2020
b31027b
Merge remote-tracking branch 'origin/master' into doctor-recreate-tables
zeripath Sep 2, 2020
fe9d2c6
Merge remote-tracking branch 'origin/master' into doctor-recreate-tables
zeripath Sep 2, 2020
6562ddc
fix postgres index relabelling
zeripath Sep 2, 2020
3117b01
Merge remote-tracking branch 'origin/master' into doctor-recreate-tables
zeripath Sep 2, 2020
c838ee7
Merge remote-tracking branch 'origin/master' into doctor-recreate-tables
zeripath Sep 3, 2020
9202bcd
Merge branch 'master' into doctor-recreate-tables
zeripath Sep 3, 2020
0c49f33
Merge branch 'master' into doctor-recreate-tables
zeripath Sep 3, 2020
96621ed
Merge remote-tracking branch 'origin/master' into doctor-recreate-tables
zeripath Sep 6, 2020
0eade14
close db between recreating tables
zeripath Sep 6, 2020
0374251
Merge branch 'master' into doctor-recreate-tables
zeripath Sep 6, 2020
29a753e
Merge branch 'master' into doctor-recreate-tables
zeripath Sep 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions cmd/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ var CmdDoctor = cli.Command{
Usage: `Name of the log file (default: "doctor.log"). Set to "-" to output to stdout, set to "" to disable`,
},
},
Subcommands: []cli.Command{
cmdRecreateTable,
},
}

var cmdRecreateTable = cli.Command{
Name: "recreate-table",
Usage: "Recreate tables from XORM definitions and copy the data.",
ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns.

This command will cause Xorm to recreate tables, copying over the data and deleting the old table.

You should back-up your database before doing this and ensure that your database is up-to-date first.`,
Action: runRecreateTable,
}

type check struct {
Expand Down Expand Up @@ -129,6 +144,37 @@ var checklist = []check{
// more checks please append here
}

func runRecreateTable(ctx *cli.Context) error {
// Redirect the default golog to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))

setting.EnableXORMLog = false
if err := initDBDisableConsole(true); err != nil {
fmt.Println(err)
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
return nil
}

if err := models.NewEngine(context.Background(), migrations.EnsureUpToDate); err != nil {
return err
}

args := ctx.Args()
names := make([]string, 0, ctx.NArg())
for i := 0; i < ctx.NArg(); i++ {
names = append(names, args.Get(i))
}

beans, err := models.NamesToBean(names...)
if err != nil {
return err
}

return models.NewEngine(context.Background(), migrations.RecreateTables(beans...))
}

func runDoctor(ctx *cli.Context) error {

// Silence the default loggers
Expand Down
30 changes: 30 additions & 0 deletions docs/content/doc/usage/command-line.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,36 @@ var checklist = []check{

This function will receive a command line context and return a list of details about the problems or error.

##### doctor recreate-table

Sometimes when there are migrations the old columns and default values may be left
unchanged in the database schema. This may lead to warning such as:

```
2020/08/02 11:32:29 ...rm/session_schema.go:360:Sync2() [W] Table user Column keep_activity_private db default is , struct default is 0
```

You can cause Gitea to recreate these tables and copy the old data into the new table
with the defaults set appropriately by using:

```
gitea doctor recreate-table user
```

You can ask gitea to recreate multiple tables using:

```
gitea doctor recreate-table table1 table2 ...
```

And if you would like Gitea to recreate all tables simply call:

```
gitea doctor recreate-table
```

It is highly recommended to back-up your database before running these commands.

#### manager

Manage running server operations:
Expand Down
13 changes: 13 additions & 0 deletions integrations/migration-test/migration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,19 @@ func doMigrationTest(t *testing.T, version string) {
err = models.NewEngine(context.Background(), wrappedMigrate)
assert.NoError(t, err)
currentEngine.Close()

err = models.SetEngine()
assert.NoError(t, err)

beans, _ := models.NamesToBean()

err = models.NewEngine(context.Background(), func(x *xorm.Engine) error {
currentEngine = x
return migrations.RecreateTables(beans...)(x)
})
assert.NoError(t, err)

currentEngine.Close()
}

func TestMigrations(t *testing.T) {
Expand Down
162 changes: 162 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package migrations

import (
"fmt"
"reflect"
"regexp"
"strings"

Expand Down Expand Up @@ -319,6 +320,167 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
return nil
}

// RecreateTables will recreate the tables for the provided beans using the newly provided bean definition and move all data to that new table
// WARNING: YOU MUST PROVIDE THE FULL BEAN DEFINITION
func RecreateTables(beans ...interface{}) func(*xorm.Engine) error {
return func(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
for _, bean := range beans {
log.Info("Recreating Table: %s for Bean: %s", x.TableName(bean), reflect.Indirect(reflect.ValueOf(bean)).Type().Name())
if err := recreateTable(sess, bean); err != nil {
return err
}
}
return sess.Commit()
}
}

// recreateTable will recreate the table using the newly provided bean definition and move all data to that new table
// WARNING: YOU MUST PROVIDE THE FULL BEAN DEFINITION
// WARNING: YOU MUST COMMIT THE SESSION AT THE END
func recreateTable(sess *xorm.Session, bean interface{}) error {
// TODO: This will not work if there are foreign keys

tableName := sess.Engine().TableName(bean)
tempTableName := fmt.Sprintf("tempzzz__%s__zztemp", tableName)
zeripath marked this conversation as resolved.
Show resolved Hide resolved

// We need to move the old table away and create a new one with the correct columns
// We will need to do this in stages to prevent data loss
//
// First create the temporary table
if err := sess.Table(tempTableName).CreateTable(bean); err != nil {
log.Error("Unable to create table %s. Error: %v", tempTableName, err)
return err
}

// Work out the column names from the bean - these are the columns to select from the old table and install into the new table
table, err := sess.Engine().TableInfo(bean)
if err != nil {
log.Error("Unable to get table info. Error: %v", err)

return err
}
newTableColumns := table.Columns()
if len(newTableColumns) == 0 {
return fmt.Errorf("no columns in new table")
}
hasID := false
for _, column := range newTableColumns {
hasID = hasID || (column.IsPrimaryKey && column.IsAutoIncrement)
}

if hasID && setting.Database.UseMSSQL {
if _, err := sess.Exec(fmt.Sprintf("SET IDENTITY_INSERT `%s` ON", tempTableName)); err != nil {
log.Error("Unable to set identity insert for table %s. Error: %v", tempTableName, err)
return err
}
}

sqlStringBuilder := &strings.Builder{}
_, _ = sqlStringBuilder.WriteString("INSERT INTO `")
_, _ = sqlStringBuilder.WriteString(tempTableName)
_, _ = sqlStringBuilder.WriteString("` (`")
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name)
_, _ = sqlStringBuilder.WriteString("`")
for _, column := range newTableColumns[1:] {
_, _ = sqlStringBuilder.WriteString(", `")
_, _ = sqlStringBuilder.WriteString(column.Name)
_, _ = sqlStringBuilder.WriteString("`")
}
_, _ = sqlStringBuilder.WriteString(")")
_, _ = sqlStringBuilder.WriteString(" SELECT ")
if newTableColumns[0].Default != "" {
_, _ = sqlStringBuilder.WriteString("COALESCE(`")
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name)
_, _ = sqlStringBuilder.WriteString("`, ")
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Default)
_, _ = sqlStringBuilder.WriteString(")")
} else {
_, _ = sqlStringBuilder.WriteString("`")
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name)
_, _ = sqlStringBuilder.WriteString("`")
}

for _, column := range newTableColumns[1:] {
if column.Default != "" {
_, _ = sqlStringBuilder.WriteString(", COALESCE(`")
_, _ = sqlStringBuilder.WriteString(column.Name)
_, _ = sqlStringBuilder.WriteString("`, ")
_, _ = sqlStringBuilder.WriteString(column.Default)
_, _ = sqlStringBuilder.WriteString(")")
} else {
_, _ = sqlStringBuilder.WriteString(", `")
_, _ = sqlStringBuilder.WriteString(column.Name)
_, _ = sqlStringBuilder.WriteString("`")
}
}
_, _ = sqlStringBuilder.WriteString(" FROM `")
_, _ = sqlStringBuilder.WriteString(tableName)
_, _ = sqlStringBuilder.WriteString("`")

if _, err := sess.Exec(sqlStringBuilder.String()); err != nil {
log.Error("Unable to set copy data in to temp table %s. Error: %v", tempTableName, err)
return err
}

if hasID && setting.Database.UseMSSQL {
if _, err := sess.Exec(fmt.Sprintf("SET IDENTITY_INSERT `%s` OFF", tempTableName)); err != nil {
log.Error("Unable to switch off identity insert for table %s. Error: %v", tempTableName, err)
return err
}
}

switch {
case setting.Database.UseSQLite3:
fallthrough
case setting.Database.UseMySQL:
// SQLite and MySQL will drop all the constraints on the old table
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil {
log.Error("Unable to drop old table %s. Error: %v", tableName, err)
return err
}

// SQLite and MySQL will move all the constraints from the temporary table to the new table
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tempTableName, tableName)); err != nil {
log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err)
return err
}
case setting.Database.UsePostgreSQL:
// CASCADE causes postgres to drop all the constraints on the old table
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s` CASCADE", tableName)); err != nil {
log.Error("Unable to drop old table %s. Error: %v", tableName, err)
return err
}

// CASCADE causes postgres to move all the constraints from the temporary table to the new table
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tempTableName, tableName)); err != nil {
log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err)
return err
}
case setting.Database.UseMSSQL:
// MSSQL will drop all the constraints on the old table
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil {
log.Error("Unable to drop old table %s. Error: %v", tableName, err)
return err
}

// MSSQL sp_rename will move all the constraints from the temporary table to the new table
if _, err := sess.Exec(fmt.Sprintf("sp_rename `%s`,`%s`", tempTableName, tableName)); err != nil {
log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err)
return err
}

default:
log.Fatal("Unrecognized DB")
}
return nil
}

// WARNING: YOU MUST COMMIT THE SESSION AT THE END
func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...string) (err error) {
if tableName == "" || len(columnNames) == 0 {
return nil
Expand Down
5 changes: 4 additions & 1 deletion models/migrations/v102.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ import (
func dropColumnHeadUserNameOnPullRequest(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
return dropTableColumns(sess, "pull_request", "head_user_name")
if err := dropTableColumns(sess, "pull_request", "head_user_name"); err != nil {
return err
}
return sess.Commit()
}
32 changes: 32 additions & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"database/sql"
"errors"
"fmt"
"reflect"
"strings"

"code.gitea.io/gitea/modules/setting"

Expand Down Expand Up @@ -210,6 +212,36 @@ func NewEngine(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err e
return nil
}

// NamesToBean return a list of beans or an error
func NamesToBean(names ...string) ([]interface{}, error) {
beans := []interface{}{}
if len(names) == 0 {
beans = append(beans, tables...)
return beans, nil
}
// Need to map provided names to beans...
beanMap := make(map[string]interface{})
for _, bean := range tables {

beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean
beanMap[strings.ToLower(x.TableName(bean))] = bean
beanMap[strings.ToLower(x.TableName(bean, true))] = bean
}

gotBean := make(map[interface{}]bool)
for _, name := range names {
bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))]
if !ok {
return nil, fmt.Errorf("No table found that matches: %s", name)
}
if !gotBean[bean] {
beans = append(beans, bean)
gotBean[bean] = true
}
}
return beans, nil
}

// Statistic contains the database statistics
type Statistic struct {
Counter struct {
Expand Down