@@ -7,6 +7,7 @@ package migrations
7
7
8
8
import (
9
9
"fmt"
10
+ "reflect"
10
11
"regexp"
11
12
"strings"
12
13
@@ -327,6 +328,221 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t
327
328
return nil
328
329
}
329
330
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
330
546
func dropTableColumns (sess * xorm.Session , tableName string , columnNames ... string ) (err error ) {
331
547
if tableName == "" || len (columnNames ) == 0 {
332
548
return nil
0 commit comments