Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

2007 lines
64 KiB
Go

// Copyright 2019 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"
"errors"
"fmt"
"io"
"strconv"
"strings"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/types"
"github.com/dolthub/vitess/go/sqltypes"
ast "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/gocraft/dbr/v2"
"github.com/gocraft/dbr/v2/dialect"
"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/env"
"github.com/dolthub/dolt/go/libraries/doltcore/schema"
"github.com/dolthub/dolt/go/libraries/doltcore/schema/typecompatibility"
"github.com/dolthub/dolt/go/libraries/doltcore/schema/typeinfo"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/sqlutil"
"github.com/dolthub/dolt/go/libraries/doltcore/table/untyped/tabular"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
"github.com/dolthub/dolt/go/libraries/utils/iohelp"
"github.com/dolthub/dolt/go/libraries/utils/set"
eventsapi "github.com/dolthub/eventsapi_schema/dolt/services/eventsapi/v1alpha1"
)
type diffOutput int
type diffPart int
const (
SchemaOnlyDiff diffPart = 1 // 0b0000 0001
DataOnlyDiff diffPart = 2 // 0b0000 0010
NameOnlyDiff diffPart = 4 // 0b0000 0100
Stat diffPart = 8 // 0b0000 1000
Summary diffPart = 16 // 0b0001 0000
SchemaAndDataDiff = SchemaOnlyDiff | DataOnlyDiff
TabularDiffOutput diffOutput = 1
SQLDiffOutput diffOutput = 2
JsonDiffOutput diffOutput = 3
)
var diffDocs = cli.CommandDocumentationContent{
ShortDesc: "Show changes between commits, commit and working tree, etc",
LongDesc: `
Show changes between the working and staged tables, changes between the working tables and the tables within a commit, or changes between tables at two commits.
{{.EmphasisLeft}}dolt diff [--options] [<tables>...]{{.EmphasisRight}}
This form is to view the changes you made relative to the staging area for the next commit. In other words, the differences are what you could tell Dolt to further add but you still haven't. You can stage these changes by using dolt add.
{{.EmphasisLeft}}dolt diff [--options] [--merge-base] <commit> [<tables>...]{{.EmphasisRight}}
This form is to view the changes you have in your working tables relative to the named {{.LessThan}}commit{{.GreaterThan}}. You can use HEAD to compare it with the latest commit, or a branch name to compare with the tip of a different branch. If {{.EmphasisLeft}}--merge-base{{.EmphasisRight}} is given, instead of using {{.LessThan}}commit{{.GreaterThan}}, use the merge base of {{.LessThan}}commit{{.GreaterThan}} and HEAD. {{.EmphasisLeft}}dolt diff --merge-base A{{.EmphasisRight}} is equivalent to {{.EmphasisLeft}}dolt diff $(dolt merge-base A HEAD){{.EmphasisRight}} and {{.EmphasisLeft}}dolt diff A...HEAD{{.EmphasisRight}}.
{{.EmphasisLeft}}dolt diff [--options] [--merge-base] <commit> <commit> [<tables>...]{{.EmphasisRight}}
This is to view the changes between two arbitrary {{.EmphasisLeft}}commit{{.EmphasisRight}}. If {{.EmphasisLeft}}--merge-base{{.EmphasisRight}} is given, use the merge base of the two commits for the "before" side. {{.EmphasisLeft}}dolt diff --merge-base A B{{.EmphasisRight}} is equivalent to {{.EmphasisLeft}}dolt diff $(dolt merge-base A B) B{{.EmphasisRight}} and {{.EmphasisLeft}}dolt diff A...B{{.EmphasisRight}}.
{{.EmphasisLeft}}dolt diff [--options] <commit>..<commit> [<tables>...]{{.EmphasisRight}}
This is synonymous to the above form (without the ..) to view the changes between two arbitrary {{.EmphasisLeft}}commit{{.EmphasisRight}}.
{{.EmphasisLeft}}dolt diff [--options] <commit>...<commit> [<tables>...]{{.EmphasisRight}}
This is to view the changes on the branch containing and up to the second {{.LessThan}}commit{{.GreaterThan}}, starting at a common ancestor of both {{.LessThan}}commit{{.GreaterThan}}. {{.EmphasisLeft}}dolt diff A...B{{.EmphasisRight}} is equivalent to {{.EmphasisLeft}}dolt diff $(dolt merge-base A B) B{{.EmphasisRight}} and {{.EmphasisLeft}}dolt diff --merge-base A B{{.EmphasisRight}}. You can omit any one of {{.LessThan}}commit{{.GreaterThan}}, which has the same effect as using HEAD instead.
The diffs displayed can be limited to show the first N by providing the parameter {{.EmphasisLeft}}--limit N{{.EmphasisRight}} where {{.EmphasisLeft}}N{{.EmphasisRight}} is the number of diffs to display.
To filter which data rows are displayed, use {{.EmphasisLeft}}--where <SQL expression>{{.EmphasisRight}}. Table column names in the filter expression must be prefixed with {{.EmphasisLeft}}from_{{.EmphasisRight}} or {{.EmphasisLeft}}to_{{.EmphasisRight}}, e.g. {{.EmphasisLeft}}to_COLUMN_NAME > 100{{.EmphasisRight}} or {{.EmphasisLeft}}from_COLUMN_NAME + to_COLUMN_NAME = 0{{.EmphasisRight}}.
To filter diff output by change type, use {{.EmphasisLeft}}--filter <type>{{.EmphasisRight}} where {{.EmphasisLeft}}<type>{{.EmphasisRight}} is one of {{.EmphasisLeft}}added{{.EmphasisRight}}, {{.EmphasisLeft}}modified{{.EmphasisRight}}, {{.EmphasisLeft}}renamed{{.EmphasisRight}}, or {{.EmphasisLeft}}dropped{{.EmphasisRight}}. The {{.EmphasisLeft}}added{{.EmphasisRight}} filter shows only additions (new tables or rows), {{.EmphasisLeft}}modified{{.EmphasisRight}} shows only schema modifications or row updates, {{.EmphasisLeft}}renamed{{.EmphasisRight}} shows only renamed tables, and {{.EmphasisLeft}}dropped{{.EmphasisRight}} shows only deletions (dropped tables or deleted rows). You can also use {{.EmphasisLeft}}removed{{.EmphasisRight}} as an alias for {{.EmphasisLeft}}dropped{{.EmphasisRight}}. For example, {{.EmphasisLeft}}dolt diff --filter=dropped{{.EmphasisRight}} shows only deleted rows and dropped tables.
The {{.EmphasisLeft}}--diff-mode{{.EmphasisRight}} argument controls how modified rows are presented when the format output is set to {{.EmphasisLeft}}tabular{{.EmphasisRight}}. When set to {{.EmphasisLeft}}row{{.EmphasisRight}}, modified rows are presented as old and new rows. When set to {{.EmphasisLeft}}line{{.EmphasisRight}}, modified rows are presented as a single row, and changes are presented using "+" and "-" within the column. When set to {{.EmphasisLeft}}in-place{{.EmphasisRight}}, modified rows are presented as a single row, and changes are presented side-by-side with a color distinction (requires a color-enabled terminal). When set to {{.EmphasisLeft}}context{{.EmphasisRight}}, rows that contain at least one column that spans multiple lines uses {{.EmphasisLeft}}line{{.EmphasisRight}}, while all other rows use {{.EmphasisLeft}}row{{.EmphasisRight}}. The default value is {{.EmphasisLeft}}context{{.EmphasisRight}}.
`,
Synopsis: []string{
`[options] [{{.LessThan}}commit{{.GreaterThan}}] [{{.LessThan}}tables{{.GreaterThan}}...]`,
`[options] {{.LessThan}}commit{{.GreaterThan}} {{.LessThan}}commit{{.GreaterThan}} [{{.LessThan}}tables{{.GreaterThan}}...]`,
},
}
type diffDisplaySettings struct {
diffParts diffPart
diffOutput diffOutput
diffMode diff.Mode
limit int
where string
skinny bool
includeCols []string
filter *diffTypeFilter
}
type diffDatasets struct {
fromRef string
toRef string
}
// hasWorkingSet reports whether either side of the diff references the working set or staging area.
func (d *diffDatasets) hasWorkingSet() bool {
return doltdb.IsWorkingSetRef(d.fromRef) || doltdb.IsWorkingSetRef(d.toRef)
}
type diffArgs struct {
*diffDisplaySettings
*diffDatasets
tableSet *set.StrSet
}
type diffStatistics struct {
TableName string
RowsUnmodified uint64
RowsAdded uint64
RowsDeleted uint64
RowsModified uint64
CellsAdded uint64
CellsDeleted uint64
CellsModified uint64
OldRowCount uint64
NewRowCount uint64
OldCellCount uint64
NewCellCount uint64
}
// diffTypeFilter manages which diff types should be included in the output.
// When filters is nil or empty, all types are included.
type diffTypeFilter struct {
// Map of diff type -> should include
// If nil or empty, includes all types
filters map[string]bool
}
// newDiffTypeFilter creates a filter for the specified diff type.
// Pass diff.DiffTypeAll or empty string to include all types.
// Accepts "removed" as an alias for "dropped" for user convenience.
func newDiffTypeFilter(filterType string) *diffTypeFilter {
if filterType == "" || filterType == diff.DiffTypeAll {
return &diffTypeFilter{filters: nil} // nil means include all
}
// Map "removed" to "dropped" (alias for user convenience)
internalFilterType := filterType
if filterType == "removed" {
internalFilterType = diff.DiffTypeDropped
}
return &diffTypeFilter{
filters: map[string]bool{
internalFilterType: true,
},
}
}
// shouldInclude checks if the given diff type should be included.
// Uses TableDeltaSummary.DiffType field for table-level filtering.
func (df *diffTypeFilter) shouldInclude(diffType string) bool {
// nil or empty filters means include everything
if df.filters == nil || len(df.filters) == 0 {
return true
}
return df.filters[diffType]
}
// isValid validates the filter configuration
func (df *diffTypeFilter) isValid() bool {
if df.filters == nil {
return true
}
for filterType := range df.filters {
if filterType != diff.DiffTypeAdded &&
filterType != diff.DiffTypeModified &&
filterType != diff.DiffTypeRenamed &&
filterType != diff.DiffTypeDropped {
return false
}
}
return true
}
// shouldSkipRow checks if a row should be skipped based on the filter settings.
// Uses the DiffType infrastructure for consistency with table-level filtering.
func shouldSkipRow(filter *diffTypeFilter, rowChangeType diff.ChangeType) bool {
if filter == nil {
return false
}
// Don't filter None - it represents "no row" on one side of the diff
if rowChangeType == diff.None {
return false
}
// Convert row-level ChangeType to table-level DiffType string
diffType := diff.ChangeTypeToDiffType(rowChangeType)
// Use the map-based shouldInclude method
return !filter.shouldInclude(diffType)
}
// shouldUseLazyHeader determines if we should delay printing the table header
// until we know there are rows to display. This prevents empty headers when
// all rows are filtered out in data-only diffs.
func shouldUseLazyHeader(dArgs *diffArgs, tableSummary diff.TableDeltaSummary) bool {
return dArgs.filter != nil && dArgs.filter.filters != nil &&
!tableSummary.SchemaChange && !tableSummary.IsRename()
}
// lazyRowWriter wraps a SqlRowDiffWriter and delays calling BeginTable
// until the first row is actually written. This prevents empty table headers
// when all rows are filtered out.
type lazyRowWriter struct {
writer diff.SqlRowDiffWriter
// Callback to invoke before first write
// Set to nil after first call
onFirstWrite func() error
}
// newLazyRowWriter creates a lazy writer that wraps the given writer.
// The onFirstWrite callback is invoked exactly once before the first write.
func newLazyRowWriter(writer diff.SqlRowDiffWriter, onFirstWrite func() error) *lazyRowWriter {
return &lazyRowWriter{
writer: writer,
onFirstWrite: onFirstWrite,
}
}
// WriteRow implements diff.SqlRowDiffWriter
func (l *lazyRowWriter) WriteRow(ctx *sql.Context, row sql.Row, diffType diff.ChangeType, colDiffTypes []diff.ChangeType) error {
// Initialize on first write
if l.onFirstWrite != nil {
if err := l.onFirstWrite(); err != nil {
return err
}
l.onFirstWrite = nil // Prevent double-initialization
}
return l.writer.WriteRow(ctx, row, diffType, colDiffTypes)
}
// WriteCombinedRow implements diff.SqlRowDiffWriter
func (l *lazyRowWriter) WriteCombinedRow(ctx *sql.Context, oldRow, newRow sql.Row, mode diff.Mode) error {
// Initialize on first write
if l.onFirstWrite != nil {
if err := l.onFirstWrite(); err != nil {
return err
}
l.onFirstWrite = nil
}
return l.writer.WriteCombinedRow(ctx, oldRow, newRow, mode)
}
// Close implements diff.SqlRowDiffWriter
func (l *lazyRowWriter) Close(ctx context.Context) error {
return l.writer.Close(ctx)
}
type DiffCmd struct{}
// Name is returns the name of the Dolt cli command. This is what is used on the command line to invoke the command
func (cmd DiffCmd) Name() string {
return "diff"
}
// Description returns a description of the command
func (cmd DiffCmd) Description() string {
return "Diff a table."
}
// EventType returns the type of the event to log
func (cmd DiffCmd) EventType() eventsapi.ClientEventType {
return eventsapi.ClientEventType_DIFF
}
func (cmd DiffCmd) Docs() *cli.CommandDocumentation {
ap := cmd.ArgParser()
return cli.NewCommandDocumentation(diffDocs, ap)
}
func (cmd DiffCmd) ArgParser() *argparser.ArgParser {
return cli.CreateDiffArgParser(false)
}
// Exec executes the command
func (cmd DiffCmd) Exec(ctx context.Context, commandStr string, args []string, _ *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cmd.ArgParser()
apr, usage, terminate, status := ParseArgsOrPrintHelp(ap, commandStr, args, diffDocs)
if terminate {
return status
}
verr := cmd.validateArgs(apr)
if verr != nil {
return HandleVErrAndExitCode(verr, usage)
}
queryist, err := cliCtx.QueryEngine(ctx)
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
updateSystemVar, err := cli.SetSystemVar(queryist.Queryist, queryist.Context, apr.Contains(cli.SystemFlag))
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
dArgs, err := parseDiffArgs(queryist.Queryist, queryist.Context, apr)
if err != nil {
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
verr = diffUserTables(queryist.Queryist, queryist.Context, dArgs)
if verr != nil {
return HandleVErrAndExitCode(verr, usage)
}
if updateSystemVar != nil {
err = updateSystemVar()
}
return HandleVErrAndExitCode(errhand.VerboseErrorFromError(err), usage)
}
func (cmd DiffCmd) validateArgs(apr *argparser.ArgParseResults) errhand.VerboseError {
if apr.Contains(cli.StatFlag) || apr.Contains(cli.SummaryFlag) {
if apr.Contains(cli.SchemaFlag) || apr.Contains(cli.DataFlag) {
return errhand.BuildDError("invalid Arguments: --stat and --summary cannot be combined with --schema or --data").Build()
}
}
if apr.Contains(cli.NameOnlyFlag) {
if apr.Contains(cli.SchemaFlag) || apr.Contains(cli.DataFlag) || apr.Contains(cli.StatFlag) || apr.Contains(cli.SummaryFlag) {
return errhand.BuildDError("invalid Arguments: --name-only cannot be combined with --schema, --data, --stat, or --summary").Build()
}
}
f, _ := apr.GetValue(FormatFlag)
switch strings.ToLower(f) {
case "tabular", "sql", "json", "":
default:
return errhand.BuildDError("invalid output format: %s", f).Build()
}
filterValue, hasFilter := apr.GetValue(cli.FilterParam)
if hasFilter {
filter := newDiffTypeFilter(filterValue)
if !filter.isValid() {
return errhand.BuildDError("invalid filter: %s. Valid values are: %s, %s, %s, %s (or %s)",
filterValue, diff.DiffTypeAdded, diff.DiffTypeModified, diff.DiffTypeRenamed, diff.DiffTypeDropped, "removed").Build()
}
}
return nil
}
func parseDiffDisplaySettings(apr *argparser.ArgParseResults) *diffDisplaySettings {
displaySettings := &diffDisplaySettings{}
displaySettings.diffParts = SchemaAndDataDiff
if apr.Contains(cli.DataFlag) && !apr.Contains(cli.SchemaFlag) {
displaySettings.diffParts = DataOnlyDiff
} else if apr.Contains(cli.SchemaFlag) && !apr.Contains(cli.DataFlag) {
displaySettings.diffParts = SchemaOnlyDiff
} else if apr.Contains(cli.StatFlag) {
displaySettings.diffParts = Stat
} else if apr.Contains(cli.SummaryFlag) {
displaySettings.diffParts = Summary
} else if apr.Contains(cli.NameOnlyFlag) {
displaySettings.diffParts = NameOnlyDiff
}
displaySettings.skinny = apr.Contains(cli.SkinnyFlag)
if cols, ok := apr.GetValueList(cli.IncludeCols); ok {
displaySettings.includeCols = cols
}
f := apr.GetValueOrDefault(FormatFlag, "tabular")
switch strings.ToLower(f) {
case "tabular":
displaySettings.diffOutput = TabularDiffOutput
switch strings.ToLower(apr.GetValueOrDefault(cli.DiffMode, "context")) {
case "row":
displaySettings.diffMode = diff.ModeRow
case "line":
displaySettings.diffMode = diff.ModeLine
case "in-place":
displaySettings.diffMode = diff.ModeInPlace
case "context":
displaySettings.diffMode = diff.ModeContext
}
case "sql":
displaySettings.diffOutput = SQLDiffOutput
case "json":
displaySettings.diffOutput = JsonDiffOutput
}
displaySettings.limit, _ = apr.GetInt(cli.LimitParam)
displaySettings.where = apr.GetValueOrDefault(cli.WhereParam, "")
filterValue := apr.GetValueOrDefault(cli.FilterParam, diff.DiffTypeAll)
displaySettings.filter = newDiffTypeFilter(filterValue)
return displaySettings
}
func parseDiffArgs(queryist cli.Queryist, sqlCtx *sql.Context, apr *argparser.ArgParseResults) (*diffArgs, error) {
dArgs := &diffArgs{
diffDisplaySettings: parseDiffDisplaySettings(apr),
}
staged := apr.Contains(cli.StagedFlag) || apr.Contains(cli.CachedFlag)
tableNames, err := dArgs.applyDiffRoots(queryist, sqlCtx, apr.Args, staged, apr.Contains(cli.MergeBase))
if err != nil {
return nil, err
}
if apr.Contains(cli.ReverseFlag) {
dArgs.diffDatasets = &diffDatasets{
fromRef: dArgs.toRef,
toRef: dArgs.fromRef,
}
}
tableSet, err := parseDiffTableSetSql(queryist, sqlCtx, dArgs.diffDatasets, tableNames)
if err != nil {
return nil, err
}
dArgs.tableSet = tableSet
return dArgs, nil
}
func parseDiffTableSetSql(queryist cli.Queryist, sqlCtx *sql.Context, datasets *diffDatasets, tableNames []string) (*set.StrSet, error) {
tablesAtFromRef, err := getTableNamesAtRef(queryist, sqlCtx, datasets.fromRef)
if err != nil {
return nil, err
}
tablesAtToRef, err := getTableNamesAtRef(queryist, sqlCtx, datasets.toRef)
if err != nil {
return nil, err
}
tableSet := set.NewStrSet(nil)
for _, tableName := range tableNames {
// verify table args exist in at least one root
_, ok := tablesAtFromRef[tableName]
if ok {
tableSet.Add(tableName)
continue
}
_, ok = tablesAtToRef[tableName]
if ok {
tableSet.Add(tableName)
continue
}
return nil, fmt.Errorf("table %s does not exist in either revision", tableName)
}
// if no tables or docs were specified as args, diff all tables and docs
if len(tableNames) == 0 {
seenTableNames := make(map[string]bool)
for _, tables := range []map[string]bool{tablesAtFromRef, tablesAtToRef} {
for tableName := range tables {
if _, ok := seenTableNames[tableName]; !ok {
seenTableNames[tableName] = true
tableSet.Add(tableName)
}
}
}
}
return tableSet, nil
}
var doltSystemTables = []string{
"dolt_procedures",
"dolt_schemas",
"dolt_tests",
}
func getTableNamesAtRef(queryist cli.Queryist, sqlCtx *sql.Context, ref string) (map[string]bool, error) {
// query for user-created tables
q, err := dbr.InterpolateForDialect("SHOW FULL TABLES AS OF ?", []interface{}{ref}, dialect.MySQL)
if err != nil {
return nil, fmt.Errorf("error interpolating query: %w", err)
}
rows, err := cli.GetRowsForSql(queryist, sqlCtx, q)
if err != nil {
return nil, err
}
tableNames := make(map[string]bool)
for _, row := range rows {
tableName, ok, err := sql.Unwrap[string](sqlCtx, row[0])
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("unexpected type for table name, expected string, found %T", row[0])
}
tableType, ok, err := sql.Unwrap[string](sqlCtx, row[1])
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("unexpected type for table type, expected string, found %T", row[1])
}
isTable := tableType == "BASE TABLE"
if isTable {
tableNames[tableName] = true
}
}
// add system tables, if they exist at this ref
for _, sysTable := range doltSystemTables {
interpolatedQuery, err := dbr.InterpolateForDialect("SHOW CREATE TABLE ? AS OF ?", []interface{}{dbr.I(sysTable), ref}, dialect.MySQL)
if err != nil {
return nil, fmt.Errorf("error interpolating query: %w", err)
}
result, err := cli.GetRowsForSql(queryist, sqlCtx, interpolatedQuery)
if err != nil {
return nil, fmt.Errorf("error getting system table %s: %w", sysTable, err)
}
if len(result) > 0 {
tableNames[sysTable] = true
}
}
return tableNames, nil
}
// applyDiffRoots applies the appropriate |from| and |to| root values to the receiver and returns the table names
// (if any) given to the command.
func (dArgs *diffArgs) applyDiffRoots(queryist cli.Queryist, sqlCtx *sql.Context, args []string, isCached, useMergeBase bool) ([]string, error) {
dArgs.diffDatasets = &diffDatasets{
fromRef: doltdb.Staged,
toRef: doltdb.Working,
}
if isCached {
dArgs.fromRef = "HEAD"
dArgs.toRef = doltdb.Staged
}
if len(args) == 0 {
if useMergeBase {
return nil, errors.New("Must supply at least one revision when using --merge-base flag")
}
// `dolt diff`
return nil, nil
}
if strings.Contains(args[0], "..") {
if useMergeBase {
return nil, errors.New("Cannot use `..` or `...` with --merge-base flag")
}
err := dArgs.applyDotRevisions(queryist, sqlCtx, args)
if err != nil {
return nil, err
}
return args[1:], err
}
fromRef := args[0]
// treat the first arg as a ref spec
_, err := getTableNamesAtRef(queryist, sqlCtx, fromRef)
if errors.Is(err, doltdb.ErrGhostCommitEncountered) {
return nil, err
}
// if it doesn't resolve, treat it as a table name
if err != nil {
// `dolt diff table`
if useMergeBase {
return nil, errors.New("Must supply at least one revision when using --merge-base flag")
}
return args, nil
}
dArgs.fromRef = fromRef
if len(args) == 1 {
// `dolt diff from_commit`
if useMergeBase {
err := dArgs.applyMergeBase(queryist, sqlCtx, args[0], "HEAD")
if err != nil {
return nil, err
}
}
return nil, nil
}
toRef := args[1]
// treat the first arg as a ref spec
_, err = getTableNamesAtRef(queryist, sqlCtx, toRef)
// if it doesn't resolve, treat it as a table name
if err != nil {
// `dolt diff from_commit [...tables]`
if useMergeBase {
err := dArgs.applyMergeBase(queryist, sqlCtx, args[0], "HEAD")
if err != nil {
return nil, err
}
}
return args[1:], nil
}
dArgs.toRef = toRef
if useMergeBase {
err := dArgs.applyMergeBase(queryist, sqlCtx, args[0], args[1])
if err != nil {
return nil, err
}
}
// `dolt diff from_commit to_commit [...tables]`
return args[2:], nil
}
// applyMergeBase applies the merge base of two revisions to the |from| root
// values.
func (dArgs *diffArgs) applyMergeBase(queryist cli.Queryist, sqlCtx *sql.Context, leftStr, rightStr string) error {
mergeBaseStr, err := getCommonAncestor(queryist, sqlCtx, leftStr, rightStr)
if err != nil {
return err
}
dArgs.fromRef = mergeBaseStr
return nil
}
func getCommonAncestor(queryist cli.Queryist, sqlCtx *sql.Context, c1, c2 string) (string, error) {
q, err := dbr.InterpolateForDialect("select dolt_merge_base(?, ?)", []interface{}{c1, c2}, dialect.MySQL)
if err != nil {
return "", fmt.Errorf("error interpolating query: %w", err)
}
rows, err := cli.GetRowsForSql(queryist, sqlCtx, q)
if err != nil {
return "", err
}
if len(rows) != 1 {
return "", errors.New("unexpected number of rows returned from dolt_merge_base")
}
ancestor := rows[0][0].(string)
return ancestor, nil
}
// applyDotRevisions applies the appropriate |from| and |to| root values to the
// receiver for arguments containing `..` or `...`
func (dArgs *diffArgs) applyDotRevisions(queryist cli.Queryist, sqlCtx *sql.Context, args []string) error {
// `dolt diff from_commit...to_commit [...tables]`
if strings.Contains(args[0], "...") {
refs := strings.Split(args[0], "...")
if len(refs[0]) > 0 {
right := refs[1]
// Use current HEAD if right side of `...` does not exist
if len(refs[1]) == 0 {
right = "HEAD"
}
err := dArgs.applyMergeBase(queryist, sqlCtx, refs[0], right)
if err != nil {
return err
}
}
if len(refs[1]) > 0 {
dArgs.toRef = refs[1]
}
return nil
}
// `dolt diff from_commit..to_commit [...tables]`
if strings.Contains(args[0], "..") {
refs := strings.Split(args[0], "..")
if len(refs[0]) > 0 {
dArgs.fromRef = refs[0]
}
if len(refs[1]) > 0 {
dArgs.toRef = refs[1]
}
return nil
}
return nil
}
var diffSummarySchema = sql.Schema{
&sql.Column{Name: "Table name", Type: types.Text, Nullable: false},
&sql.Column{Name: "Diff type", Type: types.Text, Nullable: false},
&sql.Column{Name: "Data change", Type: types.Boolean, Nullable: false},
&sql.Column{Name: "Schema change", Type: types.Boolean, Nullable: false},
}
func printDiffSummary(ctx *sql.Context, diffSummaries []diff.TableDeltaSummary, dArgs *diffArgs) errhand.VerboseError {
cliWR := iohelp.NopWrCloser(cli.OutStream)
wr := tabular.NewFixedWidthTableWriter(diffSummarySchema, cliWR, 100)
defer wr.Close(ctx)
for _, diffSummary := range diffSummaries {
// TODO: schema name
shouldPrintTables := dArgs.tableSet.Contains(diffSummary.FromTableName.Name) || dArgs.tableSet.Contains(diffSummary.ToTableName.Name)
if !shouldPrintTables {
return nil
}
tableName := diffSummary.TableName.Name
if diffSummary.DiffType == "renamed" {
tableName = fmt.Sprintf("%s -> %s", diffSummary.FromTableName.Name, diffSummary.ToTableName.Name)
}
err := wr.WriteSqlRow(ctx, sql.Row{tableName, diffSummary.DiffType, diffSummary.DataChange, diffSummary.SchemaChange})
if err != nil {
return errhand.BuildDError("could not write table delta summary").AddCause(err).Build()
}
}
return nil
}
func getDeltasBetweenRefs(queryist cli.Queryist, sqlCtx *sql.Context, fromRef, toRef string) ([]diff.TableDeltaSummary, error) {
diffSummaries, err := getDiffSummariesBetweenRefs(queryist, sqlCtx, fromRef, toRef)
if err != nil {
return nil, err
}
schemaSummaries, err := getSchemaDiffSummariesBetweenRefs(queryist, sqlCtx, fromRef, toRef)
if err != nil {
return nil, err
}
allSummaries := []diff.TableDeltaSummary{}
allSummaries = append(allSummaries, diffSummaries...)
for _, schemaSummary := range schemaSummaries {
deltaExists := false
for i, summary := range allSummaries {
deltaExists = summary.FromTableName == schemaSummary.FromTableName && summary.ToTableName == schemaSummary.ToTableName
if deltaExists {
existingSummary := allSummaries[i]
existingSummary.SchemaChange = true
existingSummary.AlterStmts = append(existingSummary.AlterStmts, schemaSummary.AlterStmts...)
allSummaries[i] = existingSummary
break
}
}
if !deltaExists {
allSummaries = append(allSummaries, schemaSummary)
}
}
return allSummaries, nil
}
func getSchemaDiffSummariesBetweenRefs(queryist cli.Queryist, sqlCtx *sql.Context, fromRef, toRef string) ([]diff.TableDeltaSummary, error) {
q, err := dbr.InterpolateForDialect("select * from dolt_schema_diff(?, ?)", []interface{}{fromRef, toRef}, dialect.MySQL)
if err != nil {
return nil, fmt.Errorf("error: unable to interpolate query: %w", err)
}
schemaDiffRows, err := cli.GetRowsForSql(queryist, sqlCtx, q)
if err != nil {
return nil, fmt.Errorf("error: unable to get schema diff from %s to %s: %w", fromRef, toRef, err)
}
var summaries []diff.TableDeltaSummary
for _, row := range schemaDiffRows {
fromTable := row[0].(string)
toTable := row[1].(string)
fromCreateStmt := row[2].(string)
toCreateStmt := row[3].(string)
var diffType = ""
var tableName = ""
switch {
case fromTable == toTable:
if fromCreateStmt != toCreateStmt {
diffType = "modified"
tableName = fromTable
}
case fromTable == "":
diffType = diff.DiffTypeAdded
tableName = toTable
case toTable == "":
diffType = diff.DiffTypeDropped
tableName = fromTable
case fromTable != "" && toTable != "" && fromTable != toTable:
diffType = diff.DiffTypeRenamed
tableName = toTable
default:
return nil, fmt.Errorf("error: unexpected schema diff case: fromTable='%s', toTable='%s'", fromTable, toTable)
}
q, err := dbr.InterpolateForDialect(
"select statement_order, statement from dolt_patch(?, ?) where diff_type='schema' and (table_name=? or table_name=?) order by statement_order asc",
[]interface{}{fromRef, toRef, fromTable, toTable},
dialect.MySQL)
if err != nil {
return nil, fmt.Errorf("error: unable to interpolate dolt_patch query: %w", err)
}
patchRows, err := cli.GetRowsForSql(queryist, sqlCtx, q)
if err != nil {
return nil, fmt.Errorf("error: unable to get dolt_patch rows from %s to %s: %w", fromRef, toRef, err)
}
alterStmts := []string{}
for _, row := range patchRows {
alterStmts = append(alterStmts, row[1].(string))
}
summary := diff.TableDeltaSummary{
TableName: doltdb.TableName{Name: tableName},
FromTableName: doltdb.TableName{Name: fromTable},
ToTableName: doltdb.TableName{Name: toTable},
DiffType: diffType,
DataChange: false,
SchemaChange: true,
AlterStmts: alterStmts,
}
summaries = append(summaries, summary)
}
return summaries, nil
}
func getDiffSummariesBetweenRefs(queryist cli.Queryist, sqlCtx *sql.Context, fromRef, toRef string) ([]diff.TableDeltaSummary, error) {
q, err := dbr.InterpolateForDialect("select * from dolt_diff_summary(?, ?)", []interface{}{fromRef, toRef}, dialect.MySQL)
dataDiffRows, err := cli.GetRowsForSql(queryist, sqlCtx, q)
if err != nil {
return nil, fmt.Errorf("error: unable to get diff summary from %s to %s: %w", fromRef, toRef, err)
}
summaries := []diff.TableDeltaSummary{}
for _, row := range dataDiffRows {
summary := diff.TableDeltaSummary{}
summary.FromTableName.Name = row[0].(string)
summary.ToTableName.Name = row[1].(string)
summary.DiffType = row[2].(string)
summary.DataChange, err = cli.QueryValueAsBool(row[3])
if err != nil {
return nil, fmt.Errorf("error: unable to parse data change value '%s': %w", row[3], err)
}
summary.SchemaChange, err = cli.QueryValueAsBool(row[4])
if err != nil {
return nil, fmt.Errorf("error: unable to parse schema change value '%s': %w", row[4], err)
}
switch summary.DiffType {
case diff.DiffTypeDropped:
summary.TableName = summary.FromTableName
case diff.DiffTypeAdded:
summary.TableName = summary.ToTableName
case diff.DiffTypeRenamed:
summary.TableName = summary.ToTableName
case diff.DiffTypeModified:
summary.TableName = summary.FromTableName
default:
return nil, fmt.Errorf("error: unexpected diff type '%s'", summary.DiffType)
}
summaries = append(summaries, summary)
}
return summaries, nil
}
func diffUserTables(queryist cli.Queryist, sqlCtx *sql.Context, dArgs *diffArgs) errhand.VerboseError {
var err error
deltas, err := getDeltasBetweenRefs(queryist, sqlCtx, dArgs.fromRef, dArgs.toRef)
if err != nil {
return errhand.BuildDError("error: unable to get diff summary").AddCause(err).Build()
}
// Validate where clause even if there are no deltas
if len(dArgs.where) > 0 && len(deltas) == 0 {
if verr := validateWhereClause(queryist, sqlCtx, dArgs); verr != nil {
return verr
}
}
if dArgs.diffParts&Summary != 0 {
return printDiffSummary(sqlCtx, deltas, dArgs)
}
// Ignore patterns govern the staging of untracked tables and do not apply to committed history,
// so they are only loaded when the diff references the working set or staging area.
var ignoredTablePatterns doltdb.IgnorePatterns
if dArgs.hasWorkingSet() {
ignoredTablePatterns, err = getIgnoredTablePatternsFromSql(queryist, sqlCtx)
if err != nil {
return errhand.VerboseErrorFromError(fmt.Errorf("couldn't get ignored table patterns, cause: %w", err))
}
}
dw, err := newDiffWriter(dArgs.diffOutput)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
doltSchemasChanged := false
var hasdiffErrors bool
for _, delta := range deltas {
if doltdb.IsFullTextTable(delta.TableName.Name) {
continue
}
ignore, err := ignoredTablePatterns.ShouldIgnoreDelta(delta.IsAdd(), delta.IsDrop(), delta.ToTableName, delta.FromTableName)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
if ignore {
continue
}
if !shouldPrintTableDelta(dArgs.tableSet, delta.ToTableName.Name, delta.FromTableName.Name) {
continue
}
// Apply table-level filtering based on diff type
if dArgs.filter != nil && dArgs.filter.filters != nil {
// For data-only changes (no schema/rename), always let them through for row-level filtering
isDataOnlyChange := !delta.SchemaChange && !delta.IsRename() && delta.DataChange
if !isDataOnlyChange && !dArgs.filter.shouldInclude(delta.DiffType) {
continue // Skip this table
}
}
if strings.HasPrefix(delta.ToTableName.Name, diff.DBPrefix) {
verr := diffDatabase(queryist, sqlCtx, delta, dArgs, dw)
if verr != nil {
return verr
}
continue
}
if isDoltSchemasTable(delta.ToTableName.Name, delta.FromTableName.Name) {
// save dolt_schemas table diff for last in diff output
doltSchemasChanged = true
} else {
verr := diffUserTable(queryist, sqlCtx, delta, dArgs, dw)
if verr != nil {
cli.PrintErrln(verr.Verbose())
hasdiffErrors = true
}
}
}
if doltSchemasChanged {
verr := diffDoltSchemasTable(queryist, sqlCtx, dArgs, dw)
if verr != nil {
return verr
}
}
err = dw.Close(sqlCtx)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
if hasdiffErrors {
errorBuilder := errhand.BuildDError("error: encountered errors during diff")
return errorBuilder.Build()
}
return nil
}
func shouldPrintTableDelta(tablesToPrint *set.StrSet, toTableName, fromTableName string) bool {
// TODO: this should be case insensitive
return tablesToPrint.Contains(fromTableName) ||
tablesToPrint.Contains(toTableName) ||
strings.HasPrefix(fromTableName, diff.DBPrefix) ||
strings.HasPrefix(toTableName, diff.DBPrefix)
}
func isDoltSchemasTable(toTableName, fromTableName string) bool {
return fromTableName == doltdb.SchemasTableName || toTableName == doltdb.SchemasTableName
}
func getTableInfoAtRef(queryist cli.Queryist, sqlCtx *sql.Context, tableName string, ref string) (diff.TableInfo, error) {
sch, createStmt, err := getTableSchemaAtRef(queryist, sqlCtx, tableName, ref)
if err != nil {
return diff.TableInfo{}, fmt.Errorf("error: unable to get schema for table '%s': %w", tableName, err)
}
tableInfo := diff.TableInfo{
Name: tableName,
Sch: sch,
CreateStmt: createStmt,
}
return tableInfo, nil
}
func getTableSchemaAtRef(queryist cli.Queryist, sqlCtx *sql.Context, tableName string, ref string) (sch schema.Schema, createStmt string, err error) {
var rows []sql.Row
interpolatedQuery, err := dbr.InterpolateForDialect("SHOW CREATE TABLE ? AS OF ?", []interface{}{dbr.I(tableName), ref}, dialect.MySQL)
if err != nil {
return sch, createStmt, fmt.Errorf("error interpolating query: %w", err)
}
rows, err = cli.GetRowsForSql(queryist, sqlCtx, interpolatedQuery)
if err != nil {
return sch, createStmt, fmt.Errorf("error: unable to get create table statement for table '%s': %w", tableName, err)
}
if len(rows) != 1 {
return sch, createStmt, fmt.Errorf("creating schema, expected 1 row, got %d", len(rows))
}
createStmt = rows[0][1].(string)
// append ; at the end, if one isn't there yet
if createStmt[len(createStmt)-1] != ';' {
createStmt += ";"
}
sch, err = schemaFromCreateTableStmt(createStmt)
if err != nil {
return sch, createStmt, err
}
return sch, createStmt, nil
}
func getDatabaseInfoAtRef(queryist cli.Queryist, sqlCtx *sql.Context, tableName string, ref string) (diff.TableInfo, error) {
createStmt, err := getDatabaseSchemaAtRef(queryist, sqlCtx, tableName, ref)
if err != nil {
return diff.TableInfo{}, fmt.Errorf("error: unable to get schema for table '%s': %w", tableName, err)
}
tableInfo := diff.TableInfo{
Name: tableName,
Sch: schema.EmptySchema,
CreateStmt: createStmt,
}
return tableInfo, nil
}
func getDatabaseSchemaAtRef(queryist cli.Queryist, sqlCtx *sql.Context, tableName string, ref string) (string, error) {
var rows []sql.Row
// TODO: implement `show create database as of ...`
tableName = strings.TrimPrefix(tableName, diff.DBPrefix)
interpolatedQuery, err := dbr.InterpolateForDialect("SHOW CREATE DATABASE ?", []interface{}{dbr.I(tableName)}, dialect.MySQL)
if err != nil {
return "", fmt.Errorf("error interpolating query: %w", err)
}
rows, err = cli.GetRowsForSql(queryist, sqlCtx, interpolatedQuery)
if err != nil {
return "", fmt.Errorf("error: unable to get create database statement for database '%s': %w", tableName, err)
}
if len(rows) != 1 {
return "", fmt.Errorf("creating schema, expected 1 row, got %d", len(rows))
}
createStmt := rows[0][1].(string)
// append ; at the end, if one isn't there yet
if createStmt[len(createStmt)-1] != ';' {
createStmt += ";"
}
return createStmt, nil
}
// schemaFromCreateTableStmt returns a schema for the CREATE TABLE statement given
// TODO: this is substantially incorrect, doesn't handle primary key ordering, probably other things too
func schemaFromCreateTableStmt(createTableStmt string) (schema.Schema, error) {
parsed, err := ast.Parse(createTableStmt)
if err != nil {
return nil, err
}
create, ok := parsed.(*ast.DDL)
if !ok {
return nil, fmt.Errorf("expected create table, found %T", parsed)
}
primaryCols := make(map[string]bool)
for _, index := range create.TableSpec.Indexes {
if index.Info.Primary {
for _, indexCol := range index.Fields {
primaryCols[indexCol.Column.Lowered()] = true
}
break
}
}
var cols []schema.Column
for _, col := range create.TableSpec.Columns {
internalTyp, err := types.ColumnTypeToType(&col.Type)
typeInfo, err := typeinfo.FromSqlType(internalTyp)
if err != nil {
return nil, err
}
defBuf := ast.NewTrackedBuffer(nil)
if col.Type.Default != nil {
col.Type.Default.Format(defBuf)
}
genBuf := ast.NewTrackedBuffer(nil)
if col.Type.GeneratedExpr != nil {
col.Type.GeneratedExpr.Format(genBuf)
}
onUpBuf := ast.NewTrackedBuffer(nil)
if col.Type.OnUpdate != nil {
col.Type.OnUpdate.Format(onUpBuf)
}
var comment string
if col.Type.Comment != nil {
comment = col.Type.Comment.String()
}
sCol := schema.Column{
Name: col.Name.String(),
Kind: typeInfo.NomsKind(),
IsPartOfPK: primaryCols[col.Name.Lowered()],
TypeInfo: typeInfo,
Default: defBuf.String(),
Generated: "", // TODO
OnUpdate: "", // TODO
Virtual: false, // TODO
AutoIncrement: col.Type.Autoincrement == true,
Comment: comment,
}
cols = append(cols, sCol)
}
sch, err := schema.NewSchema(schema.NewColCollection(cols...), nil, schema.Collation_Default, nil, nil)
if err != nil {
return nil, err
}
return sch, err
}
func getTableDiffStats(queryist cli.Queryist, sqlCtx *sql.Context, tableName, fromRef, toRef string) ([]diffStatistics, error) {
q, err := dbr.InterpolateForDialect("select * from dolt_diff_stat(?, ?, ?)", []interface{}{fromRef, toRef, tableName}, dialect.MySQL)
if err != nil {
return nil, fmt.Errorf("error interpolating query: %w", err)
}
rows, err := cli.GetRowsForSql(queryist, sqlCtx, q)
if err != nil {
return nil, fmt.Errorf("error running diff stats query: %w", err)
}
allStats := []diffStatistics{}
for _, row := range rows {
rowsUnmodified, err := coallesceNilToUint64(row[1])
if err != nil {
return nil, err
}
rowsAdded, err := coallesceNilToUint64(row[2])
if err != nil {
return nil, err
}
rowsDeleted, err := coallesceNilToUint64(row[3])
if err != nil {
return nil, err
}
rowsModified, err := coallesceNilToUint64(row[4])
if err != nil {
return nil, err
}
cellsAdded, err := coallesceNilToUint64(row[5])
if err != nil {
return nil, err
}
cellsDeleted, err := coallesceNilToUint64(row[6])
if err != nil {
return nil, err
}
cellsModified, err := coallesceNilToUint64(row[7])
if err != nil {
return nil, err
}
oldRowCount, err := coallesceNilToUint64(row[8])
if err != nil {
return nil, err
}
newRowCount, err := coallesceNilToUint64(row[9])
if err != nil {
return nil, err
}
oldCellCount, err := coallesceNilToUint64(row[10])
if err != nil {
return nil, err
}
newCellCount, err := coallesceNilToUint64(row[11])
if err != nil {
return nil, err
}
stats := diffStatistics{
TableName: row[0].(string),
RowsUnmodified: rowsUnmodified,
RowsAdded: rowsAdded,
RowsDeleted: rowsDeleted,
RowsModified: rowsModified,
CellsAdded: cellsAdded,
CellsDeleted: cellsDeleted,
CellsModified: cellsModified,
OldRowCount: oldRowCount,
NewRowCount: newRowCount,
OldCellCount: oldCellCount,
NewCellCount: newCellCount,
}
allStats = append(allStats, stats)
}
return allStats, nil
}
func coallesceNilToUint64(val interface{}) (uint64, error) {
if val == nil {
return 0, nil
}
return getUint64ColAsUint64(val)
}
func diffUserTable(
queryist cli.Queryist,
sqlCtx *sql.Context,
tableSummary diff.TableDeltaSummary,
dArgs *diffArgs,
dw diffWriter,
) errhand.VerboseError {
fromTable := tableSummary.FromTableName
toTable := tableSummary.ToTableName
if dArgs.diffParts&NameOnlyDiff == 0 && !shouldUseLazyHeader(dArgs, tableSummary) {
// TODO: schema names
err := dw.BeginTable(sqlCtx, tableSummary.FromTableName.Name, tableSummary.ToTableName.Name, tableSummary.IsAdd(), tableSummary.IsDrop())
if err != nil {
return errhand.VerboseErrorFromError(err)
}
}
var fromTableInfo, toTableInfo *diff.TableInfo
from, err := getTableInfoAtRef(queryist, sqlCtx, fromTable.Name, dArgs.fromRef)
if err == nil {
fromTableInfo = &from
}
to, err := getTableInfoAtRef(queryist, sqlCtx, toTable.Name, dArgs.toRef)
if err == nil {
toTableInfo = &to
}
tableName := fromTable
if tableName.Name == "" {
tableName = toTable
}
if dArgs.diffParts&NameOnlyDiff != 0 {
cli.Println(tableName)
return errhand.VerboseErrorFromError(nil)
}
if dArgs.diffParts&Stat != 0 {
var areTablesKeyless = false
var fromColLen = 0
var fromKeyless = false
if fromTableInfo != nil {
fromKeyless = schema.IsKeyless(fromTableInfo.Sch)
fromColLen = fromTableInfo.Sch.GetAllCols().Size()
}
var toColLen = 0
var toKeyless = false
if toTableInfo != nil {
toKeyless = schema.IsKeyless(toTableInfo.Sch)
toColLen = toTableInfo.Sch.GetAllCols().Size()
}
// nil table is neither keyless nor keyed
if fromTableInfo == nil {
areTablesKeyless = toKeyless
} else if toTableInfo == nil {
areTablesKeyless = fromKeyless
} else {
if fromKeyless && toKeyless {
areTablesKeyless = true
} else if !fromKeyless && !toKeyless {
areTablesKeyless = false
} else {
return errhand.BuildDError("mismatched keyless and keyed schemas for table %s", tableName).Build()
}
}
var diffStats []diffStatistics
diffStats, err = getTableDiffStats(queryist, sqlCtx, tableName.Name, dArgs.fromRef, dArgs.toRef)
if err != nil {
return errhand.BuildDError("cannot retrieve diff stats between '%s' and '%s'", dArgs.fromRef, dArgs.toRef).AddCause(err).Build()
}
err = dw.WriteTableDiffStats(sqlCtx, diffStats, fromColLen, toColLen, areTablesKeyless)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
return nil
}
if dArgs.diffParts&SchemaOnlyDiff != 0 {
err = dw.WriteTableSchemaDiff(sqlCtx, fromTableInfo, toTableInfo, tableSummary)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
}
if tableSummary.IsDrop() && dArgs.diffOutput == SQLDiffOutput {
return nil // don't output DELETE FROM statements after DROP TABLE
}
verr := diffRows(queryist, sqlCtx, tableSummary, fromTableInfo, toTableInfo, dArgs, dw)
if verr != nil {
return verr
}
return nil
}
func diffDoltSchemasTable(
queryist cli.Queryist,
sqlCtx *sql.Context,
dArgs *diffArgs,
dw diffWriter,
) errhand.VerboseError {
query, err := dbr.InterpolateForDialect("select from_name,to_name,from_type,to_type,from_fragment,to_fragment "+
"from dolt_diff(?, ?, ?) "+
"order by coalesce(from_type, to_type), coalesce(from_name, to_name)",
[]interface{}{dArgs.fromRef, dArgs.toRef, doltdb.SchemasTableName}, dialect.MySQL)
if err != nil {
return errhand.BuildDError("Error building diff query").AddCause(err).Build()
}
_, rowIter, _, err := queryist.Query(sqlCtx, query)
if err != nil {
return errhand.BuildDError("Error running diff query:\n%s", query).AddCause(err).Build()
}
defer rowIter.Close(sqlCtx)
for {
row, err := rowIter.Next(sqlCtx)
if err == io.EOF {
break
} else if err != nil {
return errhand.VerboseErrorFromError(err)
}
var vErr errhand.VerboseError
unwrapString := func(val interface{}) (string, errhand.VerboseError) {
s, ok, err := sql.Unwrap[string](sqlCtx, val)
if err != nil {
return "", errhand.VerboseErrorFromError(err)
}
if !ok {
return "", errhand.BuildDError("expected string, got %T", val).Build()
}
return s, nil
}
var fragmentName string
if row[0] != nil {
fragmentName, vErr = unwrapString(row[0])
if vErr != nil {
return vErr
}
} else {
fragmentName, vErr = unwrapString(row[1])
if vErr != nil {
return vErr
}
}
var fragmentType string
if row[2] != nil {
fragmentType, vErr = unwrapString(row[2])
if vErr != nil {
return vErr
}
} else {
fragmentType, vErr = unwrapString(row[3])
if vErr != nil {
return vErr
}
}
var oldFragment string
var newFragment string
if row[4] != nil {
oldFragment, vErr = unwrapString(row[4])
if vErr != nil {
return vErr
}
// Typically schema fragments have the semicolons stripped, so put it back on
if len(oldFragment) > 0 && oldFragment[len(oldFragment)-1] != ';' {
oldFragment += ";"
}
}
if row[5] != nil {
newFragment, vErr = unwrapString(row[5])
if vErr != nil {
return vErr
}
// Typically schema fragments have the semicolons stripped, so put it back on
if len(newFragment) > 0 && newFragment[len(newFragment)-1] != ';' {
newFragment += ";"
}
}
switch fragmentType {
case "event":
err := dw.WriteEventDiff(sqlCtx, fragmentName, oldFragment, newFragment)
if err != nil {
return nil
}
case "trigger":
err := dw.WriteTriggerDiff(sqlCtx, fragmentName, oldFragment, newFragment)
if err != nil {
return nil
}
case "view":
err := dw.WriteViewDiff(sqlCtx, fragmentName, oldFragment, newFragment)
if err != nil {
return nil
}
default:
cli.PrintErrf("Unrecognized schema element type: %s", fragmentType)
continue
}
}
return nil
}
func diffDatabase(
queryist cli.Queryist,
sqlCtx *sql.Context,
tableSummary diff.TableDeltaSummary,
dArgs *diffArgs,
dw diffWriter,
) errhand.VerboseError {
if dArgs.diffParts&NameOnlyDiff != 0 {
cli.Println(tableSummary.FromTableName)
return nil
}
err := dw.BeginTable(sqlCtx, tableSummary.FromTableName.Name, tableSummary.ToTableName.Name, tableSummary.IsAdd(), tableSummary.IsDrop())
if err != nil {
return errhand.VerboseErrorFromError(err)
}
if dArgs.diffParts&SchemaOnlyDiff == 0 {
return nil
}
fromTable := tableSummary.FromTableName
var fromTableInfo *diff.TableInfo
from, err := getDatabaseInfoAtRef(queryist, sqlCtx, fromTable.Name, dArgs.fromRef)
if err == nil {
// TODO: implement show create database as of ...
fromTableInfo = &from
fromTableInfo.CreateStmt = ""
}
toTable := tableSummary.ToTableName
var toTableInfo *diff.TableInfo
to, err := getDatabaseInfoAtRef(queryist, sqlCtx, toTable.Name, dArgs.toRef)
if err == nil {
toTableInfo = &to
}
err = dw.WriteTableSchemaDiff(sqlCtx, fromTableInfo, toTableInfo, tableSummary)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
verr := diffRows(queryist, sqlCtx, tableSummary, fromTableInfo, toTableInfo, dArgs, dw)
if verr != nil {
return verr
}
return nil
}
// areValueSetsDiffable checks if it's possible to meaningfully diff the tuples stored in a table
// between two commits. This is possible for some schema changes but not others.
func areValueSetsDiffable(fromSch, toSch schema.Schema) bool {
if fromSch == nil || fromSch.GetAllCols().Size() == 0 ||
toSch == nil || toSch.GetAllCols().Size() == 0 {
// If one side doesn't have a schema, the diff is trivial.
return true
}
fromCols := fromSch.GetNonPKCols()
toCols := toSch.GetNonPKCols()
var sharedCols int
if fromCols.Size() < toCols.Size() {
sharedCols = fromCols.Size()
for i := fromCols.Size(); i < toCols.Size(); i++ {
if toCols.GetColumns()[i].Default != "" {
// If a column with a default value was added, it is not possible to generate a data diff
// without running the schema change in a SQL engine.
return false
}
}
} else {
sharedCols = toCols.Size()
}
compatChecker := typecompatibility.NewTypeCompatabilityChecker()
for i := 0; i < sharedCols; i++ {
fromCol := fromCols.GetByIndex(i)
toCol := toCols.GetByIndex(i)
// TODO: We might get more accurate results from calling MapSchemaBasedOnTagAndName
if fromCol.Name != toCol.Name && fromCol.Tag != toCol.Tag {
// If fromCol.Tag == toCol.Tag, this will display a rename.
// We ought to be able to generate a data diff here, but we currently panic if a row has changed a value in this column.
// TODO: Generate SQL data diffs for renamed columns.
// If fromCol.Tag == toCol.Tag, this will display a drop and an add.
// In this case, we would need to generate an update statement for every row where this new column is not NULL.
// We cannot currently do this for rows whose bytes have not changed.
return false
}
typeChangeInfo := compatChecker.IsTypeChangeCompatible(fromCol.TypeInfo, toCol.TypeInfo)
binaryCompatible := typeChangeInfo.Compatible && !typeChangeInfo.RewriteRows
if !binaryCompatible {
// It is not possible to semantically compare the values of this column at these two commits.
// This means that it's possible for the column to have the same bytes at both commits, while not
// representing the same value. We do not have enough context to produce a correct data diff.
return false
}
}
return true
}
func diffRows(
queryist cli.Queryist,
sqlCtx *sql.Context,
tableSummary diff.TableDeltaSummary,
fromTableInfo, toTableInfo *diff.TableInfo,
dArgs *diffArgs,
dw diffWriter,
) errhand.VerboseError {
var fromSch schema.Schema = nil
var toSch schema.Schema = nil
if fromTableInfo != nil {
fromSch = fromTableInfo.Sch
}
if toTableInfo != nil {
toSch = toTableInfo.Sch
}
var toSqlSch, fromSqlSch sql.Schema
if fromTableInfo != nil {
pkSch, err := sqlutil.FromDoltSchema(sqlCtx, sqlCtx.GetCurrentDatabase(), fromTableInfo.Name, fromSch)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
fromSqlSch = pkSch.Schema
}
if toTableInfo != nil {
pkSch, err := sqlutil.FromDoltSchema(sqlCtx, sqlCtx.GetCurrentDatabase(), toTableInfo.Name, toSch)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
toSqlSch = pkSch.Schema
}
var unionSch sql.Schema
if dArgs.diffOutput == SQLDiffOutput {
// When generating SQL output, it doesn't make sense to generate a data diff for columns that no longer exist.
// Thus, we always use the schema on the target commit.
unionSch = toSqlSch
} else if fromSqlSch.Equals(toSqlSch) {
unionSch = fromSqlSch
} else {
unionSch = unionSchemas(fromSqlSch, toSqlSch)
}
// We always instantiate a RowWriter in case the diffWriter needs it to close off any work from schema output
var rowWriter diff.SqlRowDiffWriter
realWriter, err := dw.RowWriter(sqlCtx, fromTableInfo, toTableInfo, tableSummary, unionSch)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
if shouldUseLazyHeader(dArgs, tableSummary) {
// Wrap with lazy writer to delay BeginTable until first row write
onFirstWrite := func() error {
return dw.BeginTable(
sqlCtx,
tableSummary.FromTableName.Name,
tableSummary.ToTableName.Name,
tableSummary.IsAdd(),
tableSummary.IsDrop(),
)
}
rowWriter = newLazyRowWriter(realWriter, onFirstWrite)
} else {
rowWriter = realWriter
}
if !schema.ArePrimaryKeySetsDiffable(fromSch, toSch) {
err := rowWriter.Close(sqlCtx)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
err = fmt.Errorf("Primary key sets differ between revisions for table '%s', skipping data diff", tableSummary.ToTableName)
// When outputting SQL, it's an error if we can't generate a data diff. Otherwise we write to stderr and continue.
if dArgs.diffOutput == SQLDiffOutput {
return errhand.VerboseErrorFromError(err)
}
cli.PrintErrln(err)
return nil
}
if dArgs.diffOutput == SQLDiffOutput && !areValueSetsDiffable(fromSch, toSch) {
err := rowWriter.Close(sqlCtx)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
return errhand.VerboseErrorFromError(fmt.Errorf("Incompatible schema change, skipping data diff for table '%s'", tableSummary.ToTableName))
}
// no data diff requested
if dArgs.diffParts&DataOnlyDiff == 0 {
err = rowWriter.Close(sqlCtx)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
return nil
}
// do the data diff
// TODO: schema names
tableName := tableSummary.ToTableName.Name
if len(tableName) == 0 {
tableName = tableSummary.FromTableName.Name
}
if strings.HasPrefix(tableName, diff.DBPrefix) {
err = rowWriter.Close(sqlCtx)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
return nil
}
defer rowWriter.Close(sqlCtx)
columnNames, format := getColumnNames(fromTableInfo, toTableInfo)
query := fmt.Sprintf("select %s ? from dolt_diff(?, ?, ?)", format)
var params []interface{}
for _, col := range columnNames {
params = append(params, dbr.I(col))
}
params = append(params, dbr.I("diff_type"), dArgs.fromRef, dArgs.toRef, tableName)
if len(dArgs.where) > 0 {
query += " where ?"
params = append(params, dbr.Expr(dArgs.where))
}
if dArgs.limit >= 0 {
query += " limit ?"
params = append(params, dbr.Expr(strconv.Itoa(dArgs.limit)))
}
interpolatedQuery, err := dbr.InterpolateForDialect(query, params, dialect.MySQL)
if err != nil {
return errhand.BuildDError("Error building diff query:\n%s", interpolatedQuery).AddCause(err).Build()
}
sch, rowIter, _, err := queryist.Query(sqlCtx, interpolatedQuery)
if sql.ErrSyntaxError.Is(err) {
return errhand.BuildDError("Failed to parse diff query. Invalid where clause?\nDiff query: %s", interpolatedQuery).AddCause(err).Build()
} else if err != nil {
return errhand.BuildDError("Error running diff query:\n%s", interpolatedQuery).AddCause(err).Build()
}
defer rowIter.Close(sqlCtx)
var modifiedColNames map[string]bool
if dArgs.skinny {
modifiedColNames, err = getModifiedCols(sqlCtx, rowIter, unionSch, sch)
if err != nil {
return errhand.BuildDError("Error running diff query:\n%s", interpolatedQuery).AddCause(err).Build()
}
for _, col := range dArgs.includeCols {
modifiedColNames[col] = true // ensure included columns are always present
}
// instantiate a new schema that only contains the columns with changes
var filteredUnionSch sql.Schema
for _, s := range unionSch {
for colName := range modifiedColNames {
if s.Name == colName {
filteredUnionSch = append(filteredUnionSch, s)
}
}
}
// instantiate a new RowWriter with the new schema that only contains the columns with changes
rowWriter, err = dw.RowWriter(sqlCtx, fromTableInfo, toTableInfo, tableSummary, filteredUnionSch)
if err != nil {
return errhand.VerboseErrorFromError(err)
}
defer rowWriter.Close(sqlCtx)
// reset the row iterator
err = rowIter.Close(sqlCtx)
if err != nil {
return errhand.BuildDError("Error closing row iterator:\n%s", interpolatedQuery).AddCause(err).Build()
}
_, rowIter, _, err = queryist.Query(sqlCtx, interpolatedQuery)
defer rowIter.Close(sqlCtx)
if sql.ErrSyntaxError.Is(err) {
return errhand.BuildDError("Failed to parse diff query. Invalid where clause?\nDiff query: %s", interpolatedQuery).AddCause(err).Build()
} else if err != nil {
return errhand.BuildDError("Error running diff query:\n%s", interpolatedQuery).AddCause(err).Build()
}
}
err = writeDiffResults(sqlCtx, sch, unionSch, rowIter, rowWriter, modifiedColNames, dArgs)
if err != nil {
return errhand.BuildDError("Error running diff query:\n%s", interpolatedQuery).AddCause(err).Build()
}
return nil
}
func unionSchemas(s1 sql.Schema, s2 sql.Schema) sql.Schema {
var union sql.Schema
union = append(union, s1...)
for i := range s2 {
idx := union.IndexOfColName(s2[i].Name)
if idx < 0 {
union = append(union, s2[i])
} else {
// have same name. choose the one with the most 'flexible' type
orig := union[idx]
orig.Type = chooseMostFlexibleType(orig.Type, s2[i].Type)
}
}
return union
}
// chooseMostFlexibleType returns the type that is more 'flexible' than the other type, for the purposes of printing
// a diff when the type of the column has changed. There are a ton of different ways we could slice this. We'll stick to
// the following rules for the time being:
// - Going from any integer to a float, always take the float.
// - Going from any integer to a decimal, always take the decimal.
// - Going from any decimal to a decimal (with different precision/scale), always take the new decimal.
// - Going from a low precision float to a high precision float, we'll always take the high precision float.
// - Going from a low precision integer to a high precision integer, we'll always take the high precision integer.
// Currently, we only support this if the signage is the same.
// - Going from a DATE, TIME, or DATETIME to a TIMESTAMP, we'll always take the TIMESTAMP.
//
// If none of these rules apply, we'll just take the `a` type.
//
// Note this is only for printing the diff. This is not robust for other purposes.
func chooseMostFlexibleType(origA, origB sql.Type) sql.Type {
if origA.Equals(origB) {
return origA
}
at := origA.Type()
bt := origB.Type()
// If both are numbers, we'll take the float.
if sqltypes.IsIntegral(at) && sqltypes.IsFloat(bt) {
return origB
}
if sqltypes.IsIntegral(bt) && sqltypes.IsFloat(at) {
return origA
}
if bt == sqltypes.Decimal && sqltypes.IsIntegral(at) {
return origB
}
if sqltypes.IsFloat(at) && sqltypes.IsFloat(bt) {
// There are only two float types, so we'll always end up with a float64 here.
return origA.Promote()
}
if sqltypes.IsIntegral(at) && sqltypes.IsIntegral(bt) {
if (sqltypes.IsUnsigned(at) && sqltypes.IsUnsigned(bt)) || (!sqltypes.IsUnsigned(at) && !sqltypes.IsUnsigned(bt)) {
// Vitess definitions are ordered in the even that both are signed or unsigned, so take the higher one.
if bt > at {
return origB
}
return origA
}
// TODO: moving from unsigned to signed or vice versa.
}
if bt == sqltypes.Decimal && at == sqltypes.Decimal {
return origB
}
if bt == sqltypes.Timestamp && (at == sqltypes.Date || at == sqltypes.Time || at == sqltypes.Datetime) {
return origB
}
return origA
}
func getColumnNames(fromTableInfo, toTableInfo *diff.TableInfo) (colNames []string, formatText string) {
var fromSch, toSch schema.Schema
if fromTableInfo != nil {
fromSch = fromTableInfo.Sch
}
if toTableInfo != nil {
toSch = toTableInfo.Sch
}
var cols []string
if fromSch != nil {
fromSch.GetAllCols().Iter(func(_ uint64, col schema.Column) (stop bool, err error) {
cols = append(cols, fmt.Sprintf("from_%s", col.Name))
return false, nil
})
}
if toSch != nil {
toSch.GetAllCols().Iter(func(_ uint64, col schema.Column) (stop bool, err error) {
cols = append(cols, fmt.Sprintf("to_%s", col.Name))
return false, nil
})
}
colNames = cols
formatText = strings.Repeat("?, ", len(cols))
return colNames, formatText
}
func writeDiffResults(
ctx *sql.Context,
diffQuerySch sql.Schema,
targetSch sql.Schema,
iter sql.RowIter,
writer diff.SqlRowDiffWriter,
modifiedColNames map[string]bool,
dArgs *diffArgs,
) error {
ds, err := diff.NewDiffSplitter(diffQuerySch, targetSch)
if err != nil {
return err
}
for {
r, err := iter.Next(ctx)
if err == io.EOF {
return nil
} else if err != nil {
return err
}
oldRow, newRow, err := ds.SplitDiffResultRow(ctx, r)
if err != nil {
return err
}
// Apply row-level filtering based on diff type
if dArgs.filter != nil {
if shouldSkipRow(dArgs.filter, oldRow.RowDiff) || shouldSkipRow(dArgs.filter, newRow.RowDiff) {
continue
}
}
if dArgs.skinny {
var filteredOldRow, filteredNewRow diff.RowDiff
for i, changeType := range newRow.ColDiffs {
if (changeType == diff.Added|diff.Removed) || modifiedColNames[targetSch[i].Name] {
if i < len(oldRow.Row) {
filteredOldRow.Row = append(filteredOldRow.Row, oldRow.Row[i])
filteredOldRow.ColDiffs = append(filteredOldRow.ColDiffs, oldRow.ColDiffs[i])
filteredOldRow.RowDiff = oldRow.RowDiff
}
if i < len(newRow.Row) {
filteredNewRow.Row = append(filteredNewRow.Row, newRow.Row[i])
filteredNewRow.ColDiffs = append(filteredNewRow.ColDiffs, newRow.ColDiffs[i])
filteredNewRow.RowDiff = newRow.RowDiff
}
}
}
oldRow = filteredOldRow
newRow = filteredNewRow
}
// We are guaranteed to have "ModeRow" for writers that do not support combined rows
if dArgs.diffMode != diff.ModeRow && oldRow.RowDiff == diff.ModifiedOld && newRow.RowDiff == diff.ModifiedNew {
if err = writer.WriteCombinedRow(ctx, oldRow.Row, newRow.Row, dArgs.diffMode); err != nil {
return err
}
} else {
if oldRow.Row != nil {
if err = writer.WriteRow(ctx, oldRow.Row, oldRow.RowDiff, oldRow.ColDiffs); err != nil {
return err
}
}
if newRow.Row != nil {
if err = writer.WriteRow(ctx, newRow.Row, newRow.RowDiff, newRow.ColDiffs); err != nil {
return err
}
}
}
}
}
// getModifiedCols returns a set of the names of columns that are modified, as well as the name of the primary key for a particular row iterator and schema.
// In the case where rows are added or removed, all columns will be included
// unionSch refers to a joint schema between the schema before and after any schema changes pertaining to the diff,
// while diffQuerySch refers to the schema returned by the "dolt_diff" sql query.
func getModifiedCols(
ctx *sql.Context,
iter sql.RowIter,
unionSch sql.Schema,
diffQuerySch sql.Schema,
) (map[string]bool, error) {
modifiedColNames := make(map[string]bool)
for {
r, err := iter.Next(ctx)
if err == io.EOF {
break
}
ds, err := diff.NewDiffSplitter(diffQuerySch, unionSch)
if err != nil {
return modifiedColNames, err
}
oldRow, newRow, err := ds.SplitDiffResultRow(ctx, r)
if err != nil {
return modifiedColNames, err
}
for i, changeType := range newRow.ColDiffs {
if changeType != diff.None || unionSch[i].PrimaryKey {
modifiedColNames[unionSch[i].Name] = true
}
}
for i, changeType := range oldRow.ColDiffs {
if changeType != diff.None || unionSch[i].PrimaryKey {
modifiedColNames[unionSch[i].Name] = true
}
}
}
return modifiedColNames, nil
}
// validateWhereClause validates the where clause by attempting a minimal syntax check
// against the dolt_diff schema. This ensures the where clause is syntactically correct
// and references valid column patterns, even when there are no changes to show.
func validateWhereClause(queryist cli.Queryist, sqlCtx *sql.Context, dArgs *diffArgs) errhand.VerboseError {
// Build a minimal validation query that doesn't depend on having actual tables
// We use a subquery approach so the aliased columns are available in the WHERE clause
query := "select * from (select 1 as diff_type, 1 as from_pk, 1 as to_pk) as diff_validation where " + dArgs.where + " limit 0"
_, rowIter, _, err := queryist.Query(sqlCtx, query)
if err != nil {
return errhand.BuildDError("Error running diff query").AddCause(err).Build()
}
// Close the iterator since we only wanted to validate
if rowIter != nil {
rowIter.Close(sqlCtx)
}
return nil
}