Describe the feature

Is there any plan for creating a tool to generate migration files from DB (just like django does) to be integrated with goose or similar?

Motivation

My issue is that I dont wanna have to manually write all those migrations queries, but rather have them generated like django migrations work (create a diff between model/db, dump up and down queries to files and then run those migrations during deploy).


See below comments for further details

I was able to hacky do it by listening on the automigrate queries and making them no-op and redirecting them to a file, but thats far from being ideal.

For reference:

    stmts := make([]string, 0)
    if err := db.Callback().Raw().Replace("gorm:raw", func(db *gorm.DB) {
        stmts = append(stmts, db.Statement.SQL.String())
    }); err != nil {
        panic(err)
    }

    if err := db.AutoMigrate(models...); err != nil {
        panic(err)
    }

question

Comment From: jinzhu

https://v2.gorm.io/docs/migration.html

https://github.com/go-gormigrate/gormigrate

Comment From: raphaelvigee

Thank you for the v2 docs link, but I dont think your answer covers the queries generation, there is no way to generate the Up/Down migrations without copy/paste the automigrate code and add the appropriate couple (for each create, add a delete etc)

Could you suggest a way to hook into the automigrate code ?

Comment From: jinzhu

Hello @raphaelvigee

You can create a *gorm.DB with debug mode enabled and configured its logger to print executed SQL into a file.

But I would suggest you use migration tool like https://github.com/go-gormigrate/gormigrate

Comment From: raphaelvigee

Hi @jinzhu

I do plan on using https://github.com/golang-migrate/migrate. My issue is that I dont wanna have to manually write all those migrations queries, but rather have them generated like django migrations work (create a diff between model/db, dump up and down queries to files and then run those migrations during deploy).

You can create a *gorm.DB with debug mode enabled and configured its logger to print executed SQL into a file.

This would only generate up queries, and makes it difficult to generate the down ones automatically.

The solution I explored (with partial success) is creating a custom Dialector, overriding the Migrator function to return a custom Migrator:

type Dialector struct {
    gorm.Dialector
    UpOut   io.ReadWriter
    DownOut io.ReadWriter
}

func (d Dialector) Migrator(db *gorm.DB) gorm.Migrator {
    upDb := db.Session(&gorm.Session{})
    if err := upDb.Callback().Raw().Replace("gorm:raw", func(db *gorm.DB) {
        d.UpOut.Write([]byte(db.Statement.SQL.String()))
        d.UpOut.Write([]byte(";\n"))
    }); err != nil {
        panic(err)
    }

    downDb := db.Session(&gorm.Session{})
    if err := downDb.Callback().Raw().Replace("gorm:raw", func(db *gorm.DB) {
        d.DownOut.Write([]byte(db.Statement.SQL.String()))
        d.DownOut.Write([]byte(";\n"))
    }); err != nil {
        panic(err)
    }

    return GenMigrator{
        Migrator: d.Dialector.Migrator(db).(mysql.Migrator),
        Up: HookedMigrator{
            Migrator: d.Dialector.Migrator(upDb),
            Out:      d.UpOut,
        },
        Down: HookedMigrator{
            Migrator: d.Dialector.Migrator(downDb),
            Out:      d.DownOut,
        },
    }
}

type GenMigrator struct {
    mysql.Migrator
    Up   HookedMigrator
    Down HookedMigrator
}

type HookedMigrator struct {
    gorm.Migrator
    Out io.ReadWriter
}

func (m GenMigrator) CreateTable(dst ...interface{}) error {
    if err := m.Up.CreateTable(dst...); err != nil {
        return err
    }
    if err := m.Down.DropTable(dst...); err != nil {
        return err
    }

    return nil
}

... [omitted for brevity]

I'm sure this could be turned into a first class feature with your expertise

Comment From: raphaelvigee

Update, I got it to work by changing the logic that collects the statements, it seems that the two Callback().Replace() override each other, even though they are in different sessions (need to deep clone the config callbacks when creating a new session ?)

Here is the result:

====================== UP:
CREATE TABLE `images` (`id` varchar(191),`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`filename` longtext,`source_id` longtext,PRIMARY KEY (`id`),INDEX idx_images_deleted_at (`deleted_at`),CONSTRAINT `fk_sources_images` FOREIGN KEY (`source_id`) REFERENCES `sources`(`id`));
CREATE TABLE `sources` (`id` varchar(191),`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`name` longtext,`url` longtext,PRIMARY KEY (`id`),INDEX idx_sources_deleted_at (`deleted_at`));
CREATE TABLE `layers` (`id` varchar(191),`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`deleted_at` datetime(3) NULL,`source_id` longtext,`config` longtext,`popup` longtext,PRIMARY KEY (`id`),INDEX idx_layers_deleted_at (`deleted_at`),CONSTRAINT `fk_sources_layers` FOREIGN KEY (`source_id`) REFERENCES `sources`(`id`));

====================== DOWN:
SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS `layers` CASCADE;
SET FOREIGN_KEY_CHECKS = 0;
SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS `sources` CASCADE;
SET FOREIGN_KEY_CHECKS = 0;
SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS `images` CASCADE;
SET FOREIGN_KEY_CHECKS = 0;

I believe this would be worth reopening

Comment From: jinzhu

You can use http://v2.gorm.io/docs/logger.html to collect SQL

Callbacks are registered into the global *gorm.DB, not the session-level, if you require *gorm.DB with different callbacks, you need to initialize another *gorm.DB, just added it to http://v2.gorm.io/docs/write_plugins.html ;)

Comment From: raphaelvigee

You can use http://v2.gorm.io/docs/logger.html to collect SQL

Yeah my first go at this was to collect SQL through the logger, but the drawback is that it makes it hard to auto generate the down migration.

Would you be interested in a PR for auto generation of up/down migration with the above method ?

Comment From: PalanQu

Hello @raphaelvigee , do you have any new solution for this requirement?

Comment From: PalanQu

@raphaelvigee I write an example about generate up and down sql with gorm, https://github.com/PalanQu/easycoding#topic1-database-migrate

Comment From: atreya2011

@raphaelvigee Would love it if you can post your solution here! 🙏🏼

Comment From: rotemtam

Hey,

Atlas (a database schema migration tool which I'm a maintainer of) supports automatic migration planning for GORM:

https://atlasgo.io/guides/orms/gorm

Comment From: atreya2011

@rotemtam This is just what I was looking for! Awesome 🎉