Repository: ariga/atlas
Pull request #799 (2022-05-14 22:01:09)

cmd/atlascmd: env support for migrate commands


Patch:
diff --git a/cmd/atlascmd/atlascmd.go b/cmd/atlascmd/atlascmd.go
index d06ceacc..201e0ab1 100644
--- a/cmd/atlascmd/atlascmd.go
+++ b/cmd/atlascmd/atlascmd.go
@@ -29,6 +29,9 @@ var (
 	// Should be set by build script "-X 'ariga.io/atlas/cmd/action.version=${version}'"
 	version string
 
+	// selectedEnv stores the value for the global --env flag.
+	selectedEnv string
+
 	// schemaCmd represents the subcommand 'atlas version'.
 	versionCmd = &cobra.Command{
 		Use:   "version",
@@ -73,6 +76,11 @@ func init() {
 	Root.AddCommand(versionCmd)
 }
 
+// receivesEnv configures cmd to receive the common '--env' flag.
+func receivesEnv(cmd *cobra.Command) {
+	cmd.PersistentFlags().StringVarP(&selectedEnv, "env", "", "", "set which env from the project file to use")
+}
+
 // parse returns a user facing version and release notes url
 func parse(version string) (string, string) {
 	u := "https://github.com/ariga/atlas/releases/latest"
diff --git a/cmd/atlascmd/migrate.go b/cmd/atlascmd/migrate.go
index 2451e12a..173976ea 100644
--- a/cmd/atlascmd/migrate.go
+++ b/cmd/atlascmd/migrate.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"path/filepath"
 	"strings"
 
 	"ariga.io/atlas/sql/migrate"
@@ -47,6 +48,9 @@ var (
 		Short: "Manage versioned migration files",
 		Long:  "'atlas migrate' wraps several sub-commands for migration management.",
 		PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
+			if err := migrateFlagsFromEnv(cmd, nil); err != nil {
+				return err
+			}
 			// Migrate commands will not run on a broken migration directory, unless the force flag is given.
 			if !MigrateFlags.Force {
 				dir, err := dir()
@@ -79,8 +83,9 @@ directory and compares it to a given desired state and create a new migration fi
 the migration directory state to the desired schema. The desired state can be another connected database or an HCL file.`,
 		Example: `  atlas migrate diff --dev-url mysql://user:pass@localhost:3306/dev --to mysql://user:pass@localhost:3306/dbname
   atlas migrate diff --dev-url mysql://user:pass@localhost:3306/dev --to file://atlas.hcl`,
-		Args: cobra.MaximumNArgs(1),
-		RunE: CmdMigrateDiffRun,
+		Args:    cobra.MaximumNArgs(1),
+		PreRunE: migrateFlagsFromEnv,
+		RunE:    CmdMigrateDiffRun,
 	}
 	// MigrateHashCmd represents the migrate hash command.
 	MigrateHashCmd = &cobra.Command{
@@ -89,6 +94,7 @@ the migration directory state to the desired schema. The desired state can be an
 		Long: `'atlas migrate hash' computes the integrity hash sum of the migration directory and stores it in the atlas.sum file.
 This command should be used whenever a manual change in the migration directory was made.`,
 		Example: `  atlas migrate hash --force`,
+		PreRunE: migrateFlagsFromEnv,
 		RunE:    CmdMigrateHashRun,
 	}
 	// MigrateNewCmd represents the migrate new command.
@@ -98,6 +104,7 @@ This command should be used whenever a manual change in the migration directory
 		Long:    `'atlas migrate new' creates a new migration according to the configured formatter without any statements in it.`,
 		Example: `  atlas migrate new my-new-migration`,
 		Args:    cobra.MaximumNArgs(1),
+		PreRunE: migrateFlagsFromEnv,
 		RunE:    CmdMigrateNewRun,
 	}
 	// MigrateValidateCmd represents the migrate validate command.
@@ -110,7 +117,8 @@ executed on the connected database in order to validate SQL semantics.`,
 		Example: `  atlas migrate validate
   atlas migrate validate --dir /path/to/migration/directory
   atlas migrate validate --dir /path/to/migration/directory --dev-url mysql://user:pass@localhost:3306/dev`,
-		RunE: CmdMigrateValidateRun,
+		PreRunE: migrateFlagsFromEnv,
+		RunE:    CmdMigrateValidateRun,
 	}
 )
 
@@ -140,6 +148,7 @@ func init() {
 	cobra.CheckErr(MigrateDiffCmd.MarkFlagRequired(migrateDiffFlagTo))
 	// Validate flags.
 	devURL(MigrateValidateCmd.Flags())
+	receivesEnv(MigrateCmd)
 }
 
 // CmdMigrateDiffRun is the command executed when running the CLI with 'migrate diff' args.
@@ -191,7 +200,7 @@ func CmdMigrateDiffRun(cmd *cobra.Command, args []string) error {
 }
 
 // CmdMigrateHashRun is the command executed when running the CLI with 'migrate hash' args.
-func CmdMigrateHashRun(*cobra.Command, []string) error {
+func CmdMigrateHashRun(_ *cobra.Command, _ []string) error {
 	dir, err := dir()
 	if err != nil {
 		return err
@@ -336,3 +345,33 @@ func formatter() (migrate.Formatter, error) {
 		return nil, fmt.Errorf("unknown format %q", MigrateFlags.Format)
 	}
 }
+
+func migrateFlagsFromEnv(cmd *cobra.Command, _ []string) error {
+	activeEnv, err := selectEnv(selectedEnv)
+	if err != nil {
+		return err
+	}
+	if err := maySetFlag(cmd, migrateFlagDevURL, activeEnv.DevURL); err != nil {
+		return err
+	}
+	if err := maySetFlag(cmd, migrateFlagFormat, activeEnv.MigrationDir.Format); err != nil {
+		return err
+	}
+	// Transform "src" to a URL.
+	toURL := activeEnv.Source
+	if toURL != "" {
+		if toURL, err = filepath.Abs(activeEnv.Source); err != nil {
+			return fmt.Errorf("finding abs path to source: %q: %w", activeEnv.Source, err)
+		}
+		toURL = "file://" + toURL
+	}
+	if err := maySetFlag(cmd, migrateDiffFlagTo, toURL); err != nil {
+		return err
+	}
+	if s := "[" + strings.Join(activeEnv.Schemas, "") + "]"; len(activeEnv.Schemas) > 0 {
+		if err := maySetFlag(cmd, migrateFlagSchema, s); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/cmd/atlascmd/project.go b/cmd/atlascmd/project.go
index f9b1f9fb..cc212d12 100644
--- a/cmd/atlascmd/project.go
+++ b/cmd/atlascmd/project.go
@@ -19,6 +19,12 @@ type projectFile struct {
 	Envs []*Env `spec:"env"`
 }
 
+// MigrationDir represents the migration directory for the Env.
+type MigrationDir struct {
+	URL    string `spec:"url"`
+	Format string `spec:"format"`
+}
+
 // Env represents an Atlas environment.
 type Env struct {
 	// Name for this environment.
@@ -36,9 +42,17 @@ type Env struct {
 
 	// List of schemas in this database that are managed by Atlas.
 	Schemas []string `spec:"schemas"`
+
+	// Directory containing the migrations for the env.
+	MigrationDir *MigrationDir `spec:"migration_dir"`
+
 	schemaspec.DefaultExtension
 }
 
+var hclState = schemahcl.New(
+	schemahcl.WithScopedEnums("env.migration_dir.format", formatAtlas, formatFlyway, formatLiquibase, formatGoose, formatGolangMigrate),
+)
+
 // LoadEnv reads the project file in path, and loads the environment
 // with the provided name into env.
 func LoadEnv(path string, name string) (*Env, error) {
@@ -47,7 +61,7 @@ func LoadEnv(path string, name string) (*Env, error) {
 		return nil, err
 	}
 	var project projectFile
-	if err := schemahcl.New().Eval(b, &project, nil); err != nil {
+	if err := hclState.Eval(b, &project, nil); err != nil {
 		return nil, fmt.Errorf("error reading project file: %w", err)
 	}
 	projEnvs := make(map[string]*Env)
@@ -59,7 +73,10 @@ func LoadEnv(path string, name string) (*Env, error) {
 			return nil, fmt.Errorf("all envs must have names on file %q", path)
 		}
 		if e.URL == "" {
-			return nil, fmt.Errorf("no url set for e %q", e.Name)
+			return nil, fmt.Errorf("no url set for env %q", e.Name)
+		}
+		if e.Source == "" {
+			return nil, fmt.Errorf("no src set for env %q", e.Name)
 		}
 		projEnvs[e.Name] = e
 	}
@@ -67,6 +84,9 @@ func LoadEnv(path string, name string) (*Env, error) {
 	if !ok {
 		return nil, fmt.Errorf("env %q not defined in project file", name)
 	}
+	if selected.MigrationDir == nil {
+		selected.MigrationDir = &MigrationDir{}
+	}
 	return selected, nil
 }
 
diff --git a/cmd/atlascmd/schema.go b/cmd/atlascmd/schema.go
index a92d30fb..87622c8c 100644
--- a/cmd/atlascmd/schema.go
+++ b/cmd/atlascmd/schema.go
@@ -17,12 +17,19 @@ import (
 
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlclient"
-
 	"github.com/hashicorp/hcl/v2/hclwrite"
 	"github.com/manifoldco/promptui"
 	"github.com/spf13/cobra"
 )
 
+const (
+	urlFlag    = "url"
+	schemaFlag = "schema"
+	devURLFlag = "dev-url"
+	fileFlag   = "file"
+	dsnFlag    = "dsn"
+)
+
 var (
 	// schemaCmd represents the subcommand 'atlas schema'.
 	schemaCmd = &cobra.Command{
@@ -31,15 +38,22 @@ var (
 		Long:  "The `atlas schema` command groups subcommands for working with Atlas schemas.",
 	}
 
+	// SchemaFlags are common flags used by schema commands.
+	SchemaFlags struct {
+		URL     string
+		Schemas []string
+
+		// Deprecated: DSN is an alias for URL.
+		DSN string
+	}
+
 	// ApplyFlags are the flags used in SchemaApply command.
 	ApplyFlags struct {
-		URL         string
 		DevURL      string
 		File        string
 		Web         bool
 		Addr        string
 		DryRun      bool
-		Schema      []string
 		AutoApprove bool
 		Verbose     bool
 		Vars        map[string]string
@@ -55,7 +69,8 @@ migration, Atlas will print the migration plan and prompt the user for approval.
 
 If run with the "--dry-run" flag, atlas will exit after printing out the planned
 migration.`,
-		RunE: CmdApplyRun,
+		PreRunE: schemaFlagsFromEnv,
+		RunE:    CmdApplyRun,
 		Example: `  atlas schema apply -u "mysql://user:pass@localhost/dbname" -f atlas.hcl
   atlas schema apply -u "mysql://localhost" -f atlas.hcl --schema prod --schema staging
   atlas schema apply -u "mysql://user:pass@localhost:3306/dbname" -f atlas.hcl --dry-run
@@ -66,10 +81,8 @@ migration.`,
 
 	// InspectFlags are the flags used in SchemaInspect command.
 	InspectFlags struct {
-		URL    string
-		Web    bool
-		Addr   string
-		Schema []string
+		Web  bool
+		Addr string
 	}
 	// SchemaInspect represents the 'atlas schema inspect' subcommand.
 	SchemaInspect = &cobra.Command{
@@ -88,7 +101,8 @@ databases), omit the relevant part from the url, e.g. "mysql://user:pass@localho
 To select specific schemas from the databases, users may use the "--schema" (or "-s" shorthand)
 flag.
 	`,
-		RunE: CmdInspectRun,
+		PreRunE: schemaFlagsFromEnv,
+		RunE:    CmdInspectRun,
 		Example: `  atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname"
   atlas schema inspect -u "mariadb://user:pass@localhost:3306/" --schema=schemaA,schemaB -s schemaC
   atlas schema inspect --url "postgres://user:pass@host:port/dbname?sslmode=disable"
@@ -115,62 +129,123 @@ const (
 	answerAbort = "Abort"
 )
 
-// selectEnv selects the environment config from the current directory project file.
-func selectEnv(args []string) (*Env, error) {
-	if len(args) == 0 {
-		return nil, nil
-	}
-	return LoadEnv(projectFileName, args[0])
-}
-
 func init() {
+	// Common flags.
+	receivesEnv(schemaCmd)
+
 	// Schema apply flags.
 	schemaCmd.AddCommand(SchemaApply)
 	SchemaApply.Flags().SortFlags = false
-	SchemaApply.Flags().StringVarP(&ApplyFlags.File, "file", "f", "", "[/path/to/file] file containing the HCL schema.")
-	SchemaApply.Flags().StringVarP(&ApplyFlags.URL, "url", "u", "", "URL to the database using the format:\n[driver://username:password@address/dbname?param=value]")
-	SchemaApply.Flags().StringSliceVarP(&ApplyFlags.Schema, "schema", "s", nil, "Set schema names.")
-	SchemaApply.Flags().StringVarP(&ApplyFlags.DevURL, "dev-url", "", "", "URL for the dev database. Used to validate schemas and calculate diffs\nbefore running migration.")
+	SchemaApply.Flags().StringVarP(&ApplyFlags.File, fileFlag, "f", "", "[/path/to/file] file containing the HCL schema.")
+	SchemaApply.Flags().StringVarP(&SchemaFlags.URL, urlFlag, "u", "", "URL to the database using the format:\n[driver://username:password@address/dbname?param=value]")
+	SchemaApply.Flags().StringSliceVarP(&SchemaFlags.Schemas, schemaFlag, "s", nil, "Set schema names.")
+	SchemaApply.Flags().StringVarP(&ApplyFlags.DevURL, devURLFlag, "", "", "URL for the dev database. Used to validate schemas and calculate diffs\nbefore running migration.")
 	SchemaApply.Flags().BoolVarP(&ApplyFlags.DryRun, "dry-run", "", false, "Dry-run. Print SQL plan without prompting for execution.")
 	SchemaApply.Flags().BoolVarP(&ApplyFlags.AutoApprove, "auto-approve", "", false, "Auto approve. Apply the schema changes without prompting for approval.")
 	SchemaApply.Flags().BoolVarP(&ApplyFlags.Web, "web", "w", false, "Open in a local Atlas UI.")
 	SchemaApply.Flags().StringVarP(&ApplyFlags.Addr, "addr", "", ":5800", "used with -w, local address to bind the server to.")
 	SchemaApply.Flags().BoolVarP(&ApplyFlags.Verbose, migrateDiffFlagVerbose, "", false, "enable verbose logging")
 	SchemaApply.Flags().StringToStringVarP(&ApplyFlags.Vars, "var", "", nil, "input variables")
-	cobra.CheckErr(SchemaApply.MarkFlagRequired("url"))
-	fixURLFlag(SchemaApply, &ApplyFlags.URL)
+	SchemaApply.Flags().StringVarP(&SchemaFlags.DSN, dsnFlag, "d", "", "")
+	cobra.CheckErr(SchemaApply.Flags().MarkHidden(dsnFlag))
+	cobra.CheckErr(SchemaApply.MarkFlagRequired(urlFlag))
+	cobra.CheckErr(SchemaApply.MarkFlagRequired(fileFlag))
 
 	// Schema inspect flags.
 	schemaCmd.AddCommand(SchemaInspect)
-	SchemaInspect.Flags().StringVarP(&InspectFlags.URL, "url", "u", "", "[driver://username:password@protocol(address)/dbname?param=value] Select data source using the url format")
+	SchemaInspect.Flags().StringVarP(&SchemaFlags.URL, urlFlag, "u", "", "[driver://username:password@protocol(address)/dbname?param=value] Select data source using the url format")
 	SchemaInspect.Flags().BoolVarP(&InspectFlags.Web, "web", "w", false, "Open in a local Atlas UI")
 	SchemaInspect.Flags().StringVarP(&InspectFlags.Addr, "addr", "", ":5800", "Used with -w, local address to bind the server to")
-	SchemaInspect.Flags().StringSliceVarP(&InspectFlags.Schema, "schema", "s", nil, "Set schema name")
-	cobra.CheckErr(SchemaInspect.MarkFlagRequired("url"))
-	fixURLFlag(SchemaInspect, &InspectFlags.URL)
+	SchemaInspect.Flags().StringSliceVarP(&SchemaFlags.Schemas, schemaFlag, "s", nil, "Set schema name")
+	SchemaInspect.Flags().StringVarP(&SchemaFlags.DSN, dsnFlag, "d", "", "")
+	cobra.CheckErr(SchemaInspect.Flags().MarkHidden(dsnFlag))
+	cobra.CheckErr(SchemaInspect.MarkFlagRequired(urlFlag))
 
 	// Schema fmt.
 	schemaCmd.AddCommand(SchemaFmt)
 }
 
+// selectEnv returns the Env from the current project file based on the selected
+// argument. If selected is "", or no project file exists in the current directory
+// a zero-value Env is returned.
+func selectEnv(selected string) (*Env, error) {
+	env := &Env{
+		MigrationDir: &MigrationDir{},
+	}
+	if selected == "" {
+		return env, nil
+	}
+	if _, err := os.Stat(projectFileName); os.IsNotExist(err) {
+		return env, nil
+	}
+	return LoadEnv(projectFileName, selected)
+}
+
+func schemaFlagsFromEnv(cmd *cobra.Command, _ []string) error {
+	activeEnv, err := selectEnv(selectedEnv)
+	if err != nil {
+		return err
+	}
+	if err := dsn2url(cmd); err != nil {
+		return err
+	}
+	if err := maySetFlag(cmd, urlFlag, activeEnv.URL); err != nil {
+		return err
+	}
+	if err := maySetFlag(cmd, devURLFlag, activeEnv.DevURL); err != nil {
+		return err
+	}
+	if err := maySetFlag(cmd, fileFlag, activeEnv.Source); err != nil {
+		return err
+	}
+	if s := "[" + strings.Join(activeEnv.Schemas, "") + "]"; len(activeEnv.Schemas) > 0 {
+		if err := maySetFlag(cmd, schemaFlag, s); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// maySetFlag sets the flag with the provided name to envVal if such a flag exists
+// on the cmd, it was not set by the user via the command line and if envVal is not
+// an empty string.
+func maySetFlag(cmd *cobra.Command, name, envVal string) error {
+	fl := cmd.Flag(name)
+	if fl == nil {
+		return nil
+	}
+	if fl.Changed {
+		return nil
+	}
+	if envVal == "" {
+		return nil
+	}
+	return cmd.Flags().Set(name, envVal)
+}
+
+func dsn2url(cmd *cobra.Command) error {
+	dsnF, urlF := cmd.Flag(dsnFlag), cmd.Flag(urlFlag)
+	switch {
+	case dsnF.Changed && urlF.Changed:
+		return errors.New(`both flags "url" and "dsn" were set`)
+	case dsnF.Changed && !urlF.Changed:
+		return cmd.Flags().Set(urlFlag, dsnF.Value.String())
+	}
+	return nil
+}
+
 // CmdInspectRun is the command used when running CLI.
-func CmdInspectRun(cmd *cobra.Command, args []string) error {
+func CmdInspectRun(cmd *cobra.Command, _ []string) error {
 	if InspectFlags.Web {
 		return errors.New("atlas UI is not available in this release")
 	}
-	client, err := sqlclient.Open(cmd.Context(), InspectFlags.URL)
+	// Create the client.
+	client, err := sqlclient.Open(cmd.Context(), SchemaFlags.URL)
 	if err != nil {
 		return err
 	}
 	defer client.Close()
-	schemas := InspectFlags.Schema
-	activeEnv, err := selectEnv(args)
-	if err != nil {
-		return err
-	}
-	if activeEnv != nil && len(activeEnv.Schemas) > 0 {
-		schemas = activeEnv.Schemas
-	}
+	schemas := SchemaFlags.Schemas
 	if client.URL.Schema != "" {
 		schemas = append(schemas, client.URL.Schema)
 	}
@@ -189,36 +264,19 @@ func CmdInspectRun(cmd *cobra.Command, args []string) error {
 }
 
 // CmdApplyRun is the command used when running CLI.
-func CmdApplyRun(cmd *cobra.Command, args []string) error {
+func CmdApplyRun(cmd *cobra.Command, _ []string) error {
 	if ApplyFlags.Web {
 		return errors.New("atlas UI is not available in this release")
 	}
-	c, err := sqlclient.Open(cmd.Context(), ApplyFlags.URL)
+	c, err := sqlclient.Open(cmd.Context(), SchemaFlags.URL)
 	if err != nil {
 		return err
 	}
-	defer c.Close()
-	devURL := ApplyFlags.DevURL
-	activeEnv, err := selectEnv(args)
 	if err != nil {
 		return err
 	}
-	if activeEnv != nil && activeEnv.DevURL != "" {
-		devURL = activeEnv.DevURL
-	}
-	var file string
-	switch {
-	case activeEnv != nil && activeEnv.Source != "":
-		file = activeEnv.Source
-	case ApplyFlags.File != "":
-		file = ApplyFlags.File
-	default:
-		return fmt.Errorf("source file must be set via -f or project file")
-	}
-	if activeEnv != nil && activeEnv.Source != "" {
-		file = activeEnv.Source
-	}
-	return applyRun(cmd.Context(), c, devURL, file, ApplyFlags.DryRun, ApplyFlags.AutoApprove, ApplyFlags.Vars)
+	defer c.Close()
+	return applyRun(cmd.Context(), c, ApplyFlags.DevURL, ApplyFlags.File, ApplyFlags.DryRun, ApplyFlags.AutoApprove, ApplyFlags.Vars)
 }
 
 // CmdFmtRun formats all HCL files in a given directory using canonical HCL formatting
@@ -233,7 +291,7 @@ func CmdFmtRun(cmd *cobra.Command, args []string) {
 }
 
 func applyRun(ctx context.Context, client *sqlclient.Client, devURL string, file string, dryRun, autoApprove bool, input map[string]string) error {
-	schemas := ApplyFlags.Schema
+	schemas := SchemaFlags.Schemas
 	if client.URL.Schema != "" {
 		schemas = append(schemas, client.URL.Schema)
 	}
@@ -314,35 +372,6 @@ func promptUser() bool {
 	return result == answerApply
 }
 
-// fixURLFlag fixes the url flag by pulling its value either from the flag itself,
-// the (deprecated) dsn flag, or from the active environment.
-func fixURLFlag(cmd *cobra.Command, p *string) {
-	cmd.Flags().StringVarP(p, "dsn", "d", "", "")
-	cobra.CheckErr(cmd.Flags().MarkHidden("dsn"))
-	cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
-		activeEnv, err := selectEnv(args)
-		if err != nil {
-			return err
-		}
-		dsnF, urlF := cmd.Flag("dsn"), cmd.Flag("url")
-		switch {
-		case activeEnv != nil && activeEnv.URL != "":
-			urlF.Changed = true
-			if err := urlF.Value.Set(activeEnv.URL); err != nil {
-				return err
-			}
-		case !dsnF.Changed && !urlF.Changed:
-			return errors.New(`required flag "url" was not set`)
-		case dsnF.Changed && urlF.Changed:
-			return errors.New(`both flags "url" and "dsn" were set`)
-		case dsnF.Changed && !urlF.Changed:
-			urlF.Changed = true
-			urlF.Value = dsnF.Value
-		}
-		return nil
-	}
-}
-
 func handlePath(cmd *cobra.Command, path string) {
 	tasks, err := tasks(path)
 	cobra.CheckErr(err)
diff --git a/doc/md/cli/reference.md b/doc/md/cli/reference.md
index ba7b1b92..8358b803 100644
--- a/doc/md/cli/reference.md
+++ b/doc/md/cli/reference.md
@@ -61,6 +61,7 @@ atlas migrate
 #### Flags
 ```
       --dir string       select migration directory using URL format (default "file://migrations")
+      --env string       set which env from the project file to use
       --force            force a command to run on a broken migration directory state
       --format string    set migration file format (default "atlas")
       --schema strings   set schema names
@@ -174,6 +175,12 @@ atlas schema
 #### Details
 The `atlas schema` command groups subcommands for working with Atlas schemas.
 
+#### Flags
+```
+      --env string   set which env from the project file to use
+
+```
+
 
 ### atlas schema apply
 


Test patch:
diff --git a/cmd/atlascmd/project_test.go b/cmd/atlascmd/project_test.go
index 0ab3dd21..27deb192 100644
--- a/cmd/atlascmd/project_test.go
+++ b/cmd/atlascmd/project_test.go
@@ -20,6 +20,11 @@ env "local" {
 	dev = "docker://mysql/8"
 	src = "./app.hcl"
 	schemas = ["hello", "world"]
+	
+	migration_dir {
+		url = "file://migrations"
+		format = atlas
+	}
 }
 `
 	err := os.WriteFile(filepath.Join(d, projectFileName), []byte(h), 0600)
@@ -35,6 +40,10 @@ env "local" {
 			DevURL:  "docker://mysql/8",
 			Source:  "./app.hcl",
 			Schemas: []string{"hello", "world"},
+			MigrationDir: &MigrationDir{
+				URL:    "file://migrations",
+				Format: formatAtlas,
+			},
 		}, env)
 	})
 	t.Run("wrong env", func(t *testing.T) {
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index c97075de..ab14fcaa 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -209,6 +209,7 @@ func testCLISchemaInspectEnv(t T, h string, env string, unmarshaler schemaspec.U
 	cmd := exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
 		"schema",
 		"inspect",
+		"--env",
 		env,
 	)
 	stdout, stderr := bytes.NewBuffer(nil), bytes.NewBuffer(nil)
diff --git a/internal/integration/script_test.go b/internal/integration/script_test.go
index 1bf9bf42..7181d22c 100644
--- a/internal/integration/script_test.go
+++ b/internal/integration/script_test.go
@@ -360,7 +360,7 @@ func (t *liteTest) cmdExec(ts *testscript.TestScript, _ bool, args []string) {
 	cmdExec(ts, args, ts.Value(keyDB).(*sql.DB))
 }
 
-func (t *liteTest) cmdCLI(ts *testscript.TestScript, _ bool, args []string) {
+func (t *liteTest) cmdCLI(ts *testscript.TestScript, neg bool, args []string) {
 	var (
 		workDir = ts.Getenv("WORK")
 		dbURL   = fmt.Sprintf("sqlite://file:%s/atlas.sqlite?cache=shared&_fk=1", workDir)
@@ -378,10 +378,20 @@ func (t *liteTest) cmdCLI(ts *testscript.TestScript, _ bool, args []string) {
 		defer f.Close()
 		cmd := exec.Command(cliPath, args[0:l-2]...)
 		cmd.Stdout = f
+		stderr := &bytes.Buffer{}
+		cmd.Stderr = stderr
 		cmd.Dir = workDir
-		ts.Check(cmd.Run())
+		if err := cmd.Run(); err != nil {
+			ts.Fatalf("\n[stderr]\n%s", stderr)
+		}
 	default:
-		ts.Check(ts.Exec(cliPath, args...))
+		err := ts.Exec(cliPath, args...)
+		if !neg {
+			ts.Check(err)
+		}
+		if neg && err == nil {
+			ts.Fatalf("expected fail")
+		}
 	}
 }
 
diff --git a/internal/integration/sqlite_test.go b/internal/integration/sqlite_test.go
index 7ab45668..d84cd247 100644
--- a/internal/integration/sqlite_test.go
+++ b/internal/integration/sqlite_test.go
@@ -453,6 +453,7 @@ func TestSQLite_CLI(t *testing.T) {
 			env := fmt.Sprintf(`
 env "hello" {
 	url = "%s"
+	src = "./schema.hcl"
 }
 `, t.dsn())
 			wd, _ := os.Getwd()
diff --git a/internal/integration/testdata/sqlite/cli-inspect.txt b/internal/integration/testdata/sqlite/cli-inspect.txt
index 5ca194fe..fb7ff9dc 100644
--- a/internal/integration/testdata/sqlite/cli-inspect.txt
+++ b/internal/integration/testdata/sqlite/cli-inspect.txt
@@ -1,4 +1,10 @@
 apply 1.hcl
+
+# test deprecated -d flag
+atlas schema inspect -d URL > inspected.hcl
+cmp inspected.hcl 1.hcl
+
+# test url flag
 atlas schema inspect -u URL > inspected.hcl
 cmp inspected.hcl 1.hcl
 
diff --git a/internal/integration/testdata/sqlite/cli-migrate-diff.txt b/internal/integration/testdata/sqlite/cli-migrate-diff.txt
index 55fc4a76..22ca2870 100644
--- a/internal/integration/testdata/sqlite/cli-migrate-diff.txt
+++ b/internal/integration/testdata/sqlite/cli-migrate-diff.txt
@@ -1,4 +1,11 @@
 exec mkdir migrations
+
+! atlas migrate diff --to file://1.hcl --dir file://migrations
+stderr '"dev-url" not set'
+
+! atlas migrate diff --dev-url sqlite://devdb --dir file://migrations
+stderr '"to" not set'
+
 atlas migrate diff --dev-url sqlite://devdb --to file://1.hcl --dir file://migrations
 cmpmig 0 diff.sql
 -- 1.hcl --
diff --git a/internal/integration/testdata/sqlite/cli-migrate-project.txt b/internal/integration/testdata/sqlite/cli-migrate-project.txt
new file mode 100644
index 00000000..74b84d1c
--- /dev/null
+++ b/internal/integration/testdata/sqlite/cli-migrate-project.txt
@@ -0,0 +1,40 @@
+exec mkdir migrations
+atlas migrate diff --env local
+cmpmig 0 diff.sql
+
+atlas migrate validate --env local
+
+atlas migrate new 1 --env local
+cmpmig 1 empty.sql
+
+exec touch migrations/2.sql
+! atlas migrate validate --env local
+stderr 'Error: checksum mismatch'
+
+atlas migrate hash --force --env local
+atlas migrate validate --env local
+
+-- atlas.hcl --
+env "local" {
+    url = "URL"
+    dev = "sqlite://test?mode=memory&_fk=1"
+    src = "./1.hcl"
+    migration_dir {
+        url = "file://migrations"
+        format = atlas
+    }
+}
+-- 1.hcl --
+table "users" {
+  schema = schema.main
+  column "id" {
+    null = false
+    type = int
+  }
+}
+schema "main" {
+}
+-- diff.sql --
+-- create "users" table
+CREATE TABLE `users` (`id` int NOT NULL);
+-- empty.sql --
diff --git a/internal/integration/testdata/sqlite/cli-schema-project-file.txt b/internal/integration/testdata/sqlite/cli-schema-project-file.txt
index dbb6d041..2feff2da 100644
--- a/internal/integration/testdata/sqlite/cli-schema-project-file.txt
+++ b/internal/integration/testdata/sqlite/cli-schema-project-file.txt
@@ -1,7 +1,22 @@
-atlas schema apply local --auto-approve
-atlas schema inspect local > inspected.hcl
+! atlas schema inspect
+stderr '"url" not set'
+
+! atlas schema apply -f 1.hcl
+stderr '"url" not set'
+
+! atlas schema apply --url URL
+stderr '"file" not set'
+
+# Verify "url" and "src" attributes of the env are used.
+atlas schema apply --env local --auto-approve
+atlas schema inspect --env local > inspected.hcl
 cmp 1.hcl inspected.hcl
 
+# Verify the precedence of flag over project file.
+atlas schema apply --env local --auto-approve -f 2.hcl
+atlas schema inspect --env local > inspected.hcl
+cmp 2.hcl inspected.hcl
+
 -- atlas.hcl --
 env "local" {
     url = "URL"
@@ -17,3 +32,13 @@ table "users" {
 }
 schema "main" {
 }
+-- 2.hcl --
+table "other" {
+  schema = schema.main
+  column "id" {
+    null = false
+    type = int
+  }
+}
+schema "main" {
+}
\ No newline at end of file