@@ -7,6 +7,7 @@ package migrations
77
88import (
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
330546func dropTableColumns (sess * xorm.Session , tableName string , columnNames ... string ) (err error ) {
331547 if tableName == "" || len (columnNames ) == 0 {
332548 return nil
0 commit comments