github.com/iasthc/atlas/cmd/atlas@v0.0.0-20230523071841-73246df3f88d/internal/cmdlog/cmdlog.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 cmdlog
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"fmt"
    12  	"sort"
    13  	"strings"
    14  	"text/template"
    15  	"time"
    16  
    17  	"github.com/iasthc/atlas/sql/migrate"
    18  	"github.com/iasthc/atlas/sql/schema"
    19  	"github.com/iasthc/atlas/sql/sqlclient"
    20  
    21  	"github.com/fatih/color"
    22  	"github.com/olekukonko/tablewriter"
    23  )
    24  
    25  var (
    26  	// ColorTemplateFuncs are globally available functions to color strings in a report template.
    27  	ColorTemplateFuncs = template.FuncMap{
    28  		"cyan":         color.CyanString,
    29  		"green":        color.HiGreenString,
    30  		"red":          color.HiRedString,
    31  		"redBgWhiteFg": color.New(color.FgHiWhite, color.BgHiRed).SprintFunc(),
    32  		"yellow":       color.YellowString,
    33  	}
    34  )
    35  
    36  type (
    37  	// Env holds the environment information.
    38  	Env struct {
    39  		Driver string         `json:"Driver,omitempty"` // Driver name.
    40  		URL    *sqlclient.URL `json:"URL,omitempty"`    // URL to dev database.
    41  		Dir    string         `json:"Dir,omitempty"`    // Path to migration directory.
    42  	}
    43  
    44  	// Files is a slice of migrate.File. Implements json.Marshaler.
    45  	Files []migrate.File
    46  
    47  	// File wraps migrate.File to implement json.Marshaler.
    48  	File struct{ migrate.File }
    49  
    50  	// StmtError groups a statement with its execution error.
    51  	StmtError struct {
    52  		Stmt string `json:"Stmt,omitempty"` // SQL statement that failed.
    53  		Text string `json:"Text,omitempty"` // Error message as returned by the database.
    54  	}
    55  )
    56  
    57  // MarshalJSON implements json.Marshaler.
    58  func (f File) MarshalJSON() ([]byte, error) {
    59  	type local struct {
    60  		Name        string `json:"Name,omitempty"`
    61  		Version     string `json:"Version,omitempty"`
    62  		Description string `json:"Description,omitempty"`
    63  	}
    64  	return json.Marshal(local{f.Name(), f.Version(), f.Desc()})
    65  }
    66  
    67  // MarshalJSON implements json.Marshaler.
    68  func (f Files) MarshalJSON() ([]byte, error) {
    69  	files := make([]File, len(f))
    70  	for i := range f {
    71  		files[i] = File{f[i]}
    72  	}
    73  	return json.Marshal(files)
    74  }
    75  
    76  // NewEnv returns an initialized Env.
    77  func NewEnv(c *sqlclient.Client, dir migrate.Dir) Env {
    78  	e := Env{
    79  		Driver: c.Name,
    80  		URL:    c.URL,
    81  	}
    82  	if p, ok := dir.(interface{ Path() string }); ok {
    83  		e.Dir = p.Path()
    84  	}
    85  	return e
    86  }
    87  
    88  var (
    89  	// StatusTemplateFuncs are global functions available in status report templates.
    90  	StatusTemplateFuncs = merge(template.FuncMap{
    91  		"json":       jsonEncode,
    92  		"json_merge": jsonMerge,
    93  		"table":      table,
    94  		"default": func(report *MigrateStatus) (string, error) {
    95  			var buf bytes.Buffer
    96  			t, err := template.New("report").Funcs(ColorTemplateFuncs).Parse(`Migration Status:
    97  {{- if eq .Status "OK"      }} {{ green .Status }}{{ end }}
    98  {{- if eq .Status "PENDING" }} {{ yellow .Status }}{{ end }}
    99    {{ yellow "--" }} Current Version: {{ cyan .Current }}
   100  {{- if gt .Total 0 }}{{ printf " (%s statements applied)" (yellow "%d" .Count) }}{{ end }}
   101    {{ yellow "--" }} Next Version:    {{ cyan .Next }}
   102  {{- if gt .Total 0 }}{{ printf " (%s statements left)" (yellow "%d" .Left) }}{{ end }}
   103    {{ yellow "--" }} Executed Files:  {{ len .Applied }}{{ if gt .Total 0 }} (last one partially){{ end }}
   104    {{ yellow "--" }} Pending Files:   {{ len .Pending }}
   105  {{ if gt .Total 0 }}
   106  Last migration attempt had errors:
   107    {{ yellow "--" }} SQL:   {{ .SQL }}
   108    {{ yellow "--" }} {{ red "ERROR:" }} {{ .Error }}
   109  {{ end }}`)
   110  			if err != nil {
   111  				return "", err
   112  			}
   113  			err = t.Execute(&buf, report)
   114  			return buf.String(), err
   115  		},
   116  	}, ColorTemplateFuncs)
   117  
   118  	// MigrateStatusTemplate holds the default template of the 'migrate status' command.
   119  	MigrateStatusTemplate = template.Must(template.New("report").Funcs(StatusTemplateFuncs).Parse("{{ default . }}"))
   120  )
   121  
   122  // MigrateStatus contains a summary of the migration status of a database.
   123  type MigrateStatus struct {
   124  	Env       `json:"Env"`
   125  	Available Files               `json:"Available,omitempty"` // Available migration files
   126  	Pending   Files               `json:"Pending,omitempty"`   // Pending migration files
   127  	Applied   []*migrate.Revision `json:"Applied,omitempty"`   // Applied migration files
   128  	Current   string              `json:"Current,omitempty"`   // Current migration version
   129  	Next      string              `json:"Next,omitempty"`      // Next migration version
   130  	Count     int                 `json:"Count,omitempty"`     // Count of applied statements of the last revision
   131  	Total     int                 `json:"Total,omitempty"`     // Total statements of the last migration
   132  	Status    string              `json:"Status,omitempty"`    // Status of migration (OK, PENDING)
   133  	Error     string              `json:"Error,omitempty"`     // Last Error that occurred
   134  	SQL       string              `json:"SQL,omitempty"`       // SQL that caused the last Error
   135  }
   136  
   137  // NewMigrateStatus returns a new MigrateStatus.
   138  func NewMigrateStatus(c *sqlclient.Client, dir migrate.Dir) (*MigrateStatus, error) {
   139  	files, err := dir.Files()
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	return &MigrateStatus{
   144  		Env:       NewEnv(c, dir),
   145  		Available: files,
   146  	}, nil
   147  }
   148  
   149  // Left returns the amount of statements left to apply (if any).
   150  func (r *MigrateStatus) Left() int { return r.Total - r.Count }
   151  
   152  func table(report *MigrateStatus) (string, error) {
   153  	var buf strings.Builder
   154  	tbl := tablewriter.NewWriter(&buf)
   155  	tbl.SetRowLine(true)
   156  	tbl.SetAutoMergeCellsByColumnIndex([]int{0})
   157  	tbl.SetHeader([]string{
   158  		"Version",
   159  		"Description",
   160  		"Status",
   161  		"Count",
   162  		"Executed At",
   163  		"Execution Time",
   164  		"Error",
   165  		"SQL",
   166  	})
   167  	for _, r := range report.Applied {
   168  		tbl.Append([]string{
   169  			r.Version,
   170  			r.Description,
   171  			r.Type.String(),
   172  			fmt.Sprintf("%d/%d", r.Applied, r.Total),
   173  			r.ExecutedAt.Format("2006-01-02 15:04:05 MST"),
   174  			r.ExecutionTime.String(),
   175  			r.Error,
   176  			r.ErrorStmt,
   177  		})
   178  	}
   179  	for i, f := range report.Pending {
   180  		var c string
   181  		if i == 0 {
   182  			if r := report.Applied[len(report.Applied)-1]; f.Version() == r.Version && r.Applied < r.Total {
   183  				stmts, err := f.Stmts()
   184  				if err != nil {
   185  					return "", err
   186  				}
   187  				c = fmt.Sprintf("%d/%d", len(stmts)-r.Applied, len(stmts))
   188  			}
   189  		}
   190  		tbl.Append([]string{
   191  			f.Version(),
   192  			f.Desc(),
   193  			"pending",
   194  			c,
   195  			"", "", "", "",
   196  		})
   197  	}
   198  	tbl.Render()
   199  	return buf.String(), nil
   200  }
   201  
   202  // MigrateSetTemplate holds the default template of the 'migrate set' command.
   203  var MigrateSetTemplate = template.Must(template.New("set").
   204  	Funcs(ColorTemplateFuncs).Parse(`
   205  {{- if and (not .Current) .Revisions -}}
   206  All revisions deleted ({{ len .Revisions }} in total):
   207  {{ else if and .Current .Revisions -}}
   208  Current version is {{ cyan .Current.Version }} ({{ .Summary }}):
   209  {{ end }}
   210  {{- if .Revisions }}
   211  {{ range .ByVersion }}
   212    {{- $text := .ColoredVersion }}{{ with .Description }}{{ $text = printf "%s (%s)" $text . }}{{ end }}
   213    {{- printf "  %s\n" $text }}
   214  {{- end }}
   215  {{ end -}}
   216  `))
   217  
   218  type (
   219  	// MigrateSet contains a summary of the migrate set command.
   220  	MigrateSet struct {
   221  		// Revisions that were added, removed or updated.
   222  		Revisions []RevisionOp `json:"Revisions,omitempty"`
   223  		// Current version in the revisions table.
   224  		Current *migrate.Revision `json:"Latest,omitempty"`
   225  	}
   226  	// RevisionOp represents an operation done on a revision.
   227  	RevisionOp struct {
   228  		*migrate.Revision
   229  		Op string `json:"Op,omitempty"`
   230  	}
   231  )
   232  
   233  // ByVersion returns all revisions sorted by version.
   234  func (r *MigrateSet) ByVersion() []RevisionOp {
   235  	sort.Slice(r.Revisions, func(i, j int) bool {
   236  		return r.Revisions[i].Version < r.Revisions[j].Version
   237  	})
   238  	return r.Revisions
   239  }
   240  
   241  // Set records revision that was added.
   242  func (r *MigrateSet) Set(rev *migrate.Revision) {
   243  	r.Revisions = append(r.Revisions, RevisionOp{Revision: rev, Op: "set"})
   244  }
   245  
   246  // Removed records revision that was added.
   247  func (r *MigrateSet) Removed(rev *migrate.Revision) {
   248  	r.Revisions = append(r.Revisions, RevisionOp{Revision: rev, Op: "remove"})
   249  }
   250  
   251  // Summary returns a summary of the set operation.
   252  func (r *MigrateSet) Summary() string {
   253  	var s, d int
   254  	for i := range r.Revisions {
   255  		switch r.Revisions[i].Op {
   256  		case "set":
   257  			s++
   258  		default:
   259  			d++
   260  		}
   261  	}
   262  	var sum []string
   263  	if s > 0 {
   264  		sum = append(sum, fmt.Sprintf("%d set", s))
   265  	}
   266  	if d > 0 {
   267  		sum = append(sum, fmt.Sprintf("%d removed", d))
   268  	}
   269  	return strings.Join(sum, ", ")
   270  }
   271  
   272  // ColoredVersion returns the version of the revision with a color.
   273  func (r *RevisionOp) ColoredVersion() string {
   274  	c := color.HiGreenString("+")
   275  	if r.Op != "set" {
   276  		c = color.HiRedString("-")
   277  	}
   278  	return c + " " + r.Version
   279  }
   280  
   281  var (
   282  	// ApplyTemplateFuncs are global functions available in apply report templates.
   283  	ApplyTemplateFuncs = merge(ColorTemplateFuncs, template.FuncMap{
   284  		"dec":        dec,
   285  		"upper":      strings.ToUpper,
   286  		"json":       jsonEncode,
   287  		"json_merge": jsonMerge,
   288  	})
   289  
   290  	// MigrateApplyTemplate holds the default template of the 'migrate apply' command.
   291  	MigrateApplyTemplate = template.Must(template.
   292  				New("report").
   293  				Funcs(ApplyTemplateFuncs).
   294  				Parse(`{{- if not .Pending -}}
   295  No migration files to execute
   296  {{- else -}}
   297  Migrating to version {{ cyan .Target }}{{ with .Current }} from {{ cyan . }}{{ end }} ({{ len .Pending }} migrations in total):
   298  {{ range $i, $f := .Applied }}
   299    {{ yellow "--" }} migrating version {{ cyan $f.File.Version }}{{ range $f.Applied }}
   300      {{ cyan "->" }} {{ . }}{{ end }}
   301    {{- with .Error }}
   302      {{ redBgWhiteFg .Text }}
   303    {{- else }}
   304    {{ yellow "--" }} ok ({{ yellow (.End.Sub .Start).String }})
   305    {{- end }}
   306  {{ end }}
   307    {{ cyan "-------------------------" }}
   308    {{ yellow "--" }} {{ .End.Sub .Start }}
   309  {{- $files := len .Applied }}
   310  {{- $stmts := .CountStmts }}
   311  {{- if .Error }}
   312    {{ yellow "--" }} {{ dec $files }} migrations ok (1 with errors)
   313    {{ yellow "--" }} {{ dec $stmts }} sql statements ok (1 with errors)
   314  {{- else }}
   315    {{ yellow "--" }} {{ len .Applied }} migrations 
   316    {{ yellow "--" }} {{ .CountStmts  }} sql statements
   317  {{- end }}
   318  {{- end }}
   319  `))
   320  )
   321  
   322  type (
   323  	// MigrateApply contains a summary of a migration applying attempt on a database.
   324  	MigrateApply struct {
   325  		Env
   326  		Pending Files          `json:"Pending,omitempty"` // Pending migration files
   327  		Applied []*AppliedFile `json:"Applied,omitempty"` // Applied files
   328  		Current string         `json:"Current,omitempty"` // Current migration version
   329  		Target  string         `json:"Target,omitempty"`  // Target migration version
   330  		Start   time.Time
   331  		End     time.Time
   332  		// Error is set even then, if it was not caused by a statement in a migration file,
   333  		// but by Atlas, e.g. when committing or rolling back a transaction.
   334  		Error string `json:"Error,omitempty"`
   335  	}
   336  
   337  	// AppliedFile is part of an MigrateApply containing information about an applied file in a migration attempt.
   338  	AppliedFile struct {
   339  		migrate.File
   340  		Start   time.Time
   341  		End     time.Time
   342  		Skipped int      // Amount of skipped SQL statements in a partially applied file.
   343  		Applied []string // SQL statements applied with success
   344  		Error   *StmtError
   345  	}
   346  )
   347  
   348  // NewMigrateApply returns an MigrateApply.
   349  func NewMigrateApply(client *sqlclient.Client, dir migrate.Dir) *MigrateApply {
   350  	return &MigrateApply{
   351  		Env:   NewEnv(client, dir),
   352  		Start: time.Now(),
   353  	}
   354  }
   355  
   356  // Log implements migrate.Logger.
   357  func (a *MigrateApply) Log(e migrate.LogEntry) {
   358  	switch e := e.(type) {
   359  	case migrate.LogExecution:
   360  		// Do not set start time if it
   361  		// was set by the constructor.
   362  		if a.Start.IsZero() {
   363  			a.Start = time.Now()
   364  		}
   365  		a.Current = e.From
   366  		a.Target = e.To
   367  		a.Pending = e.Files
   368  	case migrate.LogFile:
   369  		if l := len(a.Applied); l > 0 {
   370  			f := a.Applied[l-1]
   371  			f.End = time.Now()
   372  		}
   373  		a.Applied = append(a.Applied, &AppliedFile{
   374  			File:    File{e.File},
   375  			Start:   time.Now(),
   376  			Skipped: e.Skip,
   377  		})
   378  	case migrate.LogStmt:
   379  		f := a.Applied[len(a.Applied)-1]
   380  		f.Applied = append(f.Applied, e.SQL)
   381  	case migrate.LogError:
   382  		if l := len(a.Applied); l > 0 {
   383  			f := a.Applied[len(a.Applied)-1]
   384  			f.End = time.Now()
   385  			a.End = f.End
   386  			f.Error = &StmtError{
   387  				Stmt: e.SQL,
   388  				Text: e.Error.Error(),
   389  			}
   390  		}
   391  	case migrate.LogDone:
   392  		n := time.Now()
   393  		if l := len(a.Applied); l > 0 {
   394  			a.Applied[l-1].End = n
   395  		}
   396  		a.End = n
   397  	}
   398  }
   399  
   400  // CountStmts returns the amount of applied statements.
   401  func (a *MigrateApply) CountStmts() (n int) {
   402  	for _, f := range a.Applied {
   403  		n += len(f.Applied)
   404  	}
   405  	return
   406  }
   407  
   408  // MarshalJSON implements json.Marshaler.
   409  func (a *MigrateApply) MarshalJSON() ([]byte, error) {
   410  	type Alias MigrateApply
   411  	var v struct {
   412  		*Alias
   413  		Message string `json:"Message,omitempty"`
   414  	}
   415  	v.Alias = (*Alias)(a)
   416  	switch {
   417  	case a.Error != "":
   418  	case len(v.Applied) == 0:
   419  		v.Message = "No migration files to execute"
   420  	default:
   421  		v.Message = fmt.Sprintf("Migrated to version %s from %s (%d migrations in total)", v.Target, v.Current, len(v.Pending))
   422  	}
   423  	return json.Marshal(v)
   424  }
   425  
   426  // MarshalJSON implements json.Marshaler.
   427  func (f *AppliedFile) MarshalJSON() ([]byte, error) {
   428  	type local struct {
   429  		Name        string     `json:"Name,omitempty"`
   430  		Version     string     `json:"Version,omitempty"`
   431  		Description string     `json:"Description,omitempty"`
   432  		Start       time.Time  `json:"Start,omitempty"`
   433  		End         time.Time  `json:"End,omitempty"`
   434  		Skipped     int        `json:"Skipped,omitempty"`
   435  		Stmts       []string   `json:"Applied,omitempty"`
   436  		Error       *StmtError `json:"Error,omitempty"`
   437  	}
   438  	return json.Marshal(local{
   439  		Name:        f.Name(),
   440  		Version:     f.Version(),
   441  		Description: f.Desc(),
   442  		Start:       f.Start,
   443  		End:         f.End,
   444  		Skipped:     f.Skipped,
   445  		Stmts:       f.Applied,
   446  		Error:       f.Error,
   447  	})
   448  }
   449  
   450  // SchemaPlanTemplate holds the default template of the 'schema apply --dry-run' command.
   451  var SchemaPlanTemplate = template.Must(template.
   452  	New("plan").
   453  	Funcs(ApplyTemplateFuncs).
   454  	Parse(`{{- with .Changes.Pending -}}
   455  -- Planned Changes:
   456  {{ range . -}}
   457  {{- if .Comment -}}
   458  {{- printf "-- %s%s\n" (slice .Comment 0 1 | upper ) (slice .Comment 1) -}}
   459  {{- end -}}
   460  {{- printf "%s;\n" .Cmd -}}
   461  {{- end -}}
   462  {{- else -}}
   463  Schema is synced, no changes to be made.
   464  {{ end -}}
   465  `))
   466  
   467  type (
   468  	// SchemaApply contains a summary of a 'schema apply' execution on a database.
   469  	SchemaApply struct {
   470  		Env
   471  		Changes Changes `json:"Changes,omitempty"`
   472  		// General error that occurred during execution.
   473  		// e.g., when committing or rolling back a transaction.
   474  		Error string `json:"Error,omitempty"`
   475  	}
   476  	// Changes represents a list of changes that are pending or applied.
   477  	Changes struct {
   478  		Applied []*migrate.Change `json:"Applied,omitempty"` // SQL changes applied with success
   479  		Pending []*migrate.Change `json:"Pending,omitempty"` // SQL changes that were not applied
   480  		Error   *StmtError        `json:"Error,omitempty"`   // Error that occurred during applying
   481  	}
   482  )
   483  
   484  // NewSchemaApply returns a SchemaApply.
   485  func NewSchemaApply(env Env, applied, pending []*migrate.Change, err *StmtError) *SchemaApply {
   486  	return &SchemaApply{
   487  		Env: env,
   488  		Changes: Changes{
   489  			Applied: applied,
   490  			Pending: pending,
   491  			Error:   err,
   492  		},
   493  	}
   494  }
   495  
   496  // NewSchemaPlan returns a SchemaApply only with pending changes.
   497  func NewSchemaPlan(env Env, pending []*migrate.Change, err *StmtError) *SchemaApply {
   498  	return NewSchemaApply(env, nil, pending, err)
   499  }
   500  
   501  // MarshalJSON implements json.Marshaler.
   502  func (c Changes) MarshalJSON() ([]byte, error) {
   503  	var v struct {
   504  		Applied []string   `json:"Applied,omitempty"`
   505  		Pending []string   `json:"Pending,omitempty"`
   506  		Error   *StmtError `json:"Error,omitempty"`
   507  	}
   508  	for i := range c.Applied {
   509  		v.Applied = append(v.Applied, c.Applied[i].Cmd)
   510  	}
   511  	for i := range c.Pending {
   512  		v.Pending = append(v.Pending, c.Pending[i].Cmd)
   513  	}
   514  	v.Error = c.Error
   515  	return json.Marshal(v)
   516  }
   517  
   518  // SchemaInspect contains a summary of the 'schema inspect' command.
   519  type SchemaInspect struct {
   520  	*sqlclient.Client `json:"-"`
   521  	Realm             *schema.Realm `json:"Schema,omitempty"` // Inspected realm.
   522  	Error             error         `json:"Error,omitempty"`  // General error that occurred during inspection.
   523  }
   524  
   525  var (
   526  	// InspectTemplateFuncs are global functions available in inspect report templates.
   527  	InspectTemplateFuncs = template.FuncMap{
   528  		"sql":  sqlInspect,
   529  		"json": jsonEncode,
   530  	}
   531  
   532  	// SchemaInspectTemplate holds the default template of the 'schema inspect' command.
   533  	SchemaInspectTemplate = template.Must(template.New("inspect").
   534  				Funcs(InspectTemplateFuncs).
   535  				Parse(`{{ with .Error }}{{ .Error }}{{ else }}{{ $.MarshalHCL }}{{ end }}`))
   536  )
   537  
   538  // MarshalHCL returns the default HCL representation of the schema.
   539  // Used by the template declared above.
   540  func (s *SchemaInspect) MarshalHCL() (string, error) {
   541  	spec, err := s.MarshalSpec(s.Realm)
   542  	if err != nil {
   543  		return "", err
   544  	}
   545  	return string(spec), nil
   546  }
   547  
   548  // MarshalJSON implements json.Marshaler.
   549  func (s *SchemaInspect) MarshalJSON() ([]byte, error) {
   550  	if s.Error != nil {
   551  		return json.Marshal(struct{ Error string }{s.Error.Error()})
   552  	}
   553  	type (
   554  		Column struct {
   555  			Name string `json:"name"`
   556  			Type string `json:"type,omitempty"`
   557  			Null bool   `json:"null,omitempty"`
   558  		}
   559  		IndexPart struct {
   560  			Desc   bool   `json:"desc,omitempty"`
   561  			Column string `json:"column,omitempty"`
   562  			Expr   string `json:"expr,omitempty"`
   563  		}
   564  		Index struct {
   565  			Name   string      `json:"name,omitempty"`
   566  			Unique bool        `json:"unique,omitempty"`
   567  			Parts  []IndexPart `json:"parts,omitempty"`
   568  		}
   569  		ForeignKey struct {
   570  			Name       string   `json:"name"`
   571  			Columns    []string `json:"columns,omitempty"`
   572  			References struct {
   573  				Table   string   `json:"table"`
   574  				Columns []string `json:"columns,omitempty"`
   575  			} `json:"references"`
   576  		}
   577  		Table struct {
   578  			Name        string       `json:"name"`
   579  			Columns     []Column     `json:"columns,omitempty"`
   580  			Indexes     []Index      `json:"indexes,omitempty"`
   581  			PrimaryKey  *Index       `json:"primary_key,omitempty"`
   582  			ForeignKeys []ForeignKey `json:"foreign_keys,omitempty"`
   583  		}
   584  		Schema struct {
   585  			Name   string  `json:"name"`
   586  			Tables []Table `json:"tables,omitempty"`
   587  		}
   588  	)
   589  	var realm struct {
   590  		Schemas []Schema `json:"schemas,omitempty"`
   591  	}
   592  	for _, s1 := range s.Realm.Schemas {
   593  		s2 := Schema{Name: s1.Name}
   594  		for _, t1 := range s1.Tables {
   595  			t2 := Table{Name: t1.Name}
   596  			for _, c1 := range t1.Columns {
   597  				t2.Columns = append(t2.Columns, Column{
   598  					Name: c1.Name,
   599  					Type: c1.Type.Raw,
   600  					Null: c1.Type.Null,
   601  				})
   602  			}
   603  			idxParts := func(idx *schema.Index) (parts []IndexPart) {
   604  				for _, p1 := range idx.Parts {
   605  					p2 := IndexPart{Desc: p1.Desc}
   606  					switch {
   607  					case p1.C != nil:
   608  						p2.Column = p1.C.Name
   609  					case p1.X != nil:
   610  						switch t := p1.X.(type) {
   611  						case *schema.Literal:
   612  							p2.Expr = t.V
   613  						case *schema.RawExpr:
   614  							p2.Expr = t.X
   615  						}
   616  					}
   617  					parts = append(parts, p2)
   618  				}
   619  				return parts
   620  			}
   621  			for _, idx1 := range t1.Indexes {
   622  				t2.Indexes = append(t2.Indexes, Index{
   623  					Name:   idx1.Name,
   624  					Unique: idx1.Unique,
   625  					Parts:  idxParts(idx1),
   626  				})
   627  			}
   628  			if t1.PrimaryKey != nil {
   629  				t2.PrimaryKey = &Index{Parts: idxParts(t1.PrimaryKey)}
   630  			}
   631  			for _, fk1 := range t1.ForeignKeys {
   632  				fk2 := ForeignKey{Name: fk1.Symbol}
   633  				for _, c1 := range fk1.Columns {
   634  					fk2.Columns = append(fk2.Columns, c1.Name)
   635  				}
   636  				fk2.References.Table = fk1.RefTable.Name
   637  				for _, c1 := range fk1.RefColumns {
   638  					fk2.References.Columns = append(fk2.References.Columns, c1.Name)
   639  				}
   640  				t2.ForeignKeys = append(t2.ForeignKeys, fk2)
   641  			}
   642  			s2.Tables = append(s2.Tables, t2)
   643  		}
   644  		realm.Schemas = append(realm.Schemas, s2)
   645  	}
   646  	return json.Marshal(realm)
   647  }
   648  
   649  func sqlInspect(report *SchemaInspect, indent ...string) (string, error) {
   650  	if report.Error != nil {
   651  		return report.Error.Error(), nil
   652  	}
   653  	var changes schema.Changes
   654  	for _, s := range report.Realm.Schemas {
   655  		// Generate commands for creating the schemas on realm-mode.
   656  		if report.Client.URL.Schema == "" {
   657  			changes = append(changes, &schema.AddSchema{S: s})
   658  		}
   659  		for _, t := range s.Tables {
   660  			changes = append(changes, &schema.AddTable{T: t})
   661  		}
   662  	}
   663  	return fmtPlan(report.Client, changes, indent)
   664  }
   665  
   666  // SchemaDiff contains a summary of the 'schema diff' command.
   667  type SchemaDiff struct {
   668  	*sqlclient.Client
   669  	Changes []schema.Change
   670  }
   671  
   672  var (
   673  	// SchemaDiffFuncs are global functions available in diff report templates.
   674  	SchemaDiffFuncs = template.FuncMap{
   675  		"sql": sqlDiff,
   676  	}
   677  	// SchemaDiffTemplate holds the default template of the 'schema diff' command.
   678  	SchemaDiffTemplate = template.Must(template.
   679  				New("schema_diff").
   680  				Funcs(SchemaDiffFuncs).
   681  				Parse(`{{- with .Changes -}}
   682  {{ sql $ }}
   683  {{- else -}}
   684  Schemas are synced, no changes to be made.
   685  {{ end -}}
   686  `))
   687  )
   688  
   689  func sqlDiff(diff *SchemaDiff, indent ...string) (string, error) {
   690  	return fmtPlan(diff.Client, diff.Changes, indent)
   691  }
   692  
   693  func fmtPlan(client *sqlclient.Client, changes schema.Changes, indent []string) (string, error) {
   694  	if len(indent) > 1 {
   695  		return "", fmt.Errorf("unexpected number of arguments: %d", len(indent))
   696  	}
   697  	plan, err := client.PlanChanges(context.Background(), "plan", changes, func(o *migrate.PlanOptions) {
   698  		// Disable tables qualifier in schema-mode.
   699  		if client.URL.Schema != "" {
   700  			o.SchemaQualifier = new(string)
   701  		}
   702  		if len(indent) > 0 {
   703  			o.Indent = indent[0]
   704  		}
   705  	})
   706  	if err != nil {
   707  		return "", err
   708  	}
   709  	switch files, err := migrate.DefaultFormatter.Format(plan); {
   710  	case err != nil:
   711  		return "", err
   712  	case len(files) != 1:
   713  		return "", fmt.Errorf("unexpected number of files: %d", len(files))
   714  	default:
   715  		return string(files[0].Bytes()), nil
   716  	}
   717  }
   718  
   719  func merge(maps ...template.FuncMap) template.FuncMap {
   720  	switch len(maps) {
   721  	case 0:
   722  		return nil
   723  	case 1:
   724  		return maps[0]
   725  	default:
   726  		m := maps[0]
   727  		for _, e := range maps[1:] {
   728  			for k, v := range e {
   729  				m[k] = v
   730  			}
   731  		}
   732  		return m
   733  	}
   734  }
   735  
   736  func jsonEncode(v any, args ...string) (string, error) {
   737  	var (
   738  		b   []byte
   739  		err error
   740  	)
   741  	switch len(args) {
   742  	case 0:
   743  		b, err = json.Marshal(v)
   744  	case 1:
   745  		b, err = json.MarshalIndent(v, "", args[0])
   746  	default:
   747  		b, err = json.MarshalIndent(v, args[0], args[1])
   748  	}
   749  	return string(b), err
   750  }
   751  
   752  func jsonMerge(objects ...string) (string, error) {
   753  	var r map[string]any
   754  	for i := range objects {
   755  		if err := json.Unmarshal([]byte(objects[i]), &r); err != nil {
   756  			return "", fmt.Errorf("json_merge: %w", err)
   757  		}
   758  	}
   759  	b, err := json.Marshal(r)
   760  	if err != nil {
   761  		return "", fmt.Errorf("json_merge: %w", err)
   762  	}
   763  	return string(b), nil
   764  }
   765  
   766  func dec(i int) int {
   767  	return i - 1
   768  }