Skip to content
Snippets Groups Projects
migrations.go 14.91 KiB
//
//  tank, Import and Playlist Daemon for Aura project
//  Copyright (C) 2017-2020 Christian Pointner <equinox@helsinki.at>
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
//
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <https://www.gnu.org/licenses/>.
//

package store

import (
	"errors"
	"time"

	"github.com/jinzhu/gorm"
	"gopkg.in/gormigrate.v1"
)

var (
	dbMigrations = []*gormigrate.Migration{
		{
			ID:       "201903131716",
			Migrate:  func(tx *gorm.DB) error { return nil },
			Rollback: func(tx *gorm.DB) error { return nil },
		},
		{
			ID: "201905160033",
			Migrate: func(tx *gorm.DB) error {
				type PlaylistEntry struct {
					ID         uint64  `json:"-" gorm:"primary_key"`
					PlaylistID uint64  `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"`
					LineNum    uint    `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"`
					URI        string  `json:"uri" gorm:"size:1024"`
					File       *File   `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"`
					FileID     *uint64 `json:"-" gorm:"index"`
				}

				type Playlist struct {
					ID          uint64          `json:"id" gorm:"primary_key"`
					CreatedAt   time.Time       `json:"created"`
					UpdatedAt   time.Time       `json:"updated"`
					Description string          `json:"description"`
					ShowName    string          `json:"show" gorm:"not null;index"`
					Show        Show            `json:"-" gorm:"association_foreignkey:Name"`
					Entries     []PlaylistEntry `json:"entries,omitempty"`
				}
				return tx.AutoMigrate(&Playlist{}).Error
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Table("playlists").DropColumn("description").Error
			},
		},
		{
			ID: "201905291602",
			Migrate: func(tx *gorm.DB) error {
				type Import struct {
					State ImportState `json:"state"`
					Error string      `json:"error,omitempty"`
				}
				type FileSource struct {
					URI    string `json:"uri" gorm:"size:1024"`
					Hash   string `json:"hash"`
					Import Import `json:"import" gorm:"embedded;embedded_prefix:import__"`
				}

				type FileMetadata struct {
					Artist string `json:"artist,omitempty" gorm:"index"`
					Title  string `json:"title,omitempty" gorm:"index"`
					Album  string `json:"album,omitempty" gorm:"index"`
				}

				type File struct {
					ID        uint64        `json:"id" gorm:"primary_key"`
					CreatedAt time.Time     `json:"created"`
					UpdatedAt time.Time     `json:"updated"`
					ShowName  string        `json:"show" gorm:"not null;index"`
					Show      Show          `json:"-" gorm:"association_foreignkey:Name"`
					Source    FileSource    `json:"source" gorm:"embedded;embedded_prefix:source__"`
					Metadata  FileMetadata  `json:"metadata" gorm:"embedded;embedded_prefix:metadata__"`
					Size      uint64        `json:"size"`
					Duration  time.Duration `json:"duration"`
				}

				return tx.AutoMigrate(&File{}).Error
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Table("files").DropColumn("source__import__error").Error
			},
		},
		{
			ID: "201906010144",
			Migrate: func(tx *gorm.DB) error {
				type ImportLog struct {
					ID         uint64 `gorm:"primary_key"`
					File       File   `gorm:"association_autoupdate:false;association_autocreate:false"`
					FileID     uint64 `gorm:"not null;index;unique_index:unique_import_log_step"`
					ImportStep string `gorm:"not null;index;unique_index:unique_import_log_step"`
					Encoded    []byte
				}

				if err := tx.AutoMigrate(&ImportLog{}).Error; err != nil {
					return err
				}
				if err := tx.Model(ImportLog{}).AddForeignKey("file_id", "files (id)", "CASCADE", "CASCADE").Error; err != nil {
					return err
				}
				return nil
			},
			Rollback: func(tx *gorm.DB) error {
				if err := tx.Model(ImportLog{}).RemoveForeignKey("file_id", "files (id)").Error; err != nil {
					return err
				}
				return tx.DropTable("import_logs").Error
			},
		},
		{
			ID: "201906051520",
			Migrate: func(tx *gorm.DB) error {
				if tx.Dialect().GetName() == "mysql" {
					tx.Model(ImportLog{}).ModifyColumn("encoded", "longblob")
				}
				return nil
			},
			Rollback: func(tx *gorm.DB) error {
				// to be exact we would need to chang `encoded` back to varbinary(255) for mysql
				// however this will open a box of worms....
				return nil
			},
		},
		{
			ID: "201908110945",
			Migrate: func(tx *gorm.DB) error {

				type PlaylistEntry struct {
					ID         uint64  `json:"-" gorm:"primary_key"`
					PlaylistID uint64  `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"`
					LineNum    uint    `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"`
					URI        string  `json:"uri" gorm:"size:1024"`
					File       *File   `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"`
					FileID     *uint64 `json:"-" gorm:"index"`
				}

				type Playlist struct {
					ID          uint64          `json:"id" gorm:"primary_key"`
					CreatedAt   time.Time       `json:"created"`
					UpdatedAt   time.Time       `json:"updated"`
					Description string          `json:"description"`
					PlayoutMode string          `json:"playoutMode"`
					ShowName    string          `json:"show" gorm:"not null;index"`
					Show        Show            `json:"-" gorm:"association_foreignkey:Name"`
					Entries     []PlaylistEntry `json:"entries,omitempty"`
				}

				return tx.AutoMigrate(&Playlist{}).Error
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Table("playlists").DropColumn("playout_mode").Error
			},
		},
		{
			ID: "201908150104",
			Migrate: func(tx *gorm.DB) error {
				type Import struct {
					State ImportState `json:"state"`
					Error string      `json:"error,omitempty"`
				}

				type FileSource struct {
					URI    string `json:"uri" gorm:"size:1024"`
					Hash   string `json:"hash"`
					Import Import `json:"import" gorm:"embedded;embedded_prefix:import__"`
				}

				type FileMetadata struct {
					Artist       string `json:"artist,omitempty" gorm:"index"`
					Title        string `json:"title,omitempty" gorm:"index"`
					Album        string `json:"album,omitempty" gorm:"index"`
					Organization string `json:"organization,omitempty" gorm:"index"`
					ISRC         string `json:"isrc,omitempty" gorm:"index"`
				}

				type File struct {
					ID        uint64        `json:"id" gorm:"primary_key"`
					CreatedAt time.Time     `json:"created"`
					UpdatedAt time.Time     `json:"updated"`
					ShowName  string        `json:"show" gorm:"not null;index"`
					Show      Show          `json:"-" gorm:"association_foreignkey:Name"`
					Source    FileSource    `json:"source" gorm:"embedded;embedded_prefix:source__"`
					Metadata  FileMetadata  `json:"metadata" gorm:"embedded;embedded_prefix:metadata__"`
					Size      uint64        `json:"size"`
					Duration  time.Duration `json:"duration"`
				}

				return tx.AutoMigrate(&File{}).Error
			},
			Rollback: func(tx *gorm.DB) error {
				if err := tx.Table("files").DropColumn("metadata__organization").Error; err != nil {
					return err
				}
				return tx.Table("files").DropColumn("metadata__isrc").Error
			},
		},
		{
			ID: "202006130203",
			Migrate: func(tx *gorm.DB) error {

				type PlaylistEntry struct {
					ID         uint64         `json:"-" gorm:"primary_key"`
					PlaylistID uint64         `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"`
					LineNum    uint           `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"`
					URI        string         `json:"uri" gorm:"size:1024"`
					Duration   *time.Duration `json:"duration,omitempty"`
					File       *File          `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"`
					FileID     *uint64        `json:"-" gorm:"index"`
				}

				// actually all playlists would need to be verfied if they still fit the new contstraint
				// that only allows a sinle non-file entry with durtion == NULL per playlist.
				// However we are still pre-first-release and all migrations will likely be squashed before that
				// release anyway...
				return tx.AutoMigrate(&PlaylistEntry{}).Error
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Table("playlist_entries").DropColumn("duration").Error
			},
		},
		{
			ID: "202309141500",
			Migrate: func(tx *gorm.DB) error {
				type File struct {
					ID        uint64       `json:"id" gorm:"primary_key"`
					CreatedAt time.Time    `json:"created"`
					UpdatedAt time.Time    `json:"updated"`
					ShowName  string       `json:"show" gorm:"not null;index"`
					Show      Show         `json:"-" gorm:"association_foreignkey:Name"`
					Source    FileSource   `json:"source" gorm:"embedded;embedded_prefix:source__"`
					Metadata  FileMetadata `json:"metadata" gorm:"embedded;embedded_prefix:metadata__"`
					Size      uint64       `json:"size"`
					Duration  float64      `json:"duration"`
				}

				if err := tx.Model(&File{}).ModifyColumn("duration", "float").Error; err != nil {
					return err
				}

				type PlaylistEntry struct {
					ID         uint64   `json:"-" gorm:"primary_key"`
					PlaylistID uint64   `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"`
					LineNum    uint     `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"`
					URI        string   `json:"uri" gorm:"size:1024"`
					Duration   *float64 `json:"duration,omitempty"`
					File       *File    `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"`
					FileID     *uint64  `json:"-" gorm:"index"`
				}

				return tx.Model(&PlaylistEntry{}).ModifyColumn("duration", "float").Error
			},
			Rollback: func(tx *gorm.DB) error {
				type File struct {
					ID        uint64        `json:"id" gorm:"primary_key"`
					CreatedAt time.Time     `json:"created"`
					UpdatedAt time.Time     `json:"updated"`
					ShowName  string        `json:"show" gorm:"not null;index"`
					Show      Show          `json:"-" gorm:"association_foreignkey:Name"`
					Source    FileSource    `json:"source" gorm:"embedded;embedded_prefix:source__"`
					Metadata  FileMetadata  `json:"metadata" gorm:"embedded;embedded_prefix:metadata__"`
					Size      uint64        `json:"size"`
					Duration  time.Duration `json:"duration"`
				}

				if err := tx.Model(&File{}).ModifyColumn("duration", "int").Error; err != nil {
					return err
				}

				type PlaylistEntry struct {
					ID         uint64         `json:"-" gorm:"primary_key"`
					PlaylistID uint64         `json:"-" gorm:"not null;index;unique_index:unique_playlist_line_numbers"`
					LineNum    uint           `json:"-" gorm:"not null;unique_index:unique_playlist_line_numbers"`
					URI        string         `json:"uri" gorm:"size:1024"`
					Duration   *time.Duration `json:"duration,omitempty"`
					File       *File          `json:"file,omitempty" gorm:"association_autoupdate:false;association_autocreate:false"`
					FileID     *uint64        `json:"-" gorm:"index"`
				}

				return tx.Model(&Playlist{}).ModifyColumn("duration", "int").Error
			},
		},
		{
			ID: "202312011500",
			Migrate: func(tx *gorm.DB) error {
				type Show struct {
					ID        uint64    `json:"id" gorm:"primary_key"`
					CreatedAt time.Time `json:"created"`
					UpdatedAt time.Time `json:"updated"`
				}
				type File struct {
					ID        uint64       `json:"id" gorm:"primary_key"`
					CreatedAt time.Time    `json:"created"`
					UpdatedAt time.Time    `json:"updated"`
					ShowID    uint64       `json:"showID" gorm:"not null;index"`
					Show      Show         `json:"-" gorm:"association_foreignkey:ID"`
					Source    FileSource   `json:"source" gorm:"embedded;embedded_prefix:source__"`
					Metadata  FileMetadata `json:"metadata" gorm:"embedded;embedded_prefix:metadata__"`
					Size      uint64       `json:"size"`
					Duration  float64      `json:"duration"`
				}
				type Playlist struct {
					ID          uint64          `json:"id" gorm:"primary_key"`
					CreatedAt   time.Time       `json:"created"`
					UpdatedAt   time.Time       `json:"updated"`
					Description string          `json:"description"`
					PlayoutMode string          `json:"playoutMode" gorm:"not null;default:'linear'"`
					ShowID      uint64          `json:"showID" gorm:"not null;index"`
					Show        Show            `json:"-" gorm:"association_foreignkey:ID"`
					Entries     []PlaylistEntry `json:"entries"`
				}

				return tx.AutoMigrate(&Show{}, &File{}, &Playlist{}).Error
			},

			Rollback: func(tx *gorm.DB) error {
				if err := tx.Table("shows").DropColumn("id").Error; err != nil {
					return err
				}
				if err := tx.Table("files").DropColumn("show_id").Error; err != nil {
					return err
				}
				return tx.Table("playlists").DropColumn("show_id").Error
			},
		},
	}
)

func initialMigration(tx *gorm.DB) (err error) {
	err = tx.AutoMigrate(
		&Show{},
		&File{},
		&ImportLog{},
		&Playlist{},
		&PlaylistEntry{},
	).Error

	if err != nil {
		return
	}

	// TODO: sadly this does not work on sqlite because constraints can only be added with create table...
	//       unfortunately gorm does not create foreign key contstraints in AutoMigrate(), see: https://github.com/jinzhu/gorm/issues/450
	if err := tx.Model(File{}).AddForeignKey("show_id", "shows (id)", "CASCADE", "CASCADE").Error; err != nil {
		return err
	}
	if err := tx.Model(ImportLog{}).AddForeignKey("file_id", "files (id)", "CASCADE", "CASCADE").Error; err != nil {
		return err
	}
	if err := tx.Model(Playlist{}).AddForeignKey("show_id", "shows (id)", "CASCADE", "CASCADE").Error; err != nil {
		return err
	}
	if err := tx.Model(PlaylistEntry{}).AddForeignKey("playlist_id", "playlists (id)", "CASCADE", "CASCADE").Error; err != nil {
		return err
	}
	if err := tx.Model(PlaylistEntry{}).AddForeignKey("file_id", "files (id)", "RESTRICT", "CASCADE").Error; err != nil {
		return err
	}

	return nil
}

func (st *Store) initDBModel(cfg DBConfig) (err error) {
	opts := gormigrate.DefaultOptions
	opts.TableName = migrationsTn
	opts.IDColumnSize = 64
	opts.UseTransaction = true
	if cfg.Type == "mysql" {
		// mySQl does not support DDL commands inside transactions
		opts.UseTransaction = false
	}

	m := gormigrate.New(st.db, opts, dbMigrations)
	m.InitSchema(initialMigration)
	if err = m.Migrate(); err != nil {
		return errors.New("running database migrations failed: " + err.Error())
	}

	if err = st.db.Table(migrationsTn).Select("id").Not("id = ?", "SCHEMA_INIT").Order("id DESC").Limit(1).Row().Scan(&st.revision); err != nil {
		return errors.New("fetching current database revision failed: " + err.Error())
	}
	return
}