diff --git a/go/cmd/dolt/cli/prompt/resolver.go b/go/cmd/dolt/cli/prompt/resolver.go index 3fe774a817..d6a276f661 100644 --- a/go/cmd/dolt/cli/prompt/resolver.go +++ b/go/cmd/dolt/cli/prompt/resolver.go @@ -16,6 +16,7 @@ package prompt import ( "errors" + "strings" "github.com/dolthub/go-mysql-server/sql" @@ -23,26 +24,25 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" ) -// Parts contains shell prompt components to render in the final prompt. +// Parts contains shell prompt components to render the SQL shell prompt. type Parts struct { - BaseDatabase string - ActiveRevision string - IsBranch bool - Dirty bool + BaseDatabase string + ActiveRevision string + RevisionDelimiter string + IsBranch bool + Dirty bool } -// Resolver resolves prompt Parts for the active session. +// revisionDelimiters follows [doltdb]'s definition to separate the base database from the revision. +var revisionDelimiters = []string{doltdb.DbRevisionDelimiter, doltdb.DbRevisionDelimiterAlias} + +// Resolver resolves prompt [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. +// sqlDBActiveBranchResolver can resolve [prompt.Parts] using the Dolt SQL functions, and supports +// [doltdb.DbRevisionDelimiter] and [doltdb.DbRevisionDelimiterAlias]. type sqlDBActiveBranchResolver struct{} // chainedResolver can resolve [prompt.Parts] through the sequential execution [prompt.Resolver](s). @@ -50,11 +50,10 @@ type chainedResolver struct { resolvers []Resolver } -// NewPartsResolver constructs an up-to-date [prompt.Resolver]. -func NewPartsResolver() Resolver { +// NewPromptResolver constructs an up-to-date [prompt.Resolver]. +func NewPromptResolver() Resolver { return chainedResolver{ resolvers: []Resolver{ - sqlBaseRevisionResolver{}, sqlDBActiveBranchResolver{}, }, } @@ -75,97 +74,75 @@ func (cr chainedResolver) Resolve(sqlCtx *sql.Context, queryist cli.Queryist) (p 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. +// Resolve resolves the base DB and revision through the SQL function `database()` and Dolt-specific `active_branch()`. func (sqlDBActiveBranchResolver) Resolve(sqlCtx *sql.Context, queryist cli.Queryist) (parts Parts, resolved bool, err error) { - parts = Parts{} 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]) + dbName, err := cli.QueryValueAsString(dbRows[0][0]) if err != nil { return parts, false, err } + // Handles non-branch revisions (i.e., commit hash, tags, etc.). parts.BaseDatabase, parts.ActiveRevision = doltdb.SplitRevisionDbName(dbName) + + parts.RevisionDelimiter = doltdb.DbRevisionDelimiter + for _, delimiter := range revisionDelimiters { + if strings.Contains(dbName, delimiter) { + parts.RevisionDelimiter = delimiter + break + } + } } - 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]) + 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.IsBranch = activeBranchRows[0][0] != nil + if parts.ActiveRevision == "" { + parts.ActiveRevision, err = cli.QueryValueAsString(activeBranchRows[0][0]) if err != nil { return parts, false, err } } } - parts.Dirty, parts.IsBranch, err = resolveDirty(sqlCtx, queryist, parts) + parts.Dirty, 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) { +func resolveDirty(sqlCtx *sql.Context, queryist cli.Queryist, parts Parts) (dirty bool, err error) { if doltdb.IsValidCommitHash(parts.ActiveRevision) { - return false, false, nil + return false, nil } rows, err := cli.GetRowsForSql(queryist, sqlCtx, "select count(table_name) > 0 as dirty from dolt_status") + // [sql.ErrTableNotFound] detects when viewing a non-Dolt database (e.g., information_schema). Older servers may + // complain about [doltdb.ErrOperationNotSupportedInDetachedHead], but read-only revisions in newer versions this + // issue should be gone. if errors.Is(err, doltdb.ErrOperationNotSupportedInDetachedHead) || sql.ErrTableNotFound.Is(err) { - return false, false, nil + return false, nil } else if err != nil { - return false, false, err + return false, err } if len(rows) == 0 || len(rows[0]) == 0 { - return false, false, nil + return false, nil } - dirty, err = cli.GetBoolColumnValue(rows[0][0]) + dirty, err = cli.QueryValueAsBool(rows[0][0]) if err != nil { - return false, false, err + return false, err } - return dirty, true, nil + return dirty, nil } diff --git a/go/cmd/dolt/cli/query_helpers.go b/go/cmd/dolt/cli/query_helpers.go index 80e4d0b1b3..8ca2ecca21 100644 --- a/go/cmd/dolt/cli/query_helpers.go +++ b/go/cmd/dolt/cli/query_helpers.go @@ -21,26 +21,6 @@ import ( "github.com/dolthub/go-mysql-server/sql" ) -// GetInt8ColAsBool returns the value of an int8 column as a bool -// This is necessary because Queryist may return an int8 column as a bool (when using SQLEngine) -// or as a string (when using ConnectionQueryist). -func GetInt8ColAsBool(col interface{}) (bool, error) { - switch v := col.(type) { - case int8: - return v != 0, nil - case string: - if v == "ON" || v == "1" { - return true, nil - } else if v == "OFF" || v == "0" { - return false, nil - } else { - return false, fmt.Errorf("unexpected value for boolean var: %v", v) - } - default: - return false, fmt.Errorf("unexpected type %T, was expecting int8", v) - } -} - // SetSystemVar sets the @@dolt_show_system_tables variable if necessary, and returns a function // resetting the variable for after the commands completion, if necessary. func SetSystemVar(queryist Queryist, sqlCtx *sql.Context, newVal bool) (func() error, error) { @@ -53,7 +33,7 @@ func SetSystemVar(queryist Queryist, sqlCtx *sql.Context, newVal bool) (func() e if err != nil { return nil, err } - prevVal, err := GetInt8ColAsBool(row[0][1]) + prevVal, err := QueryValueAsBool(row[0][1]) if err != nil { return nil, err } @@ -85,8 +65,10 @@ func GetRowsForSql(queryist Queryist, sqlCtx *sql.Context, query string) ([]sql. return rows, nil } -// GetStringColumnValue returns column values from [sql.Row] as a string. -func GetStringColumnValue(value any) (str string, err error) { +// QueryValueAsString converts a single value from a query result to a string. Use this when reading string-like +// columns from Queryist results, since the type can differ in-process [engine.SQLEngine] versus over the wire +// [sqlserver.ConnectionQueryist]. +func QueryValueAsString(value any) (str string, err error) { if value == nil { return "", nil } @@ -103,16 +85,31 @@ func GetStringColumnValue(value any) (str string, err error) { } } -// 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) { +// QueryValueAsBool interprets a query result cell as a bool. Strings are normalized and matched as "true"/"1"/"ON" +// (true) or the opposite for false; matching is case-insensitive. [Queryist] may return a tinyint column as a bool +// when utilizing the [engine.SQLEngine] or as string when using [sqlserver.ConnectionQueryist]. +func QueryValueAsBool(col interface{}) (bool, error) { switch v := col.(type) { case bool: - return col.(bool), nil + return v, nil + case byte: + return v == 1, nil + case int: + return v == 1, nil + case int8: + return v == 1, nil case string: - return strings.EqualFold(col.(string), "true") || strings.EqualFold(col.(string), "1"), nil + s := strings.TrimSpace(v) + switch { + case s == "1" || strings.EqualFold(s, "true") || strings.EqualFold(s, "ON"): + return true, nil + case s == "0" || strings.EqualFold(s, "false") || strings.EqualFold(s, "OFF"): + return false, nil + default: + return false, fmt.Errorf("unexpected value for string for bool: %v", v) + } default: - return false, fmt.Errorf("unexpected type %T, was expecting bool or string", v) + return false, fmt.Errorf("unexpected type %T, was expecting bool, int, or string", v) } } diff --git a/go/cmd/dolt/commands/cnfcmds/cat.go b/go/cmd/dolt/commands/cnfcmds/cat.go index 3e32fc9850..9d818c962d 100644 --- a/go/cmd/dolt/commands/cnfcmds/cat.go +++ b/go/cmd/dolt/commands/cnfcmds/cat.go @@ -305,7 +305,7 @@ func getMergeStatus(queryist cli.Queryist, sqlCtx *sql.Context) (mergeStatus, er } row := rows[0] - ms.isMerging, err = commands.GetTinyIntColAsBool(row[0]) + ms.isMerging, err = cli.QueryValueAsBool(row[0]) if err != nil { return ms, fmt.Errorf("error: failed to parse is_merging: %w", err) } diff --git a/go/cmd/dolt/commands/diff.go b/go/cmd/dolt/commands/diff.go index 44a5b3e891..12a96f4909 100644 --- a/go/cmd/dolt/commands/diff.go +++ b/go/cmd/dolt/commands/diff.go @@ -874,11 +874,11 @@ func getDiffSummariesBetweenRefs(queryist cli.Queryist, sqlCtx *sql.Context, fro summary.FromTableName.Name = row[0].(string) summary.ToTableName.Name = row[1].(string) summary.DiffType = row[2].(string) - summary.DataChange, err = GetTinyIntColAsBool(row[3]) + summary.DataChange, err = cli.QueryValueAsBool(row[3]) if err != nil { return nil, fmt.Errorf("error: unable to parse data change value '%s': %w", row[3], err) } - summary.SchemaChange, err = GetTinyIntColAsBool(row[4]) + summary.SchemaChange, err = cli.QueryValueAsBool(row[4]) if err != nil { return nil, fmt.Errorf("error: unable to parse schema change value '%s': %w", row[4], err) } diff --git a/go/cmd/dolt/commands/sql.go b/go/cmd/dolt/commands/sql.go index 4e61a9f106..4a1101d562 100644 --- a/go/cmd/dolt/commands/sql.go +++ b/go/cmd/dolt/commands/sql.go @@ -952,28 +952,32 @@ func preprocessQuery(query, lastQuery string, cliCtx cli.CliContext) (CommandTyp // 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) { - resolver := prompt.NewPartsResolver() + promptResolver := prompt.NewPromptResolver() var parts prompt.Parts - var resolved bool - + resolved := false err := cli.WithQueryWarningsLocked(sqlCtx, qryist, func() error { var err error - parts, resolved, err = resolver.Resolve(sqlCtx, qryist) + parts, resolved, err = promptResolver.Resolve(sqlCtx, qryist) return err }) if err != nil { cli.PrintErrln(err.Error()) } - 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) + + if !resolved { + cli.PrintErrln(color.YellowString("Failed to set new current database on post command update")) + parts.BaseDatabase, parts.ActiveRevision = doltdb.SplitRevisionDbName(sqlCtx.GetCurrentDatabase()) + return formattedPrompts(parts.BaseDatabase, parts.ActiveRevision, parts.Dirty) } + var builder strings.Builder + builder.WriteString(parts.BaseDatabase) + if parts.ActiveRevision != "" { + builder.WriteString(parts.RevisionDelimiter) + builder.WriteString(parts.ActiveRevision) + } + sqlCtx.SetCurrentDatabase(builder.String()) + return formattedPrompts(parts.BaseDatabase, parts.ActiveRevision, parts.Dirty) } diff --git a/go/cmd/dolt/commands/sql_slash.go b/go/cmd/dolt/commands/sql_slash.go index 0c99c7ccd8..28c5c43e97 100644 --- a/go/cmd/dolt/commands/sql_slash.go +++ b/go/cmd/dolt/commands/sql_slash.go @@ -150,7 +150,7 @@ Found a bug? Want additional features? Please let us know! https://github.com/do } func generateHelpPrompt(sqlCtx *sql.Context, qryist cli.Queryist) string { - resolver := prompt.NewPartsResolver() + resolver := prompt.NewPromptResolver() var parts prompt.Parts var resolved bool diff --git a/go/cmd/dolt/commands/status.go b/go/cmd/dolt/commands/status.go index 740696be32..1f98775b7e 100644 --- a/go/cmd/dolt/commands/status.go +++ b/go/cmd/dolt/commands/status.go @@ -198,7 +198,7 @@ func createPrintData(queryist cli.Queryist, sqlCtx *sql.Context, showIgnoredTabl staged := row[1] status := row[2].(string) - isStaged, err := GetTinyIntColAsBool(staged) + isStaged, err := cli.QueryValueAsBool(staged) if err != nil { return nil, err } @@ -389,7 +389,7 @@ func getMergeStatus(queryist cli.Queryist, sqlCtx *sql.Context) (bool, error) { mergeActive := false if len(mergeRows) == 1 { isMerging := mergeRows[0][0] - mergeActive, err = GetTinyIntColAsBool(isMerging) + mergeActive, err = cli.QueryValueAsBool(isMerging) if err != nil { return false, err } diff --git a/go/cmd/dolt/commands/utils.go b/go/cmd/dolt/commands/utils.go index e192ec4011..0dad61af9e 100644 --- a/go/cmd/dolt/commands/utils.go +++ b/go/cmd/dolt/commands/utils.go @@ -354,24 +354,6 @@ func InterpolateAndRunQuery(queryist cli.Queryist, sqlCtx *sql.Context, queryTem return cli.GetRowsForSql(queryist, sqlCtx, query) } -// GetTinyIntColAsBool returns the value of a tinyint column as a bool -// This is necessary because Queryist may return a tinyint column as a bool (when using SQLEngine) -// or as a string (when using ConnectionQueryist). -func GetTinyIntColAsBool(col interface{}) (bool, error) { - switch v := col.(type) { - case bool: - return v, nil - case byte: - return v == 1, nil - case int: - return v == 1, nil - case string: - return v == "1", nil - default: - return false, fmt.Errorf("unexpected type %T, was expecting bool, int, or string", v) - } -} - // getInt64ColAsInt64 returns the value of an int64 column as an int64 // This is necessary because Queryist may return an int64 column as an int64 (when using SQLEngine) // or as a string (when using ConnectionQueryist). @@ -562,7 +544,7 @@ func GetDoltStatus(queryist cli.Queryist, sqlCtx *sql.Context) (stagedChangedTab tableName := row[0].(string) staged := row[1] var isStaged bool - isStaged, err = GetTinyIntColAsBool(staged) + isStaged, err = cli.QueryValueAsBool(staged) if err != nil { return } diff --git a/go/go.mod b/go/go.mod index a7452145c4..c677c5b4b2 100644 --- a/go/go.mod +++ b/go/go.mod @@ -13,7 +13,7 @@ require ( github.com/dolthub/fslock v0.0.0-20251215194149-ef20baba2318 github.com/dolthub/ishell v0.0.0-20240701202509-2b217167d718 github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 - github.com/dolthub/vitess v0.0.0-20260202234501-b14ed9b1632b + github.com/dolthub/vitess v0.0.0-20260225173707-20566e4abe9e github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.13.0 github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 @@ -61,7 +61,7 @@ require ( github.com/dolthub/dolt-mcp v0.2.2 github.com/dolthub/eventsapi_schema v0.0.0-20260205214132-a7a3c84c84a1 github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 - github.com/dolthub/go-mysql-server v0.20.1-0.20260206233720-bbef18042f77 + github.com/dolthub/go-mysql-server v0.20.1-0.20260225184209-84b75b1e95bb github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 github.com/edsrzf/mmap-go v1.2.0 github.com/esote/minmaxheap v1.0.0 diff --git a/go/go.sum b/go/go.sum index 7a556cf1a8..eafdf65122 100644 --- a/go/go.sum +++ b/go/go.sum @@ -196,8 +196,8 @@ github.com/dolthub/fslock v0.0.0-20251215194149-ef20baba2318 h1:n+vdH5G5Db+1qnDC github.com/dolthub/fslock v0.0.0-20251215194149-ef20baba2318/go.mod h1:QWql+P17oAAMLnL4HGB5tiovtDuAjdDTPbuqx7bYfa0= github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790 h1:zxMsH7RLiG+dlZ/y0LgJHTV26XoiSJcuWq+em6t6VVc= github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE= -github.com/dolthub/go-mysql-server v0.20.1-0.20260206233720-bbef18042f77 h1:1b6Z3rm58d5LtLFQI2olPwnNTbwC1g7aTVRhrO6HJdc= -github.com/dolthub/go-mysql-server v0.20.1-0.20260206233720-bbef18042f77/go.mod h1:LEWdXw6LKjdonOv2X808RpUc8wZVtQx4ZEPvmDWkvY4= +github.com/dolthub/go-mysql-server v0.20.1-0.20260225184209-84b75b1e95bb h1:G1XtL3WMaCz11uHDFoQVqgQdkMptTQ/wQYJiWXCv5kk= +github.com/dolthub/go-mysql-server v0.20.1-0.20260225184209-84b75b1e95bb/go.mod h1:Ip8uuT18T+T6kXiRHLluThFBiJZsgbJFsFp3VhdlT4Q= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= github.com/dolthub/ishell v0.0.0-20240701202509-2b217167d718 h1:lT7hE5k+0nkBdj/1UOSFwjWpNxf+LCApbRHgnCA17XE= @@ -206,8 +206,8 @@ github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTE github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 h1:7/v8q9XGFa6q5Ap4Z/OhNkAMBaK5YeuEzwJt+NZdhiE= github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81/go.mod h1:siLfyv2c92W1eN/R4QqG/+RjjX5W2+gCTRjZxBjI3TY= -github.com/dolthub/vitess v0.0.0-20260202234501-b14ed9b1632b h1:B8QS0U5EHtJTiOptjti1cH/OiE6uczyhePtvVFigf3w= -github.com/dolthub/vitess v0.0.0-20260202234501-b14ed9b1632b/go.mod h1:eLLslh1CSPMf89pPcaMG4yM72PQbTN9OUYJeAy0fAis= +github.com/dolthub/vitess v0.0.0-20260225173707-20566e4abe9e h1:ZrRCF8F8Iq8RP0OBowkYpOwd/1NTFU34Ydp0MZ2qTq4= +github.com/dolthub/vitess v0.0.0-20260225173707-20566e4abe9e/go.mod h1:eLLslh1CSPMf89pPcaMG4yM72PQbTN9OUYJeAy0fAis= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= diff --git a/go/libraries/doltcore/doltdb/doltdb.go b/go/libraries/doltcore/doltdb/doltdb.go index 635246a819..511d31c24a 100644 --- a/go/libraries/doltcore/doltdb/doltdb.go +++ b/go/libraries/doltcore/doltdb/doltdb.go @@ -2394,28 +2394,13 @@ func RevisionDbName(baseName string, rev string) string { return baseName + DbRevisionDelimiter + rev } -// SplitRevisionDbName returns the base database name and revision from a traditional revision-qualified name. Splits on -// the first "/". +// SplitRevisionDbName returns the base database name and revision from a revision-qualified name. Resolves on the +// [DbRevisionDelimiter] and [DbRevisionDelimiterAlias]. func SplitRevisionDbName(dbName string) (string, string) { - if idx := strings.Index(dbName, DbRevisionDelimiter); idx >= 0 { - return dbName[:idx], dbName[idx+1:] + if base, revision, ok := strings.Cut(dbName, DbRevisionDelimiter); ok && base != "" { + return base, revision + } else if base, revision, ok := strings.Cut(dbName, DbRevisionDelimiterAlias); ok && base != "" { + return base, revision } return dbName, "" } - -// NormalizeRevisionDelimiter rewrites "base@revision" names to "base/revision". Names that already contain "/" are -// returned unchanged so bases that include "@" keep their existing interpretation. -func NormalizeRevisionDelimiter(dbName string) (rewrite string, usesDelimiterAlias bool) { - if strings.Contains(dbName, DbRevisionDelimiter) { - return dbName, false - } - - lastAliasIndex := strings.LastIndex(dbName, DbRevisionDelimiterAlias) - if lastAliasIndex < 0 { - return dbName, false - } - - base := dbName[:lastAliasIndex] - revision := dbName[lastAliasIndex+1:] - return RevisionDbName(base, revision), true -} diff --git a/go/libraries/doltcore/sqle/database.go b/go/libraries/doltcore/sqle/database.go index d541bf197e..e36570d8ab 100644 --- a/go/libraries/doltcore/sqle/database.go +++ b/go/libraries/doltcore/sqle/database.go @@ -1279,6 +1279,11 @@ func getStatusTableRootsProvider( concurrentmap.New[string, env.Remote]()) ws, err := sess.WorkingSet(ctx, db.RevisionQualifiedName()) if err != nil { + // Detached HEAD databases are read-only references and do not have a working set. + // Status tables should treat them as clean and return no rows. + if err == doltdb.ErrOperationNotSupportedInDetachedHead { + return nil, nil, nil + } return nil, nil, err } diff --git a/go/libraries/doltcore/sqle/database_provider.go b/go/libraries/doltcore/sqle/database_provider.go index 5780ad5348..ef3ef22280 100644 --- a/go/libraries/doltcore/sqle/database_provider.go +++ b/go/libraries/doltcore/sqle/database_provider.go @@ -418,34 +418,20 @@ func (p *DoltDatabaseProvider) HasDatabase(ctx *sql.Context, name string) bool { } func (p *DoltDatabaseProvider) AllDatabases(ctx *sql.Context) (all []sql.Database) { - normalized, usesDelimiterAlias := doltdb.NormalizeRevisionDelimiter(ctx.GetCurrentDatabase()) - baseName, currentRevision := doltdb.SplitRevisionDbName(normalized) - + _, revision := doltdb.SplitRevisionDbName(ctx.GetCurrentDatabase()) p.mu.RLock() + showBranches, err := dsess.GetBooleanSystemVar(ctx, dsess.ShowBranchDatabases) if err != nil { ctx.GetLogger().Warn(err) } - rdb, ok, err := p.databaseForRevision(ctx, normalized, ctx.GetCurrentDatabase()) - skipConflictDBName := "" - if usesDelimiterAlias && baseName != ctx.GetCurrentDatabase() { - conflictDB, conflictDBOk := p.databases[ctx.GetCurrentDatabase()] - if conflictDBOk && ok && err == nil { - skipConflictDBName = conflictDB.AliasedName() - } - } - all = make([]sql.Database, 0, len(p.databases)) for _, db := range p.databases { - if skipConflictDBName == db.AliasedName() { - continue - } - all = append(all, db) if showBranches && db.Name() != clusterdb.DoltClusterDbName { - revisionDbs, err := p.allRevisionDbs(ctx, db, normalized) + revisionDbs, err := p.allRevisionDbs(ctx, db, formatDbMapKeyName(ctx.GetCurrentDatabase())) if err != nil { // TODO: this interface is wrong, needs to return errors ctx.GetLogger().Warnf("error fetching revision databases: %s", err.Error()) @@ -456,15 +442,12 @@ func (p *DoltDatabaseProvider) AllDatabases(ctx *sql.Context) (all []sql.Databas } p.mu.RUnlock() - // If there's a revision database in use, include it in the list (but don't double-count). When showBranches is off - // we still include the current revision db if one is in use, so the active database is always visible in SHOW - // DATABASES. - if currentRevision != "" && !showBranches { + // If there's a revision database in use, include it in the list (but don't double-count). + if revision != "" && !showBranches { + rdb, ok, err := p.databaseForRevision(ctx, formatDbMapKeyName(ctx.GetCurrentDatabase()), ctx.GetCurrentDatabase()) if err != nil { // TODO: this interface is wrong, needs to return errors - if !sql.ErrDatabaseNotFound.Is(err) { - ctx.GetLogger().Warnf("error fetching revision databases: %s", err.Error()) - } + ctx.GetLogger().Warnf("error fetching revision databases: %s", err.Error()) } else if ok { all = append(all, rdb) } @@ -498,7 +481,7 @@ func (p *DoltDatabaseProvider) DoltDatabases() []dsess.SqlDatabase { } // allRevisionDbs returns all revision dbs for the database given -func (p *DoltDatabaseProvider) allRevisionDbs(ctx *sql.Context, db dsess.SqlDatabase, currDb string) ([]sql.Database, error) { +func (p *DoltDatabaseProvider) allRevisionDbs(ctx *sql.Context, db dsess.SqlDatabase, currentDB string) ([]sql.Database, error) { branches, err := db.DbData().Ddb.GetBranches(ctx) if err != nil { return nil, err @@ -510,7 +493,7 @@ func (p *DoltDatabaseProvider) allRevisionDbs(ctx *sql.Context, db dsess.SqlData requestedName := revisionQualifiedName // If the current DB matches, it means we're either using `@` or `/` delimited revision database name. So, we // replace the revisionQualifiedName with the [ctx.GetCurrentDatabase] result to maintain the exact delimiter. - if revisionQualifiedName == currDb { + if revisionQualifiedName == currentDB { requestedName = ctx.GetCurrentDatabase() } revDb, ok, err := p.databaseForRevision(ctx, revisionQualifiedName, requestedName) @@ -561,6 +544,11 @@ func commitTransaction(ctx *sql.Context, dSess *dsess.DoltSession, rsc *doltdb.R } func (p *DoltDatabaseProvider) CreateCollatedDatabase(ctx *sql.Context, name string, collation sql.CollationID) (err error) { + // On Windows we have to check before creating the directory to avoid a process lock. + if strings.ContainsAny(name, doltdb.DbRevisionDelimiterAlias+doltdb.DbRevisionDelimiter) { + return sql.ErrWrongDBName.New(name) + } + exists, isDir := p.fs.Exists(name) if exists && isDir { return sql.ErrDatabaseExists.New(name) @@ -865,10 +853,7 @@ func (p *DoltDatabaseProvider) cloneDatabaseFromRemote( // DropDatabase implements the sql.MutableDatabaseProvider interface func (p *DoltDatabaseProvider) DropDatabase(ctx *sql.Context, name string) error { _, revision := doltdb.SplitRevisionDbName(name) - normalized, usesDelimiterAlias := doltdb.NormalizeRevisionDelimiter(name) - // To maintain parity with CreateDatabase we do the same call as GMS with HasDatabase, if a revision exists using - // the `@` delimiter an error will occur. Otherwise, no error. - if revision != "" || (usesDelimiterAlias && p.HasDatabase(ctx, normalized)) { + if revision != "" { return fmt.Errorf("unable to drop revision database: %s", name) } @@ -977,6 +962,12 @@ func (p *DoltDatabaseProvider) PurgeDroppedDatabases(ctx *sql.Context) error { // function is responsible for instantiating the new Database instance and updating the tracking metadata // in this provider. If any problems are encountered while registering the new database, an error is returned. func (p *DoltDatabaseProvider) registerNewDatabase(ctx *sql.Context, name string, newEnv *env.DoltEnv) (err error) { + // Creating normal database names with revision delimiters can create ambiguity in methods that do not have access + // to some sort database table (e.g., client-side evaluations through server queries). + if strings.ContainsAny(name, doltdb.DbRevisionDelimiter+doltdb.DbRevisionDelimiterAlias) { + return sql.ErrWrongDBName.New(name) + } + // This method MUST be called with the provider's mutex locked if err = lockutil.AssertRWMutexIsLocked(p.mu); err != nil { return fmt.Errorf("unable to register new database without database provider mutex being locked") @@ -1066,15 +1057,15 @@ func (p *DoltDatabaseProvider) invalidateDbStateInAllSessions(ctx *sql.Context, } func (p *DoltDatabaseProvider) databaseForRevision(ctx *sql.Context, revisionQualifiedName string, requestedName string) (dsess.SqlDatabase, bool, error) { - if !strings.Contains(revisionQualifiedName, doltdb.DbRevisionDelimiter) { + if !strings.ContainsAny(revisionQualifiedName, doltdb.DbRevisionDelimiter+doltdb.DbRevisionDelimiterAlias) { return nil, false, nil } baseName, rev := doltdb.SplitRevisionDbName(revisionQualifiedName) p.mu.RLock() - srcDb, srcOk := p.databases[formatDbMapKeyName(baseName)] + srcDb, ok := p.databases[formatDbMapKeyName(baseName)] p.mu.RUnlock() - if !srcOk { + if !ok { return nil, false, nil } @@ -1449,70 +1440,52 @@ func (p *DoltDatabaseProvider) BaseDatabase(ctx *sql.Context, name string) (dses // SessionDatabase implements dsess.SessionDatabaseProvider func (p *DoltDatabaseProvider) SessionDatabase(ctx *sql.Context, name string) (dsess.SqlDatabase, bool, error) { - normalized, usesDelimiterAlias := doltdb.NormalizeRevisionDelimiter(name) - baseName, revision := doltdb.SplitRevisionDbName(normalized) + baseName, revision := doltdb.SplitRevisionDbName(strings.ToLower(name)) + revisionQualifiedName := formatDbMapKeyName(name) - var ok bool p.mu.RLock() - db, ok := p.databases[strings.ToLower(baseName)] - var rawDB dsess.SqlDatabase - rawDBOk := false - if usesDelimiterAlias { - rawDB, rawDBOk = p.databases[strings.ToLower(name)] - } + db, ok := p.databases[baseName] standby := *p.isStandby p.mu.RUnlock() var err error - if usesDelimiterAlias && !rawDBOk { - rawDB, err = p.databaseForClone(ctx, strings.ToLower(name)) - // Ignore error, revision needs to be evaluated first. - rawDBOk = rawDB != nil && err == nil - } - // If the database doesn't exist and this is a read replica, attempt to clone it from the remote if !ok { - db, err = p.databaseForClone(ctx, strings.ToLower(baseName)) - if err != nil && !usesDelimiterAlias { + db, err = p.databaseForClone(ctx, baseName) + if err != nil || db == nil { return nil, false, err } - ok = db != nil - if !ok && !rawDBOk { - return nil, false, nil - } } // Some DB implementations don't support addressing by versioned names, so return directly if we have one of those - if ok && !db.Versioned() { + if !db.Versioned() { return wrapForStandby(db, standby), true, nil } // Convert to a revision database before returning. If we got a non-qualified name, convert it to a qualified name // using the session's current head - sess := dsess.DSessFromSess(ctx.Session) usingDefaultBranch := false - var head string - if ok && revision == "" { - head, usingDefaultBranch, err = p.resolveCurrentOrDefaultHead(ctx, sess, db, baseName) + head := "" + if revision == "" { + head, ok, err = dsess.DSessFromSess(ctx.Session).CurrentHead(ctx, baseName) if err != nil { return nil, false, err } - normalized = baseName + doltdb.DbRevisionDelimiter + head - } - - db, ok, err = p.databaseForRevision(ctx, normalized, name) - if (!ok || err != nil) && rawDBOk { - if !rawDB.Versioned() { - return wrapForStandby(rawDB, standby), true, nil - } - head, usingDefaultBranch, err = p.resolveCurrentOrDefaultHead(ctx, sess, rawDB, name) - if err != nil { - return nil, false, err - } - normalized = name + doltdb.DbRevisionDelimiter + head - db, ok, err = p.databaseForRevision(ctx, normalized, name) + + // A newly created session may not have any info on current head stored yet, in which case we get the default + // branch for the db itself instead. + if !ok { + usingDefaultBranch = true + head, err = dsess.DefaultHead(ctx, baseName, db) + if err != nil { + return nil, false, err + } + } + + revisionQualifiedName = baseName + doltdb.DbRevisionDelimiter + head } + db, ok, err = p.databaseForRevision(ctx, revisionQualifiedName, name) if err != nil { if sql.ErrDatabaseNotFound.Is(err) && usingDefaultBranch { // We can return a better error message here in some cases @@ -1531,24 +1504,6 @@ func (p *DoltDatabaseProvider) SessionDatabase(ctx *sql.Context, name string) (d return wrapForStandby(db, standby), true, nil } -func (p *DoltDatabaseProvider) resolveCurrentOrDefaultHead(ctx *sql.Context, sess *dsess.DoltSession, db dsess.SqlDatabase, baseName string) (resolvedHead string, usedDefaultHead bool, err error) { - resolvedHead, ok, err := sess.CurrentHead(ctx, baseName) - if err != nil { - return "", false, err - } - if ok { - return resolvedHead, false, nil - } - - // A newly created session may not have any info on current head stored yet, in which case we get the default - // branch for the db itself instead. - resolvedHead, err = dsess.DefaultHead(ctx, baseName, db) - if err != nil { - return "", false, err - } - return resolvedHead, true, nil -} - // Function implements the FunctionProvider interface func (p *DoltDatabaseProvider) Function(_ *sql.Context, name string) (sql.Function, bool) { fn, ok := p.functions[strings.ToLower(name)] diff --git a/go/libraries/doltcore/sqle/dfunctions/active_revision.go b/go/libraries/doltcore/sqle/dfunctions/active_revision.go deleted file mode 100644 index 1232dfe186..0000000000 --- a/go/libraries/doltcore/sqle/dfunctions/active_revision.go +++ /dev/null @@ -1,74 +0,0 @@ -// 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 dfunctions - -import ( - "github.com/dolthub/go-mysql-server/sql" - "github.com/dolthub/go-mysql-server/sql/types" -) - -const ActiveRevisionFuncName = "active_revision" - -type ActiveRevisionFunc struct{} - -// NewActiveRevisionFunc creates a new ActiveRevisionFunc expression. -func NewActiveRevisionFunc() sql.Expression { - return &ActiveRevisionFunc{} -} - -// Eval implements the Expression interface. -func (*ActiveRevisionFunc) Eval(ctx *sql.Context, _ sql.Row) (interface{}, error) { - _, activeRevision, err := resolveSessionDatabaseIdentity(ctx) - if err != nil { - return nil, err - } - if activeRevision == "" { - return nil, nil - } - return activeRevision, nil -} - -// String implements the Stringer interface. -func (*ActiveRevisionFunc) String() string { - return "ACTIVE_REVISION()" -} - -// IsNullable implements the Expression interface. -func (*ActiveRevisionFunc) IsNullable() bool { - return true -} - -// Resolved implements the Expression interface. -func (*ActiveRevisionFunc) Resolved() bool { - return true -} - -// Type implements the Expression interface. -func (*ActiveRevisionFunc) Type() sql.Type { - return types.Text -} - -// Children implements the Expression interface. -func (*ActiveRevisionFunc) Children() []sql.Expression { - return nil -} - -// WithChildren implements the Expression interface. -func (f *ActiveRevisionFunc) WithChildren(children ...sql.Expression) (sql.Expression, error) { - if len(children) != 0 { - return nil, sql.ErrInvalidChildrenNumber.New(f, len(children), 0) - } - return NewActiveRevisionFunc(), nil -} diff --git a/go/libraries/doltcore/sqle/dfunctions/base_database.go b/go/libraries/doltcore/sqle/dfunctions/base_database.go deleted file mode 100644 index ff9907fb73..0000000000 --- a/go/libraries/doltcore/sqle/dfunctions/base_database.go +++ /dev/null @@ -1,98 +0,0 @@ -// 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 dfunctions - -import ( - "github.com/dolthub/go-mysql-server/sql" - "github.com/dolthub/go-mysql-server/sql/types" - - "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" -) - -const BaseDatabaseFuncName = "base_database" - -type BaseDatabaseFunc struct{} - -// NewBaseDatabaseFunc creates a new BaseDatabaseFunc expression. -func NewBaseDatabaseFunc() sql.Expression { - return &BaseDatabaseFunc{} -} - -// Eval implements the Expression interface. -func (*BaseDatabaseFunc) Eval(ctx *sql.Context, _ sql.Row) (interface{}, error) { - baseDatabase, _, err := resolveSessionDatabaseIdentity(ctx) - if err != nil { - return nil, err - } - if baseDatabase == "" { - return nil, nil - } - return baseDatabase, nil -} - -// String implements the Stringer interface. -func (*BaseDatabaseFunc) String() string { - return "BASE_DATABASE()" -} - -// IsNullable implements the Expression interface. -func (*BaseDatabaseFunc) IsNullable() bool { - return true -} - -// Resolved implements the Expression interface. -func (*BaseDatabaseFunc) Resolved() bool { - return true -} - -// Type implements the Expression interface. -func (*BaseDatabaseFunc) Type() sql.Type { - return types.Text -} - -// Children implements the Expression interface. -func (*BaseDatabaseFunc) Children() []sql.Expression { - return nil -} - -// WithChildren implements the Expression interface. -func (f *BaseDatabaseFunc) WithChildren(children ...sql.Expression) (sql.Expression, error) { - if len(children) != 0 { - return nil, sql.ErrInvalidChildrenNumber.New(f, len(children), 0) - } - return NewBaseDatabaseFunc(), nil -} - -// resolveSessionDatabaseIdentity resolves the base database and active revision for the current session database. -func resolveSessionDatabaseIdentity(ctx *sql.Context) (baseDatabase string, activeRevision string, err error) { - dbName := ctx.GetCurrentDatabase() - if dbName == "" { - return "", "", nil - } - - dSess := dsess.DSessFromSess(ctx.Session) - sessionDb, ok, err := dSess.Provider().SessionDatabase(ctx, dbName) - if err != nil { - return "", "", err - } - if !ok { - // Non-Dolt databases can still be current. - return dbName, "", nil - } - - baseDatabase = sessionDb.AliasedName() - activeRevision = sessionDb.Revision() - return baseDatabase, activeRevision, nil -} diff --git a/go/libraries/doltcore/sqle/dfunctions/init.go b/go/libraries/doltcore/sqle/dfunctions/init.go index 4f87cc63ca..6fb05d3294 100644 --- a/go/libraries/doltcore/sqle/dfunctions/init.go +++ b/go/libraries/doltcore/sqle/dfunctions/init.go @@ -22,8 +22,6 @@ var DoltFunctions = []sql.Function{ sql.Function0{Name: VersionFuncName, Fn: NewVersion}, sql.Function0{Name: StorageFormatFuncName, Fn: NewStorageFormat}, sql.Function0{Name: ActiveBranchFuncName, Fn: NewActiveBranchFunc}, - sql.Function0{Name: BaseDatabaseFuncName, Fn: NewBaseDatabaseFunc}, - sql.Function0{Name: ActiveRevisionFuncName, Fn: NewActiveRevisionFunc}, sql.Function2{Name: DoltMergeBaseFuncName, Fn: NewMergeBase}, sql.Function2{Name: HasAncestorFuncName, Fn: NewHasAncestor}, sql.Function1{Name: HashOfTableFuncName, Fn: NewHashOfTable}, diff --git a/go/libraries/doltcore/sqle/dsess/session.go b/go/libraries/doltcore/sqle/dsess/session.go index d60a228c25..4297619ceb 100644 --- a/go/libraries/doltcore/sqle/dsess/session.go +++ b/go/libraries/doltcore/sqle/dsess/session.go @@ -175,19 +175,7 @@ func GetTableResolver(ctx *sql.Context, dbName string) (doltdb.TableResolver, er // lookupDbState is the private version of LookupDbState, returning a struct that has more information available than // the interface returned by the public method. func (d *DoltSession) lookupDbState(ctx *sql.Context, dbName string) (*branchState, bool, error) { - dbName = strings.ToLower(dbName) - baseName, rev := doltdb.SplitRevisionDbName(dbName) - - // usesDelimiterAlias once normalized goes to false, so any `@` character will not be treated as a delimiter. Since - // it has two meanings, either a revision or normal DB, we process twice. - normalized, usesDelimiterAlias := doltdb.NormalizeRevisionDelimiter(dbName) - if usesDelimiterAlias { - state, ok, err := d.lookupDbState(ctx, normalized) - if err == nil && ok { - return state, ok, err - } - } - + baseName, rev := doltdb.SplitRevisionDbName(strings.ToLower(dbName)) d.mu.Lock() dbState, dbStateFound := d.dbStates[baseName] d.mu.Unlock() diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go b/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go index 0e0dffc004..b6836185d7 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go @@ -1219,6 +1219,11 @@ func TestDoltScripts(t *testing.T) { RunDoltScriptsTest(t, harness) } +func TestDoltProcedureScripts(t *testing.T) { + h := newDoltEnginetestHarness(t) + RunDoltProcedureScriptsTest(t, h) +} + func TestDoltTempTableScripts(t *testing.T) { harness := newDoltEnginetestHarness(t) RunDoltTempTableScripts(t, harness) diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go b/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go index 6847fa16c9..bb74828efd 100755 --- a/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_engine_tests.go @@ -513,6 +513,8 @@ func RunStoredProceduresTest(t *testing.T, h DoltEnginetestHarness) { } func RunDoltStoredProceduresTest(t *testing.T, h DoltEnginetestHarness) { + DoltProcedureTests := append(DoltProcedureTests, DoltBackupProcedureScripts...) + DoltProcedureTests = append(DoltProcedureTests, DoltStatusProcedureScripts...) for _, script := range DoltProcedureTests { func() { h := h.NewHarness(t) @@ -524,6 +526,8 @@ func RunDoltStoredProceduresTest(t *testing.T, h DoltEnginetestHarness) { } func RunDoltStoredProceduresPreparedTest(t *testing.T, h DoltEnginetestHarness) { + DoltProcedureTests := append(DoltProcedureTests, DoltBackupProcedureScripts...) + DoltProcedureTests = append(DoltProcedureTests, DoltStatusProcedureScripts...) for _, script := range DoltProcedureTests { func() { h := h.NewHarness(t) diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_procedure_queries.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries_procedures.go similarity index 97% rename from go/libraries/doltcore/sqle/enginetest/dolt_procedure_queries.go rename to go/libraries/doltcore/sqle/enginetest/dolt_queries_procedures.go index b6f81bba97..5956494953 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_procedure_queries.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries_procedures.go @@ -26,10 +26,6 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/env" ) -func init() { - DoltProcedureTests = append(DoltProcedureTests, DoltBackupProcedureScripts...) -} - // fileUrl returns a file:// URL path. func fileUrl(path string) string { path = filepath.Join(os.TempDir(), path) @@ -957,3 +953,31 @@ END }, }, } + +var DoltStatusProcedureScripts = []queries.ScriptTest{ + { + Name: "dolt_status detached head is read-only clean", + SetUpScript: []string{ + "call dolt_commit('--allow-empty', '-m', 'empty commit');", + "call dolt_tag('tag1');", + "set @head_hash = (select hashof('main') limit 1);", + "set @status_by_hash = concat('select * from `mydb/', @head_hash, '`.dolt_status;');", + "prepare status_by_hash from @status_by_hash;", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "select * from `mydb/tag1`.dolt_status;", + Expected: []sql.Row{}, + }, + { + Query: "execute status_by_hash;", + Expected: []sql.Row{}, + }, + { + Query: "select * from `information_schema`.dolt_status;", + // Non-versioned database. + ExpectedErr: sql.ErrTableNotFound, + }, + }, + }, +} diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries_revision_specs.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries_revision_specs.go index e1b9bfd4a9..0f33b14690 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_queries_revision_specs.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries_revision_specs.go @@ -15,8 +15,11 @@ package enginetest import ( + "fmt" + "github.com/dolthub/go-mysql-server/enginetest/queries" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/plan" "github.com/dolthub/go-mysql-server/sql/types" ) @@ -404,146 +407,77 @@ var DoltRevisionDbScripts = []queries.ScriptTest{ }, }, }, - { - Name: "database revision specs: db revision delimiter alias '@' is ignored when no revision exists", - SetUpScript: []string{ - "create database `mydb@branch1`;", - "create table t1(t int);", - "call dolt_commit('-Am', 'init t1');", - "create database `test-10382`;", - "use `test-10382`;", - }, - Assertions: []queries.ScriptTestAssertion{ - { - Query: "use `mydb@branch1`;", - Expected: []sql.Row{}, - }, - { - Query: "drop database `test-10382`;", - Expected: []sql.Row{{types.NewOkResult(1)}}, - }, - { - Query: "select database();", - Expected: []sql.Row{{"mydb@branch1"}}, - }, - { - Query: "show databases", - Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb@branch1"}, {"mysql"}}, - }, - { - Query: "set dolt_show_branch_databases = on;", - Expected: []sql.Row{{types.NewOkResult(0)}}, - }, - { - Query: "show databases", - Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb/main"}, {"mydb@branch1"}, {"mydb@branch1/main"}, {"mysql"}}, - }, - { - Query: "use `mydb@branch1`;", - Expected: []sql.Row{}, - }, - { - Query: "call dolt_branch('branch2');", - Expected: []sql.Row{{0}}, - }, - { - Query: "use `mydb@branch1@branch2`;", - Expected: []sql.Row{}, - }, - { - Query: "select database();", - Expected: []sql.Row{{"mydb@branch1@branch2"}}, - }, - { - Query: "use `mydb@branch1`;", - Expected: []sql.Row{}, - }, - { - Query: "show databases", - Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb/main"}, {"mydb@branch1"}, {"mydb@branch1/main"}, {"mydb@branch1/branch2"}, {"mysql"}}, - }, - { - Query: "call dolt_branch('branch@');", - Expected: []sql.Row{{0}}, - }, - { - Query: "show databases", - Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb/main"}, {"mydb@branch1"}, {"mydb@branch1/main"}, {"mydb@branch1/branch2"}, {"mydb@branch1/branch@"}, {"mysql"}}, - }, - { - Query: "set dolt_show_branch_databases = off;", - Expected: []sql.Row{{types.NewOkResult(0)}}, - }, - { - Query: "show databases", - Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb@branch1"}, {"mysql"}}, - }, - { - Query: "select * from t1;", - ExpectedErr: sql.ErrTableNotFound, - }, - { - Query: "use mydb;", - Expected: []sql.Row{}, - }, - { - Query: "select * from t1;", - Expected: []sql.Row{}, - }, - }, - }, { Name: "database revision specs: db revision delimiter alias '@'", SetUpScript: []string{ - "create table t01 (pk int primary key, c1 int);", - "call dolt_add('.');", - "call dolt_commit('-am', 'creating table t01 on main');", - "insert into t01 values (1, 1), (2, 2);", - "call dolt_commit('-am', 'adding rows to table t01 on main');", - "call dolt_tag('tag1');", + "create table t02 (pk int primary key, c1 int);", + "call dolt_commit('-Am', 'creating table t02 on main');", + "call dolt_tag('tag2');", }, Assertions: []queries.ScriptTestAssertion{ { - Query: "create database `mydb@branch1`;", - Expected: []sql.Row{{types.NewOkResult(1)}}, - }, - { - Query: "use mydb;", - Expected: []sql.Row{}, - }, - { - Query: "call dolt_branch('branch1');", - Expected: []sql.Row{{0}}, - }, - { - Query: "insert into t01 values (3, 3);", - Expected: []sql.Row{{types.NewOkResult(1)}}, - }, - { - Query: "call dolt_commit('-am', 'adding rows to table t01');", - SkipResultsCheck: true, + Query: "create database `mydb@branch1`;", + ExpectedErr: sql.ErrWrongDBName, + ExpectedErrStr: "mydb@branch1", }, { Query: "use `mydb@main`;", Expected: []sql.Row{}, }, { - Query: "show databases;", - // The mydb@branch1 database is shown, not the revision `branch1` from `mydb` cause we're on `main`. - Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb@branch1"}, {"mydb@main"}, {"mysql"}}, + // We want to show the revision database in-use as the original requested name. + Query: "show databases;", + Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb@main"}, {"mysql"}}, + }, + { + Query: "call dolt_checkout('-b', 'branch@');", + Expected: []sql.Row{{0, "Switched to branch 'branch@'"}}, + }, + { + Query: "use `mydb/branch@`", + // The `/` delimiter takes precedence over the alias. + Expected: []sql.Row{}, + }, + { + Query: "select database()", + Expected: []sql.Row{{"mydb/branch@"}}, + }, + { + Query: "drop database `mydb@branch@`", + // The first index is processed first, allowing branch names with the @ character. + ExpectedErrStr: fmt.Sprintf("unable to drop revision database: %s", "mydb@branch@"), + }, + { + Query: "use `mydb@branch@`;", + Expected: []sql.Row{}, + }, + { + Query: "call dolt_checkout('-b', 'branch1');", + // dolt_checkout works from a revision database using the alias delimiter. + Expected: []sql.Row{{0, "Switched to branch 'branch1'"}}, + }, + { + Query: "call dolt_branch('-D', 'branch@');", + Expected: []sql.Row{{0}}, + }, + { + Query: "create table t01(pk int primary key, c1 int);", + Expected: []sql.Row{{types.NewOkResult(0)}}, + }, + { + Query: "call dolt_commit('-Am', 'creating table t01 on branch1');", + Expected: []sql.Row{{doltCommit}}, }, { Query: "use `mydb@branch1`;", Expected: []sql.Row{}, }, { - Query: "show databases;", - // The revision branch1 is shown, not the `mydb@branch1` database. + Query: "show databases;", Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb@branch1"}, {"mysql"}}, }, { - Query: "select database();", - // We want to see the revision shown in the format it was requested, this is not the literal db. + Query: "select database();", Expected: []sql.Row{{"mydb@branch1"}}, }, { @@ -552,15 +486,54 @@ var DoltRevisionDbScripts = []queries.ScriptTest{ }, { Query: "show databases;", - Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb/main"}, {"mydb@branch1"}, {"mysql"}}, + Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb@branch1"}, {"mydb/main"}, {"mysql"}}, }, { Query: "select active_branch();", Expected: []sql.Row{{"branch1"}}, }, + { + Query: "select * from `mydb@branch1`.t01;", + Expected: []sql.Row{}, + }, + { + Query: "insert into `mydb@branch1`.t01 values (1, 10), (2, 20);", + Expected: []sql.Row{{types.NewOkResult(2)}}, + }, + { + Query: "select * from `mydb@branch1`.t01 order by pk;", + Expected: []sql.Row{{1, 10}, {2, 20}}, + }, + { + Query: "update `mydb@branch1`.t01 set c1 = 30 where pk = 2;", + Expected: []sql.Row{{types.OkResult{ + Info: plan.UpdateInfo{Matched: 1, Updated: 1}, + RowsAffected: 1, + }}}, + }, + { + Query: "select * from `mydb@branch1`.t01 where pk = 2;", + Expected: []sql.Row{{2, 30}}, + }, + { + Query: "delete from `mydb@branch1`.t01 where pk = 1;", + Expected: []sql.Row{{types.NewOkResult(1)}}, + }, + { + Query: "select * from `mydb@branch1`.t01 order by pk;", + Expected: []sql.Row{{2, 30}}, + }, + { + Query: "alter table `mydb@branch1`.t01 add index idx_t01_c1 (c1);", + Expected: []sql.Row{{types.NewOkResult(0)}}, + }, + { + Query: "select index_name, column_name from information_schema.statistics where table_schema = database() and table_name = 't01' and index_name = 'idx_t01_c1' order by seq_in_index;", + Expected: []sql.Row{{"idx_t01_c1", "c1"}}, + }, { Query: "select * from t01;", - Expected: []sql.Row{{1, 1}, {2, 2}}, + Expected: []sql.Row{{2, 30}}, }, { Query: "select column_name from information_schema.columns where table_schema = database() and table_name = 't01' order by ordinal_position;", @@ -570,26 +543,6 @@ var DoltRevisionDbScripts = []queries.ScriptTest{ Query: "select table_name from information_schema.tables where table_schema = database() and table_name = 't01';", Expected: []sql.Row{{"t01"}}, }, - { - Query: "use mydb;", - Expected: []sql.Row{}, - }, - { - Query: "show databases;", - Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb/main"}, {"mydb/branch1"}, {"mydb@branch1"}, {"mydb@branch1/main"}, {"mysql"}}, - }, - { - Query: "select * from `mydb@branch1`.t01;", - Expected: []sql.Row{{1, 1}, {2, 2}}, - }, - { - Query: "select * from `mydb@tag1`.t01;", - Expected: []sql.Row{{1, 1}, {2, 2}}, - }, - { - Query: "use `mydb@branch1`;", - Expected: []sql.Row{}, - }, { Query: "create table parent(id int primary key);", Expected: []sql.Row{{types.NewOkResult(0)}}, @@ -612,7 +565,7 @@ var DoltRevisionDbScripts = []queries.ScriptTest{ }, { Query: "show databases;", - Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb/main"}, {"mydb/branch1"}, {"mydb@branch1"}, {"mydb@branch1/main"}, {"mysql"}}, + Expected: []sql.Row{{"information_schema"}, {"mydb"}, {"mydb/branch1"}, {"mydb/main"}, {"mysql"}}, }, { Query: "select database();", @@ -620,17 +573,12 @@ var DoltRevisionDbScripts = []queries.ScriptTest{ }, { Query: "select * from t01;", - Expected: []sql.Row{{1, 1}, {2, 2}}, + Expected: []sql.Row{{2, 30}}, }, { Query: "select column_name from information_schema.columns where table_schema = database() and table_name = 't01' order by ordinal_position;", Expected: []sql.Row{{"pk"}, {"c1"}}, }, - { - Query: "drop database `mydb@branch1`;", - // The name above can be resolved to a real revision so we error out, keeping parity with CREATE below. - ExpectedErrStr: "unable to drop revision database: mydb@branch1", - }, { Query: "select table_name from information_schema.tables where table_schema = database() and table_name = 't01';", Expected: []sql.Row{{"t01"}}, @@ -644,65 +592,30 @@ var DoltRevisionDbScripts = []queries.ScriptTest{ Expected: []sql.Row{{1, 1}}, }, { - Query: "use mydb;", + Query: "drop table `mydb@branch1`.child;", + Expected: []sql.Row{{types.NewOkResult(0)}}, + }, + { + Query: "select table_name from information_schema.tables where table_schema = database() and table_name = 'child';", Expected: []sql.Row{}, }, { - Query: "call dolt_branch('-D', 'branch1');", - Expected: []sql.Row{{0}}, + Query: "select * from `mydb@tag2`.t02;", + Expected: []sql.Row{}, }, { - Query: "drop database `mydb@branch1`;", - Expected: []sql.Row{{types.NewOkResult(1)}}, + Query: "use `mydb/branch1`;", + Expected: []sql.Row{}, }, { - Query: "call dolt_branch('branch1');", - Expected: []sql.Row{{0}}, + Query: "select * from t01;", + Expected: []sql.Row{{2, 30}}, }, { Query: "create database `mydb@branch1`;", // This is a result of GMS' internal call to the providers' HasDatabase ExpectedErrStr: "can't create database mydb@branch1; database exists", }, - { - Query: "call dolt_branch('-D', 'branch1');", - Expected: []sql.Row{{0}}, - }, - { - Query: "create database `mydb@branch1`;", - Expected: []sql.Row{{types.NewOkResult(1)}}, - }, - }, - }, - { - Name: "database revision specs: base_database and active_revision", - SetUpScript: []string{ - "create table t1(pk int primary key);", - "call dolt_add('.');", - "call dolt_commit('-am', 'init');", - "call dolt_branch('branch1');", - }, - Assertions: []queries.ScriptTestAssertion{ - { - Query: "select base_database(), active_revision();", - Expected: []sql.Row{{"mydb", "main"}}, - }, - { - Query: "call dolt_checkout('branch1');", - Expected: []sql.Row{{0, "Switched to branch 'branch1'"}}, - }, - { - Query: "select base_database(), active_revision();", - Expected: []sql.Row{{"mydb", "branch1"}}, - }, - { - Query: "use `mydb@branch1`;", - Expected: []sql.Row{}, - }, - { - Query: "select base_database(), active_revision();", - Expected: []sql.Row{{"mydb", "branch1"}}, - }, }, }, { @@ -711,13 +624,13 @@ var DoltRevisionDbScripts = []queries.ScriptTest{ "create table t01 (pk int primary key, c1 int);", "call dolt_add('.');", "call dolt_commit('-am', 'creating table t01 on main');", - "set @h = (select hashof('main') limit 1);", - "set @use_sql = concat('use `mydb@', @h, '`');", + "set @h1 = (select hashof('main') limit 1);", + "set @use_sql = concat('use `mydb@', @h1, '`');", "prepare use_stmt from @use_sql;", "insert into t01 values (1, 1), (2, 2);", "call dolt_commit('-am', 'adding rows to table t01 on main');", - "set @h = (select hashof('main') limit 1);", - "set @select_sql = concat('select * from `mydb@', @h, '`.t01');", + "set @h2 = (select hashof('main') limit 1);", + "set @select_sql = concat('select * from `mydb@', @h2, '`.t01');", "prepare select_stmt from @select_sql;", }, Assertions: []queries.ScriptTestAssertion{ @@ -729,6 +642,10 @@ var DoltRevisionDbScripts = []queries.ScriptTest{ Query: "select length(database());", Expected: []sql.Row{{37}}, }, + { + Query: "select @h1;", + Expected: []sql.Row{{doltCommit}}, + }, { Query: "select * from t01;", Expected: []sql.Row{}, diff --git a/integration-tests/bats/sql-shell-revision-db.expect b/integration-tests/bats/sql-shell-revision-db.expect index deb698b859..0d5cfd639a 100644 --- a/integration-tests/bats/sql-shell-revision-db.expect +++ b/integration-tests/bats/sql-shell-revision-db.expect @@ -6,47 +6,56 @@ set env(NO_COLOR) 1 spawn dolt sql -expect_with_defaults {>} { send "create database `mydb@branch1`;\r" } +expect_with_defaults {>} { send "create schema `mydb@branch1`;\r" } -expect_with_defaults {>} { send "use `mydb@branch1`;\r" } +# Test twice to make sure no directory is leftover. +expect_with_defaults_after {Incorrect database name 'mydb@branch1'} {>} { send "create schema `mydb@branch1`;\r" } -expect_with_defaults_after {Database Changed} {mydb@branch1/main\*?>} { send "create schema `mydb`;\r" } +expect_with_defaults_after {Incorrect database name 'mydb@branch1'} {>} { send "create schema mydb;\r" } expect_with_defaults {>} { send "use mydb;\r" } -expect_with_defaults {>} { send "call dolt_branch('branch1');\r" } +expect_with_defaults_after {Database Changed} {mydb/main>} { send "call dolt_checkout('-b', 'branch1');\r" } -expect_with_defaults {>} { send "use \`mydb/branch1\`;\r" } +expect_with_defaults {mydb/main>} { send "use `mydb@branch1`;\r" } -expect_with_defaults {>} { send "show databases;\r" } +expect_with_defaults_after {Database Changed} {mydb/branch1>} { send "use \`mydb/branch1\`;\r" } -expect_with_defaults_after {rows in set} {mydb/branch1\*?>} { send "use mydb;\r" } +expect_with_defaults {mydb/branch1>} { send "show databases;\r" } -expect_with_defaults {>} { send "use \`mydb@branch1\`;\r" } +expect_with_defaults_after {rows in set} {mydb/branch1>} { send "use `mydb@branch1`;\r" } -expect_with_defaults_after {Database Changed} {mydb/branch1\*?>} { send "set @h = (select hashof('branch1') limit 1);\r" } +expect_with_defaults_after {Database Changed} {mydb/branch1>} { send "select database();\r" } -expect_with_defaults {>} { send "set @use_sql = concat('use `mydb@', @h, '`');\r" } +expect_with_defaults_after {mydb@branch1} {mydb/branch1>} { send "set @h = (select hashof('branch1') limit 1);\r" } -expect_with_defaults {>} { send "prepare use_stmt from @use_sql;\r" } +expect_with_defaults {mydb/branch1>} { send "call dolt_tag('tag1');\r" } -expect_with_defaults {>} { send "create table t1(i int);\r" } +expect_with_defaults {mydb/branch1>} { send "set @use_sql = concat('use `mydb@', @h, '`');\r" } -expect_with_defaults {>} { send "call dolt_commit('-Am', 'create table t1');\r" } +expect_with_defaults {mydb/branch1>} { send "prepare use_stmt from @use_sql;\r" } -expect_with_defaults {>} { send "show tables;\r" } +expect_with_defaults {mydb/branch1>} { send "create table t1(i int);\r" } -expect_with_defaults_after {t1} {>} { send "execute use_stmt;\r" } +expect_with_defaults {mydb/branch1*>} { send "call dolt_commit('-Am', 'create table t1');\r" } -expect_with_defaults_after {Empty set} {mydb/[0-9a-v]{32}\*?>} { send "show tables;\r" } +expect_with_defaults {mydb/branch1>} { send "show tables;\r" } -expect_with_defaults_after {Empty set} {mydb/[0-9a-v]{32}\*?>} { send "create database `mydb@branch1`;\r" } +expect_with_defaults_after {t1} {mydb/branch1>} { send "execute use_stmt;\r" } -expect_with_defaults_after {database exists} {>} { send "use `mydb`;\r" } +expect_with_defaults_after {Empty set} {mydb/[0-9a-v]{32}>} { send "show tables;\r" } -expect_with_defaults {>} { send "call dolt_checkout('branch1');\r" } +expect_with_defaults_after {Empty set} {mydb/[0-9a-v]{32}>} { send "select database();\r" } -expect_with_defaults_after {Switched to branch 'branch1'} {>} { send "exit;\r" } +expect_with_defaults_after {mydb@[0-9a-v]{32}} {mydb/[0-9a-v]{32}>} {send "create database `mydb@branch1`;\r" } + +expect_with_defaults_after {database exists} {mydb/[0-9a-v]{32}>} { send "use `mydb@tag1`;\r" } + +expect_with_defaults_after {Database Changed} {mydb/tag1>} { send "show tables;\r" } + +expect_with_defaults_after {Empty set} {mydb/tag1>} { send "select database();\r" } + +expect_with_defaults_after {mydb@tag1} {mydb/tag1>} { send "exit;\r" } expect eof exit 0