Skip to content

Commit f1e15e3

Browse files
committed
Add command to recreate tables
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 go-gitea#8868 Fix go-gitea#3265 Fix go-gitea#8894 Signed-off-by: Andrew Thornton <[email protected]>
1 parent 1530069 commit f1e15e3

File tree

7 files changed

+269
-3
lines changed

7 files changed

+269
-3
lines changed

cmd/doctor.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ var CmdDoctor = cli.Command{
6161
Usage: `Name of the log file (default: "doctor.log"). Set to "-" to output to stdout, set to "" to disable`,
6262
},
6363
},
64+
Subcommands: []cli.Command{
65+
cmdRecreateTable,
66+
},
67+
}
68+
69+
var cmdRecreateTable = cli.Command{
70+
Name: "recreate-table",
71+
Usage: "Recreate tables from XORM definitions and copy the data.",
72+
ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
73+
Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns.
74+
75+
This command will cause Xorm to recreate tables, copying over the data and deleting the old table.
76+
77+
You should back-up your database before doing this and ensure that your database is up-to-date first.`,
78+
Action: runRecreateTable,
6479
}
6580

6681
type check struct {
@@ -129,6 +144,37 @@ var checklist = []check{
129144
// more checks please append here
130145
}
131146

147+
func runRecreateTable(ctx *cli.Context) error {
148+
// Redirect the default golog to here
149+
golog.SetFlags(0)
150+
golog.SetPrefix("")
151+
golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
152+
153+
setting.EnableXORMLog = false
154+
if err := initDBDisableConsole(true); err != nil {
155+
fmt.Println(err)
156+
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
157+
return nil
158+
}
159+
160+
if err := models.NewEngine(context.Background(), migrations.EnsureUpToDate); err != nil {
161+
return err
162+
}
163+
164+
args := ctx.Args()
165+
names := make([]string, 0, ctx.NArg())
166+
for i := 0; i < ctx.NArg(); i++ {
167+
names = append(names, args.Get(i))
168+
}
169+
170+
beans, err := models.NamesToBean(names...)
171+
if err != nil {
172+
return err
173+
}
174+
175+
return models.NewEngine(context.Background(), migrations.RecreateTables(beans...))
176+
}
177+
132178
func runDoctor(ctx *cli.Context) error {
133179

134180
// Silence the default loggers

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

Lines changed: 30 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,12 @@ func doMigrationTest(t *testing.T, version string) {
256256

257257
err = models.NewEngine(context.Background(), wrappedMigrate)
258258
assert.NoError(t, err)
259+
260+
beans, _ := models.NamesToBean()
261+
262+
err = models.NewEngine(context.Background(), migrations.RecreateTables(beans...))
263+
assert.NoError(t, err)
264+
259265
currentEngine.Close()
260266
}
261267

models/migrations/migrations.go

Lines changed: 148 additions & 1 deletion
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

@@ -317,6 +318,153 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
317318
return nil
318319
}
319320

321+
// RecreateTables will recreate the tables for the provided beans using the newly provided bean definition and move all data to that new table
322+
// WARNING: YOU MUST PROVIDE THE FULL BEAN DEFINITION
323+
func RecreateTables(beans ...interface{}) func(*xorm.Engine) error {
324+
return func(x *xorm.Engine) error {
325+
sess := x.NewSession()
326+
defer sess.Close()
327+
if err := sess.Begin(); err != nil {
328+
return err
329+
}
330+
for _, bean := range beans {
331+
log.Info("Recreating Table: %s for Bean: %s", x.TableName(bean), reflect.Indirect(reflect.ValueOf(bean)).Type().Name())
332+
if err := recreateTable(sess, bean); err != nil {
333+
return err
334+
}
335+
}
336+
return sess.Commit()
337+
}
338+
}
339+
340+
// recreateTable will recreate the table using the newly provided bean definition and move all data to that new table
341+
// WARNING: YOU MUST PROVIDE THE FULL BEAN DEFINITION
342+
// WARNING: YOU MUST COMMIT THE SESSION AT THE END
343+
func recreateTable(sess *xorm.Session, bean interface{}) error {
344+
// TODO: This will not work if there are foreign keys
345+
346+
tableName := sess.Engine().TableName(bean)
347+
tempTableName := fmt.Sprintf("temporary__%s__temporary", tableName)
348+
349+
// We need to move the old table away and create a new one with the correct columns
350+
// We will need to do this in stages to prevent data loss
351+
//
352+
// First let's update the old table to ensure it has all the necessary columns
353+
if err := sess.CreateTable(bean); err != nil {
354+
return err
355+
}
356+
357+
// Now we create the temporary table
358+
if err := sess.Table(tempTableName).CreateTable(bean); err != nil {
359+
return err
360+
}
361+
362+
// Work out the column names from the bean - these are the columns to select from the old table and install into the new table
363+
table, err := sess.Engine().TableInfo(bean)
364+
if err != nil {
365+
return err
366+
}
367+
newTableColumns := table.Columns()
368+
if len(newTableColumns) == 0 {
369+
return fmt.Errorf("no columns in new table")
370+
}
371+
372+
if setting.Database.UseMSSQL {
373+
sess.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s ON", tempTableName))
374+
}
375+
376+
sqlStringBuilder := &strings.Builder{}
377+
_, _ = sqlStringBuilder.WriteString("INSERT INTO ")
378+
_, _ = sqlStringBuilder.WriteString(tempTableName)
379+
if setting.Database.UseMSSQL {
380+
_, _ = sqlStringBuilder.WriteString(" (`")
381+
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name)
382+
_, _ = sqlStringBuilder.WriteString("`")
383+
for _, column := range newTableColumns[1:] {
384+
_, _ = sqlStringBuilder.WriteString(", `")
385+
_, _ = sqlStringBuilder.WriteString(column.Name)
386+
_, _ = sqlStringBuilder.WriteString("`")
387+
}
388+
_, _ = sqlStringBuilder.WriteString(")")
389+
}
390+
_, _ = sqlStringBuilder.WriteString(" SELECT ")
391+
if newTableColumns[0].Default != "" {
392+
_, _ = sqlStringBuilder.WriteString("COALESCE(`")
393+
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name)
394+
_, _ = sqlStringBuilder.WriteString("`, ")
395+
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Default)
396+
_, _ = sqlStringBuilder.WriteString(")")
397+
} else {
398+
_, _ = sqlStringBuilder.WriteString("`")
399+
_, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name)
400+
_, _ = sqlStringBuilder.WriteString("`")
401+
}
402+
403+
for _, column := range newTableColumns[1:] {
404+
if column.Default != "" {
405+
_, _ = sqlStringBuilder.WriteString(", COALESCE(`")
406+
_, _ = sqlStringBuilder.WriteString(column.Name)
407+
_, _ = sqlStringBuilder.WriteString("`, ")
408+
_, _ = sqlStringBuilder.WriteString(column.Default)
409+
_, _ = sqlStringBuilder.WriteString(")")
410+
} else {
411+
_, _ = sqlStringBuilder.WriteString(", `")
412+
_, _ = sqlStringBuilder.WriteString(column.Name)
413+
_, _ = sqlStringBuilder.WriteString("`")
414+
}
415+
}
416+
_, _ = sqlStringBuilder.WriteString(" FROM ")
417+
_, _ = sqlStringBuilder.WriteString(tableName)
418+
419+
if _, err := sess.Exec(sqlStringBuilder.String()); err != nil {
420+
return err
421+
}
422+
423+
if setting.Database.UseMSSQL {
424+
sess.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s OFF", tempTableName))
425+
}
426+
427+
switch {
428+
case setting.Database.UseSQLite3:
429+
fallthrough
430+
case setting.Database.UseMySQL:
431+
// SQLite and MySQL will drop all the constraints on the old table
432+
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil {
433+
return err
434+
}
435+
436+
// SQLite and MySQL will move all the constraints from the temporary table to the new table
437+
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tempTableName, tableName)); err != nil {
438+
return err
439+
}
440+
case setting.Database.UsePostgreSQL:
441+
// CASCADE causes postgres to drop all the constraints on the old table
442+
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s` CASCADE", tableName)); err != nil {
443+
return err
444+
}
445+
446+
// CASCADE causes postgres to move all the constraints from the temporary table to the new table
447+
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tempTableName, tableName)); err != nil {
448+
return err
449+
}
450+
case setting.Database.UseMSSQL:
451+
// MSSQL will drop all the constraints on the old table
452+
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil {
453+
return err
454+
}
455+
456+
// MSSQL sp_rename will move all the constraints from the temporary table to the new table
457+
if _, err := sess.Exec(fmt.Sprintf("sp_rename '[%s]','[%s]'", tempTableName, tableName)); err != nil {
458+
return err
459+
}
460+
461+
default:
462+
log.Fatal("Unrecognized DB")
463+
}
464+
return nil
465+
}
466+
467+
// WARNING: YOU MUST COMMIT THE SESSION AT THE END
320468
func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...string) (err error) {
321469
if tableName == "" || len(columnNames) == 0 {
322470
return nil
@@ -465,7 +613,6 @@ func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
465613
return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err)
466614
}
467615

468-
return sess.Commit()
469616
default:
470617
log.Fatal("Unrecognized DB")
471618
}

models/migrations/v102.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ import (
1111
func dropColumnHeadUserNameOnPullRequest(x *xorm.Engine) error {
1212
sess := x.NewSession()
1313
defer sess.Close()
14-
return dropTableColumns(sess, "pull_request", "head_user_name")
14+
if err := dropTableColumns(sess, "pull_request", "head_user_name"); err != nil {
15+
return err
16+
}
17+
return sess.Commit()
1518
}

models/models.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"database/sql"
1111
"errors"
1212
"fmt"
13+
"reflect"
14+
"strings"
1315

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

@@ -210,6 +212,38 @@ func NewEngine(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err e
210212
return nil
211213
}
212214

215+
// NamesToBean return a list of beans or an error
216+
func NamesToBean(names ...string) ([]interface{}, error) {
217+
beans := []interface{}{}
218+
if len(names) == 0 {
219+
for _, bean := range tables {
220+
beans = append(beans, bean)
221+
}
222+
return beans, nil
223+
}
224+
// Need to map provided names to beans...
225+
beanMap := make(map[string]interface{})
226+
for _, bean := range tables {
227+
228+
beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean
229+
beanMap[strings.ToLower(x.TableName(bean))] = bean
230+
beanMap[strings.ToLower(x.TableName(bean, true))] = bean
231+
}
232+
233+
gotBean := make(map[interface{}]bool)
234+
for _, name := range names {
235+
bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))]
236+
if !ok {
237+
return nil, fmt.Errorf("No table found that matches: %s", name)
238+
}
239+
if !gotBean[bean] {
240+
beans = append(beans, bean)
241+
gotBean[bean] = true
242+
}
243+
}
244+
return beans, nil
245+
}
246+
213247
// Statistic contains the database statistics
214248
type Statistic struct {
215249
Counter struct {

models/repo_language_stats.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type LanguageStat struct {
1919
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
2020
CommitID string
2121
IsPrimary bool
22-
Language string `xorm:"VARCHAR(30) UNIQUE(s) INDEX NOT NULL"`
22+
Language string `xorm:"VARCHAR(50) UNIQUE(s) INDEX NOT NULL"`
2323
Percentage float32 `xorm:"-"`
2424
Size int64 `xorm:"NOT NULL DEFAULT 0"`
2525
Color string `xorm:"-"`

0 commit comments

Comments
 (0)