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 }