First pass on support for dolt_undrop() stored procedure to "undrop" a database after it has been dropped.

This commit is contained in:
Jason Fulghum
2023-10-02 16:15:44 -07:00
parent 958652f88b
commit 7ac5799066
8 changed files with 514 additions and 49 deletions
+227 -47
View File
@@ -18,9 +18,11 @@ import (
"context"
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/dolthub/go-mysql-server/sql"
@@ -42,6 +44,10 @@ import (
"github.com/dolthub/dolt/go/store/types"
)
// deletedDatabaseDirectoryName is the subdirectory within the data folder where Dolt moves databases after they are
// dropped. The dolt_undrop() stored procedure is then able to restore them from this location.
const deletedDatabaseDirectoryName = "dolt_deleted_databases"
type DoltDatabaseProvider struct {
// dbLocations maps a database name to its file system root
dbLocations map[string]filesys.Filesys
@@ -405,46 +411,7 @@ func (p DoltDatabaseProvider) CreateCollatedDatabase(ctx *sql.Context, name stri
}
}
// If we're running in a sql-server context, ensure the new database is locked so that it can't
// be edited from the CLI. We can't rely on looking for an existing lock file, since this could
// be the first db creation if sql-server was started from a bare directory.
_, lckDeets := sqlserver.GetRunningServer()
if lckDeets != nil {
err = newEnv.Lock(lckDeets)
if err != nil {
ctx.GetLogger().Warnf("Failed to lock newly created database: %s", err.Error())
}
}
fkChecks, err := ctx.GetSessionVariable(ctx, "foreign_key_checks")
if err != nil {
return err
}
opts := editor.Options{
Deaf: newEnv.DbEaFactory(),
// TODO: this doesn't seem right, why is this getting set in the constructor to the DB
ForeignKeyChecksDisabled: fkChecks.(int8) == 0,
}
db, err := NewDatabase(ctx, name, newEnv.DbData(), opts)
if err != nil {
return err
}
// If we have an initialization hook, invoke it. By default, this will
// be ConfigureReplicationDatabaseHook, which will setup replication
// for the new database if a remote url template is set.
err = p.InitDatabaseHook(ctx, p, name, newEnv)
if err != nil {
return err
}
formattedName := formatDbMapKeyName(db.Name())
p.databases[formattedName] = db
p.dbLocations[formattedName] = newEnv.FS
return nil
return p.registerNewDatabase(ctx, name, newEnv)
}
type InitDatabaseHook func(ctx *sql.Context, pro DoltDatabaseProvider, name string, env *env.DoltEnv) error
@@ -598,6 +565,23 @@ func (p DoltDatabaseProvider) cloneDatabaseFromRemote(
return dEnv, nil
}
// initializeDeletedDatabaseDirectory initializes the special directory Dolt uses to store dropped databases until
// they are fully removed. If the directory is already created and set up correctly, then this method is a no-op.
// If the directory doesn't exist yet, it will be created. If there are any problems initializing the directory, an
// error is returned.
func (p DoltDatabaseProvider) initializeDeletedDatabaseDirectory() error {
exists, isDir := p.fs.Exists(deletedDatabaseDirectoryName)
if exists && !isDir {
return fmt.Errorf("%s exists, but is not a directory", deletedDatabaseDirectoryName)
}
if exists {
return nil
}
return p.fs.MkDirs(deletedDatabaseDirectoryName)
}
// DropDatabase implements the sql.MutableDatabaseProvider interface
func (p DoltDatabaseProvider) DropDatabase(ctx *sql.Context, name string) error {
_, revision := dsess.SplitRevisionDbName(name)
@@ -648,7 +632,9 @@ func (p DoltDatabaseProvider) DropDatabase(ctx *sql.Context, name string) error
if err != nil {
return err
}
dirToDelete := ""
isRootDatabase := false
//dirToDelete := ""
// if the database is in the directory itself, we remove '.dolt' directory rather than
// the whole directory itself because it can have other databases that are nested.
if rootDbLoc == dropDbLoc {
@@ -656,8 +642,11 @@ func (p DoltDatabaseProvider) DropDatabase(ctx *sql.Context, name string) error
if !doltDirExists {
return sql.ErrDatabaseNotFound.New(db.Name())
}
dirToDelete = dbfactory.DoltDir
dropDbLoc = filepath.Join(dropDbLoc, dbfactory.DoltDir)
isRootDatabase = true
} else {
// TODO: Do we really need the code in this block?
// Seems like a few places are checking this.
exists, isDir := p.fs.Exists(dropDbLoc)
// Get the DB's directory
if !exists {
@@ -666,16 +655,43 @@ func (p DoltDatabaseProvider) DropDatabase(ctx *sql.Context, name string) error
} else if !isDir {
return fmt.Errorf("unexpected error: %s exists but is not a directory", dbKey)
}
dirToDelete = dropDbLoc
}
err = p.fs.Delete(dirToDelete, true)
if err != nil {
if err = p.initializeDeletedDatabaseDirectory(); err != nil {
return fmt.Errorf("unable to drop database %s: %w", name, err.Error())
}
// Move the dropped database to the Dolt deleted database directory so it can be restored if needed
_, file := filepath.Split(dropDbLoc)
var destinationDirectory string
if isRootDatabase {
// NOTE: This won't work without first creating the new subdirectory
newSubdirectory := filepath.Join(deletedDatabaseDirectoryName, name)
// TODO: If newSubdirectory exists already... then we're in trouble! (need to handle that)
// TODO: Maybe we should have this talk to the DroppedDatabaseVault API instead?
if err := p.fs.MkDirs(newSubdirectory); err != nil {
return err
}
destinationDirectory = filepath.Join(newSubdirectory, file)
} else {
destinationDirectory = filepath.Join(deletedDatabaseDirectoryName, file)
}
// Add the final directory segment and convert all hyphens to underscores in the database directory name
dir, file := filepath.Split(destinationDirectory)
if strings.Contains(file, "-") {
destinationDirectory = filepath.Join(dir, strings.ReplaceAll(file, "-", "_"))
}
if err := p.prepareToMoveDroppedDatabase(ctx, destinationDirectory); err != nil {
return err
}
if err = p.fs.MoveDir(dropDbLoc, destinationDirectory); err != nil {
return err
}
// We not only have to delete this database, but any derivative ones that we've stored as a result of USE or
// connection strings
// We not only have to delete tracking metadata for this database, but also for any derivative ones we've stored
// as a result of USE or connection strings
derivativeNamePrefix := strings.ToLower(dbKey + dsess.DbRevisionDelimiter)
for dbName := range p.databases {
if strings.HasPrefix(strings.ToLower(dbName), derivativeNamePrefix) {
@@ -688,6 +704,170 @@ func (p DoltDatabaseProvider) DropDatabase(ctx *sql.Context, name string) error
return p.invalidateDbStateInAllSessions(ctx, name)
}
// TODO: Might be helpful to group some of these DoltDeletedDatabaseDirectory helper functions into their own
//
// type. For example... maybe there's a new type for DroppedDatabaseVault that we can interact with?
func (p DoltDatabaseProvider) prepareToMoveDroppedDatabase(_ *sql.Context, targetPath string) error {
if exists, _ := p.fs.Exists(targetPath); !exists {
// If there's nothing at the desired targetPath, we're all set
return nil
}
// If there is something already there, pick a new path to move it to
newPath := fmt.Sprintf("%s.backup.%d", targetPath, time.Now().Unix())
if exists, _ := p.fs.Exists(newPath); exists {
return fmt.Errorf("unable to move existing dropped database out of the way: "+
"tried to move it to %s", newPath)
}
if err := p.fs.MoveFile(targetPath, newPath); err != nil {
return fmt.Errorf("unable to move existing dropped database out of the way: %w", err)
}
return nil
}
func (p DoltDatabaseProvider) ListUndroppableDatabases(_ *sql.Context) ([]string, error) {
if err := p.initializeDeletedDatabaseDirectory(); err != nil {
return nil, fmt.Errorf("unable to list undroppable database: %w", err.Error())
}
databaseNames := make([]string, 0, 5)
callback := func(path string, size int64, isDir bool) (stop bool) {
_, lastPathSegment := filepath.Split(path)
// TODO: Is there a common util we use for this somewhere?
lastPathSegment = strings.ReplaceAll(lastPathSegment, "-", "_")
databaseNames = append(databaseNames, lastPathSegment)
return false
}
if err := p.fs.Iter(deletedDatabaseDirectoryName, false, callback); err != nil {
return nil, err
}
return databaseNames, nil
}
// validateUndropDatabase validates that the database |name| is available to be "undropped" and that no existing
// database is already being managed that has the same (case-insensitive) name. If any problems are encountered,
// an error is returned.
func (p DoltDatabaseProvider) validateUndropDatabase(ctx *sql.Context, name string) (sourcePath, destinationPath, exactCaseName string, err error) {
// TODO: rename to ListDatabasesThatCanBeUndropped(ctx)?
availableDatabases, err := p.ListUndroppableDatabases(ctx)
if err != nil {
return "", "", "", err
}
found := false
exactCaseName = name
lowercaseName := strings.ToLower(name)
for _, s := range availableDatabases {
if lowercaseName == strings.ToLower(s) {
exactCaseName = s
found = true
break
}
}
// TODO: this error creation information could be extracted to a common function that dolt_undrop function could use
if !found {
extraInformation := "there are no databases currently available to be undropped"
if len(availableDatabases) > 0 {
extraInformation = fmt.Sprintf("available databases that can be undropped: %s", strings.Join(availableDatabases, ", "))
}
return "", "", "", fmt.Errorf("no database named '%s' found to undrop. %s", name, extraInformation)
}
// Check to see if the destination directory for restoring the database already exists (case insensitive match)
destinationPath, err = p.fs.Abs(exactCaseName)
if err != nil {
return "", "", "", err
}
sourcePath = filepath.Join(deletedDatabaseDirectoryName, exactCaseName)
// TODO: is this always a case insensitive check??? It seems like it must be since our test is working?
if exists, _ := p.fs.Exists(destinationPath); exists {
return "", "", "", fmt.Errorf("unable to undrop database '%s'; "+
"another database already exists with the same case-insensitive name", exactCaseName)
}
return sourcePath, destinationPath, exactCaseName, nil
}
func (p DoltDatabaseProvider) UndropDatabase(ctx *sql.Context, name string) (err error) {
p.mu.Lock()
defer p.mu.Unlock()
// TODO: not sure I like sourcePath and destinationPath being returned here, but seems like they're needed in this function
sourcePath, destinationPath, exactCaseName, err := p.validateUndropDatabase(ctx, name)
if err != nil {
return err
}
// TODO: Need to account for database directory renaming (i.e. converting '-' to '_')
err = p.fs.MoveDir(sourcePath, destinationPath)
if err != nil {
return err
}
newFs, err := p.fs.WithWorkingDir(exactCaseName)
if err != nil {
return err
}
newEnv := env.Load(ctx, env.GetCurrentUserHomeDir, newFs, p.dbFactoryUrl, "TODO")
return p.registerNewDatabase(ctx, exactCaseName, newEnv)
}
// registerNewDatabase registers the specified DoltEnv, |newEnv|, as a new database named |name|. This
// 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) {
// If we're running in a sql-server context, ensure the new database is locked so that it can't
// be edited from the CLI. We can't rely on looking for an existing lock file, since this could
// be the first db creation if sql-server was started from a bare directory.
_, lckDeets := sqlserver.GetRunningServer()
if lckDeets != nil {
err = newEnv.Lock(lckDeets)
if err != nil {
ctx.GetLogger().Warnf("Failed to lock newly created database: %s", err.Error())
}
}
fkChecks, err := ctx.GetSessionVariable(ctx, "foreign_key_checks")
if err != nil {
return err
}
opts := editor.Options{
Deaf: newEnv.DbEaFactory(),
// TODO: this doesn't seem right, why is this getting set in the constructor to the DB
ForeignKeyChecksDisabled: fkChecks.(int8) == 0,
}
db, err := NewDatabase(ctx, name, newEnv.DbData(), opts)
if err != nil {
return err
}
// If we have an initialization hook, invoke it. By default, this will
// be ConfigureReplicationDatabaseHook, which will setup replication
// for the new database if a remote url template is set.
err = p.InitDatabaseHook(ctx, p, name, newEnv)
if err != nil {
return err
}
// TODO: accessing p.databases requires locking!!!
// But right now we're just assuming this function is called from another function
// that has grabbed the right lock, but that's eventually going to cause a problem.
formattedName := formatDbMapKeyName(db.Name())
p.databases[formattedName] = db
p.dbLocations[formattedName] = newEnv.FS
return nil
}
// invalidateDbStateInAllSessions removes the db state for this database from every session. This is necessary when a
// database is dropped, so that other sessions don't use stale db state.
func (p DoltDatabaseProvider) invalidateDbStateInAllSessions(ctx *sql.Context, name string) error {
@@ -0,0 +1,54 @@
// Copyright 2023 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 dprocedures
import (
"fmt"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
"github.com/dolthub/go-mysql-server/sql"
"strings"
)
// doltClean is the stored procedure version for the CLI command `dolt clean`.
func doltUndrop(ctx *sql.Context, args ...string) (sql.RowIter, error) {
doltSession := dsess.DSessFromSess(ctx.Session)
provider := doltSession.Provider()
switch len(args) {
case 0:
// TODO: Are there any permission issues for undrop? probably the same as drop?
undroppableDatabases, err := provider.ListUndroppableDatabases(ctx)
if err != nil {
return nil, err
}
extraInformation := "there are no databases that can currently be undropped."
if len(undroppableDatabases) > 0 {
extraInformation = fmt.Sprintf("the following dropped databases are availble to be undropped: %s",
strings.Join(undroppableDatabases, ", "))
}
return nil, fmt.Errorf("no database name specified. %s", extraInformation)
case 1:
if err := provider.UndropDatabase(ctx, args[0]); err != nil {
return nil, err
}
return rowToIter(int64(0)), nil
default:
return nil, fmt.Errorf("dolt_undrop called with too many arguments: " +
"dolt_undrop only accepts one argument - the name of the dropped database to restore")
}
}
@@ -32,6 +32,7 @@ var DoltProcedures = []sql.ExternalStoredProcedureDetails{
{Name: "dolt_conflicts_resolve", Schema: int64Schema("status"), Function: doltConflictsResolve},
{Name: "dolt_count_commits", Schema: int64Schema("ahead", "behind"), Function: doltCountCommits, ReadOnly: true},
{Name: "dolt_fetch", Schema: int64Schema("status"), Function: doltFetch},
{Name: "dolt_undrop", Schema: int64Schema("status"), Function: doltUndrop},
// dolt_gc is enabled behind a feature flag for now, see dolt_gc.go
{Name: "dolt_gc", Schema: int64Schema("status"), Function: doltGC, ReadOnly: true},
@@ -55,6 +56,7 @@ var DoltProcedures = []sql.ExternalStoredProcedureDetails{
{Name: "dclone", Schema: int64Schema("status"), Function: doltClone},
{Name: "dcommit", Schema: stringSchema("hash"), Function: doltCommit},
{Name: "dfetch", Schema: int64Schema("status"), Function: doltFetch},
{Name: "dundrop", Schema: int64Schema("status"), Function: doltUndrop},
// {Name: "dgc", Schema: int64Schema("status"), Function: doltGC},
@@ -96,6 +96,17 @@ type DoltDatabaseProvider interface {
BaseDatabase(ctx *sql.Context, dbName string) (SqlDatabase, bool)
// DoltDatabases returns all databases known to this provider.
DoltDatabases() []SqlDatabase
// UndropDatabase attempts to restore the database |dbName| that was previously dropped.
// The restored database will appear identically when accessed through the SQL
// interface, but may be stored in a slightly different location on disk
// (e.g. a root database will be restored as a regular/non-root database,
// databases original stored with hyphens in their directory name will be rewritten
// to underscores to match their SQL database name).
// TODO: Is this a problem for anything on hosted?
// If the database is unable to be restored, an error is returned explaining why.
UndropDatabase(ctx *sql.Context, dbName string) error
// ListUndroppableDatabases returns a list of database names for dropped databases that are available to be restored.
ListUndroppableDatabases(ctx *sql.Context) ([]string, error)
}
type SessionDatabaseBranchSpec struct {
+5
View File
@@ -72,6 +72,11 @@ type WritableFS interface {
// MoveFile will move a file from the srcPath in the filesystem to the destPath
MoveFile(srcPath, destPath string) error
// MoveDir will move a directory from the srcPath in the filesystem to the destPath. For example,
// MoveDir("foo", "bar/baz") will move the "foo" directory to "bar/baz", meaning the contents of "foo" are now
// directly under the "baz" directory.
MoveDir(srcPath, destPath string) error
// TempDir returns the path of a new temporary directory.
TempDir() string
}
+4
View File
@@ -537,6 +537,10 @@ func (fs *InMemFS) MoveFile(srcPath, destPath string) error {
return os.ErrNotExist
}
func (fs InMemFS) MoveDir(srcPath, destPath string) error {
panic("not implemented!")
}
func (fs *InMemFS) CopyFile(srcPath, destPath string) error {
fs.rwLock.Lock()
defer fs.rwLock.Unlock()
+19 -2
View File
@@ -271,8 +271,7 @@ func (fs *localFS) Delete(path string, force bool) error {
}
// MoveFile will move a file from the srcPath in the filesystem to the destPath
func (fs *localFS) MoveFile(srcPath, destPath string) error {
var err error
func (fs *localFS) MoveFile(srcPath, destPath string) (err error) {
srcPath, err = fs.Abs(srcPath)
if err != nil {
@@ -288,6 +287,24 @@ func (fs *localFS) MoveFile(srcPath, destPath string) error {
return file.Rename(srcPath, destPath)
}
func (fs *localFS) MoveDir(srcPath, destPath string) (err error) {
// TODO: This is the exact same implementation as MoveFile
// Should probably at least add assertions that |srcPath| is really a dir?
// TODO: Or should we just try to make MoveFile work with dirs? It seems like the filesystem
// based implementation already does, it's just the in-memory implementation that doesn't.
srcPath, err = fs.Abs(srcPath)
if err != nil {
return err
}
destPath, err = fs.Abs(destPath)
if err != nil {
return err
}
return file.Rename(srcPath, destPath)
}
// converts a path to an absolute path. If it's already an absolute path the input path will be returned unaltered
func (fs *localFS) Abs(path string) (string, error) {
if filepath.IsAbs(path) {
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env bats
load $BATS_TEST_DIRNAME/helper/common.bash
setup() {
setup_common
}
teardown() {
assert_feature_version
teardown_common
}
@test "undrop: GC deletes dropped databases" {
# TODO: Garbage collection should remove deleted databases; implement in second milestone
skip "not supported yet"
}
@test "undrop: error messages" {
# When called without any argument, dolt_undrop() returns an error
# that includes the database names that can be undropped.
run dolt sql -q "CALL dolt_undrop();"
[ $status -eq 1 ]
[[ $output =~ "no database name specified." ]] || false
[[ $output =~ "there are no databases that can currently be undropped" ]] || false
# When called without an invalid database name, dolt_undrop() returns
# an error that includes the database names that can be undropped.
run dolt sql -q "CALL dolt_undrop('doesnotexist')"
[ $status -eq 1 ]
[[ $output =~ "no database named 'doesnotexist' found to undrop" ]] || false
[[ $output =~ "there are no databases currently available to be undropped" ]] || false
# When called with multiple arguments, dolt_undrop() returns an error
# explaining that only one argument may be specified.
run dolt sql -q "CALL dolt_undrop('one', 'two', 'three')"
[ $status -eq 1 ]
[[ $output =~ "dolt_undrop called with too many arguments" ]] || false
[[ $output =~ "dolt_undrop only accepts one argument - the name of the dropped database to restore" ]] || false
}
@test "undrop: undrop root database" {
# Create a new Dolt database directory to use as a root database
# NOTE: We use hyphens here to test how db dirs are renamed.
mkdir test-db-1 && cd test-db-1
dolt init
# Create some data and a commit in the database
dolt sql << EOF
create table t1 (pk int primary key, c1 varchar(200));
insert into t1 values (1, "one");
call dolt_commit('-Am', 'creating table t1');
EOF
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ $output =~ "test_db_1" ]] || false
# Drop the root database
dolt sql -q "drop database test_db_1;"
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ ! $output =~ "test_db_1" ]] || false
# Undrop the test_db_1 database
# NOTE: After being undropped, the database is no longer the root database,
# but contained in a subdirectory like a non-root database.
dolt sql -q "call dolt_undrop('test_db_1');"
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ $output =~ "test_db_1" ]] || false
# Sanity check querying some data
run dolt sql -r csv -q "select * from test_db_1.t1;"
[ $status -eq 0 ]
[[ $output =~ "1,one" ]] || false
}
# Asserts that a non-root database can be dropped and then restored with dolt_undrop(), even when
# the case of the database name given to dolt_undrop() doesn't match match the original case.
@test "undrop: undrop non-root database" {
# We manually create a database directory with hyphens in it to test the drop/undrop logic
# that handles translating database directory names to logical database names.
mkdir drop-me && cd drop-me
dolt init && cd ..
dolt sql << EOF
use drop_me;
create table t1 (pk int primary key, c1 varchar(200));
insert into t1 values (1, "one");
call dolt_commit('-Am', 'creating table t1');
EOF
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ $output =~ "drop_me" ]] || false
dolt sql -q "drop database drop_me;"
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ ! $output =~ "drop_me" ]] || false
# Call dolt_undrop() with non-matching case for the database name to
# ensure dolt_undrop() works with case-insensitive database names.
dolt sql -q "call dolt_undrop('DrOp_mE');"
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ $output =~ "drop_me" ]] || false
run dolt sql -r csv -q "select * from drop_me.t1;"
[ $status -eq 0 ]
[[ $output =~ "1,one" ]] || false
}
# When a database is dropped, and then a new database is recreated
# with the same name and dropped, dolt_undrop will undrop the most
# recent database with that name.
@test "undrop: drop database, recreate, and drop again" {
# Create a database named test123
dolt sql << EOF
create database test123;
use test123;
create table t1 (pk int primary key, c1 varchar(100));
insert into t1 values (1, "one");
call dolt_commit('-Am', 'adding table t1 to test123 database');
EOF
# Drop database test123 and make sure it's gone
dolt sql -q "drop database test123;"
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ ! $output =~ "test123" ]] || false
# Create a new database named test123
dolt sql << EOF
create database test123;
use test123;
create table t2 (pk int primary key, c2 varchar(100));
insert into t2 values (100, "one hundy");
call dolt_commit('-Am', 'adding table t2 to new test123 database');
EOF
# Drop the new test123 database and make sure it's gone
dolt sql -q "drop database test123;"
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ ! $output =~ "test123" ]] || false
# Undrop the database
dolt sql -q "call dolt_undrop('test123');"
run dolt sql -r csv -q "select * from test123.t2;"
[ $status -eq 0 ]
[[ $output =~ "100,one hundy" ]] || false
}
# Asserts that when there is already an existing database with the same name, a dropped database
# cannot be undropped.
# TODO: In the future, it might be useful to allow dolt_undrop() to rename the dropped database to
# a new name, but for now, keep it simple and just disallow restoring in this case.
@test "undrop: undrop conflict" {
dolt sql << EOF
create database dAtAbAsE1;
use dAtAbAsE1;
create table t1 (pk int primary key, c1 varchar(200));
insert into t1 values (1, "one");
call dolt_commit('-Am', 'creating table t1');
EOF
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ $output =~ "dAtAbAsE1" ]] || false
# Drop dAtAbAsE1
dolt sql -q "drop database dAtAbAsE1;"
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ ! $output =~ "dAtAbAsE1" ]] || false
# Create a new database named dAtAbAsE1
dolt sql << EOF
create database database1;
use database1;
create table t2 (pk int primary key, c1 varchar(200));
insert into t2 values (1000, "thousand");
call dolt_commit('-Am', 'creating table t2');
EOF
run dolt sql -q "show databases;"
[ $status -eq 0 ]
[[ $output =~ "database1" ]] || false
# Trying to undrop dAtAbAsE1 results in an error, since a database already exists
run dolt sql -q "call dolt_undrop('dAtAbAsE1');"
[ $status -eq 1 ]
[[ $output =~ "unable to undrop database 'dAtAbAsE1'" ]] || false
[[ $output =~ "another database already exists with the same case-insensitive name" ]] || false
}