Added fast forward handling and fleshed out constraint violation messages

This commit is contained in:
Daylon Wilkins
2021-07-14 01:06:06 -07:00
committed by Daylon Wilkins
parent 22a7dd9f48
commit 8d971dcd35
20 changed files with 331 additions and 121 deletions

View File

@@ -110,7 +110,7 @@ func toAddVErr(err error) errhand.VerboseError {
return bdr.Build()
case actions.IsTblInConflict(err):
case actions.IsTblInConflict(err) || actions.IsTblViolatesConstraints(err):
tbls := actions.GetTablesForError(err)
bdr := errhand.BuildDError("error: not all tables merged")

View File

@@ -169,7 +169,7 @@ func handleCommitErr(ctx context.Context, dEnv *env.DoltEnv, err error, usage cl
if actions.IsNothingStaged(err) {
notStagedTbls := actions.NothingStagedTblDiffs(err)
notStagedDocs := actions.NothingStagedDocsDiffs(err)
n := printDiffsNotStaged(ctx, dEnv, cli.CliOut, notStagedTbls, notStagedDocs, false, 0, []string{})
n := printDiffsNotStaged(ctx, dEnv, cli.CliOut, notStagedTbls, notStagedDocs, false, 0, nil, nil)
if n == 0 {
bdr := errhand.BuildDError(`no changes added to commit (use "dolt add")`)
@@ -219,12 +219,16 @@ func buildInitalCommitMsg(ctx context.Context, dEnv *env.DoltEnv) string {
if err != nil {
workingTblsInConflict = []string{}
}
workingTblsWithViolations, _, _, err := merge.GetTablesWithConstraintViolations(ctx, roots)
if err != nil {
workingTblsWithViolations = []string{}
}
stagedDocDiffs, notStagedDocDiffs, _ := diff.GetDocDiffs(ctx, roots, dEnv.DocsReadWriter())
buf := bytes.NewBuffer([]byte{})
n := printStagedDiffs(buf, stagedTblDiffs, stagedDocDiffs, true)
n = printDiffsNotStaged(ctx, dEnv, buf, notStagedTblDiffs, notStagedDocDiffs, true, n, workingTblsInConflict)
n = printDiffsNotStaged(ctx, dEnv, buf, notStagedTblDiffs, notStagedDocDiffs, true, n, workingTblsInConflict, workingTblsWithViolations)
currBranch := dEnv.RepoStateReader().CWBHeadRef()
initialCommitMessage := "\n" + "# Please enter the commit message for your changes. Lines starting" + "\n" +

View File

@@ -117,13 +117,25 @@ func (cmd MergeCmd) Exec(ctx context.Context, commandStr string, args []string,
}
// If there are any conflicts or constraint violations then we disallow the merge
if has, err := root.HasConflicts(ctx); err != nil {
hasCnf, err := root.HasConflicts(ctx)
if err != nil {
verr = errhand.BuildDError("error: failed to get conflicts").AddCause(err).Build()
} else if has {
}
hasCV, err := root.HasConstraintViolations(ctx)
if err != nil {
verr = errhand.BuildDError("error: failed to get constraint violations").AddCause(err).Build()
}
if hasCnf || hasCV {
cli.Println("error: Merging is not possible because you have unmerged tables.")
cli.Println("hint: Fix them up in the working tree, and then use 'dolt add <table>'")
cli.Println("hint: as appropriate to mark resolution and make a commit.")
cli.Println("fatal: Exiting because of an unresolved conflict.")
if hasCnf && hasCV {
cli.Println("fatal: Exiting because of an unresolved conflict and constraint violation.")
} else if hasCnf {
cli.Println("fatal: Exiting because of an unresolved conflict.")
} else {
cli.Println("fatal: Exiting because of an unresolved constraint violation.")
}
return 1
} else if mergeActive {
cli.Println("error: Merging is not possible because you have not committed an active merge.")
@@ -132,13 +144,6 @@ func (cmd MergeCmd) Exec(ctx context.Context, commandStr string, args []string,
return 1
}
if has, err := root.HasConstraintViolations(ctx); err != nil {
verr = errhand.BuildDError("error: failed to get constraint violations").AddCause(err).Build()
} else if has {
cli.Println("fatal: Exiting because of an unresolved constraint violation.")
return 1
}
if verr == nil {
verr = mergeCommitSpec(ctx, apr, dEnv, commitSpecStr)
}
@@ -224,6 +229,19 @@ func mergeCommitSpec(ctx context.Context, apr *argparser.ArgParseResults, dEnv *
}
if ok, err := cm1.CanFastForwardTo(ctx, cm2); ok {
ancRoot, err := cm1.GetRootValue()
if err != nil {
return errhand.VerboseErrorFromError(err)
}
mergedRoot, err := cm2.GetRootValue()
if err != nil {
return errhand.VerboseErrorFromError(err)
}
if cvPossible, err := merge.MayHaveConstraintViolations(ctx, ancRoot, mergedRoot); err != nil {
return errhand.VerboseErrorFromError(err)
} else if cvPossible {
return executeMerge(ctx, squash, dEnv, cm1, cm2, workingDiffs)
}
if apr.Contains(cli.NoFFParam) {
return execNoFFMerge(ctx, apr, dEnv, roots, cm2, verr, workingDiffs)
} else {
@@ -421,10 +439,16 @@ func mergedRootToWorking(
verr := UpdateWorkingWithVErr(dEnv, workingRoot)
if verr == nil {
hasConflicts := printSuccessStats(tblToStats)
hasConflicts, hasConstraintViolations := printSuccessStats(tblToStats)
if hasConflicts {
if hasConflicts && hasConstraintViolations {
cli.Println("Automatic merge failed; fix conflicts and constraint violations and then commit the result.")
} else if hasConflicts {
cli.Println("Automatic merge failed; fix conflicts and then commit the result.")
} else if hasConstraintViolations {
cli.Println("Automatic merge failed; fix constraint violations and then commit the result.\n" +
"Constraint violations for the working set may be viewed using the 'dolt_constraint_violations' system table.\n" +
"They may be queried and removed per-table using the 'dolt_constraint_violations_TABLENAME' system table.")
} else {
err = actions.SaveDocsFromWorkingExcludingFSChanges(ctx, dEnv, unstagedDocs)
if err != nil {
@@ -441,11 +465,12 @@ func mergedRootToWorking(
return verr
}
func printSuccessStats(tblToStats map[string]*merge.MergeStats) bool {
// printSuccessStats returns whether there are conflicts or constraint violations.
func printSuccessStats(tblToStats map[string]*merge.MergeStats) (conflicts bool, constraintViolations bool) {
printModifications(tblToStats)
printAdditions(tblToStats)
printDeletions(tblToStats)
return printConflicts(tblToStats)
return printConflictsAndViolations(tblToStats)
}
func printAdditions(tblToStats map[string]*merge.MergeStats) {
@@ -464,18 +489,24 @@ func printDeletions(tblToStats map[string]*merge.MergeStats) {
}
}
func printConflicts(tblToStats map[string]*merge.MergeStats) bool {
func printConflictsAndViolations(tblToStats map[string]*merge.MergeStats) (conflicts bool, constraintViolations bool) {
hasConflicts := false
hasConstraintViolations := false
for tblName, stats := range tblToStats {
if stats.Operation == merge.TableModified && stats.Conflicts > 0 {
if stats.Operation == merge.TableModified && (stats.Conflicts > 0 || stats.ConstraintViolations > 0) {
cli.Println("Auto-merging", tblName)
cli.Println("CONFLICT (content): Merge conflict in", tblName)
hasConflicts = true
if stats.Conflicts > 0 {
cli.Println("CONFLICT (content): Merge conflict in", tblName)
hasConflicts = true
}
if stats.ConstraintViolations > 0 {
cli.Println("CONSTRAINT VIOLATION (content): Merge created constraint violation in", tblName)
hasConstraintViolations = true
}
}
}
return hasConflicts
return hasConflicts, hasConstraintViolations
}
func printModifications(tblToStats map[string]*merge.MergeStats) {
@@ -486,7 +517,7 @@ func printModifications(tblToStats map[string]*merge.MergeStats) {
rowsChanged := 0
var tbls []string
for tblName, stats := range tblToStats {
if stats.Operation == merge.TableModified && stats.Conflicts == 0 {
if stats.Operation == merge.TableModified && stats.Conflicts == 0 && stats.ConstraintViolations == 0 {
tbls = append(tbls, tblName)
nameLen := len(tblName)
modCount := stats.Adds + stats.Modifications + stats.Deletes + stats.Conflicts

View File

@@ -66,14 +66,9 @@ func (cmd StatusCmd) createArgParser() *argparser.ArgParser {
// Exec executes the command
func (cmd StatusCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv) int {
ap := cmd.createArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.GetCommandDocumentation(commandStr, statusDocs, ap))
help, _ := cli.HelpAndUsagePrinters(cli.GetCommandDocumentation(commandStr, statusDocs, ap))
cli.ParseArgsOrDie(ap, args, help)
workingRoot, err := dEnv.WorkingRoot(ctx)
if err != nil {
return HandleVErrAndExitCode(errhand.BuildDError("Couldn't get working root").AddCause(err).Build(), usage)
}
roots, err := dEnv.Roots(ctx)
if err != nil {
cli.PrintErrln(toStatusVErr(err).Verbose())
@@ -81,33 +76,30 @@ func (cmd StatusCmd) Exec(ctx context.Context, commandStr string, args []string,
}
staged, notStaged, err := diff.GetStagedUnstagedTableDeltas(ctx, roots)
if err != nil {
cli.PrintErrln(toStatusVErr(err).Verbose())
return 1
}
workingTblsInConflict, _, _, err := merge.GetTablesInConflict(ctx, roots)
workingTblsInConflict, _, _, err := merge.GetTablesInConflict(ctx, roots)
if err != nil {
cli.PrintErrln(toStatusVErr(err).Verbose())
return 1
}
workingTblsWithViolations, _, _, err := merge.GetTablesWithConstraintViolations(ctx, roots)
if err != nil {
cli.PrintErrln(toStatusVErr(err).Verbose())
return 1
}
stagedDocDiffs, notStagedDocDiffs, err := diff.GetDocDiffs(ctx, roots, dEnv.DocsReadWriter())
if err != nil {
cli.PrintErrln(toStatusVErr(err).Verbose())
return 1
}
workingDocsInConflict, err := merge.GetDocsInConflict(ctx, workingRoot, dEnv.DocsReadWriter())
if err != nil {
cli.PrintErrln(toStatusVErr(err).Verbose())
return 1
}
err = printStatus(ctx, dEnv, staged, notStaged, workingTblsInConflict, workingDocsInConflict, stagedDocDiffs, notStagedDocDiffs)
err = printStatus(ctx, dEnv, staged, notStaged, workingTblsInConflict, workingTblsWithViolations, stagedDocDiffs, notStagedDocDiffs)
if err != nil {
cli.PrintErrln(toStatusVErr(err).Verbose())
return 1
@@ -147,11 +139,11 @@ const (
stagedHeaderHelp = ` (use "dolt reset <table>..." to unstage)`
unmergedTablesHeader = `You have unmerged tables.
(fix conflicts and run "dolt commit")
(fix %s and run "dolt commit")
(use "dolt merge --abort" to abort the merge)
`
allMergedHeader = `All conflicts fixed but you are still merging.
allMergedHeader = `All conflicts and constraint violations fixed but you are still merging.
(use "dolt commit" to conclude merge)
`
@@ -206,27 +198,46 @@ func printStagedDiffs(wr io.Writer, stagedTbls []diff.TableDelta, stagedDocs *di
return 0
}
func printDiffsNotStaged(ctx context.Context, dEnv *env.DoltEnv, wr io.Writer, notStagedTbls []diff.TableDelta, notStagedDocs *diff.DocDiffs, printHelp bool, linesPrinted int, workingTblsInConflict []string) int {
func printDiffsNotStaged(
ctx context.Context,
dEnv *env.DoltEnv,
wr io.Writer,
notStagedTbls []diff.TableDelta,
notStagedDocs *diff.DocDiffs,
printHelp bool,
linesPrinted int,
workingTblsInConflict, workingTblsWithViolations []string,
) int {
inCnfSet := set.NewStrSet(workingTblsInConflict)
violationSet := set.NewStrSet(workingTblsWithViolations)
if len(workingTblsInConflict) > 0 {
if len(workingTblsInConflict) > 0 || len(workingTblsWithViolations) > 0 {
if linesPrinted > 0 {
cli.Println()
}
iohelp.WriteLine(wr, mergedTableHeader)
if printHelp {
iohelp.WriteLine(wr, mergedTableHelp)
}
lines := make([]string, 0, len(notStagedTbls))
for _, tblName := range workingTblsInConflict {
lines = append(lines, fmt.Sprintf(statusFmt, bothModifiedLabel, tblName))
if len(workingTblsInConflict) > 0 {
lines := make([]string, 0, len(notStagedTbls))
for _, tblName := range workingTblsInConflict {
lines = append(lines, fmt.Sprintf(statusFmt, bothModifiedLabel, tblName))
}
iohelp.WriteLine(wr, color.RedString(strings.Join(lines, "\n")))
linesPrinted += len(lines)
}
iohelp.WriteLine(wr, color.RedString(strings.Join(lines, "\n")))
linesPrinted += len(lines)
if len(workingTblsWithViolations) > 0 {
violationOnly, _, _ := violationSet.LeftIntersectionRight(inCnfSet)
lines := make([]string, 0, len(notStagedTbls))
for _, tblName := range violationOnly.AsSortedSlice() {
lines = append(lines, fmt.Sprintf(statusFmt, "modified", tblName))
}
iohelp.WriteLine(wr, color.RedString(strings.Join(lines, "\n")))
linesPrinted += len(lines)
}
}
added := 0
@@ -245,7 +256,7 @@ func printDiffsNotStaged(ctx context.Context, dEnv *env.DoltEnv, wr io.Writer, n
numRemovedOrModified := removeModified + notStagedDocs.NumRemoved + notStagedDocs.NumModified
docsInCnf, _ := docCnfsOnWorkingRoot(ctx, dEnv)
if numRemovedOrModified-inCnfSet.Size() > 0 {
if numRemovedOrModified-inCnfSet.Size()-violationSet.Size() > 0 {
if linesPrinted > 0 {
cli.Println()
}
@@ -259,7 +270,7 @@ func printDiffsNotStaged(ctx context.Context, dEnv *env.DoltEnv, wr io.Writer, n
iohelp.WriteLine(wr, workingHeaderHelp)
}
lines := getModifiedAndRemovedNotStaged(notStagedTbls, notStagedDocs, inCnfSet)
lines := getModifiedAndRemovedNotStaged(notStagedTbls, notStagedDocs, inCnfSet, violationSet)
iohelp.WriteLine(wr, color.RedString(strings.Join(lines, "\n")))
linesPrinted += len(lines)
@@ -293,10 +304,10 @@ func printDiffsNotStaged(ctx context.Context, dEnv *env.DoltEnv, wr io.Writer, n
return linesPrinted
}
func getModifiedAndRemovedNotStaged(notStagedTbls []diff.TableDelta, notStagedDocs *diff.DocDiffs, inCnfSet *set.StrSet) (lines []string) {
func getModifiedAndRemovedNotStaged(notStagedTbls []diff.TableDelta, notStagedDocs *diff.DocDiffs, inCnfSet, violationSet *set.StrSet) (lines []string) {
lines = make([]string, 0, len(notStagedTbls)+notStagedDocs.Len())
for _, td := range notStagedTbls {
if td.IsAdd() || inCnfSet.Contains(td.CurName()) || td.CurName() == doltdb.DocTableName {
if td.IsAdd() || inCnfSet.Contains(td.CurName()) || violationSet.Contains(td.CurName()) || td.CurName() == doltdb.DocTableName {
continue
}
if td.IsDrop() {
@@ -342,7 +353,7 @@ func getAddedNotStaged(notStagedTbls []diff.TableDelta, notStagedDocs *diff.DocD
}
// TODO: working docs in conflict param not used here
func printStatus(ctx context.Context, dEnv *env.DoltEnv, stagedTbls, notStagedTbls []diff.TableDelta, workingTblsInConflict []string, workingDocsInConflict *diff.DocDiffs, stagedDocs, notStagedDocs *diff.DocDiffs) error {
func printStatus(ctx context.Context, dEnv *env.DoltEnv, stagedTbls, notStagedTbls []diff.TableDelta, workingTblsInConflict, workingTblsWithViolations []string, stagedDocs, notStagedDocs *diff.DocDiffs) error {
cli.Printf(branchHeader, dEnv.RepoStateReader().CWBHeadRef().GetPath())
mergeActive, err := dEnv.IsMergeActive(ctx)
@@ -351,15 +362,19 @@ func printStatus(ctx context.Context, dEnv *env.DoltEnv, stagedTbls, notStagedTb
}
if mergeActive {
if len(workingTblsInConflict) > 0 {
cli.Println(unmergedTablesHeader)
if len(workingTblsInConflict) > 0 && len(workingTblsWithViolations) > 0 {
cli.Println(fmt.Sprintf(unmergedTablesHeader, "conflicts and constraint violations"))
} else if len(workingTblsInConflict) > 0 {
cli.Println(fmt.Sprintf(unmergedTablesHeader, "conflicts"))
} else if len(workingTblsWithViolations) > 0 {
cli.Println(fmt.Sprintf(unmergedTablesHeader, "constraint violations"))
} else {
cli.Println(allMergedHeader)
}
}
n := printStagedDiffs(cli.CliOut, stagedTbls, stagedDocs, true)
n = printDiffsNotStaged(ctx, dEnv, cli.CliOut, notStagedTbls, notStagedDocs, true, n, workingTblsInConflict)
n = printDiffsNotStaged(ctx, dEnv, cli.CliOut, notStagedTbls, notStagedDocs, true, n, workingTblsInConflict, workingTblsWithViolations)
if !mergeActive && n == 0 {
cli.Println("nothing to commit, working tree clean")

View File

@@ -406,6 +406,16 @@ func (fkc *ForeignKeyCollection) Stage(ctx context.Context, fksToAdd []ForeignKe
}
}
// Tables returns the set of all tables that either declare a foreign key or are referenced by a foreign key.
func (fkc *ForeignKeyCollection) Tables() map[string]struct{} {
tables := make(map[string]struct{})
for _, fk := range fkc.foreignKeys {
tables[fk.TableName] = struct{}{}
tables[fk.ReferencedTableName] = struct{}{}
}
return tables
}
// String returns the SQL reference option in uppercase.
func (refOp ForeignKeyReferenceOption) String() string {
switch refOp {

View File

@@ -1261,3 +1261,23 @@ func (root *RootValue) DebugString(ctx context.Context, transitive bool) string
return buf.String()
}
// MapTableHashes returns a map of each table name and hash.
func (root *RootValue) MapTableHashes(ctx context.Context) (map[string]hash.Hash, error) {
names, err := root.GetTableNames(ctx)
if err != nil {
return nil, err
}
nameToHash := make(map[string]hash.Hash)
for _, name := range names {
h, ok, err := root.GetTableHash(ctx, name)
if err != nil {
return nil, err
} else if !ok {
return nil, fmt.Errorf("root found a table with name '%s' but no hash", name)
} else {
nameToHash[name] = h
}
}
return nameToHash, nil
}

View File

@@ -111,6 +111,9 @@ func stageTablesNoEnvUpdate(
return doltdb.Roots{}, err
}
roots.Staged, err = MoveTablesBetweenRoots(ctx, tbls, roots.Working, roots.Staged)
if err != nil {
return doltdb.Roots{}, err
}
return roots, nil
}

View File

@@ -257,17 +257,17 @@ func MergeWouldStompChanges(ctx context.Context, workingRoot *doltdb.RootValue,
return nil, nil, err
}
headTableHashes, err := mapTableHashes(ctx, headRoot)
headTableHashes, err := headRoot.MapTableHashes(ctx)
if err != nil {
return nil, nil, err
}
workingTableHashes, err := mapTableHashes(ctx, workingRoot)
workingTableHashes, err := workingRoot.MapTableHashes(ctx)
if err != nil {
return nil, nil, err
}
mergeTableHashes, err := mapTableHashes(ctx, mergeRoot)
mergeTableHashes, err := mergeRoot.MapTableHashes(ctx)
if err != nil {
return nil, nil, err
}
@@ -343,29 +343,6 @@ func GetGCKeepers(ctx context.Context, env *DoltEnv) ([]hash.Hash, error) {
return keepers, nil
}
func mapTableHashes(ctx context.Context, root *doltdb.RootValue) (map[string]hash.Hash, error) {
names, err := root.GetTableNames(ctx)
if err != nil {
return nil, err
}
nameToHash := make(map[string]hash.Hash)
for _, name := range names {
h, ok, err := root.GetTableHash(ctx, name)
if err != nil {
return nil, err
} else if !ok {
panic("GetTableNames returned a table that GetTableHash says isn't there.")
} else {
nameToHash[name] = h
}
}
return nameToHash, nil
}
func diffTableHashes(headTableHashes, otherTableHashes map[string]hash.Hash) map[string]hash.Hash {
diffs := make(map[string]hash.Hash)
for tName, hh := range headTableHashes {

View File

@@ -56,7 +56,7 @@ const (
)
// AddConstraintViolations adds all constraint violations to each table.
func AddConstraintViolations(ctx context.Context, newRoot, ourRoot, baseRoot *doltdb.RootValue) (*doltdb.RootValue, error) {
func AddConstraintViolations(ctx context.Context, newRoot, baseRoot *doltdb.RootValue) (*doltdb.RootValue, error) {
fkColl, err := newRoot.GetForeignKeyCollection(ctx)
if err != nil {
return nil, err

View File

@@ -913,14 +913,58 @@ func MergeRoots(ctx context.Context, ourRoot, theirRoot, ancRoot *doltdb.RootVal
return nil, nil, err
}
newRoot, err = AddConstraintViolations(ctx, newRoot, ourRoot, ancRoot)
newRoot, err = AddConstraintViolations(ctx, newRoot, ancRoot)
if err != nil {
return nil, nil, err
}
for tblName, stats := range tblToStats {
tbl, ok, err := newRoot.GetTable(ctx, tblName)
if err != nil {
return nil, nil, err
}
if ok {
cvMap, err := tbl.GetConstraintViolations(ctx)
if err != nil {
return nil, nil, err
}
stats.ConstraintViolations = int(cvMap.Len())
}
}
return newRoot, tblToStats, nil
}
// MayHaveConstraintViolations returns whether the given roots may have constraint violations. For example, a fast
// forward merge that does not involve any tables with foreign key constraints or check constraints will not be able
// to generate constraint violations. Unique key constraint violations would be caught during the generation of the
// merged root, therefore it is not a factor for this function.
func MayHaveConstraintViolations(ctx context.Context, ancestor, merged *doltdb.RootValue) (bool, error) {
ancTables, err := ancestor.MapTableHashes(ctx)
if err != nil {
return false, err
}
mergedTables, err := merged.MapTableHashes(ctx)
if err != nil {
return false, err
}
fkColl, err := merged.GetForeignKeyCollection(ctx)
if err != nil {
return false, err
}
tablesInFks := fkColl.Tables()
for tblName := range tablesInFks {
if ancHash, ok := ancTables[tblName]; !ok {
// If a table used in a foreign key is new then it's treated as a change
return true, nil
} else if mergedHash, ok := mergedTables[tblName]; !ok {
return false, fmt.Errorf("foreign key uses table '%s' but no hash can be found for this table", tblName)
} else if !ancHash.Equal(mergedHash) {
return true, nil
}
}
return false, nil
}
func GetTablesInConflict(ctx context.Context, roots doltdb.Roots) (
workingInConflict, stagedInConflict, headInConflict []string,
err error,
@@ -943,6 +987,28 @@ func GetTablesInConflict(ctx context.Context, roots doltdb.Roots) (
return workingInConflict, stagedInConflict, headInConflict, err
}
func GetTablesWithConstraintViolations(ctx context.Context, roots doltdb.Roots) (
workingViolations, stagedViolations, headViolations []string,
err error,
) {
headViolations, err = roots.Head.TablesWithConstraintViolations(ctx)
if err != nil {
return nil, nil, nil, err
}
stagedViolations, err = roots.Staged.TablesWithConstraintViolations(ctx)
if err != nil {
return nil, nil, nil, err
}
workingViolations, err = roots.Working.TablesWithConstraintViolations(ctx)
if err != nil {
return nil, nil, nil, err
}
return workingViolations, stagedViolations, headViolations, err
}
func GetDocsInConflict(ctx context.Context, workingRoot *doltdb.RootValue, drw env.DocsReadWriter) (*diff.DocDiffs, error) {
docs, err := drw.GetDocsOnDisk()
if err != nil {

View File

@@ -24,9 +24,10 @@ const (
)
type MergeStats struct {
Operation TableMergeOp
Adds int
Deletes int
Modifications int
Conflicts int
Operation TableMergeOp
Adds int
Deletes int
Modifications int
Conflicts int
ConstraintViolations int
}

View File

@@ -137,26 +137,38 @@ func (d DoltMergeFunc) Eval(ctx *sql.Context, row sql.Row) (interface{}, error)
}
if canFF {
if apr.Contains(cli.NoFFParam) {
ws, err = executeNoFFMerge(ctx, sess, apr, dbName, ws, dbData, headCommit, mergeCommit)
if err == doltdb.ErrUnresolvedConflicts {
// if there are unresolved conflicts, write the resulting working set back to the session and return an
// error message
wsErr := sess.SetWorkingSet(ctx, dbName, ws, nil)
if wsErr != nil {
return nil, wsErr
}
return err.Error(), nil
}
} else {
err = executeFFMerge(ctx, sess, apr.Contains(cli.SquashParam), dbName, ws, dbData, mergeCommit)
}
headRoot, err := headCommit.GetRootValue()
if err != nil {
return nil, err
}
return cmh.String(), err
mergeRoot, err := mergeCommit.GetRootValue()
if err != nil {
return nil, err
}
if cvPossible, err := merge.MayHaveConstraintViolations(ctx, headRoot, mergeRoot); err != nil {
return nil, err
} else if !cvPossible {
if apr.Contains(cli.NoFFParam) {
ws, err = executeNoFFMerge(ctx, sess, apr, dbName, ws, dbData, headCommit, mergeCommit)
if err == doltdb.ErrUnresolvedConflicts {
// if there are unresolved conflicts, write the resulting working set back to the session and return an
// error message
wsErr := sess.SetWorkingSet(ctx, dbName, ws, nil)
if wsErr != nil {
return nil, wsErr
}
return err.Error(), nil
}
} else {
err = executeFFMerge(ctx, sess, apr.Contains(cli.SquashParam), dbName, ws, dbData, mergeCommit)
}
if err != nil {
return nil, err
}
return cmh.String(), err
}
}
ws, err = executeMerge(ctx, apr.Contains(cli.SquashParam), headCommit, mergeCommit, ws)

View File

@@ -103,7 +103,19 @@ func (cf *MergeFunc) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) {
}
if canFF {
return cmh.String(), nil
ancRoot, err := head.GetRootValue()
if err != nil {
return nil, err
}
mergedRoot, err := cm.GetRootValue()
if err != nil {
return nil, err
}
if cvPossible, err := merge.MayHaveConstraintViolations(ctx, ancRoot, mergedRoot); err != nil {
return nil, err
} else if !cvPossible {
return cmh.String(), nil
}
}
mergeRoot, _, err := merge.MergeCommits(ctx, head, cm)

View File

@@ -79,7 +79,7 @@ func NewConstraintViolationsTable(ctx *sql.Context, tblName string, root *doltdb
if err != nil {
return nil, err
}
sqlSch, err := sqlutil.FromDoltSchema(doltdb.DoltConfTablePrefix+tblName, dSch)
sqlSch, err := sqlutil.FromDoltSchema(doltdb.DoltConstViolTablePrefix+tblName, dSch)
if err != nil {
return nil, err
}

View File

@@ -120,8 +120,8 @@ func (s *StrSet) Size() int {
return len(s.items)
}
// AsSlice converts the set to a slice of strings. If this is an insensitive set the resulting slice will be lowercase
// regardless of the case that was used when adding the string to the set
// AsSlice converts the set to a slice of strings. If this is an insensitive set the resulting slice will be lowercase
// regardless of the case that was used when adding the string to the set.
func (s *StrSet) AsSlice() []string {
size := len(s.items)
sl := make([]string, size)
@@ -135,6 +135,16 @@ func (s *StrSet) AsSlice() []string {
return sl
}
// AsSortedSlice converts the set to a slice of strings. If this is an insensitive set the resulting slice will be lowercase
// regardless of the case that was used when adding the string to the set. The slice is sorted in ascending order.
func (s *StrSet) AsSortedSlice() []string {
slice := s.AsSlice()
sort.Slice(slice, func(i, j int) bool {
return slice[i] < slice[j]
})
return slice
}
// Iterate accepts a callback which will be called once for each element in the set until all items have been
// exhausted or callback returns false.
func (s *StrSet) Iterate(callBack func(string) (cont bool)) {
@@ -145,7 +155,7 @@ func (s *StrSet) Iterate(callBack func(string) (cont bool)) {
}
}
// IntersectionAndMissing takes a slice of strings and returns a slice of strings containing the intersection with the
// LeftIntersectionRight takes a slice of strings and returns a slice of strings containing the intersection with the
// set, and a slice of strings for the ones missing from the set.
func (s *StrSet) LeftIntersectionRight(other *StrSet) (left *StrSet, intersection *StrSet, right *StrSet) {
left = NewStrSet(nil)

View File

@@ -453,7 +453,7 @@ teardown() {
[[ ! "$output" =~ "ours" ]] || false
[[ ! "$output" =~ "theirs" ]] || false
run dolt status
[[ "$output" =~ "All conflicts fixed but you are still merging." ]] || false
[[ "$output" =~ "All conflicts and constraint violations fixed but you are still merging." ]] || false
run dolt merge --abort
[ "$status" -eq 0 ]
[ "$output" = "" ]

View File

@@ -89,7 +89,7 @@ SQL
[[ "$output" =~ "1 rows modified" ]] || false
[[ ! "$output" =~ "CONFLICT" ]] || false
run dolt status
[[ "$output" =~ "All conflicts fixed" ]] || false
[[ "$output" =~ "All conflicts and constraint violations fixed" ]] || false
[[ "$output" =~ "Changes to be committed:" ]] || false
}
@@ -124,7 +124,7 @@ SQL
[[ "$output" =~ "1 rows modified" ]] || false
[[ ! "$output" =~ "CONFLICT" ]] || false
run dolt status
[[ "$output" =~ "All conflicts fixed" ]] || false
[[ "$output" =~ "All conflicts and constraint violations fixed" ]] || false
[[ "$output" =~ "Changes to be committed:" ]] || false
}

View File

@@ -10,6 +10,56 @@ teardown() {
teardown_common
}
@test "constraint-violations: functions blocked with violations" {
dolt sql <<"SQL"
CREATE TABLE test (pk BIGINT PRIMARY KEY, v1 BIGINT, UNIQUE INDEX(v1));
INSERT INTO test VALUES (1, 1), (2, 2);
SQL
dolt add -A
dolt commit -m "MC1"
dolt branch other
dolt sql -q "INSERT INTO test VALUES (3, 3)"
dolt add -A
dolt commit -m "MC2"
dolt checkout other
dolt sql -q "INSERT INTO test VALUES (4, 3), (9, 9)"
dolt add -A
dolt commit -m "OC1"
dolt checkout master
run dolt merge other
[ "$status" -eq "0" ]
[[ "$output" =~ "fix constraint violations" ]] || false
run dolt sql -q "SELECT * FROM dolt_constraint_violations" -r=csv
[ "$status" -eq "0" ]
[[ "$output" =~ "table,num_violations" ]] || false
[[ "$output" =~ "test,1" ]] || false
[[ "${#lines[@]}" = "2" ]] || false
run dolt status
[ "$status" -eq "0" ]
[[ "$output" =~ "fix constraint violations" ]] || false
[[ "$output" =~ "test" ]] || false
run dolt merge other
[ "$status" -eq "1" ]
[[ "$output" =~ "constraint violation" ]] || false
run dolt add test
[ "$status" -eq "1" ]
[[ "$output" =~ "test" ]] || false
run dolt commit -m "this should fail"
[ "$status" -eq "1" ]
[[ "$output" =~ "constraint violation" ]] || false
dolt sql -q "DELETE FROM dolt_constraint_violations_test"
run dolt status
[ "$status" -eq "0" ]
[[ "$output" =~ "constraint violations fixed" ]] || false
dolt add test
dolt commit -m "this works"
run dolt merge other
[ "$status" -eq "0" ]
[[ "$output" =~ "up to date" ]] || false
}
@test "constraint-violations: ancestor contains fk, main parent remove, other child add, restrict" {
dolt sql <<"SQL"
CREATE TABLE parent (pk BIGINT PRIMARY KEY, v1 BIGINT, INDEX(v1));
@@ -2579,7 +2629,6 @@ SQL
}
@test "constraint-violations: cyclic foreign keys, illegal deletion" {
skip "Figure out how we should handle fast forward with foreign key merging"
# We're deleting a reference in a cycle from each table to make sure if properly applies a violation in both instances
dolt sql <<"SQL"
CREATE TABLE t1 (pk BIGINT PRIMARY KEY, v1 BIGINT, INDEX(v1));

View File

@@ -833,7 +833,7 @@ SQL
[[ "$output" =~ "one-more-time" ]] || false
run dolt status
echo "output = $output"
[[ "$output" =~ "All conflicts fixed" ]] || false
[[ "$output" =~ "All conflicts and constraint violations fixed" ]] || false
[[ "$output" =~ "Changes to be committed:" ]] || false
[[ "$output" =~ "README.md" ]] || false
}

View File

@@ -137,7 +137,7 @@ SQL
[[ "$output" =~ "3" ]] || false
run dolt status
[[ "$output" =~ "All conflicts fixed but you are still merging" ]] || false
[[ "$output" =~ "All conflicts and constraint violations fixed but you are still merging" ]] || false
[[ "$output" =~ "Changes to be committed:" ]] || false
[[ "$output" =~ ([[:space:]]*modified:[[:space:]]*test) ]] || false