mirror of
https://github.com/dolthub/dolt.git
synced 2026-04-22 19:43:51 -05:00
386 lines
11 KiB
Go
386 lines
11 KiB
Go
// Copyright 2022 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package commands
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
|
|
textdiff "github.com/andreyvit/diff"
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/fatih/color"
|
|
|
|
"github.com/dolthub/dolt/go/cmd/dolt/cli"
|
|
"github.com/dolthub/dolt/go/cmd/dolt/errhand"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/diff"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/schema"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/sqlutil"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/table/typed/json"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/table/untyped/sqlexport"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/table/untyped/tabular"
|
|
"github.com/dolthub/dolt/go/libraries/utils/iohelp"
|
|
"github.com/dolthub/dolt/go/store/atomicerr"
|
|
)
|
|
|
|
// diffWriter is an interface that lets us write diffs in a variety of output formats
|
|
type diffWriter interface {
|
|
// BeginTable is called when a new table is about to be written, before any schema or row diffs are written
|
|
BeginTable(ctx context.Context, td diff.TableDelta) error
|
|
// WriteSchemaDiff is called to write a schema diff for the table given (if requested by args)
|
|
WriteSchemaDiff(ctx context.Context, toRoot *doltdb.RootValue, td diff.TableDelta) error
|
|
// RowWriter returns a row writer for the table delta provided, which will have Close() called on it when rows are
|
|
// done being written.
|
|
RowWriter(ctx context.Context, td diff.TableDelta, unionSch sql.Schema) (diff.SqlRowDiffWriter, error)
|
|
// Close finalizes the work of the writer
|
|
Close(ctx context.Context) error
|
|
}
|
|
|
|
// newDiffWriter returns a diffWriter for the output format given
|
|
func newDiffWriter(diffOutput diffOutput) (diffWriter, error) {
|
|
switch diffOutput {
|
|
case TabularDiffOutput:
|
|
return tabularDiffWriter{}, nil
|
|
case SQLDiffOutput:
|
|
return sqlDiffWriter{}, nil
|
|
case JsonDiffOutput:
|
|
return newJsonDiffWriter(iohelp.NopWrCloser(cli.CliOut))
|
|
default:
|
|
panic(fmt.Sprintf("unexpected diff output: %v", diffOutput))
|
|
}
|
|
}
|
|
|
|
func printDiffSummary(ctx context.Context, td diff.TableDelta, colLen int) errhand.VerboseError {
|
|
// todo: use errgroup.Group
|
|
ae := atomicerr.New()
|
|
ch := make(chan diff.DiffSummaryProgress)
|
|
go func() {
|
|
defer close(ch)
|
|
err := diff.SummaryForTableDelta(ctx, ch, td)
|
|
|
|
ae.SetIfError(err)
|
|
}()
|
|
|
|
acc := diff.DiffSummaryProgress{}
|
|
var count int64
|
|
var pos int
|
|
eP := cli.NewEphemeralPrinter()
|
|
for p := range ch {
|
|
if ae.IsSet() {
|
|
break
|
|
}
|
|
|
|
acc.Adds += p.Adds
|
|
acc.Removes += p.Removes
|
|
acc.Changes += p.Changes
|
|
acc.CellChanges += p.CellChanges
|
|
acc.NewSize += p.NewSize
|
|
acc.OldSize += p.OldSize
|
|
|
|
if count%10000 == 0 {
|
|
eP.Printf("prev size: %d, new size: %d, adds: %d, deletes: %d, modifications: %d\n", acc.OldSize, acc.NewSize, acc.Adds, acc.Removes, acc.Changes)
|
|
eP.Display()
|
|
}
|
|
|
|
count++
|
|
}
|
|
|
|
pos = cli.DeleteAndPrint(pos, "")
|
|
|
|
if err := ae.Get(); err != nil {
|
|
return errhand.BuildDError("").AddCause(err).Build()
|
|
}
|
|
|
|
keyless, err := td.IsKeyless(ctx)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
if (acc.Adds + acc.Removes + acc.Changes) == 0 {
|
|
cli.Println("No data changes. See schema changes by using -s or --schema.")
|
|
return nil
|
|
}
|
|
|
|
if keyless {
|
|
printKeylessSummary(acc)
|
|
} else {
|
|
printSummary(acc, colLen)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func printSummary(acc diff.DiffSummaryProgress, colLen int) {
|
|
rowsUnmodified := uint64(acc.OldSize - acc.Changes - acc.Removes)
|
|
unmodified := pluralize("Row Unmodified", "Rows Unmodified", rowsUnmodified)
|
|
insertions := pluralize("Row Added", "Rows Added", acc.Adds)
|
|
deletions := pluralize("Row Deleted", "Rows Deleted", acc.Removes)
|
|
changes := pluralize("Row Modified", "Rows Modified", acc.Changes)
|
|
cellChanges := pluralize("Cell Modified", "Cells Modified", acc.CellChanges)
|
|
|
|
oldValues := pluralize("Entry", "Entries", acc.OldSize)
|
|
newValues := pluralize("Entry", "Entries", acc.NewSize)
|
|
|
|
percentCellsChanged := float64(100*acc.CellChanges) / (float64(acc.OldSize) * float64(colLen))
|
|
|
|
safePercent := func(num, dom uint64) float64 {
|
|
// returns +Inf for x/0 where x > 0
|
|
if num == 0 {
|
|
return float64(0)
|
|
}
|
|
return float64(100*num) / (float64(dom))
|
|
}
|
|
|
|
cli.Printf("%s (%.2f%%)\n", unmodified, safePercent(rowsUnmodified, acc.OldSize))
|
|
cli.Printf("%s (%.2f%%)\n", insertions, safePercent(acc.Adds, acc.OldSize))
|
|
cli.Printf("%s (%.2f%%)\n", deletions, safePercent(acc.Removes, acc.OldSize))
|
|
cli.Printf("%s (%.2f%%)\n", changes, safePercent(acc.Changes, acc.OldSize))
|
|
cli.Printf("%s (%.2f%%)\n", cellChanges, percentCellsChanged)
|
|
cli.Printf("(%s vs %s)\n\n", oldValues, newValues)
|
|
}
|
|
|
|
func printKeylessSummary(acc diff.DiffSummaryProgress) {
|
|
insertions := pluralize("Row Added", "Rows Added", acc.Adds)
|
|
deletions := pluralize("Row Deleted", "Rows Deleted", acc.Removes)
|
|
|
|
cli.Printf("%s\n", insertions)
|
|
cli.Printf("%s\n", deletions)
|
|
}
|
|
|
|
func pluralize(singular, plural string, n uint64) string {
|
|
var noun string
|
|
if n != 1 {
|
|
noun = plural
|
|
} else {
|
|
noun = singular
|
|
}
|
|
return fmt.Sprintf("%s %s", humanize.Comma(int64(n)), noun)
|
|
}
|
|
|
|
type tabularDiffWriter struct{}
|
|
|
|
var _ diffWriter = (*tabularDiffWriter)(nil)
|
|
|
|
func (t tabularDiffWriter) Close(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (t tabularDiffWriter) BeginTable(ctx context.Context, td diff.TableDelta) error {
|
|
bold := color.New(color.Bold)
|
|
if td.IsDrop() {
|
|
_, _ = bold.Printf("diff --dolt a/%s b/%s\n", td.FromName, td.FromName)
|
|
_, _ = bold.Println("deleted table")
|
|
} else if td.IsAdd() {
|
|
_, _ = bold.Printf("diff --dolt a/%s b/%s\n", td.ToName, td.ToName)
|
|
_, _ = bold.Println("added table")
|
|
} else {
|
|
_, _ = bold.Printf("diff --dolt a/%s b/%s\n", td.FromName, td.ToName)
|
|
h1, err := td.FromTable.HashOf()
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
_, _ = bold.Printf("--- a/%s @ %s\n", td.FromName, h1.String())
|
|
|
|
h2, err := td.ToTable.HashOf()
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
_, _ = bold.Printf("+++ b/%s @ %s\n", td.ToName, h2.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t tabularDiffWriter) WriteSchemaDiff(ctx context.Context, toRoot *doltdb.RootValue, td diff.TableDelta) error {
|
|
fromSch, toSch, err := td.GetSchemas(ctx)
|
|
if err != nil {
|
|
return errhand.BuildDError("cannot retrieve schema for table %s", td.ToName).AddCause(err).Build()
|
|
}
|
|
|
|
var fromCreateStmt = ""
|
|
if td.FromTable != nil {
|
|
// TODO: use UserSpaceDatabase for these, no reason for this separate database implementation
|
|
sqlDb := sqle.NewSingleTableDatabase(td.FromName, fromSch, td.FromFks, td.FromFksParentSch)
|
|
sqlCtx, engine, _ := sqle.PrepareCreateTableStmt(ctx, sqlDb)
|
|
fromCreateStmt, err = sqle.GetCreateTableStmt(sqlCtx, engine, td.FromName)
|
|
if err != nil {
|
|
return errhand.VerboseErrorFromError(err)
|
|
}
|
|
}
|
|
|
|
var toCreateStmt = ""
|
|
if td.ToTable != nil {
|
|
sqlDb := sqle.NewSingleTableDatabase(td.ToName, toSch, td.ToFks, td.ToFksParentSch)
|
|
sqlCtx, engine, _ := sqle.PrepareCreateTableStmt(ctx, sqlDb)
|
|
toCreateStmt, err = sqle.GetCreateTableStmt(sqlCtx, engine, td.ToName)
|
|
if err != nil {
|
|
return errhand.VerboseErrorFromError(err)
|
|
}
|
|
}
|
|
|
|
if fromCreateStmt != toCreateStmt {
|
|
cli.Println(textdiff.LineDiff(fromCreateStmt, toCreateStmt))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t tabularDiffWriter) RowWriter(ctx context.Context, td diff.TableDelta, unionSch sql.Schema) (diff.SqlRowDiffWriter, error) {
|
|
return tabular.NewFixedWidthDiffTableWriter(unionSch, iohelp.NopWrCloser(cli.CliOut), 100), nil
|
|
}
|
|
|
|
type sqlDiffWriter struct{}
|
|
|
|
var _ diffWriter = (*tabularDiffWriter)(nil)
|
|
|
|
func (s sqlDiffWriter) Close(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (s sqlDiffWriter) BeginTable(ctx context.Context, td diff.TableDelta) error {
|
|
return nil
|
|
}
|
|
|
|
func (s sqlDiffWriter) WriteSchemaDiff(ctx context.Context, toRoot *doltdb.RootValue, td diff.TableDelta) error {
|
|
toSchemas, err := toRoot.GetAllSchemas(ctx)
|
|
if err != nil {
|
|
return errhand.BuildDError("could not read schemas from toRoot").AddCause(err).Build()
|
|
}
|
|
|
|
return writeSqlSchemaDiff(ctx, td, toSchemas)
|
|
}
|
|
|
|
func (s sqlDiffWriter) RowWriter(ctx context.Context, td diff.TableDelta, unionSch sql.Schema) (diff.SqlRowDiffWriter, error) {
|
|
targetSch := td.ToSch
|
|
if targetSch == nil {
|
|
targetSch = td.FromSch
|
|
}
|
|
|
|
return sqlexport.NewSqlDiffWriter(td.ToName, targetSch, iohelp.NopWrCloser(cli.CliOut)), nil
|
|
}
|
|
|
|
type jsonDiffWriter struct {
|
|
wr io.WriteCloser
|
|
schemaDiffWriter diff.SchemaDiffWriter
|
|
rowDiffWriter diff.SqlRowDiffWriter
|
|
schemaDiffsWritten int
|
|
tablesWritten int
|
|
}
|
|
|
|
var _ diffWriter = (*tabularDiffWriter)(nil)
|
|
|
|
func newJsonDiffWriter(wr io.WriteCloser) (*jsonDiffWriter, error) {
|
|
return &jsonDiffWriter{
|
|
wr: wr,
|
|
}, nil
|
|
}
|
|
|
|
const jsonDiffTableHeader = `{"name":"%s","schema_diff":`
|
|
const jsonDiffFooter = `}]}`
|
|
|
|
func (j *jsonDiffWriter) BeginTable(ctx context.Context, td diff.TableDelta) error {
|
|
if j.schemaDiffWriter == nil {
|
|
err := iohelp.WriteAll(j.wr, []byte(`{"tables":[`))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
err := iohelp.WriteAll(j.wr, []byte(`},`))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err := iohelp.WriteAll(j.wr, []byte(fmt.Sprintf(jsonDiffTableHeader, td.ToName)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
j.tablesWritten++
|
|
|
|
j.schemaDiffWriter, err = json.NewSchemaDiffWriter(iohelp.NopWrCloser(j.wr))
|
|
return err
|
|
}
|
|
|
|
func (j *jsonDiffWriter) WriteSchemaDiff(ctx context.Context, toRoot *doltdb.RootValue, td diff.TableDelta) error {
|
|
toSchemas, err := toRoot.GetAllSchemas(ctx)
|
|
if err != nil {
|
|
return errhand.BuildDError("could not read schemas from toRoot").AddCause(err).Build()
|
|
}
|
|
|
|
stmts, err := sqlSchemaDiff(ctx, td, toSchemas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, stmt := range stmts {
|
|
err := j.schemaDiffWriter.WriteSchemaDiff(ctx, stmt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (j *jsonDiffWriter) RowWriter(ctx context.Context, td diff.TableDelta, unionSch sql.Schema) (diff.SqlRowDiffWriter, error) {
|
|
// close off the schema diff block, start the data block
|
|
err := iohelp.WriteAll(j.wr, []byte(`],"data_diff":[`))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Translate the union schema to its dolt version
|
|
cols := schema.NewColCollection()
|
|
for i, col := range unionSch {
|
|
doltCol, err := sqlutil.ToDoltCol(uint64(i), col)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cols = cols.Append(doltCol)
|
|
}
|
|
|
|
sch, err := schema.SchemaFromCols(cols)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
j.rowDiffWriter, err = json.NewJsonDiffWriter(iohelp.NopWrCloser(cli.CliOut), sch)
|
|
return j.rowDiffWriter, err
|
|
}
|
|
|
|
func (j *jsonDiffWriter) Close(ctx context.Context) error {
|
|
if j.tablesWritten > 0 {
|
|
err := iohelp.WriteLine(j.wr, jsonDiffFooter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
err := iohelp.WriteLine(j.wr, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Writer has already been closed here during row iteration, no need to close it here
|
|
return nil
|
|
}
|