Merge pull request #3487 from dolthub/fulghum/dolt_branch

Adding support for deleting and renaming branches from SQL with `dolt_branch`
This commit is contained in:
Jason Fulghum
2022-06-02 17:41:28 -07:00
committed by GitHub
12 changed files with 614 additions and 83 deletions
+2 -2
View File
@@ -220,7 +220,7 @@ func moveBranch(ctx context.Context, dEnv *env.DoltEnv, apr *argparser.ArgParseR
force := apr.Contains(forceFlag)
src := apr.Arg(0)
dest := apr.Arg(1)
err := actions.RenameBranch(ctx, dEnv, src, apr.Arg(1), force)
err := actions.RenameBranch(ctx, dEnv.DbData(), dEnv.Config, src, apr.Arg(1), force)
var verr errhand.VerboseError
if err != nil {
@@ -285,7 +285,7 @@ func handleDeleteBranches(ctx context.Context, dEnv *env.DoltEnv, apr *argparser
for i := 0; i < apr.NArg(); i++ {
brName := apr.Arg(i)
err := actions.DeleteBranch(ctx, dEnv, brName, actions.DeleteOptions{
err := actions.DeleteBranch(ctx, dEnv.DbData(), dEnv.Config, brName, actions.DeleteOptions{
Force: force,
Remote: apr.Contains(remoteFlag),
})
+4
View File
@@ -32,6 +32,7 @@ import (
"github.com/dolthub/dolt/go/cmd/dolt/commands/engine"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
_ "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dfunctions"
"github.com/dolthub/dolt/go/libraries/doltcore/sqlserver"
)
// Serve starts a MySQL-compatible server. Returns any errors that were encountered.
@@ -62,6 +63,7 @@ func Serve(
}
serverController.StopServer()
serverController.serverStopped(closeError)
sqlserver.SetRunningServer(nil)
}()
if startError = ValidateConfig(serverConfig); startError != nil {
@@ -195,6 +197,8 @@ func Serve(
if startError != nil {
cli.PrintErr(startError)
return
} else {
sqlserver.SetRunningServer(mySQLServer)
}
var metSrv *http.Server
+14 -14
View File
@@ -154,17 +154,17 @@ func TestServerGoodParams(t *testing.T) {
tests := []ServerConfig{
DefaultServerConfig(),
DefaultServerConfig().withHost("127.0.0.1").withPort(15400),
DefaultServerConfig().withHost("localhost").withPort(15401),
//DefaultServerConfig().withHost("::1").withPort(15402), // Fails on Jenkins, assuming no IPv6 support
DefaultServerConfig().withUser("testusername").withPort(15403),
DefaultServerConfig().withPassword("hunter2").withPort(15404),
DefaultServerConfig().withTimeout(0).withPort(15405),
DefaultServerConfig().withTimeout(5).withPort(15406),
DefaultServerConfig().withLogLevel(LogLevel_Debug).withPort(15407),
DefaultServerConfig().withLogLevel(LogLevel_Info).withPort(15408),
DefaultServerConfig().withReadOnly(true).withPort(15409),
DefaultServerConfig().withUser("testusernamE").withPassword("hunter2").withTimeout(4).withPort(15410),
DefaultServerConfig().withHost("127.0.0.1").WithPort(15400),
DefaultServerConfig().withHost("localhost").WithPort(15401),
//DefaultServerConfig().withHost("::1").WithPort(15402), // Fails on Jenkins, assuming no IPv6 support
DefaultServerConfig().withUser("testusername").WithPort(15403),
DefaultServerConfig().withPassword("hunter2").WithPort(15404),
DefaultServerConfig().withTimeout(0).WithPort(15405),
DefaultServerConfig().withTimeout(5).WithPort(15406),
DefaultServerConfig().withLogLevel(LogLevel_Debug).WithPort(15407),
DefaultServerConfig().withLogLevel(LogLevel_Info).WithPort(15408),
DefaultServerConfig().withReadOnly(true).WithPort(15409),
DefaultServerConfig().withUser("testusernamE").withPassword("hunter2").withTimeout(4).WithPort(15410),
}
for _, test := range tests {
@@ -188,7 +188,7 @@ func TestServerGoodParams(t *testing.T) {
func TestServerSelect(t *testing.T) {
env := dtestutils.CreateEnvWithSeedData(t)
serverConfig := DefaultServerConfig().withLogLevel(LogLevel_Fatal).withPort(15300)
serverConfig := DefaultServerConfig().withLogLevel(LogLevel_Fatal).WithPort(15300)
sc := NewServerController()
defer sc.StopServer()
@@ -263,7 +263,7 @@ func TestServerFailsIfPortInUse(t *testing.T) {
func TestServerSetDefaultBranch(t *testing.T) {
dEnv := dtestutils.CreateEnvWithSeedData(t)
serverConfig := DefaultServerConfig().withLogLevel(LogLevel_Fatal).withPort(15302)
serverConfig := DefaultServerConfig().withLogLevel(LogLevel_Fatal).WithPort(15302)
sc := NewServerController()
defer sc.StopServer()
@@ -413,7 +413,7 @@ func TestReadReplica(t *testing.T) {
// start server as read replica
sc := NewServerController()
serverConfig := DefaultServerConfig().withLogLevel(LogLevel_Fatal).withPort(15303)
serverConfig := DefaultServerConfig().withLogLevel(LogLevel_Fatal).WithPort(15303)
func() {
os.Chdir(multiSetup.DbPaths[readReplicaDbName])
@@ -267,8 +267,8 @@ func (cfg *commandLineServerConfig) withHost(host string) *commandLineServerConf
return cfg
}
// withPort updates the port and returns the called `*commandLineServerConfig`, which is useful for chaining calls.
func (cfg *commandLineServerConfig) withPort(port int) *commandLineServerConfig {
// WithPort updates the port and returns the called `*commandLineServerConfig`, which is useful for chaining calls.
func (cfg *commandLineServerConfig) WithPort(port int) *commandLineServerConfig {
cfg.port = port
return cfg
}
+1 -1
View File
@@ -223,7 +223,7 @@ func getCommandLineServerConfig(dEnv *env.DoltEnv, apr *argparser.ArgParseResult
serverConfig.withHost(host)
}
if port, ok := apr.GetInt(portFlag); ok {
serverConfig.withPort(port)
serverConfig.WithPort(port)
}
if user, ok := apr.GetValue(userFlag); ok {
serverConfig.withUser(user)
+12 -12
View File
@@ -30,17 +30,17 @@ var ErrAlreadyExists = errors.New("already exists")
var ErrCOBranchDelete = errors.New("attempted to delete checked out branch")
var ErrUnmergedBranchDelete = errors.New("attempted to delete a branch that is not fully merged into its parent; use `-f` to force")
func RenameBranch(ctx context.Context, dEnv *env.DoltEnv, oldBranch, newBranch string, force bool) error {
func RenameBranch(ctx context.Context, dbData env.DbData, config *env.DoltCliConfig, oldBranch, newBranch string, force bool) error {
oldRef := ref.NewBranchRef(oldBranch)
newRef := ref.NewBranchRef(newBranch)
err := CopyBranch(ctx, dEnv, oldBranch, newBranch, force)
err := CopyBranchOnDB(ctx, dbData.Ddb, oldBranch, newBranch, force)
if err != nil {
return err
}
if ref.Equals(dEnv.RepoStateReader().CWBHeadRef(), oldRef) {
err = dEnv.RepoStateWriter().SetCWBHeadRef(ctx, ref.MarshalableRef{Ref: newRef})
if ref.Equals(dbData.Rsr.CWBHeadRef(), oldRef) {
err = dbData.Rsw.SetCWBHeadRef(ctx, ref.MarshalableRef{Ref: newRef})
if err != nil {
return err
}
@@ -59,13 +59,13 @@ func RenameBranch(ctx context.Context, dEnv *env.DoltEnv, oldBranch, newBranch s
// We always `force` here, because the CopyBranch up
// above created a new branch and it will have a
// working set.
err = dEnv.DoltDB.CopyWorkingSet(ctx, fromWSRef, toWSRef, true /* force */)
err = dbData.Ddb.CopyWorkingSet(ctx, fromWSRef, toWSRef, true /* force */)
if err != nil {
return err
}
}
return DeleteBranch(ctx, dEnv, oldBranch, DeleteOptions{Force: true})
return DeleteBranch(ctx, dbData, config, oldBranch, DeleteOptions{Force: true})
}
func CopyBranch(ctx context.Context, dEnv *env.DoltEnv, oldBranch, newBranch string, force bool) error {
@@ -111,7 +111,7 @@ type DeleteOptions struct {
Remote bool
}
func DeleteBranch(ctx context.Context, dEnv *env.DoltEnv, brName string, opts DeleteOptions) error {
func DeleteBranch(ctx context.Context, dbData env.DbData, config *env.DoltCliConfig, brName string, opts DeleteOptions) error {
var dref ref.DoltRef
if opts.Remote {
var err error
@@ -121,16 +121,16 @@ func DeleteBranch(ctx context.Context, dEnv *env.DoltEnv, brName string, opts De
}
} else {
dref = ref.NewBranchRef(brName)
if ref.Equals(dEnv.RepoStateReader().CWBHeadRef(), dref) {
if ref.Equals(dbData.Rsr.CWBHeadRef(), dref) {
return ErrCOBranchDelete
}
}
return DeleteBranchOnDB(ctx, dEnv, dref, opts)
return DeleteBranchOnDB(ctx, dbData, config, dref, opts)
}
func DeleteBranchOnDB(ctx context.Context, dEnv *env.DoltEnv, dref ref.DoltRef, opts DeleteOptions) error {
ddb := dEnv.DoltDB
func DeleteBranchOnDB(ctx context.Context, dbData env.DbData, config *env.DoltCliConfig, dref ref.DoltRef, opts DeleteOptions) error {
ddb := dbData.Ddb
hasRef, err := ddb.HasRef(ctx, dref)
if err != nil {
@@ -140,7 +140,7 @@ func DeleteBranchOnDB(ctx context.Context, dEnv *env.DoltEnv, dref ref.DoltRef,
}
if !opts.Force && !opts.Remote {
ms, err := doltdb.NewCommitSpec(env.GetDefaultInitBranch(dEnv.Config))
ms, err := doltdb.NewCommitSpec(env.GetDefaultInitBranch(config))
if err != nil {
return err
}
@@ -26,8 +26,11 @@ import (
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/dolt/go/libraries/doltcore/env/actions"
"github.com/dolthub/dolt/go/libraries/doltcore/ref"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
"github.com/dolthub/dolt/go/libraries/doltcore/sqlserver"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
"github.com/dolthub/dolt/go/libraries/utils/filesys"
)
const DoltBranchFuncName = "dolt_branch"
@@ -91,50 +94,130 @@ func DoDoltBranch(ctx *sql.Context, args []string) (int, error) {
switch {
case apr.Contains(cli.CopyFlag):
err = makeACopyOfBranch(ctx, dbData, apr)
if err != nil {
return 1, err
}
err = copyBranch(ctx, dbData, apr)
case apr.Contains(cli.MoveFlag):
return 1, errors.New("Renaming a branch is not supported.")
case apr.Contains(cli.DeleteFlag):
return 1, errors.New("Deleting branches is not supported.")
case apr.Contains(cli.DeleteForceFlag):
return 1, errors.New("Deleting branches is not supported.")
err = renameBranch(ctx, dbData, apr)
case apr.Contains(cli.DeleteFlag), apr.Contains(cli.DeleteForceFlag):
err = deleteBranches(ctx, apr, dbData)
default:
// regular branch - create new branch
if apr.NArg() != 1 {
return 1, InvalidArgErr
}
branchName := apr.Arg(0)
if len(branchName) == 0 {
return 1, EmptyBranchNameErr
}
err = createNewBranch(ctx, dbData, branchName)
if err != nil {
return 1, err
}
err = createNewBranch(ctx, dbData, apr)
}
return 0, nil
}
func createNewBranch(ctx *sql.Context, dbData env.DbData, branchName string) error {
// Check if the branch already exists.
isBranch, err := actions.IsBranch(ctx, dbData.Ddb, branchName)
if err != nil {
return err
} else if isBranch {
return errors.New(fmt.Sprintf("fatal: A branch named '%s' already exists.", branchName))
return 1, err
} else {
return 0, nil
}
startPt := fmt.Sprintf("head")
return actions.CreateBranchWithStartPt(ctx, dbData, branchName, startPt, false)
}
func makeACopyOfBranch(ctx *sql.Context, dbData env.DbData, apr *argparser.ArgParseResults) error {
func renameBranch(ctx *sql.Context, dbData env.DbData, apr *argparser.ArgParseResults) error {
if apr.NArg() != 2 {
return InvalidArgErr
}
oldBranchName, newBranchName := apr.Arg(0), apr.Arg(1)
if oldBranchName == "" || newBranchName == "" {
return EmptyBranchNameErr
}
force := apr.Contains(cli.ForceFlag)
if !force {
err := validateBranchNotActiveInAnySession(ctx, oldBranchName)
if err != nil {
return err
}
}
return actions.RenameBranch(ctx, dbData, loadConfig(ctx), oldBranchName, newBranchName, force)
}
func deleteBranches(ctx *sql.Context, apr *argparser.ArgParseResults, dbData env.DbData) error {
if apr.NArg() == 0 {
return InvalidArgErr
}
for _, branchName := range apr.Args {
if len(branchName) == 0 {
return EmptyBranchNameErr
}
force := apr.Contains(cli.DeleteForceFlag) || apr.Contains(cli.ForceFlag)
if !force {
err := validateBranchNotActiveInAnySession(ctx, branchName)
if err != nil {
return err
}
}
err := actions.DeleteBranch(ctx, dbData, loadConfig(ctx), branchName, actions.DeleteOptions{
Force: force,
})
if err != nil {
return err
}
}
return nil
}
// validateBranchNotActiveInAnySessions returns an error if the specified branch is currently
// selected as the active branch for any active server sessions.
func validateBranchNotActiveInAnySession(ctx *sql.Context, branchName string) error {
dbName := ctx.GetCurrentDatabase()
if dbName == "" {
return nil
}
if sqlserver.RunningInServerMode() == false {
return nil
}
runningServer := sqlserver.GetRunningServer()
if runningServer == nil {
return nil
}
sessionManager := runningServer.SessionManager()
branchRef := ref.NewBranchRef(branchName)
return sessionManager.Iter(func(session sql.Session) (bool, error) {
dsess, ok := session.(*dsess.DoltSession)
if !ok {
return false, fmt.Errorf("unexpected session type: %T", session)
}
activeBranchRef, err := dsess.CWBHeadRef(ctx, dbName)
if err != nil {
return false, err
}
if ref.Equals(branchRef, activeBranchRef) {
return false, fmt.Errorf("unsafe to delete or rename branches in use in other sessions; " +
"use --force to force the change")
}
return false, nil
})
}
func loadConfig(ctx *sql.Context) *env.DoltCliConfig {
// When executing branch actions from SQL, we don't have access to a DoltEnv like we do from
// within the CLI. We can fake it here enough to get a DoltCliConfig, but we can't rely on the
// DoltEnv because tests and production will run with different settings (e.g. in-mem versus file).
dEnv := env.Load(ctx, env.GetCurrentUserHomeDir, filesys.LocalFS, doltdb.LocalDirDoltDB, "")
return dEnv.Config
}
func createNewBranch(ctx *sql.Context, dbData env.DbData, apr *argparser.ArgParseResults) error {
if apr.NArg() != 1 {
return InvalidArgErr
}
branchName := apr.Arg(0)
if len(branchName) == 0 {
return EmptyBranchNameErr
}
return actions.CreateBranchWithStartPt(ctx, dbData, branchName, "HEAD", apr.Contains(cli.ForceFlag))
}
func copyBranch(ctx *sql.Context, dbData env.DbData, apr *argparser.ArgParseResults) error {
if apr.NArg() != 2 {
return InvalidArgErr
}
@@ -637,6 +637,12 @@ func TestDoltReset(t *testing.T) {
}
}
func TestDoltBranch(t *testing.T) {
for _, script := range DoltBranchScripts {
enginetest.TestScript(t, newDoltHarness(t), script)
}
}
// TestSingleTransactionScript is a convenience method for debugging a single transaction test. Unskip and set to the
// desired test.
func TestSingleTransactionScript(t *testing.T) {
@@ -1297,6 +1297,144 @@ var MergeScripts = []queries.ScriptTest{
},
}
var DoltBranchScripts = []queries.ScriptTest{
{
Name: "Create branches from HEAD with dolt_branch procedure",
Assertions: []queries.ScriptTestAssertion{
{
Query: "CALL DOLT_BRANCH('myNewBranch1')",
Expected: []sql.Row{{0}},
},
{
Query: "SELECT COUNT(*) FROM DOLT_BRANCHES WHERE NAME='myNewBranch1';",
Expected: []sql.Row{{1}},
},
{
// Trying to recreate that branch fails without the force flag
Query: "CALL DOLT_BRANCH('myNewBranch1')",
ExpectedErrStr: "fatal: A branch named 'myNewBranch1' already exists.",
},
{
Query: "CALL DOLT_BRANCH('-f', 'myNewBranch1')",
Expected: []sql.Row{{0}},
},
},
},
{
Name: "Rename branches with dolt_branch procedure",
Assertions: []queries.ScriptTestAssertion{
{
Query: "CALL DOLT_BRANCH('myNewBranch1')",
Expected: []sql.Row{{0}},
},
{
Query: "CALL DOLT_BRANCH('myNewBranch2')",
Expected: []sql.Row{{0}},
},
{
// Renaming to an existing name fails without the force flag
Query: "CALL DOLT_BRANCH('-m', 'myNewBranch1', 'myNewBranch2')",
ExpectedErrStr: "already exists",
},
{
Query: "CALL DOLT_BRANCH('-mf', 'myNewBranch1', 'myNewBranch2')",
Expected: []sql.Row{{0}},
},
{
Query: "CALL DOLT_BRANCH('-m', 'myNewBranch2', 'myNewBranch3')",
Expected: []sql.Row{{0}},
},
},
},
{
Name: "Copy branches from other branches using dolt_branch procedure",
SetUpScript: []string{
"CALL DOLT_BRANCH('myNewBranch1')",
},
Assertions: []queries.ScriptTestAssertion{
{
Query: "CALL DOLT_BRANCH('-c')",
ExpectedErrStr: "error: invalid usage",
},
{
Query: "CALL DOLT_BRANCH('-c', 'myNewBranch1')",
ExpectedErrStr: "error: invalid usage",
},
{
Query: "CALL DOLT_BRANCH('-c', 'myNewBranch2')",
ExpectedErrStr: "error: invalid usage",
},
{
Query: "CALL DOLT_BRANCH('-c', '', '')",
ExpectedErrStr: "error: cannot branch empty string",
},
{
Query: "CALL DOLT_BRANCH('-c', 'myNewBranch1', 'myNewBranch2')",
Expected: []sql.Row{{0}},
},
{
Query: "SELECT COUNT(*) FROM DOLT_BRANCHES WHERE NAME='myNewBranch2';",
Expected: []sql.Row{{1}},
},
{
Query: "CALL DOLT_BRANCH('-c', 'myNewBranch1', 'myNewBranch2')",
ExpectedErrStr: "fatal: A branch named 'myNewBranch2' already exists.",
},
{
Query: "CALL DOLT_BRANCH('-cf', 'myNewBranch1', 'myNewBranch2')",
Expected: []sql.Row{{0}},
},
},
},
{
Name: "Delete branches with dolt_branch procedure",
SetUpScript: []string{
"CALL DOLT_BRANCH('myNewBranch1')",
"CALL DOLT_BRANCH('myNewBranch2')",
"CALL DOLT_BRANCH('myNewBranch3')",
"CALL DOLT_BRANCH('myNewBranchWithCommit')",
"CALL DOLT_CHECKOUT('myNewBranchWithCommit')",
"CALL DOLT_COMMIT('--allow-empty', '-am', 'empty commit')",
"CALL DOLT_CHECKOUT('main')",
},
Assertions: []queries.ScriptTestAssertion{
{
Query: "CALL DOLT_BRANCH('-d')",
ExpectedErrStr: "error: invalid usage",
},
{
Query: "CALL DOLT_BRANCH('-d', '')",
ExpectedErrStr: "error: cannot branch empty string",
},
{
Query: "CALL DOLT_BRANCH('-d', 'branchDoesNotExist')",
ExpectedErrStr: "branch not found",
},
{
Query: "CALL DOLT_BRANCH('-d', 'myNewBranch1')",
Expected: []sql.Row{{0}},
},
{
Query: "SELECT COUNT(*) FROM DOLT_BRANCHES WHERE NAME='myNewBranch1'",
Expected: []sql.Row{{0}},
},
{
Query: "CALL DOLT_BRANCH('-d', 'myNewBranch2', 'myNewBranch3')",
Expected: []sql.Row{{0}},
},
{
// Trying to delete a branch with unpushed changes fails without force option
Query: "CALL DOLT_BRANCH('-d', 'myNewBranchWithCommit')",
ExpectedErrStr: "attempted to delete a branch that is not fully merged into its parent; use `-f` to force",
},
{
Query: "CALL DOLT_BRANCH('-df', 'myNewBranchWithCommit')",
Expected: []sql.Row{{0}},
},
},
},
}
var DoltReset = []queries.ScriptTest{
{
Name: "CALL DOLT_RESET('--hard') should reset the merge state after uncommitted merge",
@@ -0,0 +1,243 @@
// Copyright 2022 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 enginetest
import (
"context"
gosql "database/sql"
"math/rand"
"strings"
"testing"
"github.com/dolthub/go-mysql-server/enginetest/queries"
"github.com/dolthub/go-mysql-server/sql"
"github.com/gocraft/dbr/v2"
"github.com/stretchr/testify/require"
"github.com/dolthub/dolt/go/cmd/dolt/commands/sqlserver"
"github.com/dolthub/dolt/go/libraries/doltcore/dtestutils"
)
// DoltBranchMultiSessionScriptTests contain tests that need to be run in a multi-session server environment
// in order to fully test branch deletion and renaming logic.
var DoltBranchMultiSessionScriptTests = []queries.ScriptTest{
{
Name: "Test multi-session behavior for deleting branches",
SetUpScript: []string{
"call dolt_branch('branch1');",
"call dolt_branch('branch2');",
"call dolt_branch('branch3');",
},
Assertions: []queries.ScriptTestAssertion{
{
Query: "/* client a */ CALL DOLT_CHECKOUT('branch1');",
Expected: []sql.Row{{0}},
},
{
Query: "/* client a */ select active_branch();",
Expected: []sql.Row{{"branch1"}},
},
{
Query: "/* client b */ CALL DOLT_BRANCH('-d', 'branch1');",
ExpectedErrStr: "Error 1105: unsafe to delete or rename branches in use in other sessions; use --force to force the change",
},
{
Query: "/* client a */ CALL DOLT_CHECKOUT('branch2');",
Expected: []sql.Row{{0}},
},
{
Query: "/* client b */ CALL DOLT_BRANCH('-d', 'branch1');",
Expected: []sql.Row{{0}},
},
{
Query: "/* client b */ CALL DOLT_BRANCH('-d', 'branch2');",
ExpectedErrStr: "Error 1105: unsafe to delete or rename branches in use in other sessions; use --force to force the change",
},
{
Query: "/* client b */ CALL DOLT_BRANCH('-df', 'branch2');",
Expected: []sql.Row{{0}},
},
{
Query: "/* client b */ CALL DOLT_BRANCH('-d', 'branch3');",
Expected: []sql.Row{{0}},
},
},
},
{
Name: "Test multi-session behavior for renaming branches",
SetUpScript: []string{
"call dolt_branch('branch1');",
"call dolt_branch('branch2');",
},
Assertions: []queries.ScriptTestAssertion{
{
Query: "/* client a */ CALL DOLT_CHECKOUT('branch1');",
Expected: []sql.Row{{0}},
},
{
Query: "/* client a */ select active_branch();",
Expected: []sql.Row{{"branch1"}},
},
{
Query: "/* client b */ CALL DOLT_BRANCH('-m', 'branch1', 'movedBranch1');",
ExpectedErrStr: "Error 1105: unsafe to delete or rename branches in use in other sessions; use --force to force the change",
},
{
Query: "/* client b */ CALL DOLT_BRANCH('-mf', 'branch1', 'movedBranch1');",
Expected: []sql.Row{{0}},
},
{
Query: "/* client b */ CALL DOLT_BRANCH('-m', 'branch2', 'movedBranch2');",
Expected: []sql.Row{{0}},
},
},
},
}
// TestDoltMultiSessionBehavior runs tests that exercise multi-session logic on a running SQL server. Statements
// are sent through the server, from out of process, instead of directly to the in-process engine API.
func TestDoltMultiSessionBehavior(t *testing.T) {
// When this test runs with the new storage engine format, we get a panic about an unknown message id.
// Ex: https://github.com/dolthub/dolt/runs/6679643619?check_suite_focus=true
skipNewFormat(t)
testMultiSessionScriptTests(t, DoltBranchMultiSessionScriptTests)
}
func testMultiSessionScriptTests(t *testing.T, tests []queries.ScriptTest) {
sc, serverConfig := startServer(t)
defer sc.StopServer()
for _, test := range tests {
conn1, sess1 := newConnection(t, serverConfig)
conn2, sess2 := newConnection(t, serverConfig)
t.Run(test.Name, func(t *testing.T) {
for _, setupStatement := range test.SetUpScript {
_, err := sess1.Exec(setupStatement)
require.NoError(t, err)
}
for _, assertion := range test.Assertions {
t.Run(assertion.Query, func(t *testing.T) {
var activeSession *dbr.Session
if strings.Contains(strings.ToLower(assertion.Query), "/* client a */") {
activeSession = sess1
} else if strings.Contains(strings.ToLower(assertion.Query), "/* client b */") {
activeSession = sess2
} else {
require.Fail(t, "unsupported client specification: "+assertion.Query)
}
rows, err := activeSession.Query(assertion.Query)
if len(assertion.ExpectedErrStr) > 0 {
require.EqualError(t, err, assertion.ExpectedErrStr)
} else if assertion.ExpectedErr != nil {
require.True(t, assertion.ExpectedErr.Is(err))
} else if assertion.Expected != nil {
require.NoError(t, err)
assertResultsEqual(t, assertion.Expected, rows)
} else {
require.Fail(t, "unsupported ScriptTestAssertion property: %v", assertion)
}
if rows != nil {
require.NoError(t, rows.Close())
}
})
}
})
require.NoError(t, conn1.Close())
require.NoError(t, conn2.Close())
}
}
func makeDestinationSlice(t *testing.T, columnTypes []*gosql.ColumnType) []interface{} {
dest := make([]any, len(columnTypes))
for i, columnType := range columnTypes {
switch strings.ToLower(columnType.DatabaseTypeName()) {
case "int", "tinyint", "bigint":
var integer int
dest[i] = &integer
case "text":
var s string
dest[i] = &s
default:
require.Fail(t, "unsupported type: "+columnType.DatabaseTypeName())
}
}
return dest
}
func assertResultsEqual(t *testing.T, expected []sql.Row, rows *gosql.Rows) {
columnTypes, err := rows.ColumnTypes()
require.NoError(t, err)
dest := makeDestinationSlice(t, columnTypes)
for _, expectedRow := range expected {
ok := rows.Next()
if !ok {
require.Fail(t, "Fewer results than expected")
}
err := rows.Scan(dest...)
require.NoError(t, err)
require.Equal(t, len(expectedRow), len(dest),
"Different number of columns returned than expected")
for j, expectedValue := range expectedRow {
switch strings.ToUpper(columnTypes[j].DatabaseTypeName()) {
case "TEXT":
actualValue, ok := dest[j].(*string)
require.True(t, ok)
require.Equal(t, expectedValue, *actualValue)
case "INT", "TINYINT", "BIGINT":
actualValue, ok := dest[j].(*int)
require.True(t, ok)
require.Equal(t, expectedValue, *actualValue)
default:
require.Fail(t, "Unsupported datatype: %s", columnTypes[j].DatabaseTypeName())
}
}
}
if rows.Next() {
require.Fail(t, "More results than expected")
}
}
func startServer(t *testing.T) (*sqlserver.ServerController, sqlserver.ServerConfig) {
dEnv := dtestutils.CreateEnvWithSeedData(t)
port := 15403 + rand.Intn(25)
serverConfig := sqlserver.DefaultServerConfig().WithPort(port)
sc := sqlserver.NewServerController()
go func() {
_, _ = sqlserver.Serve(context.Background(), "", serverConfig, sc, dEnv)
}()
err := sc.WaitForStart()
require.NoError(t, err)
return sc, serverConfig
}
func newConnection(t *testing.T, serverConfig sqlserver.ServerConfig) (*dbr.Connection, *dbr.Session) {
const dbName = "dolt"
conn, err := dbr.Open("mysql", sqlserver.ConnectionString(serverConfig)+dbName, nil)
require.NoError(t, err)
sess := conn.NewSession(nil)
return conn, sess
}
@@ -0,0 +1,45 @@
// Copyright 2022 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 sqlserver
import (
"sync"
"github.com/dolthub/go-mysql-server/server"
)
var mySQLServer *server.Server
var mySQLServerMutex sync.Mutex
// RunningInServerMode returns true if the current process is running a SQL server.
func RunningInServerMode() bool {
mySQLServerMutex.Lock()
defer mySQLServerMutex.Unlock()
return mySQLServer != nil
}
// GetRunningServer returns the Server instance running in this process, or nil if no SQL server is running.
func GetRunningServer() *server.Server {
mySQLServerMutex.Lock()
defer mySQLServerMutex.Unlock()
return mySQLServer
}
// SetRunningServer sets the specified Server as the running SQL server for this process.
func SetRunningServer(server *server.Server) {
mySQLServerMutex.Lock()
defer mySQLServerMutex.Unlock()
mySQLServer = server
}
+28 -16
View File
@@ -245,36 +245,48 @@ SQL
[ "$output" = "$mainhash" ]
}
@test "sql-branch: asserts unsupported -m, -d, -D flags" {
@test "sql-branch: SELECT DOLT_BRANCH to rename and delete" {
dolt add . && dolt commit -m "1, 2, and 3 in test table"
dolt branch new_branch
run dolt sql -q "SELECT DOLT_BRANCH('-m', 'new_branch', 'changed');"
[ $status -eq 1 ]
[[ "$output" =~ "Renaming a branch is not supported." ]] || false
[ $status -eq 0 ]
run dolt sql -q "SELECT DOLT_BRANCH('-d', 'new_branch');"
[ $status -eq 1 ]
[[ "$output" =~ "Deleting branches is not supported." ]] || false
run dolt sql -q "SELECT DOLT_BRANCH('-d', 'changed');"
[ $status -eq 0 ]
run dolt sql -q "SELECT DOLT_BRANCH('-D', 'new_branch');"
dolt branch branch_with_unpushed_commit
dolt checkout branch_with_unpushed_commit
dolt commit --allow-empty -am 'empty commit'
dolt checkout main
run dolt sql -q "SELECT DOLT_BRANCH('-d', 'branch_with_unpushed_commit');"
[ $status -eq 1 ]
[[ "$output" =~ "Deleting branches is not supported." ]] || false
[[ "$output" =~ "attempted to delete a branch that is not fully merged" ]] || false
run dolt sql -q "SELECT DOLT_BRANCH('-D', 'branch_with_unpushed_commit');"
[ $status -eq 0 ]
}
@test "sql-branch: asserts unsupported -m, -d, -D flags on CALL" {
@test "sql-branch: CALL DOLT_BRANCH to rename and delete" {
dolt add . && dolt commit -m "1, 2, and 3 in test table"
dolt branch new_branch
run dolt sql -q "CALL DOLT_BRANCH('-m', 'new_branch', 'changed');"
[ $status -eq 1 ]
[[ "$output" =~ "Renaming a branch is not supported." ]] || false
[ $status -eq 0 ]
run dolt sql -q "CALL DOLT_BRANCH('-d', 'new_branch');"
[ $status -eq 1 ]
[[ "$output" =~ "Deleting branches is not supported." ]] || false
run dolt sql -q "CALL DOLT_BRANCH('-d', 'changed');"
[ $status -eq 0 ]
run dolt sql -q "CALL DOLT_BRANCH('-D', 'new_branch');"
dolt branch branch_with_unpushed_commit
dolt checkout branch_with_unpushed_commit
dolt commit --allow-empty -am 'empty commit'
dolt checkout main
run dolt sql -q "CALL DOLT_BRANCH('-d', 'branch_with_unpushed_commit');"
[ $status -eq 1 ]
[[ "$output" =~ "Deleting branches is not supported." ]] || false
[[ "$output" =~ "attempted to delete a branch that is not fully merged" ]] || false
run dolt sql -q "CALL DOLT_BRANCH('-D', 'branch_with_unpushed_commit');"
[ $status -eq 0 ]
}