diff --git a/.github/workflow.yml b/.github/workflow.yml new file mode 100644 index 0000000..070b9d0 --- /dev/null +++ b/.github/workflow.yml @@ -0,0 +1,42 @@ +name: Go Test + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + ports: + - 6666:5432 + options: >- + --health-cmd="pg_isready -U postgres" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Wait for PostgreSQL to be ready + run: | + while ! pg_isready -h 127.0.0.1 -p 5432 -U postgres; do + echo "Waiting for PostgreSQL..." + sleep 1 + done + + - name: Run tests + run: | + go test -v ./... diff --git a/.gitignore b/.gitignore index d88ed38..1a015b4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ *.iml .idea migrations -.mig +.amigo diff --git a/.mig/main.go b/.mig/main.go deleted file mode 100644 index f6a9f93..0000000 --- a/.mig/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - migrations "github.com/alexisvisco/mig/migrations" - "github.com/alexisvisco/mig/pkg/entrypoint" -) - -// Main is the entrypoint of the migrations -// It will parse the flags and execute the migrations -// Available flags are: -// - dsn: URL connection to the database -// - version: Migrate or rollback a specific version -// - direction: UP or DOWN -// - json: Print the output in JSON -// - silent: Do not print migrations output -// - timeout: Timeout for the migration is the time for the whole migrations to be applied -// - dry-run: Dry run the migration will not apply the migration to the database -// - continue-on-error: Continue on error will not rollback the migration if an error occurs -// - schema-version-table: Table name for the schema version -// - verbose: Print SQL statements -func main() { - entrypoint.MainPostgres(migrations.Migrations) -} diff --git a/cmd/context.go b/cmd/context.go index 7e19f0c..81518a0 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -11,34 +11,24 @@ const contextFileName = "config.yml" var contextCmd = &cobra.Command{ Use: "context", Short: "save flags into a context", - Long: `A context is a file inside the .mig folder that contains the flags that you use in the command line. + Long: `A context is a file inside the .amigo folder that contains the flags that you use in the command line. Example: - mig context --dsn "postgres://user:password@host:port/dbname?sslmode=disable" + amigo context --dsn "postgres://user:password@host:port/dbname?sslmode=disable" -This command will create a file .mig/context.yaml with the content: +This command will create a file .amigo/context.yaml with the content: dsn: "postgres://user:password@host:port/dbname?sslmode=disable" `, - RunE: func(cmd *cobra.Command, args []string) error { + Run: wrapCobraFunc(func(cmd *cobra.Command, args []string) error { err := viper.WriteConfig() if err != nil { return err } return nil - }, + }), } func init() { rootCmd.AddCommand(contextCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // contextCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // contextCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/create.go b/cmd/create.go index 868f85b..5fcbc72 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -2,53 +2,44 @@ package cmd import ( "fmt" - "github.com/alexisvisco/mig/pkg/mig" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/amigo" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" "github.com/spf13/cobra" "path/filepath" ) -var ( - createMigrationTypeFlag string - createDumpFlag bool - createDumpSchema string - createSkipDump bool -) - // createCmd represents the create command var createCmd = &cobra.Command{ Use: "create ", Short: "Create a new migration in the migration folder", - RunE: func(cmd *cobra.Command, args []string) error { + Run: wrapCobraFunc(func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - return fmt.Errorf("name is required: mig create ") + return fmt.Errorf("name is required: amigo create ") } - if err := validateDSN(); err != nil { + if err := cmdCtx.ValidateDSN(); err != nil { return err } - migFileType := types.MigrationFileType(createMigrationTypeFlag) - - if !migFileType.IsValid() { - return fmt.Errorf("invalid migration file type: %s, can be: %s", createMigrationTypeFlag, - utils.StringJoin(types.MigrationFileTypeValues, ", ")) + if err := cmdCtx.Create.ValidateType(); err != nil { + return err } - printer := tracker.NewLogger(jsonFlag, cmd.OutOrStdout()) + migFileType := types.MigrationFileType(cmdCtx.Create.Type) inUp := "" - if createDumpFlag { + if cmdCtx.Create.Dump { migFileType = types.MigrationFileTypeClassic - dump, err := mig.DumpSchema(&mig.DumpSchemaOptions{ - DSN: dsnFlag, - MigrationTableName: schemaVersionTableFlag, - PGDumpPath: pgDumpPathFlag, - Schema: createDumpSchema, - Shell: shellPathFlag, + + dump, err := amigo.DumpSchema(&amigo.DumpSchemaOptions{ + DSN: cmdCtx.DSN, + MigrationTableName: cmdCtx.SchemaVersionTable, + PGDumpPath: cmdCtx.PGDumpPath, + Schema: cmdCtx.Create.Schema, + Shell: cmdCtx.ShellPath, }) if err != nil { return err @@ -57,11 +48,11 @@ var createCmd = &cobra.Command{ inUp += fmt.Sprintf("s.Exec(`%s`)\n", dump) } - filePath, version, err := mig.GenerateMigrationFile(mig.GenerateMigrationFileOptions{ + filePath, version, err := amigo.GenerateMigrationFile(amigo.GenerateMigrationFileOptions{ Name: args[0], - Folder: migrationFolderFlag, - Driver: getDriver(), - Package: packageFlag, + Folder: cmdCtx.MigrationFolder, + Driver: getDriver(cmdCtx.DSN), + Package: cmdCtx.PackagePath, MigType: migFileType, InUp: inUp, }) @@ -69,44 +60,35 @@ var createCmd = &cobra.Command{ return err } - printer.AddEvent(tracker.FileAddedEvent{FileName: filePath}) + logger.Info(events.FileAddedEvent{FileName: filePath}) - if createSkipDump { - connection, err := mig.GetConnection(dsnFlag, verboseFlag) + if cmdCtx.Create.Skip { + connection, _, err := amigo.GetConnection(cmdCtx.DSN) if err != nil { return err } - _, err = connection.Exec("INSERT INTO "+schemaVersionTableFlag+" (version) VALUES ($1)", version) + _, err = connection.Exec("INSERT INTO "+cmdCtx.SchemaVersionTable+" (version) VALUES ($1)", version) if err != nil { return fmt.Errorf("unable to set migration as applied: %w", err) } - printer.AddEvent(tracker.SkipMigrationEvent{MigrationVersion: version}) + logger.Info(events.SkipMigrationEvent{MigrationVersion: version}) } - err = mig.GenerateMigrationsFile(migrationFolderFlag, packageFlag, - filepath.Join(migrationFolderFlag, migrationsFile)) + err = amigo.GenerateMigrationsFile(cmdCtx.MigrationFolder, cmdCtx.PackagePath, + filepath.Join(cmdCtx.MigrationFolder, migrationsFile)) if err != nil { return err } - printer.AddEvent(tracker.FileModifiedEvent{FileName: filepath.Join(migrationFolderFlag, migrationsFile)}) + logger.Info(events.FileModifiedEvent{FileName: filepath.Join(cmdCtx.MigrationFolder, migrationsFile)}) return nil - }, + }), } func init() { rootCmd.AddCommand(createCmd) - createCmd.Flags().StringVar(&createMigrationTypeFlag, "type", "change", - "The type of migration to create, possible values are [classic, change]") - - createCmd.Flags().BoolVarP(&createDumpFlag, "dump", "d", false, - "dump with pg_dump the current schema and add it to the current migration") - - createCmd.Flags().StringVarP(&createDumpSchema, "dump-schema", "s", "public", "the schema to dump if --dump is set") - - createCmd.Flags().BoolVar(&createSkipDump, "skip", false, - "skip will set the migration as applied without executing it") + cmdCtx.Create.Register(createCmd) } diff --git a/cmd/init.go b/cmd/init.go index 9370983..7d49eff 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -2,9 +2,10 @@ package cmd import ( "fmt" - "github.com/alexisvisco/mig/pkg/mig" - "github.com/alexisvisco/mig/pkg/templates" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/amigo" + "github.com/alexisvisco/amigo/pkg/templates" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" "github.com/spf13/cobra" "os" "path" @@ -14,37 +15,38 @@ import ( var initCmd = &cobra.Command{ Use: "init", Short: "Initialize migrations folder and add the first migration file", - RunE: func(cmd *cobra.Command, args []string) error { - fmt.Println("DSN", dsnFlag) - t := tracker.NewLogger(jsonFlag, cmd.OutOrStdout()) + Run: wrapCobraFunc(func(cmd *cobra.Command, args []string) error { + if err := cmdCtx.ValidateDSN(); err != nil { + return err + } - err := os.MkdirAll(migrationFolderFlag, 0755) + err := os.MkdirAll(cmdCtx.MigrationFolder, 0755) if err != nil { return fmt.Errorf("unable to create migration folder: %w", err) } - t.AddEvent(tracker.FolderAddedEvent{FolderName: migrationFolderFlag}) + logger.Info(events.FolderAddedEvent{FolderName: cmdCtx.MigrationFolder}) - err = os.MkdirAll(migFolderPathFlag, 0755) + err = os.MkdirAll(cmdCtx.AmigoFolderPath, 0755) if err != nil { return fmt.Errorf("unable to create main folder: %w", err) } - err = mig.GenerateMainFile(migFolderPathFlag, migrationFolderFlag) + err = amigo.GenerateMainFile(cmdCtx.AmigoFolderPath, cmdCtx.MigrationFolder) if err != nil { return err } - template, err := templates.GetInitCreateTableTemplate(templates.CreateTableData{Name: schemaVersionTableFlag}) + template, err := templates.GetInitCreateTableTemplate(templates.CreateTableData{Name: cmdCtx.SchemaVersionTable}) if err != nil { return err } - file, _, err := mig.GenerateMigrationFile(mig.GenerateMigrationFileOptions{ + file, _, err := amigo.GenerateMigrationFile(amigo.GenerateMigrationFileOptions{ Name: "schema_version", - Folder: migrationFolderFlag, - Driver: getDriver(), - Package: packageFlag, + Folder: cmdCtx.MigrationFolder, + Driver: getDriver(cmdCtx.DSN), + Package: cmdCtx.PackagePath, MigType: "change", InUp: template, InDown: "", @@ -53,21 +55,19 @@ var initCmd = &cobra.Command{ return err } - t.AddEvent(tracker.FileAddedEvent{FileName: file}) + logger.Info(events.FileAddedEvent{FileName: file}) - err = mig.GenerateMigrationsFile(migrationFolderFlag, packageFlag, - path.Join(migrationFolderFlag, migrationsFile)) + err = amigo.GenerateMigrationsFile(cmdCtx.MigrationFolder, cmdCtx.PackagePath, + path.Join(cmdCtx.MigrationFolder, migrationsFile)) if err != nil { return err } - t. - AddEvent(tracker.FileAddedEvent{FileName: path.Join(migFolderPathFlag, "main.go")}). - AddEvent(tracker.FileAddedEvent{FileName: path.Join(migrationFolderFlag, migrationsFile)}). - Measure() + logger.Info(events.FileAddedEvent{FileName: path.Join(cmdCtx.AmigoFolderPath, "main.go")}) + logger.Info(events.FileAddedEvent{FileName: path.Join(cmdCtx.MigrationFolder, migrationsFile)}) return nil - }, + }), } func init() { diff --git a/cmd/migrate.go b/cmd/migrate.go index 6f82bba..385681a 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -1,67 +1,43 @@ package cmd import ( - "errors" - "github.com/alexisvisco/mig/pkg/mig" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/amigo" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/types" "github.com/spf13/cobra" "path" - "time" -) - -var ( - migrateVersionFlag string - migrateDryRunFlag bool - migrateContinueOnErrorFlag bool - migrateTimeoutFlag time.Duration ) // migrateCmd represents the up command var migrateCmd = &cobra.Command{ Use: "migrate", Short: "Apply the database", - RunE: func(cmd *cobra.Command, args []string) error { - if err := validateDSN(); err != nil { + Run: wrapCobraFunc(func(cmd *cobra.Command, args []string) error { + if err := cmdCtx.ValidateDSN(); err != nil { return err } - t := tracker.NewLogger(jsonFlag, cmd.OutOrStdout()) - var version *string - if migrateVersionFlag != "" { - version = &migrateVersionFlag + if cmdCtx.Migrate.Version != "" { + version = &cmdCtx.Migrate.Version } - switch getDriver() { - case "postgres": - return mig.ExecuteMain(path.Join(migFolderPathFlag, "main.go"), &mig.MainOptions{ - DSN: dsnFlag, - MigrationDirection: types.MigrationDirectionUp, - Version: version, - SchemaVersionTable: schema.TableName(schemaVersionTableFlag), - DryRun: migrateDryRunFlag, - ContinueOnError: migrateContinueOnErrorFlag, - Timeout: migrateTimeoutFlag, - JSON: jsonFlag, - Shell: shellPathFlag, - Verbose: verboseFlag, - Tracker: t, - }) - default: - return errors.New("unsupported database") - } - }, + return amigo.ExecuteMain(path.Join(cmdCtx.AmigoFolderPath, "main.go"), &amigo.RunMigrationOptions{ + DSN: cmdCtx.DSN, + MigrationDirection: types.MigrationDirectionUp, + Version: version, + SchemaVersionTable: schema.TableName(cmdCtx.SchemaVersionTable), + DryRun: cmdCtx.Migrate.DryRun, + ContinueOnError: cmdCtx.Migrate.ContinueOnError, + Timeout: cmdCtx.Migrate.Timeout, + JSON: cmdCtx.JSON, + Shell: cmdCtx.ShellPath, + ShowSQL: cmdCtx.ShowSQL, + }) + }), } func init() { rootCmd.AddCommand(migrateCmd) - - migrateCmd.Flags().StringVar(&migrateVersionFlag, "version", "", - "Apply a specific version format: 20240502083700 or 20240502083700_name.go") - migrateCmd.Flags().BoolVar(&migrateDryRunFlag, "dry-run", false, "Run the migrations without applying them") - migrateCmd.Flags().BoolVar(&migrateContinueOnErrorFlag, "continue-on-error", false, - "Will not rollback the migration if an error occurs") - migrateCmd.Flags().DurationVar(&migrateTimeoutFlag, "timeout", 2*time.Minute, "The timeout for the migration") + cmdCtx.Migrate.Register(migrateCmd) } diff --git a/cmd/rollback.go b/cmd/rollback.go index f63f80d..4691191 100644 --- a/cmd/rollback.go +++ b/cmd/rollback.go @@ -1,69 +1,43 @@ package cmd import ( - "errors" - "github.com/alexisvisco/mig/pkg/mig" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/amigo" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/types" "github.com/spf13/cobra" "path" - "time" -) - -var ( - rollbackVersionFlag string - rollbackStepsFlag int - rollbackDryRunFlag bool - rollbackContinueOnErrorFlag bool - rollbackTimeoutFlag time.Duration ) // rollbackCmd represents the down command var rollbackCmd = &cobra.Command{ Use: "rollback", Short: "Rollback the database", - RunE: func(cmd *cobra.Command, args []string) error { - if err := validateDSN(); err != nil { + Run: wrapCobraFunc(func(cmd *cobra.Command, args []string) error { + if err := cmdCtx.ValidateDSN(); err != nil { return err } - t := tracker.NewLogger(jsonFlag, cmd.OutOrStdout()) - var version *string - if migrateVersionFlag != "" { - version = &rollbackVersionFlag + if cmdCtx.Rollback.Version != "" { + version = &cmdCtx.Rollback.Version } - switch getDriver() { - case "postgres": - return mig.ExecuteMain(path.Join(migFolderPathFlag, "main.go"), &mig.MainOptions{ - DSN: dsnFlag, - MigrationDirection: types.MigrationDirectionDown, - Version: version, - SchemaVersionTable: schema.TableName(schemaVersionTableFlag), - DryRun: rollbackDryRunFlag, - ContinueOnError: rollbackContinueOnErrorFlag, - Timeout: rollbackTimeoutFlag, - JSON: jsonFlag, - Shell: shellPathFlag, - Verbose: verboseFlag, - Tracker: t, - }) - default: - return errors.New("unsupported database") - } - }, + return amigo.ExecuteMain(path.Join(cmdCtx.AmigoFolderPath, "main.go"), &amigo.RunMigrationOptions{ + DSN: cmdCtx.DSN, + MigrationDirection: types.MigrationDirectionDown, + Version: version, + SchemaVersionTable: schema.TableName(cmdCtx.SchemaVersionTable), + DryRun: cmdCtx.Rollback.DryRun, + ContinueOnError: cmdCtx.Rollback.ContinueOnError, + Timeout: cmdCtx.Rollback.Timeout, + JSON: cmdCtx.JSON, + Shell: cmdCtx.ShellPath, + ShowSQL: cmdCtx.ShowSQL, + }) + }), } func init() { rootCmd.AddCommand(rollbackCmd) - rollbackCmd.Flags().StringVar(&migrateVersionFlag, "version", "", - "Apply a specific version format: 20240502083700 or 20240502083700_name.go") - rollbackCmd.Flags().IntVar(&rollbackStepsFlag, "steps", 1, "The number of steps to rollback") - rollbackCmd.Flags().BoolVar(&rollbackDryRunFlag, "dry-run", false, "Run the migrations without applying them") - rollbackCmd.Flags().BoolVar(&rollbackContinueOnErrorFlag, "continue-on-error", false, - "Will not rollback the migration if an error occurs") - rollbackCmd.Flags().DurationVar(&rollbackTimeoutFlag, "timeout", 2*time.Minute, "The timeout for the migration") - + cmdCtx.Rollback.Register(rollbackCmd) } diff --git a/cmd/root.go b/cmd/root.go index aa086ff..1f58572 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,7 +2,11 @@ package cmd import ( "fmt" - "github.com/alexisvisco/mig/pkg/types" + "github.com/alexisvisco/amigo/pkg/amigo" + "github.com/alexisvisco/amigo/pkg/amigoctx" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" "github.com/spf13/viper" "os" "path/filepath" @@ -10,17 +14,7 @@ import ( "github.com/spf13/cobra" ) -var ( - migFolderPathFlag string - dsnFlag string - jsonFlag bool - verboseFlag bool - migrationFolderFlag string - packageFlag string - schemaVersionTableFlag string - shellPathFlag string - pgDumpPathFlag string -) +var cmdCtx = amigoctx.NewContext() const ( migrationsFile = "migrations.go" @@ -28,32 +22,32 @@ const ( // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "mig", + Use: "amigo", Short: "Tool to manage database migrations with go files", Long: `Basic usage: -First you need to create a main folder with mig init: +First you need to create a main folder with amigo init: - will create a folder named .mig with a context file inside to not have to pass the dsn every time. - $ mig context --dsn "postgres://user:password@host:port/dbname?sslmode=disable" + will create a folder named .amigo with a context file inside to not have to pass the dsn every time. + $ amigo context --dsn "postgres://user:password@host:port/dbname?sslmode=disable" - $ mig init + $ amigo init note: will create: - folder named migrations with a file named migrations.go that contains the list of migrations - a new migration to create the schema version table - - a main.go in the .mig folder + - a main.go in the .amigo folder Apply migrations: - $ mig migrate + $ amigo migrate note: you can set --version to migrate a specific version Create a new migration: - $ mig create "create_table_users" + $ amigo create "create_table_users" note: you can set --dump if you already have a database and you want to create the first migration with what's already in the database. --skip will add the version of the created migration inside the schema version table. Rollback a migration: - $ mig rollback + $ amigo rollback note: you can set --step to rollback a specific number of migrations, and --version to rollback to a specific version `, @@ -61,115 +55,99 @@ Rollback a migration: } func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } + _ = rootCmd.Execute() } func init() { - rootCmd.PersistentFlags().StringVarP(&migFolderPathFlag, "mig-folder", "m", ".mig", - "The folder path to use for creating mig related files related to this repository") - - rootCmd.PersistentFlags().StringVar(&dsnFlag, "dsn", "", - "The database connection string example: postgres://user:password@host:port/dbname?sslmode=disable") - - rootCmd.PersistentFlags().BoolVarP(&jsonFlag, "json", "j", false, "Output in json format") - - rootCmd.PersistentFlags().StringVar(&migrationFolderFlag, "folder", "migrations", - "The folder where the migrations are stored") - - rootCmd.PersistentFlags().StringVarP(&packageFlag, "package", "p", "migrations", - "The package name for the migrations") - - rootCmd.PersistentFlags().StringVarP(&schemaVersionTableFlag, "schema-version-table", "t", - "public.mig_schema_versions", "The table name for the migrations") - - rootCmd.PersistentFlags().StringVar(&shellPathFlag, "shell-path", "/bin/bash", - "the shell to use (for: mig create --dump, it uses pg dump command)") - - rootCmd.PersistentFlags().BoolVarP(&verboseFlag, "verbose", "v", false, "verbose output (print SQL queries)") - - createCmd.Flags().StringVar(&pgDumpPathFlag, "pg-dump-path", "pg_dump", - "the path to the pg_dump command if --dump is set") - + cmdCtx.Register(rootCmd) initConfig() - } func initConfig() { // check if the file exists, if the file does not exist, create it - if _, err := os.Stat(filepath.Join(migFolderPathFlag, contextFileName)); os.IsNotExist(err) { - if err := viper.WriteConfigAs(filepath.Join(migFolderPathFlag, contextFileName)); err != nil { - fmt.Println("Can't write config:", err) + if _, err := os.Stat(filepath.Join(cmdCtx.AmigoFolderPath, contextFileName)); os.IsNotExist(err) { + err := os.MkdirAll(cmdCtx.AmigoFolderPath, 0755) + if err != nil { + fmt.Println("error: can't create folder:", err) + os.Exit(1) + } + if err := viper.WriteConfigAs(filepath.Join(cmdCtx.AmigoFolderPath, contextFileName)); err != nil { + fmt.Println("error: can't write config:", err) os.Exit(1) } } _ = viper.BindPFlag("dsn", rootCmd.PersistentFlags().Lookup("dsn")) - _ = viper.BindPFlag("mig-folder", rootCmd.PersistentFlags().Lookup("mig-folder")) + _ = viper.BindPFlag("amigo-folder", rootCmd.PersistentFlags().Lookup("amigo-folder")) _ = viper.BindPFlag("json", rootCmd.PersistentFlags().Lookup("json")) _ = viper.BindPFlag("folder", rootCmd.PersistentFlags().Lookup("folder")) _ = viper.BindPFlag("package", rootCmd.PersistentFlags().Lookup("package")) _ = viper.BindPFlag("schema-version-table", rootCmd.PersistentFlags().Lookup("schema-version-table")) _ = viper.BindPFlag("shell-path", rootCmd.PersistentFlags().Lookup("shell-path")) _ = viper.BindPFlag("pg-dump-path", createCmd.Flags().Lookup("pg-dump-path")) - _ = viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose")) + _ = viper.BindPFlag("sql", rootCmd.PersistentFlags().Lookup("sql")) + _ = viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) - viper.SetConfigFile(filepath.Join(migFolderPathFlag, contextFileName)) + viper.SetConfigFile(filepath.Join(cmdCtx.AmigoFolderPath, contextFileName)) if err := viper.ReadInConfig(); err != nil { - fmt.Println("Can't read config:", err) + fmt.Println("error: can't read config:", err) os.Exit(1) } if viper.IsSet("dsn") { - dsnFlag = viper.GetString("dsn") + cmdCtx.DSN = viper.GetString("dsn") } - if viper.IsSet("mig-folder") { - migFolderPathFlag = viper.GetString("mig-folder") + if viper.IsSet("amigo-folder") { + cmdCtx.AmigoFolderPath = viper.GetString("amigo-folder") } if viper.IsSet("json") { - jsonFlag = viper.GetBool("json") + cmdCtx.JSON = viper.GetBool("json") } if viper.IsSet("folder") { - migrationFolderFlag = viper.GetString("folder") + cmdCtx.MigrationFolder = viper.GetString("folder") } if viper.IsSet("package") { - packageFlag = viper.GetString("package") + cmdCtx.PackagePath = viper.GetString("package") } if viper.IsSet("schema-version-table") { - schemaVersionTableFlag = viper.GetString("schema-version-table") + cmdCtx.SchemaVersionTable = viper.GetString("schema-version-table") } if viper.IsSet("shell-path") { - shellPathFlag = viper.GetString("shell-path") + cmdCtx.ShellPath = viper.GetString("shell-path") } if viper.IsSet("pg-dump-path") { - pgDumpPathFlag = viper.GetString("pg-dump-path") + cmdCtx.PGDumpPath = viper.GetString("pg-dump-path") } - if viper.IsSet("verbose") { - verboseFlag = viper.GetBool("verbose") + if viper.IsSet("sql") { + cmdCtx.ShowSQL = viper.GetBool("sql") } -} - -func validateDSN() error { - if dsnFlag == "" { - return fmt.Errorf("dsn is required, example: postgres://user:password@host:port/dbname?sslmode=disable") + if viper.IsSet("debug") { + cmdCtx.Debug = viper.GetBool("debug") } - - return nil } -func getDriver() types.Driver { +func getDriver(_ string) types.Driver { // TODO: implement this when we have more drivers return "postgres" } + +func wrapCobraFunc(f func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) { + return func(cmd *cobra.Command, args []string) { + amigo.SetupSlog(cmdCtx.ShowSQL, cmdCtx.Debug, cmdCtx.JSON, os.Stdout) + + if err := f(cmd, args); err != nil { + logger.Error(events.MessageEvent{Message: err.Error()}) + // os.Exit(1) + } + } +} diff --git a/docs/docs/02-quick-start/01-installation.md b/docs/docs/02-quick-start/01-installation.md index 75c6973..1a14c01 100644 --- a/docs/docs/02-quick-start/01-installation.md +++ b/docs/docs/02-quick-start/01-installation.md @@ -3,11 +3,11 @@ To install the library, run the following command: ```sh -go install github.com/alexisvisco/mig +go install github.com/alexisvisco/amigo@latest ``` To check if the installation was successful, run the following command: ```sh -mig --help +amigo --help ``` diff --git a/docs/docs/02-quick-start/02-initialize.md b/docs/docs/02-quick-start/02-initialize.md index 3528bb7..0da2298 100644 --- a/docs/docs/02-quick-start/02-initialize.md +++ b/docs/docs/02-quick-start/02-initialize.md @@ -2,19 +2,19 @@ To start using mig, you need to initialize it. This process creates few things: - A `migrations` folder where you will write your migrations. -- A `.mig` folder where mig stores its configuration and the main file to run migrations. +- A `.amigo` folder where mig stores its configuration and the main file to run migrations. - A migration file to setup the table that will store the migration versions. To initialize mig, run the following command: ```sh -mig init +amigo init ``` You can also create a context to the repository to avoid passing flags each time you run a command. To do so, run the following command: ```sh -mig context --dsn "postgres://user:password@localhost:5432/dbname" +amigo context --dsn "postgres://user:password@localhost:5432/dbname" ``` A config.yml file will be created in the `.mig` folder. You can edit it to add more configurations. @@ -24,7 +24,7 @@ It contains the following fields: dsn: postgres://user:password@localhost:5432/dbname folder: migrations json: false -mig-folder: .mig +mig-folder: .amigo package: migrations pg-dump-path: pg_dump schema-version-table: public.mig_schema_versions diff --git a/docs/docs/02-quick-start/03-running-your-first-migration.md b/docs/docs/02-quick-start/03-running-your-first-migration.md index 1ff7d0f..012ed97 100644 --- a/docs/docs/02-quick-start/03-running-your-first-migration.md +++ b/docs/docs/02-quick-start/03-running-your-first-migration.md @@ -5,5 +5,5 @@ Since you have initialized mig, you can now run your first migration. To run the migration, execute the following command: ```sh -mig migrate +amigo migrate ``` diff --git a/docs/docs/03-cli/02-context.md b/docs/docs/03-cli/02-context.md index ae27e2a..d7e4935 100644 --- a/docs/docs/03-cli/02-context.md +++ b/docs/docs/03-cli/02-context.md @@ -4,7 +4,7 @@ The `context` command allow you to save root flags in a configuration file. This Example: ```sh -mig context --dsn "postgres://user:password@localhost:5432/dbname" --folder mgs --package mgs +amigo context --dsn "postgres://user:password@localhost:5432/dbname" --folder mgs --package mgs ``` Will create a `config.yml` file in the `.mig` folder with the following content: @@ -17,4 +17,6 @@ package: mgs pg-dump-path: pg_dump schema-version-table: public.mig_schema_versions shell-path: /bin/bash -verbose: false +debug: false +show-sql: false +``` diff --git a/docs/docs/03-cli/03-create.md b/docs/docs/03-cli/03-create.md index 6221082..e80beb3 100644 --- a/docs/docs/03-cli/03-create.md +++ b/docs/docs/03-cli/03-create.md @@ -10,7 +10,7 @@ There is two kinds of migrations files: To create a new migration file, run the following command: ```sh -mig create +amigo create ``` It will create a new migration file `_.go` in the `migrations` folder. diff --git a/docs/docs/03-cli/04-migrate.md b/docs/docs/03-cli/04-migrate.md index b7592ad..40ad93a 100644 --- a/docs/docs/03-cli/04-migrate.md +++ b/docs/docs/03-cli/04-migrate.md @@ -5,7 +5,7 @@ The `migrate` command allows you to apply the migrations. To apply the migrations, run the following command: ```sh -mig migrate +amigo migrate ``` ## Flags diff --git a/docs/docs/03-cli/05-rollback.md b/docs/docs/03-cli/05-rollback.md index 8ce4852..c2a840d 100644 --- a/docs/docs/03-cli/05-rollback.md +++ b/docs/docs/03-cli/05-rollback.md @@ -5,7 +5,7 @@ The `rollback` command allows you to rollback the last migration. To rollback the last migration, run the following command: ```sh -mig rollback +amigo rollback ``` ## Flags diff --git a/docs/docs/04-api/01-postgres/01-constructive.md b/docs/docs/04-api/01-postgres/01-constructive.md index ed0c201..e0db83e 100644 --- a/docs/docs/04-api/01-postgres/01-constructive.md +++ b/docs/docs/04-api/01-postgres/01-constructive.md @@ -2,23 +2,23 @@ They are the operations that create, alter, or drop tables, columns, indexes, constraints, and so on. -- [CreateTable(tableName schema.TableName, f func(*PostgresTableDef), opts ...schema.TableOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.CreateTable) +- [CreateTable(tableName schema.TableName, f func(*PostgresTableDef), opts ...schema.TableOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.CreateTable) -- [AddColumn(tableName schema.TableName, columnName string, columnType schema.ColumnType, opts ...schema.ColumnOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.AddColumn) +- [AddColumn(tableName schema.TableName, columnName string, columnType schema.ColumnType, opts ...schema.ColumnOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddColumn) -- [AddColumnComment(tableName schema.TableName, columnName string, comment *string, opts ...schema.ColumnCommentOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.AddColumnComment) +- [AddColumnComment(tableName schema.TableName, columnName string, comment *string, opts ...schema.ColumnCommentOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddColumnComment) -- [AddCheckConstraint(tableName schema.TableName, constraintName string, expression string, opts ...schema.CheckConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.AddCheckConstraint) +- [AddCheckConstraint(tableName schema.TableName, constraintName string, expression string, opts ...schema.CheckConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddCheckConstraint) -- [AddExtension(name string, option ...schema.ExtensionOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.AddExtension) +- [AddExtension(name string, option ...schema.ExtensionOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddExtension) -- [AddForeignKey(fromTable, toTable schema.TableName, opts ...schema.AddForeignKeyConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.AddForeignKeyConstraint) +- [AddForeignKey(fromTable, toTable schema.TableName, opts ...schema.AddForeignKeyConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddForeignKeyConstraint) -- [AddIndexConstraint(table schema.TableName, columns []string, option ...schema.IndexOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.AddIndexConstraint) +- [AddIndexConstraint(table schema.TableName, columns []string, option ...schema.IndexOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddIndexConstraint) -- [AddPrimaryKeyConstraint(tableName schema.TableName, columns []string, opts ...schema.PrimaryKeyConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.AddPrimaryKeyConstraint) +- [AddPrimaryKeyConstraint(tableName schema.TableName, columns []string, opts ...schema.PrimaryKeyConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddPrimaryKeyConstraint) -- [AddVersion(version string)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.AddVersion) +- [AddVersion(version string)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.AddVersion) Each of this functions are reversible, it means that in a migration that implement the `change` function, when you diff --git a/docs/docs/04-api/01-postgres/02-destructive.md b/docs/docs/04-api/01-postgres/02-destructive.md index 5ca83e1..f2be249 100644 --- a/docs/docs/04-api/01-postgres/02-destructive.md +++ b/docs/docs/04-api/01-postgres/02-destructive.md @@ -3,23 +3,23 @@ They are the operations that drop tables, columns, indexes, constraints, and so on. -- [DropCheckConstraint(tableName schema.TableName, constraintName string, opts ...schema.DropCheckConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.DropCheckConstraint) +- [DropCheckConstraint(tableName schema.TableName, constraintName string, opts ...schema.DropCheckConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.DropCheckConstraint) -- [DropColumn(tableName schema.TableName, columnName string, opts ...schema.DropColumnOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.DropColumn) +- [DropColumn(tableName schema.TableName, columnName string, opts ...schema.DropColumnOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.DropColumn) -- [DropExtension(name string, opts ...schema.DropExtensionOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.DropExtension) +- [DropExtension(name string, opts ...schema.DropExtensionOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.DropExtension) -- [DropForeignKeyConstraint(fromTable, toTable schema.TableName, opts ...schema.DropForeignKeyConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.DropForeignKeyConstraint) +- [DropForeignKeyConstraint(fromTable, toTable schema.TableName, opts ...schema.DropForeignKeyConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.DropForeignKeyConstraint) -- [DropIndex(table schema.TableName, columns []string, opts ...schema.DropIndexOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.DropIndex) +- [DropIndex(table schema.TableName, columns []string, opts ...schema.DropIndexOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.DropIndex) -- [DropPrimaryKeyConstraint(tableName schema.TableName, opts ...schema.DropPrimaryKeyConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.DropPrimaryKeyConstraint) +- [DropPrimaryKeyConstraint(tableName schema.TableName, opts ...schema.DropPrimaryKeyConstraintOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.DropPrimaryKeyConstraint) -- [DropTable(tableName schema.TableName, opts ...schema.DropTableOptions)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.DropTable) +- [DropTable(tableName schema.TableName, opts ...schema.DropTableOptions)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.DropTable) -- [RemoveVersion(version string)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.RemoveVersion) +- [RemoveVersion(version string)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.RemoveVersion) -- [RenameColumn(tableName schema.TableName, oldColumnName, newColumnName string)](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.RenameColumn) +- [RenameColumn(tableName schema.TableName, oldColumnName, newColumnName string)](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.RenameColumn) Usually you will use these functions in the `down` function of a migration, but you can use them in the `up` function too. If you want to have the reverse operation of a destructive operation, you can use the `reversible` options. diff --git a/docs/docs/04-api/01-postgres/03-informative.md b/docs/docs/04-api/01-postgres/03-informative.md index 94c66e3..4b18fcd 100644 --- a/docs/docs/04-api/01-postgres/03-informative.md +++ b/docs/docs/04-api/01-postgres/03-informative.md @@ -3,16 +3,16 @@ They are the operations that give you information about the database schema. -- [TableExist(tableName schema.TableName) bool](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.TableExist) +- [TableExist(tableName schema.TableName) bool](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.TableExist) -- [ColumnExist(tableName schema.TableName, columnName string) bool](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.ColumnExist) +- [ColumnExist(tableName schema.TableName, columnName string) bool](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.ColumnExist) -- [ConstraintExist(tableName schema.TableName, constraintName string) bool](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.ConstraintExist) +- [ConstraintExist(tableName schema.TableName, constraintName string) bool](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.ConstraintExist) -- [IndexExist(tableName schema.TableName, indexName string) bool](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.IndexExist) +- [IndexExist(tableName schema.TableName, indexName string) bool](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.IndexExist) -- [PrimaryKeyExist(tableName schema.TableName) bool](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.PrimaryKeyExist) +- [PrimaryKeyExist(tableName schema.TableName) bool](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.PrimaryKeyExist) -- [FindAppliedVersions() []string](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.FindAppliedVersions) +- [FindAppliedVersions() []string](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.FindAppliedVersions) These functions are not reversible. diff --git a/docs/docs/04-api/01-postgres/04-others.md b/docs/docs/04-api/01-postgres/04-others.md index 65fc0bf..aca11cd 100644 --- a/docs/docs/04-api/01-postgres/04-others.md +++ b/docs/docs/04-api/01-postgres/04-others.md @@ -2,7 +2,7 @@ They are functions that are not in the constructive, destructive, or informative categories. -- [Exec(query string, args ...interface{})](https://pkg.go.dev/github.com/alexisvisco/mig/pkg/schema/pg#Schema.Exec) +- [Exec(query string, args ...interface{})](https://pkg.go.dev/github.com/alexisvisco/amigo/pkg/schema/pg#Schema.Exec) If you want to reverse a query in a `change` function you should use the Reversible method. diff --git a/docs/docs/index.md b/docs/docs/index.md index 12a11c9..fdc57a0 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,12 +1,17 @@ --- sidebar_position: 1 --- +[![GoDoc](https://pkg.go.dev/badge/alexisvisco/mig)](https://pkg.go.dev/alexisvisco/mig) # Introduction -## MIG - Migrate SQL with Go language. +## AMIGO - Migrate SQL with Go language. -MIG is a SQL migration tool that uses Go language to write migrations. +A Migration In Golang (AMIGO) is a library that allows you to write migrations in Go language. +It provides you with all the benefits of Go, including type safety, simplicity, and strong tooling support. +AMIGO is designed to be easy to use and integrate into existing projects. + +General documentation: [https://amigo.alexisvis.co](https://amigo.alexisvis.co) ## Features @@ -20,14 +25,14 @@ MIG is a SQL migration tool that uses Go language to write migrations. To install the library, run the following command: ```sh -go install github.com/alexisvisco/mig +go install github.com/alexisvisco/amigo@latest ``` ## First usage ```sh -mig context --dsn "postgres://user:password@localhost:5432/dbname" # optional but it avoid to pass the dsn each time -mig init # create the migrations folder, the main file to run migration +amigo context --dsn "postgres://user:password@localhost:5432/dbname" # optional but it avoid to pass the dsn each time +amigo init # create the migrations folder, the main file to run migration mit migrate # apply the migration ``` @@ -37,8 +42,8 @@ mit migrate # apply the migration package migrations import ( - "github.com/alexisvisco/mig/pkg/schema/pg" - "github.com/alexisvisco/mig/pkg/schema" + "github.com/alexisvisco/amigo/pkg/schema/pg" + "github.com/alexisvisco/amigo/pkg/schema" "time" ) @@ -61,4 +66,11 @@ func (m Migration20240502155033SchemaVersion) Date() time.Time { ``` +## Supported databases + +- Postgres + +## Next supported databases +- SQLite +- MySQL diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index d8a1202..90a5709 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -6,12 +6,12 @@ const darkCodeTheme = require('prism-react-renderer').themes.dracula; /** @type {import('@docusaurus/types').Config} */ const config = { - title: 'mig docs', - tagline: 'A migration tool with migrations as code.', + title: 'amigo docs', + tagline: 'A migration tool in Golang with a powerful API', favicon: 'img/favicon.ico', // Set the production url of your site here - url: 'https://mig-go.alexisvis.co', + url: 'https://amigo.alexisvis.co', // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' baseUrl: '/', @@ -19,7 +19,7 @@ const config = { // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. organizationName: 'alexisvisco', // Usually your GitHub org/user name. - projectName: 'mig', // Usually your repo name. + projectName: 'amigo', // Usually your repo name. onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', @@ -54,7 +54,7 @@ const config = { // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: - 'https://github.com/alexisvisco/mig/tree/main/docs/', + 'https://github.com/alexisvisco/amigo/tree/main/docs/', }, blog: false, theme: { @@ -82,7 +82,7 @@ const config = { label: 'Docs', }, { - href: 'https://github.com/alexisvisco/mig', + href: 'https://github.com/alexisvisco/amigo', label: 'GitHub', position: 'right', }, diff --git a/docs/static/img/favicon.ico b/docs/static/img/favicon.ico index 96f0944..e8d8bc8 100644 Binary files a/docs/static/img/favicon.ico and b/docs/static/img/favicon.ico differ diff --git a/docs/static/img/logo.svg b/docs/static/img/logo.svg index 16adc66..5fea2c8 100644 --- a/docs/static/img/logo.svg +++ b/docs/static/img/logo.svg @@ -1,8 +1,7 @@ - - + diff --git a/docs/static/img/social-card.png b/docs/static/img/social-card.png index 2589cea..c36b347 100644 Binary files a/docs/static/img/social-card.png and b/docs/static/img/social-card.png differ diff --git a/go.mod b/go.mod index 2d711fa..85a9fa2 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,10 @@ -module github.com/alexisvisco/mig +module github.com/alexisvisco/amigo go 1.22 require ( + github.com/alecthomas/chroma/v2 v2.13.0 + github.com/charmbracelet/lipgloss v0.10.0 github.com/georgysavva/scany/v2 v2.1.3 github.com/gobuffalo/flect v1.0.2 github.com/jackc/pgx/v5 v5.5.5 @@ -16,9 +18,7 @@ require ( ) require ( - github.com/alecthomas/chroma/v2 v2.13.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/lipgloss v0.10.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect diff --git a/go.sum b/go.sum index 7791d75..d1f69f8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ +github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= +github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI= github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= @@ -27,6 +31,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= diff --git a/main.go b/main.go index 6040d73..3aef4b7 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,6 @@ package main -import "github.com/alexisvisco/mig/cmd" +import "github.com/alexisvisco/amigo/cmd" func main() { cmd.Execute() diff --git a/pkg/mig/dump_schema.go b/pkg/amigo/dump_schema.go similarity index 92% rename from pkg/mig/dump_schema.go rename to pkg/amigo/dump_schema.go index 2abb786..1e39492 100644 --- a/pkg/mig/dump_schema.go +++ b/pkg/amigo/dump_schema.go @@ -1,9 +1,9 @@ -package mig +package amigo import ( "fmt" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/utils/cmdexec" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/utils/cmdexec" "regexp" "strings" ) diff --git a/pkg/amigo/execute_main.go b/pkg/amigo/execute_main.go new file mode 100644 index 0000000..8a38995 --- /dev/null +++ b/pkg/amigo/execute_main.go @@ -0,0 +1,143 @@ +package amigo + +import ( + "database/sql" + "fmt" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils/cmdexec" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" + "os" + "path" + "strings" + "time" +) + +type RunMigrationOptions struct { + // DSN is the data source name. Example "postgres://user:password@localhost:5432/dbname?sslmode=disable" + DSN string + + // Connection is the database connection to use. If Connection is set, DSN is ignored. + Connection *sql.DB + + // MigrationDirection is the direction of the migration. + // It can be either up or down. + MigrationDirection types.MigrationDirection + + // Version is the version of the migration to apply. + Version *string + + // Steps is the number of migrations to apply. + Steps *int + + // SchemaVersionTable is the name of the table that will store the schema version. + SchemaVersionTable schema.TableName + + // DryRun specifies if the migrator should perform the migrations without actually applying them.q + DryRun bool + + // ContinueOnError specifies if the migrator should continue running migrations even if an error occurs. + ContinueOnError bool + + // Timeout is the maximum time the migration can take. + Timeout time.Duration + + // Migrations is the list of all existing migrations. + Migrations []schema.Migration + + JSON bool + ShowSQL bool + Debug bool + + // Shell is the shell to use, only used by the CLI + Shell string +} + +func ExecuteMain(mainLocation string, options *RunMigrationOptions) error { + mainFolder := path.Dir(mainLocation) + + stat, err := os.Stat(mainFolder) + if os.IsNotExist(err) { + err := os.MkdirAll(mainFolder, 0755) + if err != nil { + return fmt.Errorf("unable to create main folder: %w", err) + } + } else if err != nil { + return fmt.Errorf("unable to check if main folder exist: %w", err) + } + + if stat != nil && !stat.IsDir() { + return fmt.Errorf("main folder is not a directory") + } + + mainFilePath := path.Join(mainFolder, "main.go") + _, err = os.Stat(mainFilePath) + if os.IsNotExist(err) { + return fmt.Errorf("%s file does not exist, please run 'amigo init' to create it", mainFilePath) + } + + // build binary + args := []string{ + "go", "build", + "-o", path.Join(mainFolder, "main"), + path.Join(mainFolder, "main.go"), + } + + err = cmdexec.ExecToWriter(options.Shell, []string{"-c", strings.Join(args, " ")}, nil, os.Stdout, os.Stderr) + if err != nil { + return err + } + + args = []string{ + "./" + path.Join(mainFolder, "main"), + "-dsn", options.DSN, + "-direction", string(options.MigrationDirection), + "-timeout", options.Timeout.String(), + "-schema-version-table", string(options.SchemaVersionTable), + } + + if options.Steps != nil { + args = append(args, "-steps", fmt.Sprintf("%d", *options.Steps)) + } + + if options.ShowSQL { + args = append(args, "-sql") + } + + if options.ContinueOnError { + args = append(args, "-continue-on-error") + } + + if options.DryRun { + args = append(args, "-dry-run") + } + + if options.Debug { + args = append(args, "-debug") + } + + if options.Version != nil { + v, err := utils.ParseMigrationVersion(*options.Version) + if err != nil { + return fmt.Errorf("unable to parse version: %w", err) + } + args = append(args, "-version", v) + } + + if options.JSON { + args = append(args, "-json") + } + + if options.Debug { + logger.Debug(events.MessageEvent{Message: fmt.Sprintf("executing %s", args)}) + } + + err = cmdexec.ExecToWriter(options.Shell, []string{"-c", strings.Join(args, " ")}, nil, os.Stdout, os.Stderr) + if err != nil { + return fmt.Errorf("%s throw an error: %w", mainFilePath, err) + } + + return nil +} diff --git a/pkg/mig/generate_main_file.go b/pkg/amigo/generate_main_file.go similarity index 85% rename from pkg/mig/generate_main_file.go rename to pkg/amigo/generate_main_file.go index ee2c268..5f3acd1 100644 --- a/pkg/mig/generate_main_file.go +++ b/pkg/amigo/generate_main_file.go @@ -1,9 +1,9 @@ -package mig +package amigo import ( "fmt" - "github.com/alexisvisco/mig/pkg/templates" - "github.com/alexisvisco/mig/pkg/utils" + "github.com/alexisvisco/amigo/pkg/templates" + "github.com/alexisvisco/amigo/pkg/utils" "os" "path" ) diff --git a/pkg/mig/generate_migration_file.go b/pkg/amigo/generate_migration_file.go similarity index 90% rename from pkg/mig/generate_migration_file.go rename to pkg/amigo/generate_migration_file.go index 40ded2d..4618647 100644 --- a/pkg/mig/generate_migration_file.go +++ b/pkg/amigo/generate_migration_file.go @@ -1,10 +1,10 @@ -package mig +package amigo import ( "fmt" - "github.com/alexisvisco/mig/pkg/templates" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils" + "github.com/alexisvisco/amigo/pkg/templates" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils" "github.com/gobuffalo/flect" "os" "path" diff --git a/pkg/mig/generate_migrations_file.go b/pkg/amigo/generate_migrations_file.go similarity index 94% rename from pkg/mig/generate_migrations_file.go rename to pkg/amigo/generate_migrations_file.go index 81054dd..5b96d39 100644 --- a/pkg/mig/generate_migrations_file.go +++ b/pkg/amigo/generate_migrations_file.go @@ -1,9 +1,9 @@ -package mig +package amigo import ( "fmt" - "github.com/alexisvisco/mig/pkg/templates" - "github.com/alexisvisco/mig/pkg/utils" + "github.com/alexisvisco/amigo/pkg/templates" + "github.com/alexisvisco/amigo/pkg/utils" "os" "path/filepath" "sort" diff --git a/pkg/amigo/get_connection.go b/pkg/amigo/get_connection.go new file mode 100644 index 0000000..ea3cffd --- /dev/null +++ b/pkg/amigo/get_connection.go @@ -0,0 +1,37 @@ +package amigo + +import ( + "database/sql" + "errors" + "fmt" + "github.com/alexisvisco/amigo/pkg/utils/dblog" + _ "github.com/jackc/pgx/v5/stdlib" + sqldblogger "github.com/simukti/sqldb-logger" + "strings" +) + +func GetConnection(dsn string) (*sql.DB, *dblog.Logger, error) { + if dsn == "" { + return nil, nil, errors.New("dsn is required, example: postgres://user:password@localhost:5432/dbname?sslmode=disable") + } + + var db *sql.DB + + if strings.HasPrefix(dsn, "postgres") { + dbx, err := sql.Open("pgx", dsn) + if err != nil { + return nil, nil, fmt.Errorf("failed to connect to database: %w", err) + } + + db = dbx + } + + dblogger := dblog.NewLogger() + db = sqldblogger.OpenDriver(dsn, db.Driver(), dblogger) + + if db != nil { + return db, dblogger, nil + } + + return nil, nil, errors.New("unsupported database") +} diff --git a/pkg/amigo/run_migration.go b/pkg/amigo/run_migration.go new file mode 100644 index 0000000..fef0839 --- /dev/null +++ b/pkg/amigo/run_migration.go @@ -0,0 +1,60 @@ +package amigo + +import ( + "context" + "database/sql" + "github.com/alexisvisco/amigo/pkg/amigoctx" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/schema/pg" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils/dblog" + "log/slog" + "os" + "time" +) + +// RunPostgresMigrations migrates the database, it is launched via the generated main file or manually in a codebase. +func RunPostgresMigrations(options *RunMigrationOptions) (bool, error) { + var ( + db *sql.DB + dblogger *dblog.Logger + err error + conn *sql.DB + ) + + if options.SchemaVersionTable == "" { + options.SchemaVersionTable = schema.TableName(amigoctx.DefaultSchemaVersionTable) + } + + if options.MigrationDirection == "" { + options.MigrationDirection = types.MigrationDirectionUp + } + + if options.Timeout == 0 { + options.Timeout = amigoctx.DefaultTimeout + } + + if options.Connection != nil { + conn = options.Connection + } else { + db, dblogger, err = GetConnection(options.DSN) + if err != nil { + return false, err + } + conn = db + } + + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(options.Timeout)) + defer cancel() + + migrator := schema.NewMigrator(ctx, conn, pg.NewPostgres, &schema.MigratorOption{ + DryRun: options.DryRun, + ContinueOnError: options.ContinueOnError, + SchemaVersionTable: options.SchemaVersionTable, + DBLogger: dblogger, + }) + + slog.New(slog.NewJSONHandler(os.Stdout, nil)) + return migrator.Apply(options.MigrationDirection, options.Version, options.Steps, options.Migrations), nil + +} diff --git a/pkg/amigo/setup_slog.go b/pkg/amigo/setup_slog.go new file mode 100644 index 0000000..29d841e --- /dev/null +++ b/pkg/amigo/setup_slog.go @@ -0,0 +1,24 @@ +package amigo + +import ( + "github.com/alexisvisco/amigo/pkg/utils/logger" + "io" + "log/slog" +) + +func SetupSlog(showSQL bool, debug bool, json bool, writer io.Writer) { + logger.ShowSQLEvents = showSQL + + if json { + slog.SetDefault(slog.New(slog.NewJSONHandler(writer, nil))) + } else { + slog.SetDefault(slog.New(logger.NewHandler(writer, nil))) + } + + level := slog.LevelInfo + if debug { + level = slog.LevelDebug + } + + slog.SetLogLoggerLevel(level) +} diff --git a/pkg/amigoctx/ctx.go b/pkg/amigoctx/ctx.go new file mode 100644 index 0000000..5e6a654 --- /dev/null +++ b/pkg/amigoctx/ctx.go @@ -0,0 +1,187 @@ +package amigoctx + +import ( + "errors" + "fmt" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/spf13/cobra" + "regexp" + "strings" + "time" +) + +var ( + ErrDSNEmpty = errors.New("dsn is empty") +) + +var ( + DefaultSchemaVersionTable = "public.mig_schema_versions" + DefaultAmigoFolder = ".amigo" + DefaultMigrationFolder = "migrations" + DefaultPackagePath = "migrations" + DefaultShellPath = "/bin/bash" + DefaultPGDumpPath = "pg_dump" + DefaultTimeout = 2 * time.Minute +) + +type Context struct { + *Root + + Migrate *Migrate + Rollback *Rollback + Create *Create +} + +func NewContext() *Context { + return &Context{ + Root: &Root{}, + Migrate: &Migrate{}, + Rollback: &Rollback{}, + Create: &Create{}, + } +} + +type Root struct { + AmigoFolderPath string + DSN string + JSON bool + ShowSQL bool + MigrationFolder string + PackagePath string + SchemaVersionTable string + ShellPath string + PGDumpPath string + Debug bool +} + +func (r *Root) Register(cmd *cobra.Command) { + cmd.PersistentFlags().StringVarP(&r.AmigoFolderPath, "amigo-folder", "m", DefaultAmigoFolder, + "The folder path to use for creating amigo related files related to this repository") + + cmd.PersistentFlags().StringVar(&r.DSN, "dsn", "", + "The database connection string example: postgres://user:password@host:port/dbname?sslmode=disable") + + cmd.PersistentFlags().BoolVarP(&r.JSON, "json", "j", false, "Output in json format") + + cmd.PersistentFlags().StringVar(&r.MigrationFolder, "folder", DefaultMigrationFolder, + "The folder where the migrations are stored") + + cmd.PersistentFlags().StringVarP(&r.PackagePath, "package", "p", DefaultPackagePath, + "The package name for the migrations") + + cmd.PersistentFlags().StringVarP(&r.SchemaVersionTable, "schema-version-table", "t", + DefaultSchemaVersionTable, "The table name for the migrations") + + cmd.PersistentFlags().StringVar(&r.ShellPath, "shell-path", DefaultShellPath, + "the shell to use (for: amigo create --dump, it uses pg dump command)") + + cmd.PersistentFlags().BoolVar(&r.ShowSQL, "sql", false, "Print SQL queries") + + cmd.Flags().StringVar(&r.PGDumpPath, "pg-dump-path", DefaultPGDumpPath, + "the path to the pg_dump command if --dump is set") + + cmd.PersistentFlags().BoolVar(&r.Debug, "debug", false, "Print debug information") +} + +func (r *Root) ValidateDSN() error { + if r.DSN == "" { + return ErrDSNEmpty + } + + allowedDrivers := []string{"postgres"} + + for _, driver := range allowedDrivers { + if strings.Contains(r.DSN, driver) { + return nil + } + } + + return fmt.Errorf("unsupported driver, allowed drivers are: %s", strings.Join(allowedDrivers, ", ")) +} + +type Migrate struct { + Version string + DryRun bool + ContinueOnError bool + Timeout time.Duration +} + +func (m *Migrate) Register(cmd *cobra.Command) { + cmd.Flags().StringVar(&m.Version, "version", "", + "Apply a specific version format: 20240502083700 or 20240502083700_name.go") + cmd.Flags().BoolVar(&m.DryRun, "dry-run", false, "Run the migrations without applying them") + cmd.Flags().BoolVar(&m.ContinueOnError, "continue-on-error", false, + "Will not rollback the migration if an error occurs") + cmd.Flags().DurationVar(&m.Timeout, "timeout", DefaultTimeout, "The timeout for the migration") +} + +type Rollback struct { + Version string + Steps int + DryRun bool + ContinueOnError bool + Timeout time.Duration +} + +func (r *Rollback) Register(cmd *cobra.Command) { + cmd.Flags().StringVar(&r.Version, "version", "", + "Apply a specific version format: 20240502083700 or 20240502083700_name.go") + cmd.Flags().IntVar(&r.Steps, "steps", 1, "The number of steps to rollback") + cmd.Flags().BoolVar(&r.DryRun, "dry-run", false, "Run the migrations without applying them") + cmd.Flags().BoolVar(&r.ContinueOnError, "continue-on-error", false, + "Will not rollback the migration if an error occurs") + cmd.Flags().DurationVar(&r.Timeout, "timeout", DefaultTimeout, "The timeout for the migration") +} + +func (r *Rollback) ValidateSteps() error { + if r.Steps < 0 { + return fmt.Errorf("steps must be greater than 0") + } + + return nil +} + +func (r *Rollback) ValidateVersion() error { + if r.Version == "" { + return nil + } + + re := regexp.MustCompile(`\d{14}(_\w+)?\.go`) + if !re.MatchString(r.Version) { + return fmt.Errorf("version must be in the format: 20240502083700 or 20240502083700_name.go") + } + + return nil +} + +type Create struct { + Type string + Dump bool + Schema string + Skip bool +} + +func (c *Create) Register(cmd *cobra.Command) { + cmd.Flags().StringVar(&c.Type, "type", "change", + "The type of migration to create, possible values are [classic, change]") + + cmd.Flags().BoolVarP(&c.Dump, "dump", "d", false, + "dump with pg_dump the current schema and add it to the current migration") + + cmd.Flags().StringVarP(&c.Schema, "dump-schema", "s", "public", "the schema to dump if --dump is set") + + cmd.Flags().BoolVar(&c.Skip, "skip", false, + "skip will set the migration as applied without executing it") +} + +func (c *Create) ValidateType() error { + allowedTypes := []string{string(types.MigrationFileTypeClassic), string(types.MigrationFileTypeChange)} + + for _, t := range allowedTypes { + if c.Type == t { + return nil + } + } + + return fmt.Errorf("unsupported type, allowed types are: %s", strings.Join(allowedTypes, ", ")) +} diff --git a/pkg/entrypoint/main.go b/pkg/entrypoint/main.go index 9c5cbc7..8ed674b 100644 --- a/pkg/entrypoint/main.go +++ b/pkg/entrypoint/main.go @@ -1,12 +1,14 @@ package entrypoint import ( + "bytes" "flag" "fmt" - "github.com/alexisvisco/mig/pkg/mig" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/amigo" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" "io" "os" "text/tabwriter" @@ -15,7 +17,7 @@ import ( func MainPostgres(migrations []schema.Migration) { opts := createMainOptions(migrations) - ok, err := mig.MigratePostgres(opts) + ok, err := amigo.RunPostgresMigrations(opts) if err != nil { fmt.Fprintf(os.Stderr, "unable to %s database: %v\n", opts.MigrationDirection, err) @@ -27,7 +29,7 @@ func MainPostgres(migrations []schema.Migration) { } } -func createMainOptions(migrations []schema.Migration) *mig.MainOptions { +func createMainOptions(migrations []schema.Migration) *amigo.RunMigrationOptions { dsnFlag := flag.String("dsn", "", "URL connection to the database") versionFlag := flag.String("version", "", "Apply or rollback a specific version") directionFlag := flag.String("direction", "", "Possibles values are: migrate or rollback") @@ -38,30 +40,35 @@ func createMainOptions(migrations []schema.Migration) *mig.MainOptions { dryRunFlag := flag.Bool("dry-run", false, "Dry run the migration will not apply the migration to the database") continueOnErrorFlag := flag.Bool("continue-on-error", false, "Continue on error will not rollback the migration if an error occurs") - schemaVersionTableFlag := flag.String("schema-version-table", "schema_version", "Table name for the schema version") - verboseFlag := flag.Bool("verbose", false, "Print SQL statements") + schemaVersionTableFlag := flag.String("schema-version-table", "mig_schema_versions", + "Table name for the schema version") + showSQLFlag := flag.Bool("sql", false, "Print SQL statements") stepsFlag := flag.Int("steps", 1, "Number of steps to rollback") + debugFlag := flag.Bool("debug", false, "Print debug information") // Parse flags flag.Parse() - if *verboseFlag { - fmt.Println("-- flags:") - tw := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) + var out io.Writer = os.Stdout + if *silentFlag { + out = io.Discard + } + + amigo.SetupSlog(*showSQLFlag, *debugFlag, *jsonFlag, out) + + if *debugFlag { + buf := bytes.NewBuffer(nil) + + tw := tabwriter.NewWriter(buf, 1, 1, 1, ' ', 0) flag.VisitAll(func(f *flag.Flag) { fmt.Fprintf(tw, " %s\t%v\n", f.Name, f.Value) }) tw.Flush() - } - var out io.Writer = os.Stdout - if *silentFlag { - out = io.Discard + logger.Debug(events.MessageEvent{Message: fmt.Sprintf("flags: \n%s", buf.String())}) } - t := tracker.NewLogger(*jsonFlag, out) - - return &mig.MainOptions{ + return &amigo.RunMigrationOptions{ DSN: *dsnFlag, Version: versionFlag, MigrationDirection: types.MigrationDirection(*directionFlag), @@ -70,8 +77,7 @@ func createMainOptions(migrations []schema.Migration) *mig.MainOptions { DryRun: *dryRunFlag, ContinueOnError: *continueOnErrorFlag, SchemaVersionTable: schema.TableName(*schemaVersionTableFlag), - Verbose: *verboseFlag, + ShowSQL: *showSQLFlag, Steps: stepsFlag, - Tracker: t, } } diff --git a/pkg/mig/execute_main.go b/pkg/mig/execute_main.go deleted file mode 100644 index 5507c44..0000000 --- a/pkg/mig/execute_main.go +++ /dev/null @@ -1,108 +0,0 @@ -package mig - -import ( - "database/sql" - "fmt" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils" - "github.com/alexisvisco/mig/pkg/utils/cmdexec" - "github.com/alexisvisco/mig/pkg/utils/tracker" - "os" - "path" - "strings" - "time" -) - -type MainOptions struct { - DSN string - Connection *sql.DB - - MigrationDirection types.MigrationDirection - - Version *string - Steps *int - SchemaVersionTable schema.TableName - DryRun bool - ContinueOnError bool - Timeout time.Duration - Migrations []schema.Migration - - JSON bool - Verbose bool - - Shell string - Tracker tracker.Tracker `json:"-"` -} - -func ExecuteMain(mainLocation string, options *MainOptions) error { - mainFolder := path.Dir(mainLocation) - - stat, err := os.Stat(mainFolder) - if os.IsNotExist(err) { - err := os.MkdirAll(mainFolder, 0755) - if err != nil { - return fmt.Errorf("unable to create main folder: %w", err) - } - } else if err != nil { - return fmt.Errorf("unable to check if main folder exist: %w", err) - } - - if stat != nil && !stat.IsDir() { - return fmt.Errorf("main folder is not a directory") - } - - mainFilePath := path.Join(mainFolder, "main.go") - _, err = os.Stat(mainFilePath) - if os.IsNotExist(err) { - return fmt.Errorf("%s file does not exist, please run 'mig init' to create it", mainFilePath) - } - - args := []string{ - "go", - "run", - path.Join(mainFolder, "main.go"), - "-dsn", options.DSN, - "-direction", string(options.MigrationDirection), - "-timeout", options.Timeout.String(), - "-schema-version-table", string(options.SchemaVersionTable), - } - - if options.Steps != nil { - args = append(args, "-steps", fmt.Sprintf("%d", *options.Steps)) - } - - if options.Verbose { - args = append(args, "-verbose") - } - - if options.ContinueOnError { - args = append(args, "-continue-on-error") - } - - if options.DryRun { - args = append(args, "-dry-run") - } - if options.Version != nil { - v, err := utils.ParseMigrationVersion(*options.Version) - if err != nil { - return fmt.Errorf("unable to parse version: %w", err) - } - args = append(args, "-version", v) - } - - if options.JSON { - args = append(args, "-json") - } - - if options.Verbose { - options.Tracker.AddEvent(tracker.InfoEvent{Message: fmt.Sprintf("executing %s", args)}) - } - - err = cmdexec.ExecToWriter(options.Shell, []string{"-c", strings.Join(args, " ")}, nil, os.Stdout, os.Stderr) - if err != nil { - return fmt.Errorf("unable to execute %s: %w", mainFilePath, err) - } - - return nil -} diff --git a/pkg/mig/get_connection.go b/pkg/mig/get_connection.go deleted file mode 100644 index 9da4142..0000000 --- a/pkg/mig/get_connection.go +++ /dev/null @@ -1,46 +0,0 @@ -package mig - -import ( - "database/sql" - "errors" - "fmt" - "github.com/alexisvisco/mig/pkg/utils/dblog" - _ "github.com/jackc/pgx/v5/stdlib" - "github.com/lmittmann/tint" - sqldblogger "github.com/simukti/sqldb-logger" - "log/slog" - "os" - "strings" - "time" -) - -func GetConnection(dsn string, verbose bool) (*sql.DB, error) { - if dsn == "" { - return nil, errors.New("dsn is required, example: postgres://user:password@localhost:5432/dbname?sslmode=disable") - } - - var db *sql.DB - - if strings.HasPrefix(dsn, "postgres") { - dbx, err := sql.Open("pgx", dsn) - if err != nil { - return nil, fmt.Errorf("failed to connect to database: %w", err) - } - - db = dbx - } - - if verbose && db != nil { - recorder := dblog.NewLogger(slog.New(tint.NewHandler(os.Stdout, &tint.Options{ - TimeFormat: time.Kitchen, - }))) - - db = sqldblogger.OpenDriver(dsn, db.Driver(), recorder) - } - - if db != nil { - return db, nil - } - - return nil, errors.New("unsupported database") -} diff --git a/pkg/mig/postgres.go b/pkg/mig/postgres.go deleted file mode 100644 index dc601e3..0000000 --- a/pkg/mig/postgres.go +++ /dev/null @@ -1,34 +0,0 @@ -package mig - -import ( - "context" - "database/sql" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/schema/pg" - "time" -) - -// MigratePostgres migrates the database, it is launched via the generated main file. -func MigratePostgres(options *MainOptions) (bool, error) { - var conn *sql.DB - if options.Connection != nil { - conn = options.Connection - } else { - db, err := GetConnection(options.DSN, options.Verbose) - if err != nil { - return false, err - } - conn = db - } - - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(options.Timeout)) - defer cancel() - - migrator := schema.NewMigrator(ctx, conn, options.Tracker, pg.NewPostgres, &schema.MigratorOption{ - DryRun: options.DryRun, - ContinueOnError: options.ContinueOnError, - SchemaVersionTable: options.SchemaVersionTable, - }) - - return migrator.Apply(options.MigrationDirection, options.Version, options.Steps, options.Migrations), nil -} diff --git a/pkg/schema/migrator.go b/pkg/schema/migrator.go index c7afc58..3f323bf 100644 --- a/pkg/schema/migrator.go +++ b/pkg/schema/migrator.go @@ -3,11 +3,12 @@ package schema import ( "context" "database/sql" - "errors" "fmt" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils/dblog" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" "slices" "time" ) @@ -23,6 +24,8 @@ type MigratorOption struct { ContinueOnError bool SchemaVersionTable TableName + + DBLogger dblog.DatabaseLogger } // Migration is the interface that describes a migration at is simplest form. @@ -61,7 +64,6 @@ type Migrator[T Schema] struct { func NewMigrator[T Schema]( ctx context.Context, db *sql.DB, - tracker tracker.Tracker, schemaFactory SchemaFactory[T], opts *MigratorOption, ) *Migrator[T] { @@ -72,7 +74,6 @@ func NewMigrator[T Schema]( Context: ctx, MigratorOptions: opts, MigrationEvents: &MigrationEvents{}, - Track: tracker, }, } } @@ -85,7 +86,7 @@ func (m *Migrator[T]) Apply(direction types.MigrationDirection, version *string, // the first migration is always the creation of the schema version table migrationsToExecute = append(migrationsToExecute, migrations[0]) } else { - migrationsToExecute = m.deduceMigrationsToExecute(db, + migrationsToExecute = m.findMigrationsToExecute(db, direction, migrations, version, @@ -93,10 +94,13 @@ func (m *Migrator[T]) Apply(direction types.MigrationDirection, version *string, } if len(migrationsToExecute) == 0 { - m.ctx.Track.AddEvent(tracker.InfoEvent{Message: "no migrations to apply"}) + logger.Info(events.MessageEvent{Message: "Found 0 migrations to apply"}) return true } + m.ToggleDBLog(true) + defer m.ToggleDBLog(false) + for _, migration := range migrationsToExecute { var migrationFunc func(T) @@ -112,14 +116,16 @@ func (m *Migrator[T]) Apply(direction types.MigrationDirection, version *string, case SimpleMigration[T]: migrationFunc = t.Change default: - m.ctx.RaiseError(errors.New("invalid migration type")) + logger.Error(events.MessageEvent{Message: fmt.Sprintf("Migration %s is not a valid migration", + migration.Name())}) + return false } switch direction { case types.MigrationDirectionUp: - m.ctx.Track.AddEvent(tracker.MigrateUpEvent{MigrationName: migration.Name(), Time: migration.Date()}) + logger.Info(events.MigrateUpEvent{MigrationName: migration.Name(), Time: migration.Date()}) case types.MigrationDirectionDown: - m.ctx.Track.AddEvent(tracker.MigrateDownEvent{MigrationName: migration.Name(), Time: migration.Date()}) + logger.Info(events.MigrateDownEvent{MigrationName: migration.Name(), Time: migration.Date()}) } if !m.run(direction, fmt.Sprint(migration.Date().UTC().Format(utils.FormatTime)), migrationFunc) { @@ -130,10 +136,10 @@ func (m *Migrator[T]) Apply(direction types.MigrationDirection, version *string, return true } -func (m *Migrator[T]) deduceMigrationsToExecute( +func (m *Migrator[T]) findMigrationsToExecute( s Schema, - migtype types.MigrationDirection, - migrations []Migration, + migrationDirection types.MigrationDirection, + allMigrations []Migration, version *string, steps *int, // only used for rollback ) []Migration { @@ -142,12 +148,12 @@ func (m *Migrator[T]) deduceMigrationsToExecute( var migrationsTimeFormat []string var versionToMigration = make(map[string]Migration) - for _, migration := range migrations { + for _, migration := range allMigrations { migrationsTimeFormat = append(migrationsTimeFormat, migration.Date().UTC().Format(utils.FormatTime)) versionToMigration[migrationsTimeFormat[len(migrationsTimeFormat)-1]] = migration } - switch migtype { + switch migrationDirection { case types.MigrationDirectionUp: if version != nil && *version != "" { if versionToMigration[*version] == nil { @@ -162,9 +168,6 @@ func (m *Migrator[T]) deduceMigrationsToExecute( break } - fmt.Println(appliedVersions) - fmt.Println(migrationsTimeFormat) - for _, currentMigrationVersion := range migrationsTimeFormat { if !slices.Contains(appliedVersions, currentMigrationVersion) { versionsToApply = append(versionsToApply, versionToMigration[currentMigrationVersion]) @@ -189,7 +192,7 @@ func (m *Migrator[T]) deduceMigrationsToExecute( step = *steps } - for i := len(migrations) - 1; i >= 0; i-- { + for i := len(allMigrations) - 1; i >= 0; i-- { if slices.Contains(appliedVersions, migrationsTimeFormat[i]) { versionsToApply = append(versionsToApply, versionToMigration[migrationsTimeFormat[i]]) } @@ -211,7 +214,7 @@ func (m *Migrator[T]) run(migrationType types.MigrationDirection, version string tx, err := m.db.BeginTx(currentContext.Context, nil) if err != nil { - m.ctx.Track.AddEvent(tracker.InfoEvent{Message: "unable to start transaction"}) + logger.Error(events.MessageEvent{Message: "unable to start transaction"}) return false } @@ -219,11 +222,11 @@ func (m *Migrator[T]) run(migrationType types.MigrationDirection, version string handleError := func(err any) { if err != nil { - m.ctx.Track.AddEvent(tracker.InfoEvent{Message: fmt.Sprintf("migration failed, rollback due to: %v", err)}) + logger.Error(events.MessageEvent{Message: fmt.Sprintf("migration failed, rollback due to: %v", err)}) err := tx.Rollback() if err != nil { - m.ctx.Track.AddEvent(tracker.InfoEvent{Message: "unable to rollback transaction"}) + logger.Error(events.MessageEvent{Message: "unable to rollback transaction"}) } ok = false @@ -238,6 +241,7 @@ func (m *Migrator[T]) run(migrationType types.MigrationDirection, version string f(schema) + fmt.Println("Migration type ", migrationType) switch migrationType { case types.MigrationDirectionUp: schema.AddVersion(version) @@ -246,16 +250,16 @@ func (m *Migrator[T]) run(migrationType types.MigrationDirection, version string } if m.ctx.MigratorOptions.DryRun { - m.ctx.Track.AddEvent(tracker.InfoEvent{Message: "migration in dry run mode, rollback transaction..."}) + logger.Info(events.MessageEvent{Message: "migration in dry run mode, rollback transaction..."}) err := tx.Rollback() if err != nil { - m.ctx.Track.AddEvent(tracker.InfoEvent{Message: "unable to rollback transaction"}) + logger.Error(events.MessageEvent{Message: "unable to rollback transaction"}) } return true } else { err := tx.Commit() if err != nil { - m.ctx.Track.AddEvent(tracker.InfoEvent{Message: "unable to commit transaction"}) + logger.Error(events.MessageEvent{Message: "unable to commit transaction"}) return false } } @@ -271,3 +275,9 @@ func (m *Migrator[T]) NewSchema() T { func (m *Migrator[T]) Options() MigratorOption { return *m.ctx.MigratorOptions } + +func (m *Migrator[T]) ToggleDBLog(b bool) { + if m.Options().DBLogger != nil { + m.Options().DBLogger.ToggleLogger(b) + } +} diff --git a/pkg/schema/migrator_context.go b/pkg/schema/migrator_context.go index 6db8a3f..129d14b 100644 --- a/pkg/schema/migrator_context.go +++ b/pkg/schema/migrator_context.go @@ -4,8 +4,9 @@ import ( "context" "errors" "fmt" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" ) // MigratorContext is the context of the migrator. @@ -16,8 +17,6 @@ type MigratorContext struct { MigratorOptions *MigratorOption MigrationEvents *MigrationEvents - Track tracker.Tracker - MigrationDirection types.MigrationDirection } @@ -29,6 +28,7 @@ type MigrationEvents struct { columnsCreated []ColumnOptions primaryKeysCreated []PrimaryKeyConstraintOptions tablesCreated []TableOptions + versionCreated []string columnsRenamed []RenameColumnOptions columnComments []ColumnCommentOptions @@ -40,6 +40,7 @@ type MigrationEvents struct { checkConstraintsDropped []DropCheckConstraintOptions foreignKeyConstraintsDropped []DropForeignKeyConstraintOptions primaryKeyConstraintsDropped []DropPrimaryKeyConstraintOptions + versionDeleted []string } // ForceStopError is an error that stops the migration process even if the `continue_on_error` option is set. @@ -61,7 +62,7 @@ func (m *MigratorContext) RaiseError(err error) { if !m.MigratorOptions.ContinueOnError && !isForceStopError { panic(err) } else { - m.Track.AddEvent(tracker.InfoEvent{ + logger.Info(events.MessageEvent{ Message: fmt.Sprintf("migration error found, continue due to `continue_on_error` option: %s", err.Error()), }) } @@ -83,80 +84,90 @@ func (m *MigratorContext) addError(err error) { func (m *MigratorContext) AddCheckConstraintCreated(options CheckConstraintOptions) { m.MigrationEvents.checkConstraintsCreated = append(m.MigrationEvents.checkConstraintsCreated, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddForeignKeyCreated(options AddForeignKeyConstraintOptions) { m.MigrationEvents.foreignKeysCreated = append(m.MigrationEvents.foreignKeysCreated, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddIndexCreated(options IndexOptions) { m.MigrationEvents.indexesCreated = append(m.MigrationEvents.indexesCreated, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddExtensionCreated(options ExtensionOptions) { m.MigrationEvents.extensionsCreated = append(m.MigrationEvents.extensionsCreated, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddExtensionDropped(options DropExtensionOptions) { m.MigrationEvents.extensionsDropped = append(m.MigrationEvents.extensionsDropped, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddColumnCreated(options ColumnOptions) { m.MigrationEvents.columnsCreated = append(m.MigrationEvents.columnsCreated, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddPrimaryKeyCreated(options PrimaryKeyConstraintOptions) { m.MigrationEvents.primaryKeysCreated = append(m.MigrationEvents.primaryKeysCreated, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddTableCreated(options TableOptions) { m.MigrationEvents.tablesCreated = append(m.MigrationEvents.tablesCreated, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddTableDropped(options DropTableOptions) { m.MigrationEvents.tablesDropped = append(m.MigrationEvents.tablesDropped, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddIndexDropped(options DropIndexOptions) { m.MigrationEvents.indexesDropped = append(m.MigrationEvents.indexesDropped, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddRenameColumn(options RenameColumnOptions) { m.MigrationEvents.columnsRenamed = append(m.MigrationEvents.columnsRenamed, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddColumnDropped(options DropColumnOptions) { m.MigrationEvents.columnsDropped = append(m.MigrationEvents.columnsDropped, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddColumnComment(options ColumnCommentOptions) { m.MigrationEvents.columnComments = append(m.MigrationEvents.columnComments, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddCheckConstraintDropped(options DropCheckConstraintOptions) { m.MigrationEvents.checkConstraintsDropped = append(m.MigrationEvents.checkConstraintsDropped, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddForeignKeyConstraintDropped(options DropForeignKeyConstraintOptions) { m.MigrationEvents.foreignKeyConstraintsDropped = append(m.MigrationEvents.foreignKeyConstraintsDropped, options) - m.Track.AddEvent(options) + logger.Info(options) } func (m *MigratorContext) AddPrimaryKeyConstraintDropped(options DropPrimaryKeyConstraintOptions) { m.MigrationEvents.primaryKeyConstraintsDropped = append(m.MigrationEvents.primaryKeyConstraintsDropped, options) - m.Track.AddEvent(options) + logger.Info(options) +} + +func (m *MigratorContext) AddVersionCreated(version string) { + m.MigrationEvents.versionCreated = append(m.MigrationEvents.versionCreated, version) + logger.Info(events.VersionAddedEvent{Version: version}) +} + +func (m *MigratorContext) AddVersionDeleted(version string) { + m.MigrationEvents.versionDeleted = append(m.MigrationEvents.versionDeleted, version) + logger.Info(events.VersionDeletedEvent{Version: version}) } diff --git a/pkg/schema/options.go b/pkg/schema/options.go index f83e523..7a70214 100644 --- a/pkg/schema/options.go +++ b/pkg/schema/options.go @@ -2,7 +2,7 @@ package schema import ( "fmt" - "github.com/alexisvisco/mig/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils" "strings" ) diff --git a/pkg/schema/pg/postgres.go b/pkg/schema/pg/postgres.go index 3b8aa02..22f3eea 100644 --- a/pkg/schema/pg/postgres.go +++ b/pkg/schema/pg/postgres.go @@ -2,9 +2,9 @@ package pg import ( "fmt" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils" "github.com/georgysavva/scany/v2/dbscan" ) @@ -163,6 +163,8 @@ func (p *Schema) DropExtension(name string, opt ...schema.DropExtensionOptions) p.Context.AddExtensionDropped(options) } +// AddVersion adds a new version to the schema_migrations table. +// This function is not reversible. func (p *Schema) AddVersion(version string) { sql := `INSERT INTO {version_table} (id) VALUES ($1)` @@ -176,9 +178,11 @@ func (p *Schema) AddVersion(version string) { return } - // todo: add version to context + p.Context.AddVersionCreated(version) } +// RemoveVersion removes a version from the schema_migrations table. +// This function is not reversible. func (p *Schema) RemoveVersion(version string) { sql := `DELETE FROM {version_table} WHERE id = $1` @@ -192,9 +196,10 @@ func (p *Schema) RemoveVersion(version string) { return } - // todo: add remove version to context + p.Context.AddVersionDeleted(version) } +// FindAppliedVersions returns all the applied versions in the schema_migrations table. func (p *Schema) FindAppliedVersions() []string { sql := `SELECT id FROM {version_table} ORDER BY id ASC` diff --git a/pkg/schema/pg/postgres_column.go b/pkg/schema/pg/postgres_column.go index 5b686cd..2781db2 100644 --- a/pkg/schema/pg/postgres_column.go +++ b/pkg/schema/pg/postgres_column.go @@ -2,10 +2,11 @@ package pg import ( "fmt" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" "strings" ) @@ -293,7 +294,7 @@ func (p *Schema) DropColumn(tableName schema.TableName, columnName string, opt . if options.Reversible != nil { p.rollbackMode().AddColumn(tableName, columnName, options.Reversible.ColumnType, *options.Reversible) } else { - p.Context.Track.AddEvent(tracker.InfoEvent{ + logger.Warn(events.MessageEvent{ Message: fmt.Sprintf("unable to recreate the column %s.%s", tableName, columnName), }) } diff --git a/pkg/schema/pg/postgres_column_test.go b/pkg/schema/pg/postgres_column_test.go index 2a14fd8..d85d7d6 100644 --- a/pkg/schema/pg/postgres_column_test.go +++ b/pkg/schema/pg/postgres_column_test.go @@ -1,9 +1,9 @@ package pg import ( - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/utils" - "github.com/alexisvisco/mig/pkg/utils/testutils" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils/testutils" "github.com/stretchr/testify/require" "testing" ) @@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() p.AddColumn(schema.Table("articles", sc), "name", "text", schema.ColumnOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "name", DataType: "text"}, @@ -38,7 +38,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() Default: "default_name", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "name", DataType: "text", ColumnDefault: utils.Ptr("'default_name'::text")}, @@ -53,7 +53,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() Limit: 255, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "name", DataType: "character varying"}, @@ -67,7 +67,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() p.AddColumn(schema.Table("articles", sc), "id", schema.ColumnTypePrimaryKey, schema.ColumnOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "id", DataType: "integer", PrimaryKey: true, ColumnDefault: utils.Ptr("nextval")}, @@ -86,7 +86,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() Limit: 2, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "a", DataType: "integer", ColumnDefault: utils.Ptr("nextval")}, @@ -117,7 +117,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() }) }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "a", DataType: "numeric"}, @@ -135,7 +135,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() p.AddColumn(schema.Table("articles", sc), "id_plus_1", "numeric GENERATED ALWAYS AS (id + 1) STORED", schema.ColumnOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "id", DataType: "integer", ColumnDefault: utils.Ptr("nextval")}, @@ -158,7 +158,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() Scale: 2, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "name", DataType: "ARRAY"}, @@ -174,7 +174,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() NotNull: true, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "name", DataType: "text"}, @@ -202,7 +202,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles() Comment: "this is a comment", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "name", DataType: "text"}, @@ -224,7 +224,7 @@ func TestPostgres_DropColumn(t *testing.T) { p.DropColumn(schema.Table("articles", sc), "id") - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "name", DataType: "text"}, @@ -238,7 +238,7 @@ func TestPostgres_DropColumn(t *testing.T) { p.DropColumn(schema.Table("articles", sc), "id", schema.DropColumnOptions{IfExists: true}) p.DropColumn(schema.Table("articles", sc), "id", schema.DropColumnOptions{IfExists: true}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "name", DataType: "text"}, @@ -260,7 +260,7 @@ func TestPostgres_AddColumnComment(t *testing.T) { p.AddColumnComment(schema.Table("articles", sc), "id", utils.Ptr("this is a comment"), schema.ColumnCommentOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) }) t.Run("null comment", func(t *testing.T) { @@ -269,7 +269,7 @@ func TestPostgres_AddColumnComment(t *testing.T) { p.AddColumnComment(schema.Table("articles", sc), "id", nil, schema.ColumnCommentOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) }) } @@ -286,7 +286,7 @@ func TestPostgres_RenameColumn(t *testing.T) { p.RenameColumn(schema.Table("articles", sc), "id", "new_id") - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "name", DataType: "text"}, {ColumnName: "new_id", DataType: "integer"}, diff --git a/pkg/schema/pg/postgres_constraint.go b/pkg/schema/pg/postgres_constraint.go index d00819f..8e7cec3 100644 --- a/pkg/schema/pg/postgres_constraint.go +++ b/pkg/schema/pg/postgres_constraint.go @@ -2,16 +2,17 @@ package pg import ( "fmt" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" "github.com/gobuffalo/flect" "strings" ) // AddCheckConstraint Adds a new check constraint to the Table. -// expression parameter is a String representation of verifiable boolean condition. +// expression parameter is a FormatRecords representation of verifiable boolean condition. // // Example: // @@ -111,7 +112,7 @@ func (p *Schema) DropCheckConstraint(tableName schema.TableName, constraintName p.rollbackMode().AddCheckConstraint(tableName, constraintName, options.Reversible.Expression, *options.Reversible) } else { - p.Context.Track.AddEvent(tracker.InfoEvent{ + logger.Warn(events.MessageEvent{ Message: fmt.Sprintf("unable re-creating check constraint %s", options.ConstraintName), }) } @@ -304,7 +305,7 @@ func (p *Schema) DropForeignKeyConstraint(from, to schema.TableName, opt ...sche if options.Reversible != nil { p.rollbackMode().AddForeignKeyConstraint(from, to, *options.Reversible) } else { - p.Context.Track.AddEvent(tracker.InfoEvent{ + logger.Error(events.MessageEvent{ Message: fmt.Sprintf("unable re-creating foreign key %s", options.ForeignKeyName), }) } @@ -447,7 +448,7 @@ func (p *Schema) DropPrimaryKeyConstraint(tableName schema.TableName, opts ...sc if options.Reversible != nil { p.rollbackMode().AddPrimaryKeyConstraint(tableName, options.Reversible.Columns, *options.Reversible) } else { - p.Context.Track.AddEvent(tracker.InfoEvent{ + logger.Error(events.MessageEvent{ Message: fmt.Sprintf("unable re-creating primary key %s", options.PrimaryKeyName), }) } diff --git a/pkg/schema/pg/postgres_constraint_test.go b/pkg/schema/pg/postgres_constraint_test.go index 0f148e7..a52ca40 100644 --- a/pkg/schema/pg/postgres_constraint_test.go +++ b/pkg/schema/pg/postgres_constraint_test.go @@ -1,9 +1,9 @@ package pg import ( - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/utils" - "github.com/alexisvisco/mig/pkg/utils/testutils" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils/testutils" "github.com/stretchr/testify/require" "testing" ) @@ -27,7 +27,7 @@ func TestPostgres_AddCheckConstraint(t *testing.T) { "name <> ''", schema.CheckConstraintOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintExist(t, p, schema.Table("test_table", sc), "ck_test_table_constraint_1") }) @@ -45,7 +45,7 @@ func TestPostgres_AddCheckConstraint(t *testing.T) { }, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintExist(t, p, schema.Table("test_table", sc), "lalalalala") }) @@ -59,7 +59,7 @@ func TestPostgres_AddCheckConstraint(t *testing.T) { "name <> ''", schema.CheckConstraintOptions{Validate: utils.Ptr(false)}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintExist(t, p, schema.Table("test_table", sc), "ck_test_table_constraint_3") }) @@ -105,7 +105,7 @@ func TestPostgres_DropCheckConstraint(t *testing.T) { p.DropCheckConstraint(schema.Table("test_table", sc), "constraint_1") - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintNotExist(t, p, schema.Table("test_table", sc), "table_constraint_1") }) @@ -118,7 +118,7 @@ func TestPostgres_DropCheckConstraint(t *testing.T) { ConstraintName: "ck_test_table_constraint_1", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintNotExist(t, p, schema.Table("test_table", sc), "ck_test_table_constraint_1") }) @@ -178,7 +178,7 @@ CREATE TABLE IF NOT EXISTS {schema}.authors }, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintExist(t, p, schema.Table("articles", sc), "lalalalala") }) @@ -206,7 +206,7 @@ CREATE TABLE IF NOT EXISTS {schema}.authors Column: "user_id", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) }) t.Run("custom primary key", func(t *testing.T) { @@ -233,7 +233,7 @@ CREATE TABLE IF NOT EXISTS {schema}.authors PrimaryKey: "ref", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintExist(t, p, schema.Table("articles", sc), "fk_articles_authors") }) @@ -262,7 +262,7 @@ CREATE TABLE IF NOT EXISTS {schema}.orders CompositePrimaryKey: []string{"shop_id", "user_id"}, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintExist(t, p, schema.Table("orders", sc), "fk_orders_carts") }) @@ -276,7 +276,7 @@ CREATE TABLE IF NOT EXISTS {schema}.orders OnDelete: "cascade", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintExist(t, p, schema.Table("articles", sc), "fk_articles_authors") }) @@ -290,7 +290,7 @@ CREATE TABLE IF NOT EXISTS {schema}.orders OnUpdate: "cascade", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintExist(t, p, schema.Table("articles", sc), "fk_articles_authors") }) @@ -304,7 +304,7 @@ CREATE TABLE IF NOT EXISTS {schema}.orders Deferrable: "DEFERRABLE INITIALLY DEFERRED", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintExist(t, p, schema.Table("articles", sc), "fk_articles_authors") }) @@ -318,7 +318,7 @@ CREATE TABLE IF NOT EXISTS {schema}.orders Validate: utils.Ptr(false), }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintExist(t, p, schema.Table("articles", sc), "fk_articles_authors") }) @@ -357,7 +357,7 @@ alter table {schema}.articles add constraint fk_articles_authors foreign key (au p.DropForeignKeyConstraint(schema.Table("articles", sc), schema.Table("authors", sc)) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintNotExist(t, p, schema.Table("articles", sc), "fk_articles_authors") }) @@ -371,7 +371,7 @@ alter table {schema}.articles add constraint fk_articles_authors foreign key (au ForeignKeyName: "fk_articles_authors", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertConstraintNotExist(t, p, schema.Table("articles", sc), "fk_articles_authors") }) @@ -405,7 +405,7 @@ func TestPostgres_AddPrimaryKeyConstraint(t *testing.T) { p.AddPrimaryKeyConstraint(schema.Table("articles", sc), []string{"id"}, schema.PrimaryKeyConstraintOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "id", DataType: "integer", PrimaryKey: true}, @@ -420,7 +420,7 @@ func TestPostgres_AddPrimaryKeyConstraint(t *testing.T) { p.AddPrimaryKeyConstraint(schema.Table("articles", sc), []string{"id", "name"}, schema.PrimaryKeyConstraintOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "id", DataType: "integer", PrimaryKey: true}, @@ -456,7 +456,7 @@ func TestPostgres_DropPrimaryKeyConstraint(t *testing.T) { p.DropPrimaryKeyConstraint(schema.Table("articles", sc)) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) require.Equal(t, []columnInfo{ {ColumnName: "id", DataType: "integer"}, diff --git a/pkg/schema/pg/postgres_exists.go b/pkg/schema/pg/postgres_exists.go index 2368843..76ca5c4 100644 --- a/pkg/schema/pg/postgres_exists.go +++ b/pkg/schema/pg/postgres_exists.go @@ -3,7 +3,7 @@ package pg import ( "context" "fmt" - "github.com/alexisvisco/mig/pkg/schema" + "github.com/alexisvisco/amigo/pkg/schema" "github.com/georgysavva/scany/v2/dbscan" ) diff --git a/pkg/schema/pg/postgres_exists_test.go b/pkg/schema/pg/postgres_exists_test.go index db63157..40ecc28 100644 --- a/pkg/schema/pg/postgres_exists_test.go +++ b/pkg/schema/pg/postgres_exists_test.go @@ -1,8 +1,8 @@ package pg import ( - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/utils" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/utils" "testing" ) diff --git a/pkg/schema/pg/postgres_index.go b/pkg/schema/pg/postgres_index.go index 56d2ec0..1265a8d 100644 --- a/pkg/schema/pg/postgres_index.go +++ b/pkg/schema/pg/postgres_index.go @@ -2,10 +2,11 @@ package pg import ( "fmt" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" "strings" ) @@ -219,7 +220,7 @@ func (p *Schema) DropIndex(table schema.TableName, columns []string, opt ...sche if options.Reversible != nil { p.rollbackMode().AddIndexConstraint(table, columns, *options.Reversible) } else { - p.Context.Track.AddEvent(tracker.InfoEvent{ + logger.Warn(events.MessageEvent{ Message: fmt.Sprintf("unable re-creating index %s", options.IndexName), }) } diff --git a/pkg/schema/pg/postgres_index_test.go b/pkg/schema/pg/postgres_index_test.go index 31dd405..d9b6dc9 100644 --- a/pkg/schema/pg/postgres_index_test.go +++ b/pkg/schema/pg/postgres_index_test.go @@ -1,8 +1,8 @@ package pg import ( - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/utils/testutils" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/utils/testutils" "github.com/stretchr/testify/require" "testing" ) @@ -25,7 +25,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles p.AddIndexConstraint(schema.Table("articles", sc), []string{"name"}, schema.IndexOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) asserIndexExist(t, p, schema.Table("articles", sc), "idx_articles_name") }) @@ -36,7 +36,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles p.AddIndexConstraint(schema.Table("articles", sc), []string{"name"}, schema.IndexOptions{Unique: true}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) asserIndexExist(t, p, schema.Table("articles", sc), "idx_articles_name") }) @@ -51,7 +51,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles }, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) asserIndexExist(t, p, schema.Table("articles", sc), "lalalalala") }) @@ -64,7 +64,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles Method: "btree", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) asserIndexExist(t, p, schema.Table("articles", sc), "idx_articles_name") }) @@ -77,7 +77,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles Concurrent: true, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) asserIndexExist(t, p, schema.Table("articles", sc), "idx_articles_name") }) @@ -90,7 +90,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles Order: "DESC", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) asserIndexExist(t, p, schema.Table("articles", sc), "idx_articles_name") }) @@ -101,7 +101,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles p.AddIndexConstraint(schema.Table("articles", sc), []string{"name", "id"}, schema.IndexOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) asserIndexExist(t, p, schema.Table("articles", sc), "idx_articles_name_id") }) @@ -117,7 +117,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles }, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) asserIndexExist(t, p, schema.Table("articles", sc), "idx_articles_name_id") }) @@ -130,7 +130,7 @@ CREATE TABLE IF NOT EXISTS {schema}.articles Predicate: "name IS NOT NULL", }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) asserIndexExist(t, p, schema.Table("articles", sc), "idx_articles_name") }) diff --git a/pkg/schema/pg/postgres_table.go b/pkg/schema/pg/postgres_table.go index 32a5a7d..45e7078 100644 --- a/pkg/schema/pg/postgres_table.go +++ b/pkg/schema/pg/postgres_table.go @@ -2,11 +2,12 @@ package pg import ( "fmt" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/types" - "github.com/alexisvisco/mig/pkg/utils" - "github.com/alexisvisco/mig/pkg/utils/orderedmap" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/types" + "github.com/alexisvisco/amigo/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" + "github.com/alexisvisco/amigo/pkg/utils/orderedmap" "strings" ) @@ -16,7 +17,7 @@ import ( // // p.CreateTable("users", func(t *pg.PostgresTableDef) { // t.Serial("id") -// t.String("name") +// t.FormatRecords("name") // t.Integer("age") // }) // @@ -29,7 +30,7 @@ import ( // To create a table without a primary key: // // p.CreateTable("users", func(t *pg.PostgresTableDef) { -// t.String("name") +// t.FormatRecords("name") // }, schema.TableOptions{ WithoutPrimaryKey: true }) // // Generates: @@ -39,7 +40,7 @@ import ( // To create a table with a composite primary key: // // p.CreateTable("users", func(t *pg.PostgresTableDef) { -// t.String("name") +// t.FormatRecords("name") // t.Integer("age") // }, schema.TableOptions{ PrimaryKeys: []string{"name", "age"} }) // @@ -53,7 +54,7 @@ import ( // To add index to the table: // // p.CreateTable("users", func(t *pg.PostgresTableDef) { -// t.String("name") +// t.FormatRecords("name") // t.Index([]string{"name"}) // }) // @@ -65,7 +66,7 @@ import ( // To add foreign key to the table: // // p.CreateTable("users", func(t *pg.PostgresTableDef) { -// t.String("name") +// t.FormatRecords("name") // t.Integer("article_id") // t.ForeignKey("articles") // }) @@ -78,7 +79,7 @@ import ( // To add created_at, updated_at Columns to the table: // // p.CreateTable("users", func(t *pg.PostgresTableDef) { -// t.String("name") +// t.FormatRecords("name") // t.Timestamps() // }) // @@ -394,7 +395,7 @@ func (p *PostgresTableDef) ForeignKey(toTable schema.TableName, opts ...schema.A // schema.TableName: "users", // TableDefinition: Innerschema.Tablefunc(t *PostgresTableDef) { // t.Serial("id") -// t.String("name") +// t.FormatRecords("name") // }), // }}) // @@ -411,7 +412,7 @@ func (p *Schema) DropTable(tableName schema.TableName, opts ...schema.DropTableO if options.Reversible != nil { p.rollbackMode().CreateTable(tableName, func(t *PostgresTableDef) {}, *options.Reversible) } else { - p.Context.Track.AddEvent(tracker.InfoEvent{ + logger.Warn(events.MessageEvent{ Message: fmt.Sprintf("unable to reverse dropping table %s", tableName.String()), }) } diff --git a/pkg/schema/pg/postgres_table_test.go b/pkg/schema/pg/postgres_table_test.go index 216ff5f..5eedbcb 100644 --- a/pkg/schema/pg/postgres_table_test.go +++ b/pkg/schema/pg/postgres_table_test.go @@ -1,8 +1,8 @@ package pg import ( - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/utils/testutils" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/utils/testutils" "github.com/stretchr/testify/require" "testing" ) @@ -31,7 +31,7 @@ func TestPostgres_CreateTable(t *testing.T) { t.Integer("views") }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertTableExist(t, p, schema.Table("articles", sc)) }) @@ -47,7 +47,7 @@ func TestPostgres_CreateTable(t *testing.T) { PrimaryKeys: []string{"custom_id"}, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertTableExist(t, p, schema.Table("articles", sc)) }) @@ -64,7 +64,7 @@ func TestPostgres_CreateTable(t *testing.T) { PrimaryKeys: []string{"id", "author_id"}, }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertTableExist(t, p, schema.Table("articles", sc)) }) @@ -86,7 +86,7 @@ func TestPostgres_CreateTable(t *testing.T) { t.ForeignKey(schema.Table("articles", sc)) }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertTableExist(t, p, schema.Table("articles", sc)) assertTableExist(t, p, schema.Table("authors", sc)) }) @@ -106,7 +106,7 @@ func TestPostgres_CreateTable(t *testing.T) { t.Index([]string{"content", "views"}) }) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertTableExist(t, p, schema.Table("articles", sc)) }) @@ -126,7 +126,7 @@ func TestPostgres_CreateTable(t *testing.T) { t.String("title") }) - testutils.AssertSnapshotDiff(t, r.String(), true) + testutils.AssertSnapshotDiff(t, r.FormatRecords(), true) assertTableExist(t, p, schema.Table("articles", sc)) assertTableExist(t, p, schema.Table("articles_without_id", sc)) }) @@ -158,7 +158,7 @@ func TestPostgres_DropTable(t *testing.T) { p.DropTable(schema.Table("articles", sc)) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertTableNotExist(t, p, schema.Table("articles", sc)) }) @@ -171,7 +171,7 @@ func TestPostgres_DropTable(t *testing.T) { }) p.DropTable(schema.Table("articles", sc), schema.DropTableOptions{IfExists: true}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) assertTableNotExist(t, p, schema.Table("articles", sc)) }) } diff --git a/pkg/schema/pg/postgres_test.go b/pkg/schema/pg/postgres_test.go index 8df70cd..7a77d7c 100644 --- a/pkg/schema/pg/postgres_test.go +++ b/pkg/schema/pg/postgres_test.go @@ -1,25 +1,22 @@ package pg import ( - "bytes" "context" "database/sql" "fmt" - "github.com/alexisvisco/mig/pkg/schema" - "github.com/alexisvisco/mig/pkg/utils" - "github.com/alexisvisco/mig/pkg/utils/dblog" - "github.com/alexisvisco/mig/pkg/utils/testutils" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/schema" + "github.com/alexisvisco/amigo/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils/dblog" + "github.com/alexisvisco/amigo/pkg/utils/logger" + "github.com/alexisvisco/amigo/pkg/utils/testutils" "github.com/georgysavva/scany/v2/dbscan" _ "github.com/jackc/pgx/v5/stdlib" - "github.com/lmittmann/tint" sqldblogger "github.com/simukti/sqldb-logger" "github.com/stretchr/testify/require" "log/slog" "os" "strings" "testing" - "time" ) var ( @@ -41,25 +38,21 @@ var ( postgresDB) ) -func init() { - //enableSnapshot["all"] = struct{}{} -} - -func connect(t *testing.T) (*sql.DB, testutils.Recorder) { +func connect(t *testing.T) (*sql.DB, dblog.DatabaseLogger) { db, err := sql.Open("pgx", conn) require.NoError(t, err) - recorder := dblog.NewLogger(slog.New(tint.NewHandler(os.Stdout, &tint.Options{ - TimeFormat: time.Kitchen, - }))) + logger.ShowSQLEvents = true + slog.SetDefault(slog.New(logger.NewHandler(os.Stdout, &logger.Options{}))) + recorder := dblog.NewLogger() db = sqldblogger.OpenDriver(conn, db.Driver(), recorder) return db, recorder } -func initSchema(t *testing.T, name string, number ...int32) (*sql.DB, testutils.Recorder, *schema.Migrator[*Schema], string) { +func initSchema(t *testing.T, name string, number ...int32) (*sql.DB, dblog.DatabaseLogger, *schema.Migrator[*Schema], string) { conn, recorder := connect(t) t.Cleanup(func() { _ = conn.Close() @@ -75,9 +68,7 @@ func initSchema(t *testing.T, name string, number ...int32) (*sql.DB, testutils. _, err = conn.ExecContext(context.Background(), fmt.Sprintf("CREATE SCHEMA %s", schemaName)) require.NoError(t, err) - out := bytes.Buffer{} - mig := schema.NewMigrator(context.Background(), conn, tracker.NewLogger(false, &out), NewPostgres, - &schema.MigratorOption{}) + mig := schema.NewMigrator(context.Background(), conn, NewPostgres, &schema.MigratorOption{}) return conn, recorder, mig, schemaName } @@ -98,7 +89,7 @@ func TestPostgres_AddExtension(t *testing.T) { p.DropExtension("hstore", schema.DropExtensionOptions{IfExists: true}) p.AddExtension("hstore", schema.ExtensionOptions{Schema: schemaName}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) }) t.Run("without schema", func(t *testing.T) { @@ -107,7 +98,7 @@ func TestPostgres_AddExtension(t *testing.T) { p.DropExtension("hstore", schema.DropExtensionOptions{IfExists: true}) p.AddExtension("hstore", schema.ExtensionOptions{}) - testutils.AssertSnapshotDiff(t, r.String()) + testutils.AssertSnapshotDiff(t, r.FormatRecords()) }) t.Run("with IfNotExists", func(t *testing.T) { @@ -138,7 +129,7 @@ func asserIndexExist(t *testing.T, p *Schema, tableName schema.TableName, indexN require.True(t, p.IndexExist(tableName, indexName)) } -func baseTest(t *testing.T, init string, schema string, number ...int32) (postgres *Schema, rec testutils.Recorder, schem string) { +func baseTest(t *testing.T, init string, schema string, number ...int32) (postgres *Schema, rec dblog.DatabaseLogger, schem string) { conn, rec, mig, schem := initSchema(t, schema, number...) replacer := utils.Replacer{ @@ -152,6 +143,7 @@ func baseTest(t *testing.T, init string, schema string, number ...int32) (postgr p := mig.NewSchema() + rec.ToggleLogger(true) rec.SetRecord(true) return p, rec, schem diff --git a/pkg/schema/pg/utils_test.go b/pkg/schema/pg/utils_test.go index 90889fc..8296347 100644 --- a/pkg/schema/pg/utils_test.go +++ b/pkg/schema/pg/utils_test.go @@ -2,7 +2,7 @@ package pg import ( "context" - "github.com/alexisvisco/mig/pkg/schema" + "github.com/alexisvisco/amigo/pkg/schema" "github.com/stretchr/testify/require" "testing" ) diff --git a/pkg/schema/reversible.go b/pkg/schema/reversible.go index a995542..52e4b6c 100644 --- a/pkg/schema/reversible.go +++ b/pkg/schema/reversible.go @@ -1,6 +1,6 @@ package schema -import "github.com/alexisvisco/mig/pkg/types" +import "github.com/alexisvisco/amigo/pkg/types" type ReversibleMigrationExecutor struct { migratorContext *MigratorContext diff --git a/pkg/templates/main.go.tmpl b/pkg/templates/main.go.tmpl index c17c380..067a59a 100644 --- a/pkg/templates/main.go.tmpl +++ b/pkg/templates/main.go.tmpl @@ -2,7 +2,7 @@ package main import ( migrations "{{ .PackagePath }}" - "github.com/alexisvisco/mig/pkg/entrypoint" + "github.com/alexisvisco/amigo/pkg/entrypoint" ) // Main is the entrypoint of the migrations diff --git a/pkg/templates/migration_change.go.tmpl b/pkg/templates/migration_change.go.tmpl index c624bad..59aa153 100644 --- a/pkg/templates/migration_change.go.tmpl +++ b/pkg/templates/migration_change.go.tmpl @@ -2,7 +2,7 @@ package {{ .Package }} import ( "{{ .PackageDriverPath }}" - "github.com/alexisvisco/mig/pkg/schema" + "github.com/alexisvisco/amigo/pkg/schema" "time" ) diff --git a/pkg/templates/migration_classic.go.tmpl b/pkg/templates/migration_classic.go.tmpl index 9128223..37b98c8 100644 --- a/pkg/templates/migration_classic.go.tmpl +++ b/pkg/templates/migration_classic.go.tmpl @@ -2,7 +2,7 @@ package migrations import ( "{{ .PackageDriverPath }}" - "github.com/alexisvisco/mig/pkg/schema" + "github.com/alexisvisco/amigo/pkg/schema" "time" ) diff --git a/pkg/templates/migrations.go.tmpl b/pkg/templates/migrations.go.tmpl index 4eca8ac..3a6b032 100644 --- a/pkg/templates/migrations.go.tmpl +++ b/pkg/templates/migrations.go.tmpl @@ -3,7 +3,7 @@ package {{ .Package }} import ( - "github.com/alexisvisco/mig/pkg/schema" + "github.com/alexisvisco/amigo/pkg/schema" ) var Migrations = []schema.Migration{ diff --git a/pkg/templates/util.go b/pkg/templates/util.go index 9c08b60..923a971 100644 --- a/pkg/templates/util.go +++ b/pkg/templates/util.go @@ -3,7 +3,7 @@ package templates import ( "bytes" _ "embed" - "github.com/alexisvisco/mig/pkg/types" + "github.com/alexisvisco/amigo/pkg/types" "text/template" ) diff --git a/pkg/types/types.go b/pkg/types/types.go index f97ce11..caa82a1 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -49,7 +49,7 @@ var DriverValues = []Driver{ func (d Driver) PackagePath() string { switch d { case DriverPostgres: - return "github.com/alexisvisco/mig/pkg/schema/pg" + return "github.com/alexisvisco/amigo/pkg/schema/pg" } return "" diff --git a/pkg/utils/dblog/log.go b/pkg/utils/dblog/log.go index 908ac35..671dafe 100644 --- a/pkg/utils/dblog/log.go +++ b/pkg/utils/dblog/log.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "github.com/alecthomas/chroma/v2/quick" - "github.com/alexisvisco/mig/pkg/utils/tracker" + "github.com/alexisvisco/amigo/pkg/utils/events" + "github.com/alexisvisco/amigo/pkg/utils/logger" "github.com/charmbracelet/lipgloss" sqldblogger "github.com/simukti/sqldb-logger" - "log/slog" "strings" ) @@ -16,16 +16,23 @@ var ( blue = lipgloss.Color("#11DAF9") ) +type DatabaseLogger interface { + Log(context.Context, sqldblogger.Level, string, map[string]interface{}) + Record(func()) string + SetRecord(bool) + FormatRecords() string + ToggleLogger(bool) +} + type Logger struct { - l *slog.Logger record bool + log bool queries []string params [][]any - tracker tracker.Tracker } -func NewLogger(l *slog.Logger) *Logger { - return &Logger{l: l} +func NewLogger() *Logger { + return &Logger{} } func (l *Logger) Record(f func()) string { @@ -35,12 +42,16 @@ func (l *Logger) Record(f func()) string { f() l.record = false - str := l.String() + str := l.FormatRecords() return str } -func (l *Logger) String() string { +func (l *Logger) ToggleLogger(b bool) { + l.log = b +} + +func (l *Logger) FormatRecords() string { str := "" for i, query := range l.queries { @@ -69,7 +80,11 @@ func (l *Logger) SetRecord(v bool) { l.record = v } -func (l *Logger) Log(ctx context.Context, level sqldblogger.Level, msg string, data map[string]interface{}) { +func (l *Logger) Log(_ context.Context, _ sqldblogger.Level, _ string, data map[string]interface{}) { + if !l.log { + return + } + if log, ok := data["query"]; ok && l.record { l.queries = append(l.queries, log.(string)) @@ -80,33 +95,16 @@ func (l *Logger) Log(ctx context.Context, level sqldblogger.Level, msg string, d } } - //attrs := make([]slog.Attr, 0, len(data)) - //for k, v := range data { - // attrs = append(attrs, slog.Any(k, v)) - //} - - //var lvl slog.Level - //switch level { - //case sqldblogger.LevelTrace: - // lvl = slog.LevelDebug - 1 - // attrs = append(attrs, slog.Any("LOG_LEVEL", level)) - //case sqldblogger.LevelDebug: - // lvl = slog.LevelDebug - //case sqldblogger.LevelInfo: - // lvl = slog.LevelInfo - //case sqldblogger.LevelError: - // lvl = slog.LevelError - //default: - // lvl = slog.LevelError - // attrs = append(attrs, slog.Any("LOG_LEVEL", level)) - //} - mayDuration := data["duration"] mayQuery := data["query"] mayArgs := data["args"] s := &strings.Builder{} + if mayQuery == nil || mayQuery.(string) == "" { + return + } + durLenght := 0 if mayDuration != nil { str := fmt.Sprintf("(%v) ", mayDuration) @@ -128,5 +126,5 @@ func (l *Logger) Log(ctx context.Context, level sqldblogger.Level, msg string, d mayArgs))) } - fmt.Println(s.String()) + logger.Info(events.SQLQueryEvent{Query: s.String()}) } diff --git a/pkg/utils/dblog/log_test.go b/pkg/utils/dblog/log_test.go deleted file mode 100644 index 5ff8f30..0000000 --- a/pkg/utils/dblog/log_test.go +++ /dev/null @@ -1 +0,0 @@ -package dblog diff --git a/pkg/utils/tracker/events.go b/pkg/utils/events/events.go similarity index 66% rename from pkg/utils/tracker/events.go rename to pkg/utils/events/events.go index 19439af..9c2d1f0 100644 --- a/pkg/utils/tracker/events.go +++ b/pkg/utils/events/events.go @@ -1,11 +1,15 @@ -package tracker +package events import ( "fmt" - "github.com/alexisvisco/mig/pkg/utils" + "github.com/alexisvisco/amigo/pkg/utils" "time" ) +type EventName interface { + EventName() string +} + type FileAddedEvent struct{ FileName string } func (p FileAddedEvent) String() string { return fmt.Sprintf("+ file: %s", p.FileName) } @@ -18,9 +22,9 @@ type FolderAddedEvent struct{ FolderName string } func (p FolderAddedEvent) String() string { return fmt.Sprintf("+ folder: %s", p.FolderName) } -type InfoEvent struct{ Message string } +type MessageEvent struct{ Message string } -func (p InfoEvent) String() string { return fmt.Sprintf("%s", p.Message) } +func (p MessageEvent) String() string { return fmt.Sprintf("%s", p.Message) } type RawEvent struct{ Message string } @@ -55,3 +59,27 @@ type SkipMigrationEvent struct { func (s SkipMigrationEvent) String() string { return fmt.Sprintf("------> skip migration: %d", s.MigrationVersion) } + +type SQLQueryEvent struct { + Query string +} + +func (s SQLQueryEvent) String() string { + return fmt.Sprintf(s.Query) +} + +type VersionAddedEvent struct { + Version string +} + +func (v VersionAddedEvent) String() string { + return fmt.Sprintf("------> version migrated: %s", v.Version) +} + +type VersionDeletedEvent struct { + Version string +} + +func (v VersionDeletedEvent) String() string { + return fmt.Sprintf("------> version rolled back: %s", v.Version) +} diff --git a/pkg/utils/logger/slog.go b/pkg/utils/logger/slog.go new file mode 100644 index 0000000..f70a1d3 --- /dev/null +++ b/pkg/utils/logger/slog.go @@ -0,0 +1,438 @@ +// Package logger is almost copy/paste from tint handler but with some modificatoins +package logger + +import ( + "context" + "encoding" + "fmt" + "github.com/alexisvisco/amigo/pkg/utils/events" + "io" + "log/slog" + "path/filepath" + "reflect" + "runtime" + "strconv" + "sync" + "time" + "unicode" +) + +const errKey = "err" + +var ( + defaultLevel = slog.LevelInfo + defaultTimeFormat = time.StampMilli +) + +var ShowSQLEvents = false + +// Options for a slog.Handler that writes tinted logs. A zero Options consists +// entirely of default values. +// +// Options can be used as a drop-in replacement for [slog.HandlerOptions]. +type Options struct { + // Enable source code location (Default: false) + AddSource bool + + // Minimum level to log (Default: slog.LevelInfo) + Level slog.Leveler + + // ReplaceAttr is called to rewrite each non-group attribute before it is logged. + // See https://pkg.go.dev/log/slog#HandlerOptions for details. + ReplaceAttr func(groups []string, attr slog.Attr) slog.Attr +} + +// NewHandler creates a [slog.Handler] that writes tinted logs to Writer w, +// using the default options. If opts is nil, the default options are used. +func NewHandler(w io.Writer, opts *Options) slog.Handler { + h := &handler{ + w: w, + level: defaultLevel, + timeFormat: defaultTimeFormat, + } + if opts == nil { + return h + } + + h.addSource = opts.AddSource + if opts.Level != nil { + h.level = opts.Level + } + h.replaceAttr = opts.ReplaceAttr + return h +} + +// handler implements a [slog.Handler]. +type handler struct { + attrsPrefix string + groupPrefix string + groups []string + + mu sync.Mutex + w io.Writer + + addSource bool + level slog.Leveler + replaceAttr func([]string, slog.Attr) slog.Attr + timeFormat string + noColor bool +} + +func (h *handler) clone() *handler { + return &handler{ + attrsPrefix: h.attrsPrefix, + groupPrefix: h.groupPrefix, + groups: h.groups, + w: h.w, + addSource: h.addSource, + level: h.level, + replaceAttr: h.replaceAttr, + timeFormat: h.timeFormat, + noColor: h.noColor, + } +} + +func (h *handler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.level.Level() +} + +func (h *handler) Handle(_ context.Context, r slog.Record) error { + // get a buffer from the sync pool + buf := newBuffer() + defer buf.Free() + + rep := h.replaceAttr + + // write level + if rep == nil { + h.appendLevel(buf, r.Level) + if r.Level == slog.LevelDebug || r.Level == slog.LevelError || r.Level == slog.LevelWarn { + buf.WriteByte(' ') + } + } else if a := rep(nil /* groups */, slog.Any(slog.LevelKey, r.Level)); a.Key != "" { + h.appendValue(buf, a.Value, false) + buf.WriteByte(' ') + } + + // write source + if h.addSource { + fs := runtime.CallersFrames([]uintptr{r.PC}) + f, _ := fs.Next() + if f.File != "" { + src := &slog.Source{ + Function: f.Function, + File: f.File, + Line: f.Line, + } + + if rep == nil { + h.appendSource(buf, src) + buf.WriteByte(' ') + } else if a := rep(nil /* groups */, slog.Any(slog.SourceKey, src)); a.Key != "" { + h.appendValue(buf, a.Value, false) + buf.WriteByte(' ') + } + } + } + + // if attrs has an event and + + // write message + if rep == nil { + if r.Message != "" { + buf.WriteString(r.Message) + buf.WriteByte(' ') + } + } else if a := rep(nil /* groups */, slog.String(slog.MessageKey, r.Message)); a.Key != "" { + h.appendValue(buf, a.Value, false) + buf.WriteByte(' ') + } + + // write handler attributes + if len(h.attrsPrefix) > 0 { + buf.WriteString(h.attrsPrefix) + } + + // write attributes + r.Attrs(func(attr slog.Attr) bool { + h.appendAttr(buf, attr, h.groupPrefix, h.groups) + return true + }) + + if len(*buf) == 0 { + return nil + } + (*buf)[len(*buf)-1] = '\n' // replace last space with newline + + h.mu.Lock() + defer h.mu.Unlock() + + _, err := h.w.Write(*buf) + return err +} + +func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler { + if len(attrs) == 0 { + return h + } + h2 := h.clone() + + buf := newBuffer() + defer buf.Free() + + // write attributes to buffer + for _, attr := range attrs { + h.appendAttr(buf, attr, h.groupPrefix, h.groups) + } + h2.attrsPrefix = h.attrsPrefix + string(*buf) + return h2 +} + +func (h *handler) WithGroup(name string) slog.Handler { + if name == "" { + return h + } + h2 := h.clone() + h2.groupPrefix += name + "." + h2.groups = append(h2.groups, name) + return h2 +} + +func (h *handler) appendTime(buf *buffer, t time.Time) { + *buf = t.AppendFormat(*buf, h.timeFormat) +} + +func (h *handler) appendLevel(buf *buffer, level slog.Level) { + switch { + case level == slog.LevelDebug: + buf.WriteString("debug:") + appendLevelDelta(buf, level-slog.LevelDebug) + case level == slog.LevelError: + buf.WriteString("\u001B[31merror:\033[0m") + case level == slog.LevelWarn: + buf.WriteString("\u001B[33mwarning:\033[0m") + } +} + +func appendLevelDelta(buf *buffer, delta slog.Level) { + if delta == 0 { + return + } else if delta > 0 { + buf.WriteByte('+') + } + *buf = strconv.AppendInt(*buf, int64(delta), 10) +} + +func (h *handler) appendSource(buf *buffer, src *slog.Source) { + dir, file := filepath.Split(src.File) + + buf.WriteString(filepath.Join(filepath.Base(dir), file)) + buf.WriteByte(':') + buf.WriteString(strconv.Itoa(src.Line)) +} + +func (h *handler) appendAttr(buf *buffer, attr slog.Attr, groupsPrefix string, groups []string) { + if attr.Key != "event" { + return + } + + attr.Value = attr.Value.Resolve() + if rep := h.replaceAttr; rep != nil && attr.Value.Kind() != slog.KindGroup { + attr = rep(groups, attr) + attr.Value = attr.Value.Resolve() + } + + if attr.Equal(slog.Attr{}) { + return + } + + if attr.Value.Kind() == slog.KindGroup { + if attr.Key != "" && attr.Key != "event" { + groupsPrefix += attr.Key + "." + groups = append(groups, attr.Key) + } + for _, groupAttr := range attr.Value.Group() { + h.appendAttr(buf, groupAttr, groupsPrefix, groups) + } + } else if err, ok := attr.Value.Any().(tintError); ok { + // append tintError + h.appendTintError(buf, err, groupsPrefix) + buf.WriteByte(' ') + } else { + if attr.Key != "event" { + h.appendKey(buf, attr.Key, groupsPrefix) + } + h.appendValue(buf, attr.Value, true) + buf.WriteByte(' ') + } +} + +func (h *handler) appendKey(buf *buffer, key, groups string) { + appendString(buf, groups+key, true) + buf.WriteByte('=') +} + +func (h *handler) appendValue(buf *buffer, v slog.Value, quote bool) { + switch v.Kind() { + case slog.KindString: + appendString(buf, v.String(), quote) + case slog.KindInt64: + *buf = strconv.AppendInt(*buf, v.Int64(), 10) + case slog.KindUint64: + *buf = strconv.AppendUint(*buf, v.Uint64(), 10) + case slog.KindFloat64: + *buf = strconv.AppendFloat(*buf, v.Float64(), 'g', -1, 64) + case slog.KindBool: + *buf = strconv.AppendBool(*buf, v.Bool()) + case slog.KindDuration: + appendString(buf, v.Duration().String(), quote) + case slog.KindTime: + appendString(buf, v.Time().String(), quote) + case slog.KindAny: + switch cv := v.Any().(type) { + case slog.Level: + h.appendLevel(buf, cv) + case encoding.TextMarshaler: + data, err := cv.MarshalText() + if err != nil { + break + } + appendString(buf, string(data), quote) + case fmt.Stringer: + appendString(buf, cv.String(), quote) + case *slog.Source: + h.appendSource(buf, cv) + default: + appendString(buf, fmt.Sprintf("%+v", v.Any()), quote) + } + } +} + +func (h *handler) appendTintError(buf *buffer, err error, groupsPrefix string) { + appendString(buf, groupsPrefix+errKey, true) + buf.WriteByte('=') + appendString(buf, err.Error(), true) +} + +func appendString(buf *buffer, s string, _ bool) { + buf.WriteString(s) +} + +func needsQuoting(s string) bool { + if len(s) == 0 { + return true + } + for _, r := range s { + if unicode.IsSpace(r) || r == '"' || r == '=' || !unicode.IsPrint(r) { + return true + } + } + return false +} + +type tintError struct{ error } + +// Err returns a tinted (colorized) [slog.Attr] that will be written in red color +// by the [tint.Handler]. When used with any other [slog.Handler], it behaves as +// +// slog.Any("err", err) +func Err(err error) slog.Attr { + if err != nil { + err = tintError{err} + } + return slog.Any(errKey, err) +} + +type buffer []byte + +var bufPool = sync.Pool{ + New: func() any { + b := make(buffer, 0, 1024) + return (*buffer)(&b) + }, +} + +func newBuffer() *buffer { + return bufPool.Get().(*buffer) +} + +func (b *buffer) Free() { + // To reduce peak allocation, return only smaller buffers to the pool. + const maxBufferSize = 16 << 10 + if cap(*b) <= maxBufferSize { + *b = (*b)[:0] + bufPool.Put(b) + } +} +func (b *buffer) Write(bytes []byte) (int, error) { + *b = append(*b, bytes...) + return len(bytes), nil +} + +func (b *buffer) WriteByte(char byte) error { + *b = append(*b, char) + return nil +} + +func (b *buffer) WriteString(str string) (int, error) { + *b = append(*b, str...) + return len(str), nil +} + +func (b *buffer) WriteStringIf(ok bool, str string) (int, error) { + if !ok { + return 0, nil + } + return b.WriteString(str) +} + +func event(event any) *slog.Logger { + name := reflect.TypeOf(event).Name() + if en, ok := event.(events.EventName); ok { + name = en.EventName() + } + + return slog.With(slog.Any("event", event), slog.String("event_name", name)) +} + +func Info(evt any) { + if !canLogEvent(evt) { + return + } + event(evt).Info("") +} + +func Error(evt any) { + if !canLogEvent(evt) { + return + } + event(evt).Error("") +} + +func Debug(evt any) { + if !canLogEvent(evt) { + return + } + event(evt).Debug("") +} + +func Warn(evt any) { + if !canLogEvent(evt) { + return + } + + event(evt).Warn("") +} + +func isSQLQueryEvent(event any) bool { + _, ok := event.(events.SQLQueryEvent) + return ok +} + +func canLogEvent(event any) bool { + if isSQLQueryEvent(event) && !ShowSQLEvents { + return false + } + + return true +} diff --git a/pkg/utils/logger/slog_test.go b/pkg/utils/logger/slog_test.go new file mode 100644 index 0000000..ce13dc7 --- /dev/null +++ b/pkg/utils/logger/slog_test.go @@ -0,0 +1,16 @@ +package logger + +import ( + "github.com/alexisvisco/amigo/pkg/utils/events" + "log/slog" + "os" + "testing" +) + +func TestLog(t *testing.T) { + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil))) + + evt := events.FileAddedEvent{FileName: "test.txt"} + + Info(evt) +} diff --git a/pkg/utils/testutils/recorder.go b/pkg/utils/testutils/recorder.go index de27bd8..95c4dc9 100644 --- a/pkg/utils/testutils/recorder.go +++ b/pkg/utils/testutils/recorder.go @@ -1,10 +1 @@ package testutils - -import "fmt" - -// Recorder is an interface to record a function. -type Recorder interface { - Record(f func()) string - SetRecord(v bool) - fmt.Stringer -} diff --git a/pkg/utils/tracker/event_tracker.go b/pkg/utils/tracker/event_tracker.go deleted file mode 100644 index 52c9ccc..0000000 --- a/pkg/utils/tracker/event_tracker.go +++ /dev/null @@ -1,79 +0,0 @@ -package tracker - -import ( - "encoding/json" - "fmt" - "io" - "reflect" - "time" -) - -type Event struct { - Type string - Event fmt.Stringer -} - -type EventName interface { - EventName() string -} - -type Tracker interface { - AddEvent(event fmt.Stringer) Tracker - Measure() Tracker - io.Writer -} - -type EventLogger struct { - Events []Event - - writer io.Writer - json bool - - timeTracker time.Time -} - -func NewLogger(json bool, writer io.Writer) *EventLogger { - return &EventLogger{ - timeTracker: time.Now(), - writer: writer, - json: json, - } -} - -func (p *EventLogger) AddEvent(event fmt.Stringer) Tracker { - name := reflect.TypeOf(event).Name() - if en, ok := event.(EventName); ok { - name = en.EventName() - } - - p.Events = append(p.Events, Event{ - Type: name, - Event: event, - }) - - if p.writer != nil { - str := event.String() - if p.json { - indent, _ := json.Marshal(p.Events) - str = string(indent) + "\n" - } - - // if last char is not a newline, add one - if len(str) > 0 && str[len(str)-1] != '\n' { - str += "\n" - } - - _, _ = p.Write([]byte(str)) - } - - return p -} - -func (p *EventLogger) Write(p0 []byte) (n int, err error) { - return p.writer.Write(p0) -} -func (p *EventLogger) Measure() Tracker { - p.AddEvent(MeasurementEvent{TimeElapsed: time.Since(p.timeTracker)}) - p.timeTracker = time.Now() - return p -} diff --git a/readme.md b/readme.md index 27c535e..a34247d 100644 --- a/readme.md +++ b/readme.md @@ -1,56 +1,73 @@ -# mig +[![GoDoc](https://pkg.go.dev/badge/alexisvisco/mig)](https://pkg.go.dev/alexisvisco/mig) -First rails like migration tool for golang. +# Introduction -Run migrations in go for go. +## MIG - Migrate SQL with Go language. -Example of a migration : +Migration In Golang (MIG) is a library that allows you to write migrations in Go language. +It provides you with all the benefits of Go, including type safety, simplicity, and strong tooling support. +MIG is designed to be easy to use and integrate into existing projects. -```go +General documentation: [https://mig-go.alexisvis.co](https://mig-go.alexisvis.co) + +## Features + +- **Go Language**: The library allows you to write migrations in Go, making it easy to define schema changes in a programming language you are already familiar with. +- **Type Safety**: Writing migrations in Go provides you with all the language's benefits, including type safety, simplicity, and strong tooling support. +- **Version Control**: Migrations are version controlled. +- **Compatibility**: The library supports working with already migrated databases and allows you to seamlessly integrate it into existing projects. + +## Installation + +To install the library, run the following command: + +```sh +go install github.com/alexisvisco/amigo@latest +``` + +## First usage + +```sh +amigo context --dsn "postgres://user:password@localhost:5432/dbname" # optional but it avoid to pass the dsn each time +amigo init # create the migrations folder, the main file to run migration +mit migrate # apply the migration +``` + +## Example of migration + +```templ package migrations import ( - "github.com/alexisvisco/mig/pkg/schema" - "time" + "github.com/alexisvisco/mig/pkg/schema/pg" + "github.com/alexisvisco/mig/pkg/schema" + "time" ) -type MigrationNewTable struct{} - -func (m MigrationNewTable) Change(t schema.Postgres) { - t.AddForeignKeyConstraint("users", "articles", schema.AddForeignKeyOptions{}) - t.AddCheckConstraint(schema.Table("users", "myschema"), "constraint_1", "name <> ''", - schema.CheckConstraintOptions{}) - - t.Reversible(schema.Directions{ - Up: func() { - // Add a thing here - }, - Down: func() { - // reverse the thing here - }, - }) +type Migration20240502155033SchemaVersion struct {} +func (m Migration20240502155033SchemaVersion) Change(s *pg.Schema) { + s.CreateTable("public.mig_schema_versions", func(s *pg.PostgresTableDef) { + s.String("id") + }) } -func (m MigrationNewTable) Name() string { - return "new_table" +func (m Migration20240502155033SchemaVersion) Name() string { + return "schema_version" } -func (m MigrationNewTable) CreatedDate() (time.Time, error) { - return time.Parse(time.RFC3339, "2021-08-01T00:00:00Z") +func (m Migration20240502155033SchemaVersion) Date() time.Time { + t, _ := time.Parse(time.RFC3339, "2024-05-02T17:50:33+02:00") + return t } ``` -What could go wrong ? -## Installation +## Supported databases -### For including it in your project -```shell -go get github.com/alexisvisco/mig -``` +- Postgres -### For using it as a cli -```shell -go install github.com/alexisvisco/mig/cmd/mig -``` +## Next supported databases + +- SQLite +- MySQL