From 9e54544cdc29f91b87e5a00a7dea9858d2926b3c Mon Sep 17 00:00:00 2001 From: elianddb Date: Mon, 15 Jun 2026 17:53:05 +0000 Subject: [PATCH 1/8] support working and staged in three-dot revisions --- .../doltcore/sqle/dfunctions/merge_base.go | 9 +- .../sqle/dtablefunctions/dolt_diff.go | 26 +-- .../doltcore/sqle/dtablefunctions/dolt_log.go | 34 +--- .../dolt_preview_merge_conflicts_summary.go | 5 +- .../doltcore/sqle/enginetest/dolt_queries.go | 32 ++++ .../sqle/enginetest/dolt_queries_diff.go | 152 ++++++++++++++++++ integration-tests/bats/diff.bats | 53 ++++++ 7 files changed, 256 insertions(+), 55 deletions(-) diff --git a/go/libraries/doltcore/sqle/dfunctions/merge_base.go b/go/libraries/doltcore/sqle/dfunctions/merge_base.go index 75bfec748b5..238b1e98e28 100644 --- a/go/libraries/doltcore/sqle/dfunctions/merge_base.go +++ b/go/libraries/doltcore/sqle/dfunctions/merge_base.go @@ -94,8 +94,15 @@ func (d MergeBase) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return mergeBase.String(), nil } -// ResolveRefSpec resolves a commit spec string to a Commit object using the provided headRef and doltDB. +// ResolveRefSpec resolves the commit spec |spec| to a commit using |headRef| and |doltDB|. +// The pseudo-refs WORKING and STAGED name uncommitted roots rather than commits, so when one +// of them is given it resolves to the commit at |headRef|, which is the commit those roots are +// built on top of. func ResolveRefSpec(ctx *sql.Context, headRef ref.DoltRef, doltDB *doltdb.DoltDB, spec string) (*doltdb.Commit, error) { + if doltdb.IsWorkingSetRef(spec) { + return doltDB.ResolveCommitRef(ctx, headRef) + } + cs, err := doltdb.NewCommitSpec(spec) if err != nil { return nil, err diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go index 28af324890f..9d2c258671e 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go @@ -29,8 +29,8 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/diff" "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" "github.com/dolthub/dolt/go/libraries/doltcore/merge" - "github.com/dolthub/dolt/go/libraries/doltcore/ref" "github.com/dolthub/dolt/go/libraries/doltcore/schema" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dfunctions" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dtables" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/index" @@ -345,17 +345,17 @@ func resolveCommitStrings(ctx *sql.Context, fromRef, toRef, dotRef interface{}, return "", "", err } - rightCm, err := resolveCommit(ctx, db.DbData().Ddb, headRef, refs[0]) + leftCm, err := dfunctions.ResolveRefSpec(ctx, headRef, db.DbData().Ddb, refs[0]) if err != nil { return "", "", err } - leftCm, err := resolveCommit(ctx, db.DbData().Ddb, headRef, refs[1]) + rightCm, err := dfunctions.ResolveRefSpec(ctx, headRef, db.DbData().Ddb, refs[1]) if err != nil { return "", "", err } - mergeBase, err := merge.MergeBase(ctx, rightCm, leftCm) + mergeBase, err := merge.MergeBase(ctx, leftCm, rightCm) if err != nil { return "", "", err } @@ -404,24 +404,6 @@ func interfaceToString(r interface{}) (string, error) { return str, nil } -func resolveCommit(ctx *sql.Context, ddb *doltdb.DoltDB, headRef ref.DoltRef, cSpecStr string) (*doltdb.Commit, error) { - cs, err := doltdb.NewCommitSpec(cSpecStr) - if err != nil { - return nil, err - } - - optCmt, err := ddb.Resolve(ctx, cs, headRef) - if err != nil { - return nil, err - } - cm, ok := optCmt.ToCommit() - if !ok { - return nil, doltdb.ErrGhostCommitEncountered - } - - return cm, nil -} - // WithChildren implements the sql.Node interface func (dtf *DiffTableFunction) WithChildren(ctx *sql.Context, node ...sql.Node) (sql.Node, error) { if len(node) != 0 { diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go index 5cda9235637..efd8c74c86f 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go @@ -28,6 +28,7 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/env/actions/commitwalk" "github.com/dolthub/dolt/go/libraries/doltcore/merge" "github.com/dolthub/dolt/go/libraries/doltcore/schema" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dfunctions" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dtables" "github.com/dolthub/dolt/go/store/datas" @@ -496,38 +497,20 @@ func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter } for _, revisionStr := range revisionValStrs { - cs, err := doltdb.NewCommitSpec(revisionStr) + commit, err = dfunctions.ResolveRefSpec(ctx, headRef, sqledb.DbData().Ddb, revisionStr) if err != nil { return nil, err } - optCmt, err := sqledb.DbData().Ddb.Resolve(ctx, cs, headRef) - if err != nil { - return nil, err - } - commit, ok = optCmt.ToCommit() - if err != nil { - return nil, doltdb.ErrGhostCommitEncountered - } - commits = append(commits, commit) } var notCommits []*doltdb.Commit for _, notRevisionStr := range notRevisionValStrs { - cs, err := doltdb.NewCommitSpec(notRevisionStr) - if err != nil { - return nil, err - } - - optCmt, err := sqledb.DbData().Ddb.Resolve(ctx, cs, headRef) + notCommit, err := dfunctions.ResolveRefSpec(ctx, headRef, sqledb.DbData().Ddb, notRevisionStr) if err != nil { return nil, err } - notCommit, ok := optCmt.ToCommit() - if !ok { - return nil, doltdb.ErrGhostCommitEncountered - } notCommits = append(notCommits, notCommit) } @@ -538,20 +521,11 @@ func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter return nil, err } - mergeCs, err := doltdb.NewCommitSpec(mergeBase.String()) - if err != nil { - return nil, err - } - // Use merge base as excluding commit - optCmt, err := sqledb.DbData().Ddb.Resolve(ctx, mergeCs, nil) + mergeCommit, err := dfunctions.ResolveRefSpec(ctx, nil, sqledb.DbData().Ddb, mergeBase.String()) if err != nil { return nil, err } - mergeCommit, ok := optCmt.ToCommit() - if !ok { - return nil, doltdb.ErrGhostCommitEncountered - } notCommits = append(notCommits, mergeCommit) diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go index 30726e49ab3..4cc4d415e73 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go @@ -26,6 +26,7 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" "github.com/dolthub/dolt/go/libraries/doltcore/merge" "github.com/dolthub/dolt/go/libraries/doltcore/schema" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dfunctions" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" "github.com/dolthub/dolt/go/store/prolly/tree" ) @@ -297,12 +298,12 @@ func resolveBranchesToRoots(ctx *sql.Context, db dsess.SqlDatabase, leftBranch, return rootInfo{}, err } - leftCm, err := resolveCommit(ctx, db.DbData().Ddb, headRef, leftBranch) + leftCm, err := dfunctions.ResolveRefSpec(ctx, headRef, db.DbData().Ddb, leftBranch) if err != nil { return rootInfo{}, err } - rightCm, err := resolveCommit(ctx, db.DbData().Ddb, headRef, rightBranch) + rightCm, err := dfunctions.ResolveRefSpec(ctx, headRef, db.DbData().Ddb, rightBranch) if err != nil { return rootInfo{}, err } diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go index 9255ae36314..14ba428e106 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go @@ -5714,6 +5714,38 @@ var LogTableFunctionScriptTests = []queries.ScriptTest{ }, }, }, + { + // See https://github.com/dolthub/dolt/issues/11204 + Name: "dolt_log: three dot diff accepts WORKING and STAGED", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_add('.');", + "call dolt_commit('-m', 'commit 1');", + "call dolt_checkout('-b', 'new-branch');", + "insert into t values (1);", + "call dolt_commit('-am', 'commit 2');", + "insert into t values (2);", + "call dolt_commit('-am', 'commit 3');", + "call dolt_checkout('main');", + "insert into t values (99);", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT count(*) from dolt_log('main...new-branch');", + Expected: []sql.Row{{2}}, + }, + { + Query: "SELECT count(*) from dolt_log('WORKING...new-branch');", + // WORKING is not a commit, so it resolves to the current branch HEAD and + // this counts the same commits as main...new-branch. + Expected: []sql.Row{{2}}, + }, + { + Query: "SELECT count(*) from dolt_log('STAGED...new-branch');", + Expected: []sql.Row{{2}}, + }, + }, + }, } var JsonDiffTableFunctionScriptTests = []queries.ScriptTest{ diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go index 449b32aedf8..44947a8402b 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go @@ -2016,6 +2016,94 @@ on a.to_pk = b.to_pk;`, }, }, }, + { + // See https://github.com/dolthub/dolt/issues/11204 + Name: "dolt_diff: three dot diff supports WORKING and STAGED", + SetUpScript: []string{ + "CREATE TABLE t (pk int primary key, c1 varchar(20));", + "INSERT INTO t VALUES (1, 'one');", + "CALL DOLT_ADD('.');", + "CALL DOLT_COMMIT('-m', 'init');", + + "CALL DOLT_BRANCH('bar');", + "CALL DOLT_CHECKOUT('bar');", + "INSERT INTO t VALUES (2, 'two');", + "CALL DOLT_COMMIT('-am', 'bar change');", + + // Row 3 is staged and row 4 is left in the working tree so that the staged + // root and the working root differ from each other and from HEAD. + "CALL DOLT_CHECKOUT('main');", + "INSERT INTO t VALUES (3, 'three');", + "CALL DOLT_ADD('.');", + "INSERT INTO t VALUES (4, 'four');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('HEAD...bar', 't') ORDER BY COALESCE(from_pk, to_pk);", + Expected: []sql.Row{ + {nil, 2, "added"}, + }, + }, + { + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('WORKING...bar', 't') ORDER BY COALESCE(from_pk, to_pk);", + // A revision to the left of the three dots only chooses the merge base, so + // the uncommitted change on the current branch is not part of the diff and + // the result is the same as for HEAD. + Expected: []sql.Row{ + {nil, 2, "added"}, + }, + }, + { + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('STAGED...bar', 't') ORDER BY COALESCE(from_pk, to_pk);", + Expected: []sql.Row{ + {nil, 2, "added"}, + }, + }, + { + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('bar...WORKING', 't') ORDER BY COALESCE(from_pk, to_pk);", + // A revision to the right of the three dots is the diff target, so the + // working root contributes both of its uncommitted rows. + Expected: []sql.Row{ + {nil, 3, "added"}, + {nil, 4, "added"}, + }, + }, + { + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('bar...STAGED', 't') ORDER BY COALESCE(from_pk, to_pk);", + // The staged root contains row 3 but not row 4, which was never added. + Expected: []sql.Row{ + {nil, 3, "added"}, + }, + }, + { + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('WORKING...STAGED', 't') ORDER BY COALESCE(from_pk, to_pk);", + // The merge base of two revisions on the same branch is that branch HEAD, + // so this compares HEAD against the staged root. + Expected: []sql.Row{ + {nil, 3, "added"}, + }, + }, + { + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('STAGED...WORKING', 't') ORDER BY COALESCE(from_pk, to_pk);", + Expected: []sql.Row{ + {nil, 3, "added"}, + {nil, 4, "added"}, + }, + }, + { + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('HEAD...WORKING', 't') ORDER BY COALESCE(from_pk, to_pk);", + Expected: []sql.Row{ + {nil, 3, "added"}, + {nil, 4, "added"}, + }, + }, + { + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('WORKING...HEAD', 't') ORDER BY COALESCE(from_pk, to_pk);", + // The merge base and the target are both HEAD, so there is no diff. + Expected: []sql.Row{}, + }, + }, + }, } var DiffStatTableFunctionScriptTests = []queries.ScriptTest{ @@ -2718,6 +2806,38 @@ inner join t as of @Commit3 on rows_unmodified = t.pk;`, }, }, }, + { + // See https://github.com/dolthub/dolt/issues/11204 + Name: "dolt_diff_stat: three dot diff supports WORKING and STAGED", + SetUpScript: []string{ + "CREATE TABLE t (pk int primary key, c1 varchar(20));", + "INSERT INTO t VALUES (1, 'one');", + "CALL DOLT_ADD('.');", + "CALL DOLT_COMMIT('-m', 'init');", + "CALL DOLT_BRANCH('bar');", + "CALL DOLT_CHECKOUT('bar');", + "INSERT INTO t VALUES (2, 'two');", + "CALL DOLT_COMMIT('-am', 'bar change');", + "CALL DOLT_CHECKOUT('main');", + "INSERT INTO t VALUES (3, 'three');", + "CALL DOLT_ADD('.');", + "INSERT INTO t VALUES (4, 'four');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT table_name, rows_added FROM dolt_diff_stat('WORKING...bar', 't');", + Expected: []sql.Row{{"t", 1}}, + }, + { + Query: "SELECT table_name, rows_added FROM dolt_diff_stat('bar...WORKING', 't');", + Expected: []sql.Row{{"t", 2}}, + }, + { + Query: "SELECT table_name, rows_added FROM dolt_diff_stat('bar...STAGED', 't');", + Expected: []sql.Row{{"t", 1}}, + }, + }, + }, } var DiffSummaryTableFunctionScriptTests = []queries.ScriptTest{ @@ -3493,6 +3613,38 @@ var DiffSummaryTableFunctionScriptTests = []queries.ScriptTest{ }, }, }, + { + // See https://github.com/dolthub/dolt/issues/11204 + Name: "dolt_diff_summary: three dot diff supports WORKING and STAGED", + SetUpScript: []string{ + "CREATE TABLE t (pk int primary key, c1 varchar(20));", + "INSERT INTO t VALUES (1, 'one');", + "CALL DOLT_ADD('.');", + "CALL DOLT_COMMIT('-m', 'init');", + "CALL DOLT_BRANCH('bar');", + "CALL DOLT_CHECKOUT('bar');", + "INSERT INTO t VALUES (2, 'two');", + "CALL DOLT_COMMIT('-am', 'bar change');", + "CALL DOLT_CHECKOUT('main');", + "INSERT INTO t VALUES (3, 'three');", + "CALL DOLT_ADD('.');", + "INSERT INTO t VALUES (4, 'four');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "SELECT * from dolt_diff_summary('WORKING...bar', 't');", + Expected: []sql.Row{{"t", "t", "modified", true, false}}, + }, + { + Query: "SELECT * from dolt_diff_summary('bar...WORKING', 't');", + Expected: []sql.Row{{"t", "t", "modified", true, false}}, + }, + { + Query: "SELECT * from dolt_diff_summary('bar...STAGED', 't');", + Expected: []sql.Row{{"t", "t", "modified", true, false}}, + }, + }, + }, } var PatchTableFunctionScriptTests = []queries.ScriptTest{ diff --git a/integration-tests/bats/diff.bats b/integration-tests/bats/diff.bats index 6ec1ee7c37a..1e2ec95e830 100644 --- a/integration-tests/bats/diff.bats +++ b/integration-tests/bats/diff.bats @@ -491,6 +491,59 @@ SQL [[ ! "$output" =~ "- | 2" ]] || false } +@test "diff: three dot diff supports WORKING and STAGED" { + # See https://github.com/dolthub/dolt/issues/11204 + # TODO: remove this once dolt checkout is migrated + if [ "$SQL_ENGINE" = "remote-engine" ]; then + skip "This test relies on dolt checkout, which has not been migrated yet." + fi + + dolt checkout main + dolt sql -q 'insert into test values (0,0,0,0,0,0)' + dolt add . + dolt commit -m table + dolt checkout -b branch1 + dolt sql -q 'insert into test values (1,1,1,1,1,1)' + dolt add . + dolt commit -m row + dolt checkout main + # Row 2 is staged and row 3 is left in the working set so that the staged root and + # the working root differ from each other. + dolt sql -q 'insert into test values (2,2,2,2,2,2)' + dolt add . + dolt sql -q 'insert into test values (3,3,3,3,3,3)' + + # A revision to the left of the three dots only chooses the merge base, so the + # uncommitted rows do not appear and the result is the same as for HEAD. + run dolt diff WORKING...branch1 + [ "$status" -eq 0 ] + [[ "$output" =~ "+ | 1" ]] || false + [[ ! "$output" =~ "+ | 2" ]] || false + [[ ! "$output" =~ "+ | 3" ]] || false + + run dolt diff STAGED...branch1 + [ "$status" -eq 0 ] + [[ "$output" =~ "+ | 1" ]] || false + [[ ! "$output" =~ "+ | 3" ]] || false + + # A revision to the right of the three dots is the diff target, so the working and + # staged rows are surfaced. + run dolt diff branch1...WORKING + [ "$status" -eq 0 ] + [[ "$output" =~ "+ | 2" ]] || false + [[ "$output" =~ "+ | 3" ]] || false + [[ ! "$output" =~ "+ | 1" ]] || false + + run dolt diff branch1...STAGED + [ "$status" -eq 0 ] + [[ "$output" =~ "+ | 2" ]] || false + [[ ! "$output" =~ "+ | 3" ]] || false + + run dolt sql -q "SELECT dolt_merge_base('branch1', 'WORKING') = dolt_merge_base('branch1', 'HEAD') as eq" + [ "$status" -eq 0 ] + [[ "$output" =~ "true" ]] || false +} + @test "diff: data and schema changes" { dolt sql < Date: Mon, 15 Jun 2026 21:24:12 +0000 Subject: [PATCH 2/8] move ref spec resolution onto doltdb --- go/libraries/doltcore/doltdb/doltdb.go | 26 ++++++++++++++++ .../doltcore/sqle/dfunctions/has_ancestor.go | 4 +-- .../doltcore/sqle/dfunctions/merge_base.go | 30 ++----------------- .../doltcore/sqle/dprocedures/dolt_rebase.go | 3 +- .../sqle/dtablefunctions/dolt_diff.go | 5 ++-- .../doltcore/sqle/dtablefunctions/dolt_log.go | 7 ++--- .../dolt_preview_merge_conflicts_summary.go | 5 ++-- 7 files changed, 38 insertions(+), 42 deletions(-) diff --git a/go/libraries/doltcore/doltdb/doltdb.go b/go/libraries/doltcore/doltdb/doltdb.go index ee1205f3a99..d73818b4b01 100644 --- a/go/libraries/doltcore/doltdb/doltdb.go +++ b/go/libraries/doltcore/doltdb/doltdb.go @@ -642,6 +642,32 @@ func (ddb *DoltDB) ResolveCommitRef(ctx context.Context, doltRef ref.DoltRef) (* return NewCommit(ctx, ddb.vrw, ddb.ns, commitVal) } +// ResolveRefSpec resolves the commit spec |spec| to a commit using |headRef|. +// The pseudo-refs WORKING and STAGED name uncommitted roots rather than commits, so when one +// of them is given it resolves to the commit at |headRef|, which is the commit those roots are +// built on top of. +func (ddb *DoltDB) ResolveRefSpec(ctx context.Context, headRef ref.DoltRef, spec string) (*Commit, error) { + if IsWorkingSetRef(spec) { + return ddb.ResolveCommitRef(ctx, headRef) + } + + cs, err := NewCommitSpec(spec) + if err != nil { + return nil, err + } + + optCmt, err := ddb.Resolve(ctx, cs, headRef) + if err != nil { + return nil, err + } + commit, ok := optCmt.ToCommit() + if !ok { + return nil, ErrGhostCommitEncountered + } + + return commit, nil +} + // ResolveCommitRefAtRoot takes a DoltRef and returns a Commit, or an error if the commit cannot be found. The ref given must // point to a Commit. func (ddb *DoltDB) ResolveCommitRefAtRoot(ctx context.Context, ref ref.DoltRef, nomsRoot hash.Hash) (*Commit, error) { diff --git a/go/libraries/doltcore/sqle/dfunctions/has_ancestor.go b/go/libraries/doltcore/sqle/dfunctions/has_ancestor.go index 78977177537..b01225b52e8 100644 --- a/go/libraries/doltcore/sqle/dfunctions/has_ancestor.go +++ b/go/libraries/doltcore/sqle/dfunctions/has_ancestor.go @@ -69,7 +69,7 @@ func (a *HasAncestor) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { if err != nil { return nil, err } - headCommit, err = ResolveRefSpec(ctx, headRef, ddb, headStr.(string)) + headCommit, err = ddb.ResolveRefSpec(ctx, headRef, headStr.(string)) if err != nil { return nil, err } @@ -85,7 +85,7 @@ func (a *HasAncestor) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { if err != nil { return nil, err } - ancCommit, err = ResolveRefSpec(ctx, headRef, ddb, ancStr.(string)) + ancCommit, err = ddb.ResolveRefSpec(ctx, headRef, ancStr.(string)) if err != nil { return nil, err } diff --git a/go/libraries/doltcore/sqle/dfunctions/merge_base.go b/go/libraries/doltcore/sqle/dfunctions/merge_base.go index 238b1e98e28..6d1ebbf5e3e 100644 --- a/go/libraries/doltcore/sqle/dfunctions/merge_base.go +++ b/go/libraries/doltcore/sqle/dfunctions/merge_base.go @@ -22,9 +22,7 @@ import ( "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/types" - "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" "github.com/dolthub/dolt/go/libraries/doltcore/merge" - "github.com/dolthub/dolt/go/libraries/doltcore/ref" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" ) @@ -77,11 +75,11 @@ func (d MergeBase) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, errors.New("right value is not a string") } - left, err := ResolveRefSpec(ctx, headRef, doltDB, leftStr) + left, err := doltDB.ResolveRefSpec(ctx, headRef, leftStr) if err != nil { return nil, err } - right, err := ResolveRefSpec(ctx, headRef, doltDB, rightStr) + right, err := doltDB.ResolveRefSpec(ctx, headRef, rightStr) if err != nil { return nil, err } @@ -94,30 +92,6 @@ func (d MergeBase) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return mergeBase.String(), nil } -// ResolveRefSpec resolves the commit spec |spec| to a commit using |headRef| and |doltDB|. -// The pseudo-refs WORKING and STAGED name uncommitted roots rather than commits, so when one -// of them is given it resolves to the commit at |headRef|, which is the commit those roots are -// built on top of. -func ResolveRefSpec(ctx *sql.Context, headRef ref.DoltRef, doltDB *doltdb.DoltDB, spec string) (*doltdb.Commit, error) { - if doltdb.IsWorkingSetRef(spec) { - return doltDB.ResolveCommitRef(ctx, headRef) - } - - cs, err := doltdb.NewCommitSpec(spec) - if err != nil { - return nil, err - } - optCmt, err := doltDB.Resolve(ctx, cs, headRef) - if err != nil { - return nil, err - } - commit, ok := optCmt.ToCommit() - if !ok { - return nil, doltdb.ErrGhostCommitEncountered - } - return commit, err -} - // String implements the sql.Expression interface. func (d MergeBase) String() string { return fmt.Sprintf("DOLT_MERGE_BASE(%s,%s)", d.Left().String(), d.Right().String()) diff --git a/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go b/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go index c42eb4eb9c8..3014b0048c8 100644 --- a/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go +++ b/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go @@ -32,7 +32,6 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/merge" "github.com/dolthub/dolt/go/libraries/doltcore/rebase" "github.com/dolthub/dolt/go/libraries/doltcore/ref" - "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dfunctions" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" "github.com/dolthub/dolt/go/libraries/utils/argparser" ) @@ -875,7 +874,7 @@ func commitManuallyStagedChangesForStep(ctx *sql.Context, step rebase.RebasePlan return err } - originalCommit, err := dfunctions.ResolveRefSpec(ctx, headRef, doltDB, step.CommitHash) + originalCommit, err := doltDB.ResolveRefSpec(ctx, headRef, step.CommitHash) if err != nil { return err } diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go index 9d2c258671e..12d86a7ced4 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go @@ -30,7 +30,6 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" "github.com/dolthub/dolt/go/libraries/doltcore/merge" "github.com/dolthub/dolt/go/libraries/doltcore/schema" - "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dfunctions" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dtables" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/index" @@ -345,12 +344,12 @@ func resolveCommitStrings(ctx *sql.Context, fromRef, toRef, dotRef interface{}, return "", "", err } - leftCm, err := dfunctions.ResolveRefSpec(ctx, headRef, db.DbData().Ddb, refs[0]) + leftCm, err := db.DbData().Ddb.ResolveRefSpec(ctx, headRef, refs[0]) if err != nil { return "", "", err } - rightCm, err := dfunctions.ResolveRefSpec(ctx, headRef, db.DbData().Ddb, refs[1]) + rightCm, err := db.DbData().Ddb.ResolveRefSpec(ctx, headRef, refs[1]) if err != nil { return "", "", err } diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go index efd8c74c86f..00d5cba2d98 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go @@ -28,7 +28,6 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/env/actions/commitwalk" "github.com/dolthub/dolt/go/libraries/doltcore/merge" "github.com/dolthub/dolt/go/libraries/doltcore/schema" - "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dfunctions" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dtables" "github.com/dolthub/dolt/go/store/datas" @@ -497,7 +496,7 @@ func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter } for _, revisionStr := range revisionValStrs { - commit, err = dfunctions.ResolveRefSpec(ctx, headRef, sqledb.DbData().Ddb, revisionStr) + commit, err := sqledb.DbData().Ddb.ResolveRefSpec(ctx, headRef, revisionStr) if err != nil { return nil, err } @@ -507,7 +506,7 @@ func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter var notCommits []*doltdb.Commit for _, notRevisionStr := range notRevisionValStrs { - notCommit, err := dfunctions.ResolveRefSpec(ctx, headRef, sqledb.DbData().Ddb, notRevisionStr) + notCommit, err := sqledb.DbData().Ddb.ResolveRefSpec(ctx, headRef, notRevisionStr) if err != nil { return nil, err } @@ -522,7 +521,7 @@ func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter } // Use merge base as excluding commit - mergeCommit, err := dfunctions.ResolveRefSpec(ctx, nil, sqledb.DbData().Ddb, mergeBase.String()) + mergeCommit, err := sqledb.DbData().Ddb.ResolveRefSpec(ctx, nil, mergeBase.String()) if err != nil { return nil, err } diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go index 4cc4d415e73..894a02459ea 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go @@ -26,7 +26,6 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" "github.com/dolthub/dolt/go/libraries/doltcore/merge" "github.com/dolthub/dolt/go/libraries/doltcore/schema" - "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dfunctions" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" "github.com/dolthub/dolt/go/store/prolly/tree" ) @@ -298,12 +297,12 @@ func resolveBranchesToRoots(ctx *sql.Context, db dsess.SqlDatabase, leftBranch, return rootInfo{}, err } - leftCm, err := dfunctions.ResolveRefSpec(ctx, headRef, db.DbData().Ddb, leftBranch) + leftCm, err := db.DbData().Ddb.ResolveRefSpec(ctx, headRef, leftBranch) if err != nil { return rootInfo{}, err } - rightCm, err := dfunctions.ResolveRefSpec(ctx, headRef, db.DbData().Ddb, rightBranch) + rightCm, err := db.DbData().Ddb.ResolveRefSpec(ctx, headRef, rightBranch) if err != nil { return rootInfo{}, err } From 692d98199b2252a433d2cd534e61ab516f0bd6a9 Mon Sep 17 00:00:00 2001 From: elianddb Date: Tue, 16 Jun 2026 16:05:27 +0000 Subject: [PATCH 3/8] rename and guard the doltdb commit spec resolver --- go/libraries/doltcore/doltdb/doltdb.go | 15 ++++++++++----- .../doltcore/sqle/dfunctions/has_ancestor.go | 4 ++-- .../doltcore/sqle/dfunctions/merge_base.go | 4 ++-- .../doltcore/sqle/dprocedures/dolt_rebase.go | 2 +- .../doltcore/sqle/dtablefunctions/dolt_diff.go | 4 ++-- .../doltcore/sqle/dtablefunctions/dolt_log.go | 6 +++--- .../dolt_preview_merge_conflicts_summary.go | 4 ++-- 7 files changed, 22 insertions(+), 17 deletions(-) diff --git a/go/libraries/doltcore/doltdb/doltdb.go b/go/libraries/doltcore/doltdb/doltdb.go index d73818b4b01..f3fafc7b659 100644 --- a/go/libraries/doltcore/doltdb/doltdb.go +++ b/go/libraries/doltcore/doltdb/doltdb.go @@ -642,12 +642,17 @@ func (ddb *DoltDB) ResolveCommitRef(ctx context.Context, doltRef ref.DoltRef) (* return NewCommit(ctx, ddb.vrw, ddb.ns, commitVal) } -// ResolveRefSpec resolves the commit spec |spec| to a commit using |headRef|. -// The pseudo-refs WORKING and STAGED name uncommitted roots rather than commits, so when one -// of them is given it resolves to the commit at |headRef|, which is the commit those roots are -// built on top of. -func (ddb *DoltDB) ResolveRefSpec(ctx context.Context, headRef ref.DoltRef, spec string) (*Commit, error) { +// ResolveCommitSpecStr resolves the commit spec string |spec| to a commit, using |headRef| to +// anchor relative specs such as HEAD. The pseudo-refs WORKING and STAGED name uncommitted roots +// rather than commits, so when one of them is given it resolves to the commit at |headRef| that +// those roots are built on top of. A working set exists only relative to a branch, so |headRef| +// must be non-nil for WORKING or STAGED, otherwise ResolveCommitSpecStr returns +// ErrOperationNotSupportedInDetachedHead. +func (ddb *DoltDB) ResolveCommitSpecStr(ctx context.Context, spec string, headRef ref.DoltRef) (*Commit, error) { if IsWorkingSetRef(spec) { + if headRef == nil { + return nil, ErrOperationNotSupportedInDetachedHead + } return ddb.ResolveCommitRef(ctx, headRef) } diff --git a/go/libraries/doltcore/sqle/dfunctions/has_ancestor.go b/go/libraries/doltcore/sqle/dfunctions/has_ancestor.go index b01225b52e8..60ac6e0b6a3 100644 --- a/go/libraries/doltcore/sqle/dfunctions/has_ancestor.go +++ b/go/libraries/doltcore/sqle/dfunctions/has_ancestor.go @@ -69,7 +69,7 @@ func (a *HasAncestor) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { if err != nil { return nil, err } - headCommit, err = ddb.ResolveRefSpec(ctx, headRef, headStr.(string)) + headCommit, err = ddb.ResolveCommitSpecStr(ctx, headStr.(string), headRef) if err != nil { return nil, err } @@ -85,7 +85,7 @@ func (a *HasAncestor) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { if err != nil { return nil, err } - ancCommit, err = ddb.ResolveRefSpec(ctx, headRef, ancStr.(string)) + ancCommit, err = ddb.ResolveCommitSpecStr(ctx, ancStr.(string), headRef) if err != nil { return nil, err } diff --git a/go/libraries/doltcore/sqle/dfunctions/merge_base.go b/go/libraries/doltcore/sqle/dfunctions/merge_base.go index 6d1ebbf5e3e..560238e7c98 100644 --- a/go/libraries/doltcore/sqle/dfunctions/merge_base.go +++ b/go/libraries/doltcore/sqle/dfunctions/merge_base.go @@ -75,11 +75,11 @@ func (d MergeBase) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, errors.New("right value is not a string") } - left, err := doltDB.ResolveRefSpec(ctx, headRef, leftStr) + left, err := doltDB.ResolveCommitSpecStr(ctx, leftStr, headRef) if err != nil { return nil, err } - right, err := doltDB.ResolveRefSpec(ctx, headRef, rightStr) + right, err := doltDB.ResolveCommitSpecStr(ctx, rightStr, headRef) if err != nil { return nil, err } diff --git a/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go b/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go index 3014b0048c8..0d8ebd5e239 100644 --- a/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go +++ b/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go @@ -874,7 +874,7 @@ func commitManuallyStagedChangesForStep(ctx *sql.Context, step rebase.RebasePlan return err } - originalCommit, err := doltDB.ResolveRefSpec(ctx, headRef, step.CommitHash) + originalCommit, err := doltDB.ResolveCommitSpecStr(ctx, step.CommitHash, headRef) if err != nil { return err } diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go index 12d86a7ced4..32f71df53a8 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go @@ -344,12 +344,12 @@ func resolveCommitStrings(ctx *sql.Context, fromRef, toRef, dotRef interface{}, return "", "", err } - leftCm, err := db.DbData().Ddb.ResolveRefSpec(ctx, headRef, refs[0]) + leftCm, err := db.DbData().Ddb.ResolveCommitSpecStr(ctx, refs[0], headRef) if err != nil { return "", "", err } - rightCm, err := db.DbData().Ddb.ResolveRefSpec(ctx, headRef, refs[1]) + rightCm, err := db.DbData().Ddb.ResolveCommitSpecStr(ctx, refs[1], headRef) if err != nil { return "", "", err } diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go index 00d5cba2d98..2a963e56bd3 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go @@ -496,7 +496,7 @@ func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter } for _, revisionStr := range revisionValStrs { - commit, err := sqledb.DbData().Ddb.ResolveRefSpec(ctx, headRef, revisionStr) + commit, err := sqledb.DbData().Ddb.ResolveCommitSpecStr(ctx, revisionStr, headRef) if err != nil { return nil, err } @@ -506,7 +506,7 @@ func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter var notCommits []*doltdb.Commit for _, notRevisionStr := range notRevisionValStrs { - notCommit, err := sqledb.DbData().Ddb.ResolveRefSpec(ctx, headRef, notRevisionStr) + notCommit, err := sqledb.DbData().Ddb.ResolveCommitSpecStr(ctx, notRevisionStr, headRef) if err != nil { return nil, err } @@ -521,7 +521,7 @@ func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter } // Use merge base as excluding commit - mergeCommit, err := sqledb.DbData().Ddb.ResolveRefSpec(ctx, nil, mergeBase.String()) + mergeCommit, err := sqledb.DbData().Ddb.ResolveCommitSpecStr(ctx, mergeBase.String(), nil) if err != nil { return nil, err } diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go index 894a02459ea..fedc3ade269 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_preview_merge_conflicts_summary.go @@ -297,12 +297,12 @@ func resolveBranchesToRoots(ctx *sql.Context, db dsess.SqlDatabase, leftBranch, return rootInfo{}, err } - leftCm, err := db.DbData().Ddb.ResolveRefSpec(ctx, headRef, leftBranch) + leftCm, err := db.DbData().Ddb.ResolveCommitSpecStr(ctx, leftBranch, headRef) if err != nil { return rootInfo{}, err } - rightCm, err := db.DbData().Ddb.ResolveRefSpec(ctx, headRef, rightBranch) + rightCm, err := db.DbData().Ddb.ResolveCommitSpecStr(ctx, rightBranch, headRef) if err != nil { return rootInfo{}, err } From 178bd57ee805eb11ec8ef0b568bfb2aa2148e10a Mon Sep 17 00:00:00 2001 From: elianddb Date: Tue, 16 Jun 2026 16:05:27 +0000 Subject: [PATCH 4/8] strengthen three-dot working and staged tests --- .../doltcore/sqle/enginetest/dolt_queries.go | 36 +++++++++++++++---- .../sqle/enginetest/dolt_queries_diff.go | 10 ++++-- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go index 14ba428e106..d72e0345367 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go @@ -5731,18 +5731,40 @@ var LogTableFunctionScriptTests = []queries.ScriptTest{ }, Assertions: []queries.ScriptTestAssertion{ { - Query: "SELECT count(*) from dolt_log('main...new-branch');", - Expected: []sql.Row{{2}}, + Query: "SELECT message from dolt_log('main...new-branch') ORDER BY date DESC;", + Expected: []sql.Row{{"commit 3"}, {"commit 2"}}, }, { - Query: "SELECT count(*) from dolt_log('WORKING...new-branch');", + Query: "SELECT message from dolt_log('WORKING...new-branch') ORDER BY date DESC;", // WORKING is not a commit, so it resolves to the current branch HEAD and - // this counts the same commits as main...new-branch. - Expected: []sql.Row{{2}}, + // the merge base excludes exactly the commits main...new-branch does. + Expected: []sql.Row{{"commit 3"}, {"commit 2"}}, }, { - Query: "SELECT count(*) from dolt_log('STAGED...new-branch');", - Expected: []sql.Row{{2}}, + Query: "SELECT message from dolt_log('STAGED...new-branch') ORDER BY date DESC;", + Expected: []sql.Row{{"commit 3"}, {"commit 2"}}, + }, + }, + }, + { + // See https://github.com/dolthub/dolt/issues/11204 + Name: "dolt_log: three dot WORKING on a detached head returns an error", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_add('.');", + "call dolt_commit('-m', 'commit 1');", + "call dolt_tag('v1');", + "call dolt_checkout('-b', 'new-branch');", + "insert into t values (1);", + "call dolt_commit('-am', 'commit 2');", + "use `mydb/v1`;", + }, + Assertions: []queries.ScriptTestAssertion{ + { + // A working set exists only relative to a branch, so resolving WORKING + // without a checked out branch returns an error. + Query: "SELECT count(*) from dolt_log('WORKING...new-branch');", + ExpectedErrStr: "this operation is not supported while in a detached head state", }, }, }, diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go index 44947a8402b..c41b8536823 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go @@ -2102,6 +2102,10 @@ on a.to_pk = b.to_pk;`, // The merge base and the target are both HEAD, so there is no diff. Expected: []sql.Row{}, }, + { + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('bar...nope', 't');", + ExpectedErrStr: "branch not found: nope", + }, }, }, } @@ -3632,15 +3636,15 @@ var DiffSummaryTableFunctionScriptTests = []queries.ScriptTest{ }, Assertions: []queries.ScriptTestAssertion{ { - Query: "SELECT * from dolt_diff_summary('WORKING...bar', 't');", + Query: "SELECT from_table_name, to_table_name, diff_type, data_change, schema_change from dolt_diff_summary('WORKING...bar', 't');", Expected: []sql.Row{{"t", "t", "modified", true, false}}, }, { - Query: "SELECT * from dolt_diff_summary('bar...WORKING', 't');", + Query: "SELECT from_table_name, to_table_name, diff_type, data_change, schema_change from dolt_diff_summary('bar...WORKING', 't');", Expected: []sql.Row{{"t", "t", "modified", true, false}}, }, { - Query: "SELECT * from dolt_diff_summary('bar...STAGED', 't');", + Query: "SELECT from_table_name, to_table_name, diff_type, data_change, schema_change from dolt_diff_summary('bar...STAGED', 't');", Expected: []sql.Row{{"t", "t", "modified", true, false}}, }, }, From ec4376d8cf1ddc33c4f8f28931c7b050f7bfb021 Mon Sep 17 00:00:00 2001 From: elianddb Date: Tue, 16 Jun 2026 16:15:23 +0000 Subject: [PATCH 5/8] document detached head three-dot parity gap --- .../doltcore/sqle/enginetest/dolt_queries.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go index d72e0345367..cdb9bad6b52 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go @@ -5748,7 +5748,7 @@ var LogTableFunctionScriptTests = []queries.ScriptTest{ }, { // See https://github.com/dolthub/dolt/issues/11204 - Name: "dolt_log: three dot WORKING on a detached head returns an error", + Name: "dolt_log: three dot WORKING on a detached head", SetUpScript: []string{ "create table t (pk int primary key);", "call dolt_add('.');", @@ -5762,10 +5762,18 @@ var LogTableFunctionScriptTests = []queries.ScriptTest{ Assertions: []queries.ScriptTestAssertion{ { // A working set exists only relative to a branch, so resolving WORKING - // without a checked out branch returns an error. - Query: "SELECT count(*) from dolt_log('WORKING...new-branch');", + // without a checked out branch returns an error today. + Query: "SELECT message from dolt_log('WORKING...new-branch') ORDER BY date DESC;", ExpectedErrStr: "this operation is not supported while in a detached head state", }, + { + // TODO: dolt cannot yet resolve a relative ref for a merge base on a detached + // head, so unlike git this errors instead of listing what new-branch added. + // Remove Skip once dolt resolves WORKING against the detached HEAD commit. + Skip: true, + Query: "SELECT message from dolt_log('WORKING...new-branch') ORDER BY date DESC;", + Expected: []sql.Row{{"commit 2"}}, + }, }, }, } From bae86643792315ec25e3bfea5293a6443c87f441 Mon Sep 17 00:00:00 2001 From: elianddb Date: Tue, 16 Jun 2026 17:21:05 +0000 Subject: [PATCH 6/8] run three-dot diff bats on sql-server --- integration-tests/bats/diff.bats | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/integration-tests/bats/diff.bats b/integration-tests/bats/diff.bats index 1e2ec95e830..634ec59e2e3 100644 --- a/integration-tests/bats/diff.bats +++ b/integration-tests/bats/diff.bats @@ -493,20 +493,11 @@ SQL @test "diff: three dot diff supports WORKING and STAGED" { # See https://github.com/dolthub/dolt/issues/11204 - # TODO: remove this once dolt checkout is migrated - if [ "$SQL_ENGINE" = "remote-engine" ]; then - skip "This test relies on dolt checkout, which has not been migrated yet." - fi - - dolt checkout main dolt sql -q 'insert into test values (0,0,0,0,0,0)' dolt add . dolt commit -m table - dolt checkout -b branch1 - dolt sql -q 'insert into test values (1,1,1,1,1,1)' - dolt add . - dolt commit -m row - dolt checkout main + dolt sql -q "call dolt_branch('branch1')" + dolt --branch branch1 sql -q "insert into test values (1,1,1,1,1,1); call dolt_add('.'); call dolt_commit('-m', 'row');" # Row 2 is staged and row 3 is left in the working set so that the staged root and # the working root differ from each other. dolt sql -q 'insert into test values (2,2,2,2,2,2)' @@ -539,9 +530,9 @@ SQL [[ "$output" =~ "+ | 2" ]] || false [[ ! "$output" =~ "+ | 3" ]] || false - run dolt sql -q "SELECT dolt_merge_base('branch1', 'WORKING') = dolt_merge_base('branch1', 'HEAD') as eq" - [ "$status" -eq 0 ] - [[ "$output" =~ "true" ]] || false + working_base=$(dolt sql -r csv -q "SELECT dolt_merge_base('branch1', 'WORKING')" | tail -n 1) + head_base=$(dolt sql -r csv -q "SELECT dolt_merge_base('branch1', 'HEAD')" | tail -n 1) + [ "$working_base" = "$head_base" ] } @test "diff: data and schema changes" { From b461da5e6f5ebf24082210b147465ff7759bcede Mon Sep 17 00:00:00 2001 From: elianddb Date: Wed, 17 Jun 2026 19:01:50 +0000 Subject: [PATCH 7/8] scope working and staged to merge base resolution --- go/libraries/doltcore/doltdb/doltdb.go | 31 ++++++++++++------- .../doltcore/sqle/dfunctions/merge_base.go | 4 +-- .../sqle/dtablefunctions/dolt_diff.go | 10 ++++-- .../doltcore/sqle/dtablefunctions/dolt_log.go | 13 +++++++- .../doltcore/sqle/enginetest/dolt_queries.go | 10 ++++++ .../sqle/enginetest/dolt_queries_diff.go | 18 +++++------ 6 files changed, 59 insertions(+), 27 deletions(-) diff --git a/go/libraries/doltcore/doltdb/doltdb.go b/go/libraries/doltcore/doltdb/doltdb.go index f3fafc7b659..a37841a375d 100644 --- a/go/libraries/doltcore/doltdb/doltdb.go +++ b/go/libraries/doltcore/doltdb/doltdb.go @@ -644,18 +644,9 @@ func (ddb *DoltDB) ResolveCommitRef(ctx context.Context, doltRef ref.DoltRef) (* // ResolveCommitSpecStr resolves the commit spec string |spec| to a commit, using |headRef| to // anchor relative specs such as HEAD. The pseudo-refs WORKING and STAGED name uncommitted roots -// rather than commits, so when one of them is given it resolves to the commit at |headRef| that -// those roots are built on top of. A working set exists only relative to a branch, so |headRef| -// must be non-nil for WORKING or STAGED, otherwise ResolveCommitSpecStr returns -// ErrOperationNotSupportedInDetachedHead. +// rather than commits and are not valid here. Callers that need to treat them as a commit for the +// purpose of a merge base should use [DoltDB.ResolveCommitSpecStrForMergeBase] instead. func (ddb *DoltDB) ResolveCommitSpecStr(ctx context.Context, spec string, headRef ref.DoltRef) (*Commit, error) { - if IsWorkingSetRef(spec) { - if headRef == nil { - return nil, ErrOperationNotSupportedInDetachedHead - } - return ddb.ResolveCommitRef(ctx, headRef) - } - cs, err := NewCommitSpec(spec) if err != nil { return nil, err @@ -673,6 +664,24 @@ func (ddb *DoltDB) ResolveCommitSpecStr(ctx context.Context, spec string, headRe return commit, nil } +// ResolveCommitSpecStrForMergeBase resolves the commit spec string |spec| to the commit that +// stands in for it when computing a merge base, using |headRef| to anchor relative specs. It is +// the merge base variant of [DoltDB.ResolveCommitSpecStr]. The pseudo-refs WORKING and STAGED name +// uncommitted roots rather than commits, so they resolve to the commit at |headRef| that those +// roots are built on top of, which is the fork point a three dot diff or log anchors against. A +// working set exists only relative to a branch, so |headRef| must be non-nil for WORKING or +// STAGED, otherwise it returns [ErrOperationNotSupportedInDetachedHead]. +func (ddb *DoltDB) ResolveCommitSpecStrForMergeBase(ctx context.Context, spec string, headRef ref.DoltRef) (*Commit, error) { + if IsWorkingSetRef(spec) { + if headRef == nil { + return nil, ErrOperationNotSupportedInDetachedHead + } + return ddb.ResolveCommitRef(ctx, headRef) + } + + return ddb.ResolveCommitSpecStr(ctx, spec, headRef) +} + // ResolveCommitRefAtRoot takes a DoltRef and returns a Commit, or an error if the commit cannot be found. The ref given must // point to a Commit. func (ddb *DoltDB) ResolveCommitRefAtRoot(ctx context.Context, ref ref.DoltRef, nomsRoot hash.Hash) (*Commit, error) { diff --git a/go/libraries/doltcore/sqle/dfunctions/merge_base.go b/go/libraries/doltcore/sqle/dfunctions/merge_base.go index 560238e7c98..f141d40e12c 100644 --- a/go/libraries/doltcore/sqle/dfunctions/merge_base.go +++ b/go/libraries/doltcore/sqle/dfunctions/merge_base.go @@ -75,11 +75,11 @@ func (d MergeBase) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { return nil, errors.New("right value is not a string") } - left, err := doltDB.ResolveCommitSpecStr(ctx, leftStr, headRef) + left, err := doltDB.ResolveCommitSpecStrForMergeBase(ctx, leftStr, headRef) if err != nil { return nil, err } - right, err := doltDB.ResolveCommitSpecStr(ctx, rightStr, headRef) + right, err := doltDB.ResolveCommitSpecStrForMergeBase(ctx, rightStr, headRef) if err != nil { return nil, err } diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go index 32f71df53a8..2e84e64267e 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go @@ -339,17 +339,23 @@ func resolveCommitStrings(ctx *sql.Context, fromRef, toRef, dotRef interface{}, if strings.Contains(dotStr, "...") { refs := strings.Split(dotStr, "...") + // A merge base is a commit, so a three dot range needs at least one committed side. + // WORKING and STAGED are uncommitted roots, leaving no fork point when both are given. + if doltdb.IsWorkingSetRef(refs[0]) && doltdb.IsWorkingSetRef(refs[1]) { + return "", "", fmt.Errorf("ambiguous three dot range '%s': at least one side must be a commit", dotStr) + } + headRef, err := sess.CWBHeadRef(ctx, db.Name()) if err != nil { return "", "", err } - leftCm, err := db.DbData().Ddb.ResolveCommitSpecStr(ctx, refs[0], headRef) + leftCm, err := db.DbData().Ddb.ResolveCommitSpecStrForMergeBase(ctx, refs[0], headRef) if err != nil { return "", "", err } - rightCm, err := db.DbData().Ddb.ResolveCommitSpecStr(ctx, refs[1], headRef) + rightCm, err := db.DbData().Ddb.ResolveCommitSpecStrForMergeBase(ctx, refs[1], headRef) if err != nil { return "", "", err } diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go index 2a963e56bd3..c6db3cd9b1c 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go @@ -495,8 +495,19 @@ func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter return nil, err } + // A merge base is a commit, so a three dot range needs at least one committed side. WORKING + // and STAGED are uncommitted roots, leaving no fork point when both are given. + if threeDot && doltdb.IsWorkingSetRef(revisionValStrs[0]) && doltdb.IsWorkingSetRef(revisionValStrs[1]) { + return nil, fmt.Errorf("ambiguous three dot range '%s...%s': at least one side must be a commit", revisionValStrs[0], revisionValStrs[1]) + } + for _, revisionStr := range revisionValStrs { - commit, err := sqledb.DbData().Ddb.ResolveCommitSpecStr(ctx, revisionStr, headRef) + var commit *doltdb.Commit + if threeDot { + commit, err = sqledb.DbData().Ddb.ResolveCommitSpecStrForMergeBase(ctx, revisionStr, headRef) + } else { + commit, err = sqledb.DbData().Ddb.ResolveCommitSpecStr(ctx, revisionStr, headRef) + } if err != nil { return nil, err } diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go index cdb9bad6b52..c99042e781f 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go @@ -899,6 +899,16 @@ var DoltScripts = []queries.ScriptTest{ Query: "select has_ancestor('tag_btwo1', 'HEAD'), has_ancestor('tag_btwo1', @bone2), has_ancestor('tag_btwo1', @onetwo1), has_ancestor('tag_btwo1', @btwo1), has_ancestor('tag_btwo1', @main2), has_ancestor('tag_btwo1', @main1)", Expected: []sql.Row{{false, false, false, true, false, true}}, }, + { + // WORKING and STAGED name uncommitted roots, not commits, so ancestry against + // them is rejected. + Query: "select has_ancestor('HEAD', 'WORKING')", + ExpectedErrStr: "branch not found: WORKING", + }, + { + Query: "select has_ancestor('STAGED', 'HEAD')", + ExpectedErrStr: "branch not found: STAGED", + }, { Query: "use `mydb/onetwo`;", }, diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go index c41b8536823..ea0f8f6c25b 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go @@ -2076,22 +2076,18 @@ on a.to_pk = b.to_pk;`, }, }, { - Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('WORKING...STAGED', 't') ORDER BY COALESCE(from_pk, to_pk);", - // The merge base of two revisions on the same branch is that branch HEAD, - // so this compares HEAD against the staged root. - Expected: []sql.Row{ - {nil, 3, "added"}, - }, + // Two working-set revisions have no commit fork point to anchor a merge base. + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('WORKING...STAGED', 't') ORDER BY COALESCE(from_pk, to_pk);", + ExpectedErrStr: "ambiguous three dot range 'WORKING...STAGED': at least one side must be a commit", }, { - Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('STAGED...WORKING', 't') ORDER BY COALESCE(from_pk, to_pk);", - Expected: []sql.Row{ - {nil, 3, "added"}, - {nil, 4, "added"}, - }, + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('STAGED...WORKING', 't') ORDER BY COALESCE(from_pk, to_pk);", + ExpectedErrStr: "ambiguous three dot range 'STAGED...WORKING': at least one side must be a commit", }, { Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('HEAD...WORKING', 't') ORDER BY COALESCE(from_pk, to_pk);", + // A real commit on the left keeps the working set as the diff target, so this + // surfaces both uncommitted rows. Expected: []sql.Row{ {nil, 3, "added"}, {nil, 4, "added"}, From 5361c1f175135113172966b1b8f34a3a7c92669f Mon Sep 17 00:00:00 2001 From: elianddb Date: Mon, 22 Jun 2026 19:23:28 +0000 Subject: [PATCH 8/8] reject ambiguous three-dot ranges in the cli --- go/cmd/dolt/commands/diff.go | 4 ++++ .../doltcore/sqle/dtablefunctions/dolt_diff.go | 8 +++++--- .../doltcore/sqle/dtablefunctions/dolt_log.go | 16 +++++++--------- .../doltcore/sqle/enginetest/dolt_queries.go | 12 ++---------- .../sqle/enginetest/dolt_queries_diff.go | 9 ++++----- integration-tests/bats/diff.bats | 12 ++++++------ 6 files changed, 28 insertions(+), 33 deletions(-) diff --git a/go/cmd/dolt/commands/diff.go b/go/cmd/dolt/commands/diff.go index 7038c078a9c..e1ef2b9d507 100644 --- a/go/cmd/dolt/commands/diff.go +++ b/go/cmd/dolt/commands/diff.go @@ -676,6 +676,10 @@ func (dArgs *diffArgs) applyDotRevisions(queryist cli.Queryist, sqlCtx *sql.Cont if strings.Contains(args[0], "...") { refs := strings.Split(args[0], "...") + if doltdb.IsWorkingSetRef(refs[0]) && doltdb.IsWorkingSetRef(refs[1]) { + return fmt.Errorf("ambiguous three dot range '%s': at least one side must be a commit", args[0]) + } + if len(refs[0]) > 0 { right := refs[1] // Use current HEAD if right side of `...` does not exist diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go index 2e84e64267e..c10f639acfa 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_diff.go @@ -44,6 +44,10 @@ const diffTableDefaultRowCount = 1000 var ErrInvalidNonLiteralArgument = errors.NewKind("Invalid argument to %s: %s – only literal values supported") var ErrInvalidTableName = errors.NewKind("Invalid table name %s.") +// ErrAmbiguousThreeDotRange is returned for a three dot range whose sides are both working set +// refs, which has no commit fork point to anchor a merge base. +var ErrAmbiguousThreeDotRange = errors.NewKind("ambiguous three dot range '%s': at least one side must be a commit") + var _ sql.TableFunction = (*DiffTableFunction)(nil) var _ sql.IndexedTable = (*DiffTableFunction)(nil) var _ sql.TableNode = (*DiffTableFunction)(nil) @@ -339,10 +343,8 @@ func resolveCommitStrings(ctx *sql.Context, fromRef, toRef, dotRef interface{}, if strings.Contains(dotStr, "...") { refs := strings.Split(dotStr, "...") - // A merge base is a commit, so a three dot range needs at least one committed side. - // WORKING and STAGED are uncommitted roots, leaving no fork point when both are given. if doltdb.IsWorkingSetRef(refs[0]) && doltdb.IsWorkingSetRef(refs[1]) { - return "", "", fmt.Errorf("ambiguous three dot range '%s': at least one side must be a commit", dotStr) + return "", "", ErrAmbiguousThreeDotRange.New(dotStr) } headRef, err := sess.CWBHeadRef(ctx, db.Name()) diff --git a/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go b/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go index c6db3cd9b1c..f3d58cb676a 100644 --- a/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go +++ b/go/libraries/doltcore/sqle/dtablefunctions/dolt_log.go @@ -495,19 +495,17 @@ func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter return nil, err } - // A merge base is a commit, so a three dot range needs at least one committed side. WORKING - // and STAGED are uncommitted roots, leaving no fork point when both are given. if threeDot && doltdb.IsWorkingSetRef(revisionValStrs[0]) && doltdb.IsWorkingSetRef(revisionValStrs[1]) { - return nil, fmt.Errorf("ambiguous three dot range '%s...%s': at least one side must be a commit", revisionValStrs[0], revisionValStrs[1]) + return nil, ErrAmbiguousThreeDotRange.New(revisionValStrs[0] + "..." + revisionValStrs[1]) } + // Only a three dot range accepts WORKING and STAGED, where they anchor the merge base. + resolve := sqledb.DbData().Ddb.ResolveCommitSpecStr + if threeDot { + resolve = sqledb.DbData().Ddb.ResolveCommitSpecStrForMergeBase + } for _, revisionStr := range revisionValStrs { - var commit *doltdb.Commit - if threeDot { - commit, err = sqledb.DbData().Ddb.ResolveCommitSpecStrForMergeBase(ctx, revisionStr, headRef) - } else { - commit, err = sqledb.DbData().Ddb.ResolveCommitSpecStr(ctx, revisionStr, headRef) - } + commit, err := resolve(ctx, revisionStr, headRef) if err != nil { return nil, err } diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go index c99042e781f..4ec791cbe52 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries.go @@ -5771,19 +5771,11 @@ var LogTableFunctionScriptTests = []queries.ScriptTest{ }, Assertions: []queries.ScriptTestAssertion{ { - // A working set exists only relative to a branch, so resolving WORKING - // without a checked out branch returns an error today. + // TODO(elianddb): unlike git, dolt cannot resolve WORKING for a merge base on a + // detached head, so this errors today instead of listing what new-branch added. Query: "SELECT message from dolt_log('WORKING...new-branch') ORDER BY date DESC;", ExpectedErrStr: "this operation is not supported while in a detached head state", }, - { - // TODO: dolt cannot yet resolve a relative ref for a merge base on a detached - // head, so unlike git this errors instead of listing what new-branch added. - // Remove Skip once dolt resolves WORKING against the detached HEAD commit. - Skip: true, - Query: "SELECT message from dolt_log('WORKING...new-branch') ORDER BY date DESC;", - Expected: []sql.Row{{"commit 2"}}, - }, }, }, } diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go index ea0f8f6c25b..bfeb36ead00 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries_diff.go @@ -2076,13 +2076,12 @@ on a.to_pk = b.to_pk;`, }, }, { - // Two working-set revisions have no commit fork point to anchor a merge base. - Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('WORKING...STAGED', 't') ORDER BY COALESCE(from_pk, to_pk);", - ExpectedErrStr: "ambiguous three dot range 'WORKING...STAGED': at least one side must be a commit", + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('WORKING...STAGED', 't') ORDER BY COALESCE(from_pk, to_pk);", + ExpectedErr: dtablefunctions.ErrAmbiguousThreeDotRange, }, { - Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('STAGED...WORKING', 't') ORDER BY COALESCE(from_pk, to_pk);", - ExpectedErrStr: "ambiguous three dot range 'STAGED...WORKING': at least one side must be a commit", + Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('STAGED...WORKING', 't') ORDER BY COALESCE(from_pk, to_pk);", + ExpectedErr: dtablefunctions.ErrAmbiguousThreeDotRange, }, { Query: "SELECT from_pk, to_pk, diff_type FROM DOLT_DIFF('HEAD...WORKING', 't') ORDER BY COALESCE(from_pk, to_pk);", diff --git a/integration-tests/bats/diff.bats b/integration-tests/bats/diff.bats index 634ec59e2e3..85b9c2b83c1 100644 --- a/integration-tests/bats/diff.bats +++ b/integration-tests/bats/diff.bats @@ -498,14 +498,11 @@ SQL dolt commit -m table dolt sql -q "call dolt_branch('branch1')" dolt --branch branch1 sql -q "insert into test values (1,1,1,1,1,1); call dolt_add('.'); call dolt_commit('-m', 'row');" - # Row 2 is staged and row 3 is left in the working set so that the staged root and - # the working root differ from each other. + # Stage row 2 and leave row 3 in the working set so STAGED and WORKING differ. dolt sql -q 'insert into test values (2,2,2,2,2,2)' dolt add . dolt sql -q 'insert into test values (3,3,3,3,3,3)' - # A revision to the left of the three dots only chooses the merge base, so the - # uncommitted rows do not appear and the result is the same as for HEAD. run dolt diff WORKING...branch1 [ "$status" -eq 0 ] [[ "$output" =~ "+ | 1" ]] || false @@ -517,8 +514,6 @@ SQL [[ "$output" =~ "+ | 1" ]] || false [[ ! "$output" =~ "+ | 3" ]] || false - # A revision to the right of the three dots is the diff target, so the working and - # staged rows are surfaced. run dolt diff branch1...WORKING [ "$status" -eq 0 ] [[ "$output" =~ "+ | 2" ]] || false @@ -530,8 +525,13 @@ SQL [[ "$output" =~ "+ | 2" ]] || false [[ ! "$output" =~ "+ | 3" ]] || false + run dolt diff STAGED...WORKING + [ "$status" -eq 1 ] + [[ "$output" =~ "at least one side must be a commit" ]] || false + working_base=$(dolt sql -r csv -q "SELECT dolt_merge_base('branch1', 'WORKING')" | tail -n 1) head_base=$(dolt sql -r csv -q "SELECT dolt_merge_base('branch1', 'HEAD')" | tail -n 1) + [ -n "$working_base" ] [ "$working_base" = "$head_base" ] }