Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
92cb7dc
add implementation for new `@` database revision delimiter for use in…
elianddb Feb 5, 2026
6ed4751
add new `ScriptTest`s in `DoltRevisionDbScripts` for database revisio…
elianddb Feb 5, 2026
f303fd4
fix ORM CI tests to run automatically, add new Prisma branch test usi…
elianddb Feb 5, 2026
48f708d
rm unused orm-tests github action
elianddb Feb 6, 2026
1040b9a
fix behavior when `dolt_show_branch_databases` interacts with `dolt_e…
elianddb Feb 6, 2026
26c2de9
fix sql shell current database to resolve new delimiter and fix revis…
elianddb Feb 7, 2026
c1de39e
mv docker orm tests to separate branch
elianddb Feb 12, 2026
e196152
fix shell promp current db split system
elianddb Feb 13, 2026
792ce9d
add tests for commit revisions
elianddb Feb 13, 2026
bb71e47
amend sess var querying
elianddb Feb 13, 2026
fa4da6b
rm configuration for delimiter
elianddb Feb 16, 2026
6a0b0fc
fix normal db compatibility that use `@` character
elianddb Feb 19, 2026
e078b89
implement full prompt resolver for base db, revision, and dirty state
elianddb Feb 19, 2026
3a532d4
amend base db and rev resolver to use new active_revision() and base_…
elianddb Feb 20, 2026
332316e
add skip on windows
elianddb Feb 20, 2026
37b922f
fix dolt_status check on non-dolt databases
elianddb Feb 20, 2026
4a77378
mv delimiter alias to be parsed on last index
elianddb Feb 21, 2026
5841bf0
amend sql-shell resolver to evaluate using `active_branch()` and `dat…
elianddb Feb 25, 2026
a436dac
Merge remote-tracking branch 'origin/main' into elian/10382
elianddb Feb 25, 2026
fd4c81f
amend to gms pr
elianddb Feb 25, 2026
3b583ef
fix dolt_status test
elianddb Feb 25, 2026
74d5d60
mv delimiter checks to dolt
elianddb Feb 25, 2026
451b3af
Merge remote-tracking branch 'origin/main' into elian/10382
elianddb Feb 25, 2026
831952b
amend gms ver.
elianddb Feb 25, 2026
cb6dc3b
amend delimiter precedence to be the same left-to-right evaluation, a…
elianddb Feb 26, 2026
f49fdf6
Merge remote-tracking branch 'origin/main' into elian/10382
elianddb Feb 26, 2026
ca74006
amend gms ver. to dolthub/go-mysql-server#3448
elianddb Feb 26, 2026
b6eaad2
rm string.ToLower() on SessionDatabase entirely, gms expects case-sen…
elianddb Feb 26, 2026
710564a
Merge remote-tracking branch 'origin/main' into elian/10382
elianddb Feb 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ CLAUDE.md
.gitattributes

.de/
.cursor
AGENTS.md
171 changes: 171 additions & 0 deletions go/cmd/dolt/cli/prompt/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright 2026 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 prompt

import (
"errors"

"github.com/dolthub/go-mysql-server/sql"

"github.com/dolthub/dolt/go/cmd/dolt/cli"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
)

// Parts contains shell prompt components to render in the final prompt.
type Parts struct {
BaseDatabase string
ActiveRevision string
IsBranch bool
Dirty bool
}

// Resolver resolves prompt Parts for the active session.
type Resolver interface {
Resolve(sqlCtx *sql.Context, queryist cli.Queryist) (parts Parts, resolved bool, err error)
}

// sqlBaseRevisionResolver can resolve [prompt.Parts] using Dolt specific SQL functions that return canonical base
// database and revision, even when the [doltdb.DbRevisionDelimiterAlias] is in use.
type sqlBaseRevisionResolver struct{}

// sqlDBActiveBranchResolver can resolve [prompt.Parts] using the SQL-specific functions. It is a fallback for older
// servers, and is the method older shells use in general. This resolver does not support
// [doltdb.DbRevisionDelimiterAlias] as a revision delimiter.
type sqlDBActiveBranchResolver struct{}

// chainedResolver can resolve [prompt.Parts] through the sequential execution [prompt.Resolver](s).
type chainedResolver struct {
resolvers []Resolver
}

// NewPartsResolver constructs an up-to-date [prompt.Resolver].
func NewPartsResolver() Resolver {
return chainedResolver{
resolvers: []Resolver{
sqlBaseRevisionResolver{},
sqlDBActiveBranchResolver{},
},
}
}

// Resolve resolves [prompt.Parts] through a chain of [prompt.Resolver](s) in sequential order. If a Resolver encounters
// an error, it is returned immediately and no other Resolver executes.
func (cr chainedResolver) Resolve(sqlCtx *sql.Context, queryist cli.Queryist) (parts Parts, resolved bool, err error) {
for _, resolver := range cr.resolvers {
parts, resolved, err := resolver.Resolve(sqlCtx, queryist)
if err != nil {
return Parts{}, false, err
}
if resolved {
return parts, true, nil
}
}
return Parts{}, false, nil
}

// Resolve resolves [prompt.Parts] through SQL functions `base_database()` and `active_revision()`.
func (sqlBaseRevisionResolver) Resolve(sqlCtx *sql.Context, queryist cli.Queryist) (parts Parts, resolved bool, err error) {
parts = Parts{}

rows, err := cli.GetRowsForSql(queryist, sqlCtx, "select base_database() as base_database, active_revision() as active_revision")
if sql.ErrFunctionNotFound.Is(err) {
// Running on an older version.
return parts, false, nil
} else if err != nil {
return parts, false, err
}

if len(rows) > 0 {
if len(rows[0]) > 0 {
parts.BaseDatabase, err = cli.GetStringColumnValue(rows[0][0])
if err != nil {
return parts, false, err
}
}
if len(rows[0]) > 1 {
parts.ActiveRevision, err = cli.GetStringColumnValue(rows[0][1])
if err != nil {
return parts, false, err
}
}
}

parts.Dirty, parts.IsBranch, err = resolveDirty(sqlCtx, queryist, parts)
if err != nil {
return parts, false, err
}
return parts, true, nil
}

// Resolve resolves the base database and active revision through the SQL-specific functions `database()` and
// `active_branch()`. Unfortunately, to maintain support for ORMs that rely on the database in their connection URL,
// this method cannot interpret [doltdb.DbRevisionDelimiterAlias] as a revision delimiter.
Comment thread
elianddb marked this conversation as resolved.
Outdated
func (sqlDBActiveBranchResolver) Resolve(sqlCtx *sql.Context, queryist cli.Queryist) (parts Parts, resolved bool, err error) {
parts = Parts{}
Comment thread
elianddb marked this conversation as resolved.
Outdated
dbRows, err := cli.GetRowsForSql(queryist, sqlCtx, "select database() as db")
if err != nil {
return parts, false, err
}
if len(dbRows) > 0 && len(dbRows[0]) > 0 {
dbName, err := cli.GetStringColumnValue(dbRows[0][0])
if err != nil {
return parts, false, err
}
parts.BaseDatabase, parts.ActiveRevision = doltdb.SplitRevisionDbName(dbName)
}

if parts.ActiveRevision == "" {
activeBranchRows, err := cli.GetRowsForSql(queryist, sqlCtx, "select active_branch() as branch")
if err != nil {
return parts, false, err
}
if len(activeBranchRows) > 0 && len(activeBranchRows[0]) > 0 {
parts.ActiveRevision, err = cli.GetStringColumnValue(activeBranchRows[0][0])
if err != nil {
return parts, false, err
}
}
}

parts.Dirty, parts.IsBranch, err = resolveDirty(sqlCtx, queryist, parts)
if err != nil {
return parts, false, err
}
return parts, true, nil
}

// resolveDirty resolves the dirty state of the current branch and whether the revision type is a branch.
func resolveDirty(sqlCtx *sql.Context, queryist cli.Queryist, parts Parts) (dirty bool, isBranch bool, err error) {
if doltdb.IsValidCommitHash(parts.ActiveRevision) {
return false, false, nil
}

rows, err := cli.GetRowsForSql(queryist, sqlCtx, "select count(table_name) > 0 as dirty from dolt_status")
if errors.Is(err, doltdb.ErrOperationNotSupportedInDetachedHead) || sql.ErrTableNotFound.Is(err) {
return false, false, nil
} else if err != nil {
return false, false, err
}

if len(rows) == 0 || len(rows[0]) == 0 {
return false, false, nil
}
dirty, err = cli.GetBoolColumnValue(rows[0][0])
if err != nil {
return false, false, err
}

return dirty, true, nil
}
50 changes: 50 additions & 0 deletions go/cmd/dolt/cli/query_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package cli

import (
"fmt"
"strings"

"github.com/dolthub/go-mysql-server/sql"
)
Expand Down Expand Up @@ -83,3 +84,52 @@ func GetRowsForSql(queryist Queryist, sqlCtx *sql.Context, query string) ([]sql.

return rows, nil
}

// GetStringColumnValue returns column values from [sql.Row] as a string.
Comment thread
elianddb marked this conversation as resolved.
Outdated
func GetStringColumnValue(value any) (str string, err error) {
if value == nil {
return "", nil
}

switch v := value.(type) {
case string:
return v, nil
case []byte:
return string(v), nil
case fmt.Stringer:
return v.String(), nil
default:
return "", fmt.Errorf("unexpected type %T, expected string-like column value", value)
}
}

// GetBoolColumnValue returns the value of the input as a bool. This is required because depending on if we go over the
// wire or not we may get a string or a bool when we expect a bool.
func GetBoolColumnValue(col interface{}) (bool, error) {
switch v := col.(type) {
case bool:
return col.(bool), nil
case string:
return strings.EqualFold(col.(string), "true") || strings.EqualFold(col.(string), "1"), nil
default:
return false, fmt.Errorf("unexpected type %T, was expecting bool or string", v)
}
}

// WithQueryWarningsLocked runs queries with a preserved warning buffer. Internal shell queries run on the same SQL
// session as user queries. Without warning locks, these housekeeping queries can clear or overwrite warnings that users
// expect.
func WithQueryWarningsLocked(sqlCtx *sql.Context, queryist Queryist, fn func() error) error {
_, _, _, err := queryist.Query(sqlCtx, "set lock_warnings = 1")
if err != nil {
return err
}

runErr := fn()

_, _, _, err = queryist.Query(sqlCtx, "set lock_warnings = 0")
if err != nil {
return err
}
return runErr
}
115 changes: 24 additions & 91 deletions go/cmd/dolt/commands/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"gopkg.in/src-d/go-errors.v1"

"github.com/dolthub/dolt/go/cmd/dolt/cli"
"github.com/dolthub/dolt/go/cmd/dolt/cli/prompt"
"github.com/dolthub/dolt/go/cmd/dolt/commands/engine"
"github.com/dolthub/dolt/go/cmd/dolt/errhand"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
Expand Down Expand Up @@ -705,13 +706,7 @@ func execShell(sqlCtx *sql.Context, qryist cli.Queryist, format engine.PrintResu
_ = iohelp.WriteLine(cli.CliOut, welcomeMsg)
historyFile := filepath.Join(".sqlhistory") // history file written to working dir

db, branch, _ := getDBBranchFromSession(sqlCtx, qryist)
dirty := false
if branch != "" {
dirty, _ = isDirty(sqlCtx, qryist)
}

initialPrompt, initialMultilinePrompt := formattedPrompts(db, branch, dirty)
initialPrompt, initialMultilinePrompt := postCommandUpdate(sqlCtx, qryist)

rlConf := readline.Config{
Prompt: initialPrompt,
Expand Down Expand Up @@ -954,18 +949,32 @@ func preprocessQuery(query, lastQuery string, cliCtx cli.CliContext) (CommandTyp
return SqlShellCommand, nil, query, nil
}

// postCommandUpdate is a helper function that is run after the shell has completed a command. It updates the the database
// postCommandUpdate is a helper function that is run after the shell has completed a command. It updates the database
// if needed, and generates new prompts for the shell (based on the branch and if the workspace is dirty).
func postCommandUpdate(sqlCtx *sql.Context, qryist cli.Queryist) (string, string) {
db, branch, ok := getDBBranchFromSession(sqlCtx, qryist)
if ok {
sqlCtx.SetCurrentDatabase(db)
resolver := prompt.NewPartsResolver()
var parts prompt.Parts
var resolved bool

err := cli.WithQueryWarningsLocked(sqlCtx, qryist, func() error {
var err error
parts, resolved, err = resolver.Resolve(sqlCtx, qryist)
return err
})
if err != nil {
cli.PrintErrln(err.Error())
}
dirty := false
if branch != "" {
dirty, _ = isDirty(sqlCtx, qryist)
if resolved && parts.ActiveRevision != "" {
sqlCtx.SetCurrentDatabase(parts.BaseDatabase + doltdb.DbRevisionDelimiter + parts.ActiveRevision)
} else if resolved {
sqlCtx.SetCurrentDatabase(parts.BaseDatabase)
} else {
cli.PrintErrln(color.YellowString("Failed to set new current database for the post command update"))
baseDatabase, activeRevision := doltdb.SplitRevisionDbName(sqlCtx.GetCurrentDatabase())
return formattedPrompts(baseDatabase, activeRevision, false)
}
return formattedPrompts(db, branch, dirty)

return formattedPrompts(parts.BaseDatabase, parts.ActiveRevision, parts.Dirty)
}

// formattedPrompts returns the prompt and multiline prompt for the current session. If the db is empty, the prompt will
Expand Down Expand Up @@ -997,82 +1006,6 @@ func formattedPrompts(db, branch string, dirty bool) (string, string) {
return fmt.Sprintf("%s/%s%s> ", cyanDb, yellowBr, dirtyStr), multi
}

// getDBBranchFromSession returns the current database name and current branch for the session, handling all the errors
// along the way by printing red error messages to the CLI. If there was an issue getting the db name, the ok return
// value will be false and the strings will be empty.
func getDBBranchFromSession(sqlCtx *sql.Context, qryist cli.Queryist) (db string, branch string, ok bool) {
_, _, _, err := qryist.Query(sqlCtx, "set lock_warnings = 1")
if err != nil {
cli.Println(color.RedString(err.Error()))
return "", "", false
}
defer qryist.Query(sqlCtx, "set lock_warnings = 0")

_, resp, _, err := qryist.Query(sqlCtx, "select database() as db, active_branch() as branch")
if err != nil {
cli.Println(color.RedString("Failure to get DB Name for session: " + err.Error()))
return db, branch, false
}
// Expect single row result, with two columns: db name, branch name.
row, err := resp.Next(sqlCtx)
if err != nil {
cli.Println(color.RedString("Failure to get DB Name for session: " + err.Error()))
return db, branch, false
}
if len(row) != 2 {
cli.Println(color.RedString("Runtime error. Invalid column count."))
return db, branch, false
}

if row[1] == nil {
branch = ""
} else {
branch = row[1].(string)
}
if row[0] == nil {
db = ""
} else {
db = row[0].(string)

// It is possible to `use mydb/branch`, and as far as your session is concerned your database is mydb/branch. We
// allow that, but also want to show the user the branch name in the prompt. So we munge the DB in this case.
if strings.HasSuffix(strings.ToLower(db), strings.ToLower("/"+branch)) {
db = db[:len(db)-len(branch)-1]
}
}

return db, branch, true
}

// isDirty returns true if the workspace is dirty, false otherwise. This function _assumes_ you are on a database
// with a branch. If you are not, you will get an error.
func isDirty(sqlCtx *sql.Context, qryist cli.Queryist) (bool, error) {
_, _, _, err := qryist.Query(sqlCtx, "set lock_warnings = 1")
if err != nil {
return false, err
}
defer qryist.Query(sqlCtx, "set lock_warnings = 0")

_, resp, _, err := qryist.Query(sqlCtx, "select count(table_name) > 0 as dirty from dolt_status")

if err != nil {
cli.Println(color.RedString("Failure to get DB Name for session: " + err.Error()))
return false, err
}
// Expect single row result, with one boolean column.
row, err := resp.Next(sqlCtx)
if err != nil {
cli.Println(color.RedString("Failure to get DB Name for session: " + err.Error()))
return false, err
}
if len(row) != 1 {
cli.Println(color.RedString("Runtime error. Invalid column count."))
return false, fmt.Errorf("invalid column count")
}

return getStrBoolColAsBool(row[0])
}

// Returns a new auto completer with table names, column names, and SQL keywords.
// TODO: update the completer on DDL, branch change, etc.
func newCompleter(
Expand Down
Loading
Loading