github.com/iasthc/atlas/cmd/atlas@v0.0.0-20230523071841-73246df3f88d/internal/cmdapi/schema.go (about)

     1  // Copyright 2021-present The Atlas Authors. All rights reserved.
     2  // This source code is licensed under the Apache 2.0 license found
     3  // in the LICENSE file in the root directory of this source tree.
     4  
     5  package cmdapi
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"io/fs"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  	"text/template"
    17  
    18  	"github.com/iasthc/atlas/cmd/atlas/internal/cmdlog"
    19  	"github.com/iasthc/atlas/sql/migrate"
    20  	"github.com/iasthc/atlas/sql/schema"
    21  	"github.com/iasthc/atlas/sql/sqlclient"
    22  
    23  	"github.com/hashicorp/hcl/v2/hclwrite"
    24  	"github.com/manifoldco/promptui"
    25  	"github.com/spf13/cobra"
    26  )
    27  
    28  func init() {
    29  	schemaCmd := schemaCmd()
    30  	schemaCmd.AddCommand(
    31  		schemaApplyCmd(),
    32  		schemaCleanCmd(),
    33  		schemaDiffCmd(),
    34  		schemaFmtCmd(),
    35  		schemaInspectCmd(),
    36  	)
    37  	Root.AddCommand(schemaCmd)
    38  }
    39  
    40  // schemaCmd represents the subcommand 'atlas schema'.
    41  func schemaCmd() *cobra.Command {
    42  	cmd := &cobra.Command{
    43  		Use:   "schema",
    44  		Short: "Work with atlas schemas.",
    45  		Long:  "The `atlas schema` command groups subcommands working with declarative Atlas schemas.",
    46  	}
    47  	addGlobalFlags(cmd.PersistentFlags())
    48  	return cmd
    49  }
    50  
    51  type schemaApplyFlags struct {
    52  	url         string   // URL of database to apply the changes on.
    53  	devURL      string   // URL of the dev database.
    54  	paths       []string // Paths to HCL files.
    55  	toURLs      []string // URLs of the desired state.
    56  	schemas     []string // Schemas to take into account when diffing.
    57  	exclude     []string // List of glob patterns used to filter resources from applying (see schema.InspectOptions).
    58  	dryRun      bool     // Only show SQL on screen instead of applying it.
    59  	autoApprove bool     // Don't prompt for approval before applying SQL.
    60  	logFormat   string   // Log format.
    61  }
    62  
    63  // schemaApplyCmd represents the 'atlas schema apply' subcommand.
    64  func schemaApplyCmd() *cobra.Command {
    65  	var (
    66  		flags schemaApplyFlags
    67  		cmd   = &cobra.Command{
    68  			Use:   "apply",
    69  			Short: "Apply an atlas schema to a target database.",
    70  			// Use 80-columns as max width.
    71  			Long: `'atlas schema apply' plans and executes a database migration to bring a given
    72  database to the state described in the provided Atlas schema. Before running the
    73  migration, Atlas will print the migration plan and prompt the user for approval.
    74  
    75  The schema is provided by one or more URLs (to a HCL file or 
    76  directory, database or migration directory) using the "--to, -t" flag:
    77    atlas schema apply -u URL --to file://file1.hcl --to file://file2.hcl
    78    atlas schema apply -u URL --to file://schema/ --to file://override.hcl
    79  
    80  As a convenience, schema URLs may also be provided via an environment definition in
    81  the project file (see: https://atlasgo.io/cli/projects).
    82  
    83  If run with the "--dry-run" flag, atlas will exit after printing out the planned
    84  migration.`,
    85  			Example: `  atlas schema apply -u "mysql://user:pass@localhost/dbname" --to file://atlas.hcl
    86    atlas schema apply -u "mysql://localhost" --to file://schema.hcl --schema prod --schema staging
    87    atlas schema apply -u "mysql://user:pass@localhost:3306/dbname" --to file://schema.hcl --dry-run
    88    atlas schema apply -u "mariadb://user:pass@localhost:3306/dbname" --to file://schema.hcl
    89    atlas schema apply --url "postgres://user:pass@host:port/dbname?sslmode=disable" --to file://schema.hcl
    90    atlas schema apply -u "sqlite://file:ex1.db?_fk=1" --to file://schema.hcl`,
    91  			RunE: func(cmd *cobra.Command, args []string) error {
    92  				switch {
    93  				case GlobalFlags.SelectedEnv == "":
    94  					env, err := selectEnv(cmd)
    95  					if err != nil {
    96  						return err
    97  					}
    98  					return schemaApplyRun(cmd, flags, env)
    99  				default:
   100  					_, envs, err := EnvByName(GlobalFlags.SelectedEnv, WithInput(GlobalFlags.Vars))
   101  					if err != nil {
   102  						return err
   103  					}
   104  					return cmdEnvsRun(envs, setSchemaEnvFlags, cmd, func(e *Env) error {
   105  						return schemaApplyRun(cmd, flags, e)
   106  					})
   107  				}
   108  			},
   109  		}
   110  	)
   111  	cmd.Flags().SortFlags = false
   112  	addFlagURL(cmd.Flags(), &flags.url)
   113  	addFlagToURLs(cmd.Flags(), &flags.toURLs)
   114  	addFlagExclude(cmd.Flags(), &flags.exclude)
   115  	addFlagSchemas(cmd.Flags(), &flags.schemas)
   116  	addFlagDevURL(cmd.Flags(), &flags.devURL)
   117  	addFlagDryRun(cmd.Flags(), &flags.dryRun)
   118  	addFlagAutoApprove(cmd.Flags(), &flags.autoApprove)
   119  	addFlagLog(cmd.Flags(), &flags.logFormat)
   120  	addFlagFormat(cmd.Flags(), &flags.logFormat)
   121  	// Hidden support for the deprecated -f flag.
   122  	cmd.Flags().StringSliceVarP(&flags.paths, flagFile, "f", nil, "[paths...] file or directory containing HCL or SQL files")
   123  	cobra.CheckErr(cmd.Flags().MarkHidden(flagFile))
   124  	cmd.MarkFlagsMutuallyExclusive(flagFile, flagTo)
   125  	cmd.MarkFlagsMutuallyExclusive(flagLog, flagFormat)
   126  	return cmd
   127  }
   128  
   129  func schemaApplyRun(cmd *cobra.Command, flags schemaApplyFlags, env *Env) error {
   130  	switch {
   131  	case flags.url == "":
   132  		return errors.New(`required flag(s) "url" not set`)
   133  	case len(flags.paths) == 0 && len(flags.toURLs) == 0:
   134  		return errors.New(`one of flag(s) "file" or "to" is required`)
   135  	case flags.logFormat != "" && !flags.dryRun && !flags.autoApprove:
   136  		return errors.New(`--log and --format can only be used with --dry-run or --auto-approve`)
   137  	}
   138  	// If the old -f flag is given convert them to the URL format. If both are given,
   139  	// cobra would throw an error since they are marked as mutually exclusive.
   140  	for _, p := range flags.paths {
   141  		if !isURL(p) {
   142  			p = "file://" + p
   143  		}
   144  		flags.toURLs = append(flags.toURLs, p)
   145  	}
   146  	var (
   147  		err    error
   148  		dev    *sqlclient.Client
   149  		ctx    = cmd.Context()
   150  		format = cmdlog.SchemaPlanTemplate
   151  	)
   152  	if v := flags.logFormat; v != "" {
   153  		if format, err = template.New("format").Funcs(cmdlog.ApplyTemplateFuncs).Parse(v); err != nil {
   154  			return fmt.Errorf("parse log format: %w", err)
   155  		}
   156  	}
   157  	if flags.devURL != "" {
   158  		if dev, err = sqlclient.Open(ctx, flags.devURL); err != nil {
   159  			return err
   160  		}
   161  		defer dev.Close()
   162  	}
   163  	from, err := stateReader(ctx, &stateReaderConfig{
   164  		urls:    []string{flags.url},
   165  		schemas: flags.schemas,
   166  		exclude: flags.exclude,
   167  	})
   168  	if err != nil {
   169  		return err
   170  	}
   171  	defer from.Close()
   172  	client, ok := from.Closer.(*sqlclient.Client)
   173  	if !ok {
   174  		return errors.New("--url must be a database connection")
   175  	}
   176  	to, err := stateReader(ctx, &stateReaderConfig{
   177  		urls:    flags.toURLs,
   178  		dev:     dev,
   179  		client:  client,
   180  		schemas: flags.schemas,
   181  		exclude: flags.exclude,
   182  		vars:    GlobalFlags.Vars,
   183  	})
   184  	if err != nil {
   185  		return err
   186  	}
   187  	defer to.Close()
   188  	changes, err := computeDiff(ctx, client, from, to, env.DiffOptions()...)
   189  	if err != nil {
   190  		return err
   191  	}
   192  	// Returning at this stage should
   193  	// not trigger the help message.
   194  	cmd.SilenceUsage = true
   195  	switch {
   196  	case len(changes) == 0:
   197  		return format.Execute(cmd.OutOrStdout(), &cmdlog.SchemaApply{})
   198  	case flags.logFormat != "" && flags.autoApprove:
   199  		var (
   200  			applied int
   201  			plan    *migrate.Plan
   202  			cause   *cmdlog.StmtError
   203  			out     = cmd.OutOrStdout()
   204  		)
   205  		if plan, err = client.PlanChanges(ctx, "", changes); err != nil {
   206  			return err
   207  		}
   208  		if err = client.ApplyChanges(ctx, changes); err == nil {
   209  			applied = len(plan.Changes)
   210  		} else if i, ok := err.(interface{ Applied() int }); ok && i.Applied() < len(plan.Changes) {
   211  			applied, cause = i.Applied(), &cmdlog.StmtError{Stmt: plan.Changes[i.Applied()].Cmd, Text: err.Error()}
   212  		} else {
   213  			cause = &cmdlog.StmtError{Text: err.Error()}
   214  		}
   215  		apply := cmdlog.NewSchemaApply(cmdlog.NewEnv(client, nil), plan.Changes[:applied], plan.Changes[applied:], cause)
   216  		if err1 := format.Execute(out, apply); err1 != nil {
   217  			if err != nil {
   218  				err1 = fmt.Errorf("%w: %v", err, err1)
   219  			}
   220  			err = err1
   221  		}
   222  		return err
   223  	default:
   224  		if err := summary(cmd, client, changes, format); err != nil {
   225  			return err
   226  		}
   227  		if !flags.dryRun && (flags.autoApprove || promptUser()) {
   228  			return client.ApplyChanges(ctx, changes)
   229  		}
   230  		return nil
   231  	}
   232  }
   233  
   234  type schemeCleanFlags struct {
   235  	URL         string // URL of database to apply the changes on.
   236  	AutoApprove bool   // Don't prompt for approval before applying SQL.
   237  }
   238  
   239  // schemaCleanCmd represents the 'atlas schema clean' subcommand.
   240  func schemaCleanCmd() *cobra.Command {
   241  	var (
   242  		flags schemeCleanFlags
   243  		cmd   = &cobra.Command{
   244  			Use:   "clean [flags]",
   245  			Short: "Removes all objects from the connected database.",
   246  			Long: `'atlas schema clean' drops all objects in the connected database and leaves it in an empty state.
   247  As a safety feature, 'atlas schema clean' will ask for confirmation before attempting to execute any SQL.`,
   248  			Example: `  atlas schema clean -u mysql://user:pass@localhost:3306/dbname
   249    atlas schema clean -u mysql://user:pass@localhost:3306/`,
   250  			PreRunE: func(cmd *cobra.Command, _ []string) error {
   251  				return schemaFlagsFromConfig(cmd)
   252  			},
   253  			RunE: func(cmd *cobra.Command, args []string) error {
   254  				return schemaCleanRun(cmd, args, flags)
   255  			},
   256  		}
   257  	)
   258  	cmd.Flags().SortFlags = false
   259  	addFlagURL(cmd.Flags(), &flags.URL)
   260  	addFlagAutoApprove(cmd.Flags(), &flags.AutoApprove)
   261  	cobra.CheckErr(cmd.MarkFlagRequired(flagURL))
   262  	return cmd
   263  }
   264  
   265  func schemaCleanRun(cmd *cobra.Command, _ []string, flags schemeCleanFlags) error {
   266  	// Open a client to the database.
   267  	c, err := sqlclient.Open(cmd.Context(), flags.URL)
   268  	if err != nil {
   269  		return err
   270  	}
   271  	defer c.Close()
   272  	var drop []schema.Change
   273  	// If the connection is bound to a schema, only drop the resources inside the schema.
   274  	switch c.URL.Schema {
   275  	case "":
   276  		r, err := c.InspectRealm(cmd.Context(), nil)
   277  		if err != nil {
   278  			return err
   279  		}
   280  		drop, err = c.RealmDiff(r, schema.NewRealm())
   281  		if err != nil {
   282  			return err
   283  		}
   284  	default:
   285  		s, err := c.InspectSchema(cmd.Context(), c.URL.Schema, nil)
   286  		if err != nil {
   287  			return err
   288  		}
   289  		drop, err = c.SchemaDiff(s, schema.New(s.Name))
   290  		if err != nil {
   291  			return err
   292  		}
   293  	}
   294  	if len(drop) == 0 {
   295  		cmd.Println("Nothing to drop")
   296  		return nil
   297  	}
   298  	if err := summary(cmd, c, drop, cmdlog.SchemaPlanTemplate); err != nil {
   299  		return err
   300  	}
   301  	if flags.AutoApprove || promptUser() {
   302  		if err := c.ApplyChanges(cmd.Context(), drop); err != nil {
   303  			return err
   304  		}
   305  	}
   306  	return nil
   307  }
   308  
   309  type schemaDiffFlags struct {
   310  	fromURL []string
   311  	toURL   []string
   312  	devURL  string
   313  	schemas []string
   314  	exclude []string
   315  	format  string
   316  }
   317  
   318  // schemaDiffCmd represents the 'atlas schema diff' subcommand.
   319  func schemaDiffCmd() *cobra.Command {
   320  	var (
   321  		flags schemaDiffFlags
   322  		cmd   = &cobra.Command{
   323  			Use:   "diff",
   324  			Short: "Calculate and print the diff between two schemas.",
   325  			Long: `'atlas schema diff' reads the state of two given schema definitions, 
   326  calculates the difference in their schemas, and prints a plan of
   327  SQL statements to migrate the "from" database to the schema of the "to" database.
   328  The database states can be read from a connected database, an HCL project or a migration directory.`,
   329  			Example: `  atlas schema diff --from mysql://user:pass@localhost:3306/test --to file://schema.hcl
   330    atlas schema diff --from mysql://user:pass@localhost:3306 --to file://schema_1.hcl --to file://schema_2.hcl
   331    atlas schema diff --from mysql://user:pass@localhost:3306 --to file://migrations --format '{{ sql . "  " }}'`,
   332  			PreRunE: func(cmd *cobra.Command, _ []string) error {
   333  				return schemaFlagsFromConfig(cmd)
   334  			},
   335  			RunE: func(cmd *cobra.Command, args []string) error {
   336  				env, err := selectEnv(cmd)
   337  				if err != nil {
   338  					return err
   339  				}
   340  				return schemaDiffRun(cmd, args, flags, env)
   341  			},
   342  		}
   343  	)
   344  	cmd.Flags().SortFlags = false
   345  	addFlagURLs(cmd.Flags(), &flags.fromURL, flagFrom, flagFromShort)
   346  	addFlagToURLs(cmd.Flags(), &flags.toURL)
   347  	addFlagDevURL(cmd.Flags(), &flags.devURL)
   348  	addFlagSchemas(cmd.Flags(), &flags.schemas)
   349  	addFlagExclude(cmd.Flags(), &flags.exclude)
   350  	addFlagFormat(cmd.Flags(), &flags.format)
   351  	cobra.CheckErr(cmd.MarkFlagRequired(flagFrom))
   352  	cobra.CheckErr(cmd.MarkFlagRequired(flagTo))
   353  	return cmd
   354  }
   355  
   356  func schemaDiffRun(cmd *cobra.Command, _ []string, flags schemaDiffFlags, env *Env) error {
   357  	var (
   358  		ctx = cmd.Context()
   359  		c   *sqlclient.Client
   360  	)
   361  	// We need a driver for diffing and planning. If given, dev database has precedence.
   362  	if flags.devURL != "" {
   363  		var err error
   364  		c, err = sqlclient.Open(ctx, flags.devURL)
   365  		if err != nil {
   366  			return err
   367  		}
   368  		defer c.Close()
   369  	}
   370  	from, err := stateReader(ctx, &stateReaderConfig{
   371  		urls:    flags.fromURL,
   372  		dev:     c,
   373  		vars:    GlobalFlags.Vars,
   374  		schemas: flags.schemas,
   375  		exclude: flags.exclude,
   376  	})
   377  	if err != nil {
   378  		return err
   379  	}
   380  	defer from.Close()
   381  	to, err := stateReader(ctx, &stateReaderConfig{
   382  		urls:    flags.toURL,
   383  		dev:     c,
   384  		vars:    GlobalFlags.Vars,
   385  		schemas: flags.schemas,
   386  		exclude: flags.exclude,
   387  	})
   388  	if err != nil {
   389  		return err
   390  	}
   391  	defer to.Close()
   392  	if c == nil {
   393  		// If not both states are provided by a database connection, the call to state-reader would have returned
   394  		// an error already. If we land in this case, we can assume both states are database connections.
   395  		c = to.Closer.(*sqlclient.Client)
   396  	}
   397  	format := cmdlog.SchemaDiffTemplate
   398  	if v := flags.format; v != "" {
   399  		if format, err = template.New("format").Funcs(cmdlog.SchemaDiffFuncs).Parse(v); err != nil {
   400  			return fmt.Errorf("parse log format: %w", err)
   401  		}
   402  	}
   403  	changes, err := computeDiff(ctx, c, from, to, env.DiffOptions()...)
   404  	if err != nil {
   405  		return err
   406  	}
   407  	return format.Execute(cmd.OutOrStdout(), &cmdlog.SchemaDiff{
   408  		Client:  c,
   409  		Changes: changes,
   410  	})
   411  }
   412  
   413  type schemaInspectFlags struct {
   414  	url       string   // URL of resource to inspect.
   415  	devURL    string   // URL of the dev database.
   416  	logFormat string   // Format of the log output.
   417  	schemas   []string // Schemas to take into account when diffing.
   418  	exclude   []string // List of glob patterns used to filter resources from applying (see schema.InspectOptions).
   419  }
   420  
   421  // schemaInspectCmd represents the 'atlas schema inspect' subcommand.
   422  func schemaInspectCmd() *cobra.Command {
   423  	var (
   424  		flags schemaInspectFlags
   425  		cmd   = &cobra.Command{
   426  			Use:   "inspect",
   427  			Short: "Inspect a database and print its schema in Atlas DDL syntax.",
   428  			Long: `'atlas schema inspect' connects to the given database and inspects its schema.
   429  It then prints to the screen the schema of that database in Atlas DDL syntax. This output can be
   430  saved to a file, commonly by redirecting the output to a file named with a ".hcl" suffix:
   431  
   432    atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname" > schema.hcl
   433  
   434  This file can then be edited and used with the` + " `atlas schema apply` " + `command to plan
   435  and execute schema migrations against the given database. In cases where users wish to inspect
   436  all multiple schemas in a given database (for instance a MySQL server may contain multiple named
   437  databases), omit the relevant part from the url, e.g. "mysql://user:pass@localhost:3306/".
   438  To select specific schemas from the databases, users may use the "--schema" (or "-s" shorthand)
   439  flag.
   440  	`,
   441  			Example: `  atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname"
   442    atlas schema inspect -u "mariadb://user:pass@localhost:3306/" --schema=schemaA,schemaB -s schemaC
   443    atlas schema inspect --url "postgres://user:pass@host:port/dbname?sslmode=disable"
   444    atlas schema inspect -u "sqlite://file:ex1.db?_fk=1"`,
   445  			PreRunE: func(cmd *cobra.Command, _ []string) error {
   446  				return schemaFlagsFromConfig(cmd)
   447  			},
   448  			RunE: func(cmd *cobra.Command, args []string) error {
   449  				return schemaInspectRun(cmd, args, flags)
   450  			},
   451  		}
   452  	)
   453  	cmd.Flags().SortFlags = false
   454  	addFlagURL(cmd.Flags(), &flags.url)
   455  	addFlagDevURL(cmd.Flags(), &flags.devURL)
   456  	addFlagSchemas(cmd.Flags(), &flags.schemas)
   457  	addFlagExclude(cmd.Flags(), &flags.exclude)
   458  	addFlagLog(cmd.Flags(), &flags.logFormat)
   459  	addFlagFormat(cmd.Flags(), &flags.logFormat)
   460  	cobra.CheckErr(cmd.MarkFlagRequired(flagURL))
   461  	cmd.MarkFlagsMutuallyExclusive(flagLog, flagFormat)
   462  	return cmd
   463  }
   464  
   465  func schemaInspectRun(cmd *cobra.Command, _ []string, flags schemaInspectFlags) error {
   466  	var (
   467  		ctx = cmd.Context()
   468  		dev *sqlclient.Client
   469  	)
   470  	if flags.devURL != "" {
   471  		var err error
   472  		dev, err = sqlclient.Open(ctx, flags.devURL)
   473  		if err != nil {
   474  			return err
   475  		}
   476  		defer dev.Close()
   477  	}
   478  	r, err := stateReader(ctx, &stateReaderConfig{
   479  		urls:    []string{flags.url},
   480  		dev:     dev,
   481  		vars:    GlobalFlags.Vars,
   482  		schemas: flags.schemas,
   483  		exclude: flags.exclude,
   484  	})
   485  	if err != nil {
   486  		return err
   487  	}
   488  	defer r.Close()
   489  	client, ok := r.Closer.(*sqlclient.Client)
   490  	if !ok && dev != nil {
   491  		client = dev
   492  	}
   493  	format := cmdlog.SchemaInspectTemplate
   494  	if v := flags.logFormat; v != "" {
   495  		if format, err = template.New("format").Funcs(cmdlog.InspectTemplateFuncs).Parse(v); err != nil {
   496  			return fmt.Errorf("parse log format: %w", err)
   497  		}
   498  	}
   499  	s, err := r.ReadState(ctx)
   500  	return format.Execute(cmd.OutOrStdout(), &cmdlog.SchemaInspect{
   501  		Client: client,
   502  		Realm:  s,
   503  		Error:  err,
   504  	})
   505  }
   506  
   507  // schemaFmtCmd represents the 'atlas schema fmt' subcommand.
   508  func schemaFmtCmd() *cobra.Command {
   509  	cmd := &cobra.Command{
   510  		Use:   "fmt [path ...]",
   511  		Short: "Formats Atlas HCL files",
   512  		Long: `'atlas schema fmt' formats all ".hcl" files under the given paths using
   513  canonical HCL layout style as defined by the github.com/hashicorp/hcl/v2/hclwrite package.
   514  Unless stated otherwise, the fmt command will use the current directory.
   515  
   516  After running, the command will print the names of the files it has formatted. If all
   517  files in the directory are formatted, no input will be printed out.
   518  `,
   519  		RunE: func(cmd *cobra.Command, args []string) error {
   520  			return schemaFmtRun(cmd, args)
   521  		},
   522  	}
   523  	return cmd
   524  }
   525  
   526  func schemaFmtRun(cmd *cobra.Command, args []string) error {
   527  	if len(args) == 0 {
   528  		args = append(args, "./")
   529  	}
   530  	for _, path := range args {
   531  		tasks, err := tasks(path)
   532  		if err != nil {
   533  			return err
   534  		}
   535  		for _, task := range tasks {
   536  			changed, err := fmtFile(task)
   537  			if err != nil {
   538  				return err
   539  			}
   540  			if changed {
   541  				cmd.Println(task.path)
   542  			}
   543  		}
   544  	}
   545  	return nil
   546  }
   547  
   548  // selectEnv returns the Env from the current project file based on the selected
   549  // argument. If selected is "", or no project file exists in the current directory
   550  // a zero-value Env is returned.
   551  func selectEnv(cmd *cobra.Command) (*Env, error) {
   552  	switch name := GlobalFlags.SelectedEnv; {
   553  	// A config file was passed without an env.
   554  	case name == "" && cmd.Flags().Changed(flagConfig):
   555  		p, envs, err := EnvByName(name, WithInput(GlobalFlags.Vars))
   556  		if err != nil {
   557  			return nil, err
   558  		}
   559  		if len(envs) != 0 {
   560  			return nil, fmt.Errorf("unexpected number of envs found: %d", len(envs))
   561  		}
   562  		return &Env{Lint: p.Lint, Diff: p.Diff, cfg: p.cfg, Migration: &Migration{}}, nil
   563  	// No config nor env was passed.
   564  	case name == "":
   565  		return &Env{Lint: &Lint{}, Migration: &Migration{}}, nil
   566  	// Env was passed.
   567  	default:
   568  		_, envs, err := EnvByName(name, WithInput(GlobalFlags.Vars))
   569  		if err != nil {
   570  			return nil, err
   571  		}
   572  		if len(envs) > 1 {
   573  			return nil, fmt.Errorf("multiple envs found for %q", name)
   574  		}
   575  		return envs[0], nil
   576  	}
   577  }
   578  
   579  func schemaFlagsFromConfig(cmd *cobra.Command) error {
   580  	env, err := selectEnv(cmd)
   581  	if err != nil {
   582  		return err
   583  	}
   584  	return setSchemaEnvFlags(cmd, env)
   585  }
   586  
   587  func setSchemaEnvFlags(cmd *cobra.Command, env *Env) error {
   588  	if err := inputValuesFromEnv(cmd, env); err != nil {
   589  		return err
   590  	}
   591  	if err := maySetFlag(cmd, flagURL, env.URL); err != nil {
   592  		return err
   593  	}
   594  	if err := maySetFlag(cmd, flagDevURL, env.DevURL); err != nil {
   595  		return err
   596  	}
   597  	srcs, err := env.Sources()
   598  	if err != nil {
   599  		return err
   600  	}
   601  	if err := maySetFlag(cmd, flagFile, strings.Join(srcs, ",")); err != nil {
   602  		return err
   603  	}
   604  	if err := maySetFlag(cmd, flagSchema, strings.Join(env.Schemas, ",")); err != nil {
   605  		return err
   606  	}
   607  	if err := maySetFlag(cmd, flagExclude, strings.Join(env.Exclude, ",")); err != nil {
   608  		return err
   609  	}
   610  	switch cmd.Name() {
   611  	case "apply":
   612  		if err := maySetFlag(cmd, flagFormat, env.Format.Schema.Apply); err != nil {
   613  			return err
   614  		}
   615  	case "diff":
   616  		if err := maySetFlag(cmd, flagFormat, env.Format.Schema.Diff); err != nil {
   617  			return err
   618  		}
   619  	}
   620  	return nil
   621  }
   622  
   623  func computeDiff(ctx context.Context, differ *sqlclient.Client, from, to *stateReadCloser, opts ...schema.DiffOption) ([]schema.Change, error) {
   624  	current, err := from.ReadState(ctx)
   625  	if err != nil {
   626  		return nil, err
   627  	}
   628  	desired, err := to.ReadState(ctx)
   629  	if err != nil {
   630  		return nil, err
   631  	}
   632  	var diff []schema.Change
   633  	switch {
   634  	// In case an HCL file is compared against a specific database schema (not a realm).
   635  	case to.hcl && len(desired.Schemas) == 1 && from.schema != "" && desired.Schemas[0].Name != from.schema:
   636  		return nil, fmt.Errorf("mismatched HCL and database schemas: %q <> %q", from.schema, desired.Schemas[0].Name)
   637  	// Compare realm if the desired state is an HCL file or both connections are not bound to a schema.
   638  	case from.hcl, to.hcl, from.schema == "" && to.schema == "":
   639  		diff, err = differ.RealmDiff(current, desired, opts...)
   640  		if err != nil {
   641  			return nil, err
   642  		}
   643  	case from.schema == "", to.schema == "":
   644  		return nil, fmt.Errorf("cannot diff a schema with a database connection: %q <> %q", from.schema, to.schema)
   645  	default:
   646  		// SchemaDiff checks for name equality which is irrelevant in the case
   647  		// the user wants to compare their contents, reset them to allow the comparison.
   648  		current.Schemas[0].Name, desired.Schemas[0].Name = "", ""
   649  		diff, err = differ.SchemaDiff(current.Schemas[0], desired.Schemas[0], opts...)
   650  		if err != nil {
   651  			return nil, err
   652  		}
   653  	}
   654  	return diff, nil
   655  }
   656  
   657  func summary(cmd *cobra.Command, c *sqlclient.Client, changes []schema.Change, t *template.Template) error {
   658  	p, err := c.PlanChanges(cmd.Context(), "", changes)
   659  	if err != nil {
   660  		return err
   661  	}
   662  	return t.Execute(
   663  		cmd.OutOrStdout(),
   664  		cmdlog.NewSchemaPlan(cmdlog.NewEnv(c, nil), p.Changes, nil),
   665  	)
   666  }
   667  
   668  const (
   669  	answerApply = "Apply"
   670  	answerAbort = "Abort"
   671  )
   672  
   673  func promptUser() bool {
   674  	prompt := promptui.Select{
   675  		Label: "Are you sure?",
   676  		Items: []string{answerApply, answerAbort},
   677  	}
   678  	_, result, err := prompt.Run()
   679  	if err != nil && err != promptui.ErrInterrupt {
   680  		// Fail in case of unexpected errors.
   681  		cobra.CheckErr(err)
   682  	}
   683  	return result == answerApply
   684  }
   685  
   686  func tasks(path string) ([]fmttask, error) {
   687  	var tasks []fmttask
   688  	stat, err := os.Stat(path)
   689  	if err != nil {
   690  		return nil, err
   691  	}
   692  	if !stat.IsDir() {
   693  		if strings.HasSuffix(path, ".hcl") {
   694  			tasks = append(tasks, fmttask{
   695  				path: path,
   696  				info: stat,
   697  			})
   698  		}
   699  		return tasks, nil
   700  	}
   701  	all, err := os.ReadDir(path)
   702  	if err != nil {
   703  		return nil, err
   704  	}
   705  	for _, f := range all {
   706  		if f.IsDir() {
   707  			continue
   708  		}
   709  		if strings.HasSuffix(f.Name(), ".hcl") {
   710  			i, err := f.Info()
   711  			if err != nil {
   712  				return nil, err
   713  			}
   714  			tasks = append(tasks, fmttask{
   715  				path: filepath.Join(path, f.Name()),
   716  				info: i,
   717  			})
   718  		}
   719  	}
   720  	return tasks, nil
   721  }
   722  
   723  type fmttask struct {
   724  	path string
   725  	info fs.FileInfo
   726  }
   727  
   728  // fmtFile tries to format a file and reports if formatting occurred.
   729  func fmtFile(task fmttask) (bool, error) {
   730  	orig, err := os.ReadFile(task.path)
   731  	if err != nil {
   732  		return false, err
   733  	}
   734  	formatted := hclwrite.Format(orig)
   735  	if !bytes.Equal(formatted, orig) {
   736  		return true, os.WriteFile(task.path, formatted, task.info.Mode())
   737  	}
   738  	return false, nil
   739  }