Skip to content

Commit 1b9d507

Browse files
authored
Add command to recreate tables (#12407)
Provides new command: `gitea doctor recreate-table` which will recreate db tables and copy the old data in to the new table. This function can be used to remove the old warning of struct defaults being out of date. Fix #8868 Fix #3265 Fix #8894 Signed-off-by: Andrew Thornton <[email protected]>
1 parent ad2bf37 commit 1b9d507

File tree

5 files changed

+365
-0
lines changed

5 files changed

+365
-0
lines changed

cmd/doctor.go

+63
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"code.gitea.io/gitea/modules/setting"
2727
"code.gitea.io/gitea/modules/util"
2828
"xorm.io/builder"
29+
"xorm.io/xorm"
2930

3031
"github.com/urfave/cli"
3132
)
@@ -62,6 +63,27 @@ var CmdDoctor = cli.Command{
6263
Usage: `Name of the log file (default: "doctor.log"). Set to "-" to output to stdout, set to "" to disable`,
6364
},
6465
},
66+
Subcommands: []cli.Command{
67+
cmdRecreateTable,
68+
},
69+
}
70+
71+
var cmdRecreateTable = cli.Command{
72+
Name: "recreate-table",
73+
Usage: "Recreate tables from XORM definitions and copy the data.",
74+
ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
75+
Flags: []cli.Flag{
76+
cli.BoolFlag{
77+
Name: "debug",
78+
Usage: "Print SQL commands sent",
79+
},
80+
},
81+
Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns.
82+
83+
This command will cause Xorm to recreate tables, copying over the data and deleting the old table.
84+
85+
You should back-up your database before doing this and ensure that your database is up-to-date first.`,
86+
Action: runRecreateTable,
6587
}
6688

6789
type check struct {
@@ -136,6 +158,47 @@ var checklist = []check{
136158
// more checks please append here
137159
}
138160

161+
func runRecreateTable(ctx *cli.Context) error {
162+
// Redirect the default golog to here
163+
golog.SetFlags(0)
164+
golog.SetPrefix("")
165+
golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
166+
167+
setting.NewContext()
168+
setting.InitDBConfig()
169+
170+
setting.EnableXORMLog = ctx.Bool("debug")
171+
setting.Database.LogSQL = ctx.Bool("debug")
172+
setting.Cfg.Section("log").Key("XORM").SetValue(",")
173+
174+
setting.NewXORMLogService(!ctx.Bool("debug"))
175+
if err := models.SetEngine(); err != nil {
176+
fmt.Println(err)
177+
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
178+
return nil
179+
}
180+
181+
args := ctx.Args()
182+
names := make([]string, 0, ctx.NArg())
183+
for i := 0; i < ctx.NArg(); i++ {
184+
names = append(names, args.Get(i))
185+
}
186+
187+
beans, err := models.NamesToBean(names...)
188+
if err != nil {
189+
return err
190+
}
191+
recreateTables := migrations.RecreateTables(beans...)
192+
193+
return models.NewEngine(context.Background(), func(x *xorm.Engine) error {
194+
if err := migrations.EnsureUpToDate(x); err != nil {
195+
return err
196+
}
197+
return recreateTables(x)
198+
})
199+
200+
}
201+
139202
func runDoctor(ctx *cli.Context) error {
140203

141204
// Silence the default loggers

docs/content/doc/usage/command-line.en-us.md

+30
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,36 @@ var checklist = []check{
319319

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

322+
##### doctor recreate-table
323+
324+
Sometimes when there are migrations the old columns and default values may be left
325+
unchanged in the database schema. This may lead to warning such as:
326+
327+
```
328+
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
329+
```
330+
331+
You can cause Gitea to recreate these tables and copy the old data into the new table
332+
with the defaults set appropriately by using:
333+
334+
```
335+
gitea doctor recreate-table user
336+
```
337+
338+
You can ask gitea to recreate multiple tables using:
339+
340+
```
341+
gitea doctor recreate-table table1 table2 ...
342+
```
343+
344+
And if you would like Gitea to recreate all tables simply call:
345+
346+
```
347+
gitea doctor recreate-table
348+
```
349+
350+
It is highly recommended to back-up your database before running these commands.
351+
322352
#### manager
323353

324354
Manage running server operations:

integrations/migration-test/migration_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,30 @@ func doMigrationTest(t *testing.T, version string) {
258258
err = models.NewEngine(context.Background(), wrappedMigrate)
259259
assert.NoError(t, err)
260260
currentEngine.Close()
261+
262+
err = models.SetEngine()
263+
assert.NoError(t, err)
264+
265+
beans, _ := models.NamesToBean()
266+
267+
err = models.NewEngine(context.Background(), func(x *xorm.Engine) error {
268+
currentEngine = x
269+
return migrations.RecreateTables(beans...)(x)
270+
})
271+
assert.NoError(t, err)
272+
currentEngine.Close()
273+
274+
// We do this a second time to ensure that there is not a problem with retained indices
275+
err = models.SetEngine()
276+
assert.NoError(t, err)
277+
278+
err = models.NewEngine(context.Background(), func(x *xorm.Engine) error {
279+
currentEngine = x
280+
return migrations.RecreateTables(beans...)(x)
281+
})
282+
assert.NoError(t, err)
283+
284+
currentEngine.Close()
261285
}
262286

263287
func TestMigrations(t *testing.T) {

models/migrations/migrations.go

+216
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package migrations
77

88
import (
99
"fmt"
10+
"reflect"
1011
"regexp"
1112
"strings"
1213

@@ -327,6 +328,221 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
327328
return nil
328329
}
329330

331+
// RecreateTables will recreate the tables for the provided beans using the newly provided bean definition and move all data to that new table
332+
// WARNING: YOU MUST PROVIDE THE FULL BEAN DEFINITION
333+
func RecreateTables(beans ...interface{}) func(*xorm.Engine) error {
334+
return func(x *xorm.Engine) error {
335+
sess := x.NewSession()
336+
defer sess.Close()
337+
if err := sess.Begin(); err != nil {
338+
return err
339+
}
340+
sess = sess.StoreEngine("InnoDB")
341+
for _, bean := range beans {
342+
log.Info("Recreating Table: %s for Bean: %s", x.TableName(bean), reflect.Indirect(reflect.ValueOf(bean)).Type().Name())
343+
if err := recreateTable(sess, bean); err != nil {
344+
return err
345+
}
346+
}
347+
return sess.Commit()
348+
}
349+
}
350+
351+
// recreateTable will recreate the table using the newly provided bean definition and move all data to that new table
352+
// WARNING: YOU MUST PROVIDE THE FULL BEAN DEFINITION
353+
// WARNING: YOU MUST COMMIT THE SESSION AT THE END
354+
func recreateTable(sess *xorm.Session, bean interface{}) error {
355+
// TODO: This will not work if there are foreign keys
356+
357+
tableName := sess.Engine().TableName(bean)
358+
tempTableName := fmt.Sprintf("tmp_recreate__%s", tableName)
359+
360+
// We need to move the old table away and create a new one with the correct columns
361+
// We will need to do this in stages to prevent data loss
362+
//
363+
// First create the temporary table
364+
if err := sess.Table(tempTableName).CreateTable(bean); err != nil {
365+
log.Error("Unable to create table %s. Error: %v", tempTableName, err)
366+
return err
367+
}
368+
369+
if err := sess.Table(tempTableName).CreateUniques(bean); err != nil {
370+
log.Error("Unable to create uniques for table %s. Error: %v", tempTableName, err)
371+
return err
372+
}
373+
374+
if err := sess.Table(tempTableName).CreateIndexes(bean); err != nil {
375+
log.Error("Unable to create indexes for table %s. Error: %v", tempTableName, err)
376+
return err
377+
}
378+
379+
// Work out the column names from the bean - these are the columns to select from the old table and install into the new table
380+
table, err := sess.Engine().TableInfo(bean)
381+
if err != nil {
382+
log.Error("Unable to get table info. Error: %v", err)
383+
384+
return err
385+
}
386+
newTableColumns := table.Columns()
387+
if len(newTableColumns) == 0 {
388+
return fmt.Errorf("no columns in new table")
389+
}
390+
hasID := false
391+
for _, column := range newTableColumns {
392+
hasID = hasID || (column.IsPrimaryKey && column.IsAutoIncrement)
393+
}
394+
395+
if hasID && setting.Database.UseMSSQL {
396+
if _, err := sess.Exec(fmt.Sprintf("SET IDENTITY_INSERT `%s` ON", tempTableName)); err != nil {
397+
log.Error("Unable to set identity insert for table %s. Error: %v", tempTableName, err)
398+
return err
399+
}
400+
}
401+
402+
sqlStringBuilder := &strings.Builder{}
403+
_, _ = sqlStringBuilder.WriteString("INSERT INTO `")
404+
_, _ = sqlStringBuilder.WriteString(tempTableName)
405+
_, _ = sqlStringBuilder.WriteString("` (`")
406+
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name)
407+
_, _ = sqlStringBuilder.WriteString("`")
408+
for _, column := range newTableColumns[1:] {
409+
_, _ = sqlStringBuilder.WriteString(", `")
410+
_, _ = sqlStringBuilder.WriteString(column.Name)
411+
_, _ = sqlStringBuilder.WriteString("`")
412+
}
413+
_, _ = sqlStringBuilder.WriteString(")")
414+
_, _ = sqlStringBuilder.WriteString(" SELECT ")
415+
if newTableColumns[0].Default != "" {
416+
_, _ = sqlStringBuilder.WriteString("COALESCE(`")
417+
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name)
418+
_, _ = sqlStringBuilder.WriteString("`, ")
419+
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Default)
420+
_, _ = sqlStringBuilder.WriteString(")")
421+
} else {
422+
_, _ = sqlStringBuilder.WriteString("`")
423+
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name)
424+
_, _ = sqlStringBuilder.WriteString("`")
425+
}
426+
427+
for _, column := range newTableColumns[1:] {
428+
if column.Default != "" {
429+
_, _ = sqlStringBuilder.WriteString(", COALESCE(`")
430+
_, _ = sqlStringBuilder.WriteString(column.Name)
431+
_, _ = sqlStringBuilder.WriteString("`, ")
432+
_, _ = sqlStringBuilder.WriteString(column.Default)
433+
_, _ = sqlStringBuilder.WriteString(")")
434+
} else {
435+
_, _ = sqlStringBuilder.WriteString(", `")
436+
_, _ = sqlStringBuilder.WriteString(column.Name)
437+
_, _ = sqlStringBuilder.WriteString("`")
438+
}
439+
}
440+
_, _ = sqlStringBuilder.WriteString(" FROM `")
441+
_, _ = sqlStringBuilder.WriteString(tableName)
442+
_, _ = sqlStringBuilder.WriteString("`")
443+
444+
if _, err := sess.Exec(sqlStringBuilder.String()); err != nil {
445+
log.Error("Unable to set copy data in to temp table %s. Error: %v", tempTableName, err)
446+
return err
447+
}
448+
449+
if hasID && setting.Database.UseMSSQL {
450+
if _, err := sess.Exec(fmt.Sprintf("SET IDENTITY_INSERT `%s` OFF", tempTableName)); err != nil {
451+
log.Error("Unable to switch off identity insert for table %s. Error: %v", tempTableName, err)
452+
return err
453+
}
454+
}
455+
456+
switch {
457+
case setting.Database.UseSQLite3:
458+
// SQLite will drop all the constraints on the old table
459+
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil {
460+
log.Error("Unable to drop old table %s. Error: %v", tableName, err)
461+
return err
462+
}
463+
464+
if err := sess.Table(tempTableName).DropIndexes(bean); err != nil {
465+
log.Error("Unable to drop indexes on temporary table %s. Error: %v", tempTableName, err)
466+
return err
467+
}
468+
469+
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tempTableName, tableName)); err != nil {
470+
log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err)
471+
return err
472+
}
473+
474+
if err := sess.Table(tableName).CreateIndexes(bean); err != nil {
475+
log.Error("Unable to recreate indexes on table %s. Error: %v", tableName, err)
476+
return err
477+
}
478+
479+
if err := sess.Table(tableName).CreateUniques(bean); err != nil {
480+
log.Error("Unable to recreate uniques on table %s. Error: %v", tableName, err)
481+
return err
482+
}
483+
484+
case setting.Database.UseMySQL:
485+
// MySQL will drop all the constraints on the old table
486+
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil {
487+
log.Error("Unable to drop old table %s. Error: %v", tableName, err)
488+
return err
489+
}
490+
491+
// SQLite and MySQL will move all the constraints from the temporary table to the new table
492+
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tempTableName, tableName)); err != nil {
493+
log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err)
494+
return err
495+
}
496+
case setting.Database.UsePostgreSQL:
497+
// CASCADE causes postgres to drop all the constraints on the old table
498+
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s` CASCADE", tableName)); err != nil {
499+
log.Error("Unable to drop old table %s. Error: %v", tableName, err)
500+
return err
501+
}
502+
503+
// CASCADE causes postgres to move all the constraints from the temporary table to the new table
504+
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tempTableName, tableName)); err != nil {
505+
log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err)
506+
return err
507+
}
508+
509+
var indices []string
510+
schema := sess.Engine().Dialect().URI().Schema
511+
sess.Engine().SetSchema("")
512+
if err := sess.Table("pg_indexes").Cols("indexname").Where("tablename = ? ", tableName).Find(&indices); err != nil {
513+
log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err)
514+
return err
515+
}
516+
sess.Engine().SetSchema(schema)
517+
518+
for _, index := range indices {
519+
newIndexName := strings.Replace(index, "tmp_recreate__", "", 1)
520+
if _, err := sess.Exec(fmt.Sprintf("ALTER INDEX `%s` RENAME TO `%s`", index, newIndexName)); err != nil {
521+
log.Error("Unable to rename %s to %s. Error: %v", index, newIndexName, err)
522+
return err
523+
}
524+
}
525+
526+
case setting.Database.UseMSSQL:
527+
// MSSQL will drop all the constraints on the old table
528+
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil {
529+
log.Error("Unable to drop old table %s. Error: %v", tableName, err)
530+
return err
531+
}
532+
533+
// MSSQL sp_rename will move all the constraints from the temporary table to the new table
534+
if _, err := sess.Exec(fmt.Sprintf("sp_rename `%s`,`%s`", tempTableName, tableName)); err != nil {
535+
log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err)
536+
return err
537+
}
538+
539+
default:
540+
log.Fatal("Unrecognized DB")
541+
}
542+
return nil
543+
}
544+
545+
// WARNING: YOU MUST COMMIT THE SESSION AT THE END
330546
func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...string) (err error) {
331547
if tableName == "" || len(columnNames) == 0 {
332548
return nil

0 commit comments

Comments
 (0)