Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

278 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 sqle
import (
"fmt"
"path/filepath"
"strings"
"time"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/dolt/go/libraries/doltcore/dbfactory"
"github.com/dolthub/dolt/go/libraries/utils/errors"
"github.com/dolthub/dolt/go/libraries/utils/filesys"
)
// droppedDatabaseDirectoryName 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 droppedDatabaseDirectoryName = ".dolt_dropped_databases"
// droppedDatabaseManager is responsible for dropping databases and "undropping", or restoring, dropped databases. It
// is given a Filesys where all database directories can be found. When dropping a database, instead of deleting the
// database directory, it will move it to a new ".dolt_dropped_databases" directory where databases can be restored.
type droppedDatabaseManager struct {
fs filesys.Filesys
}
// newDroppedDatabaseManager creates a new droppedDatabaseManager instance using the specified |fs| as the location
// where databases can be found. It will create a new ".dolt_dropped_databases" directory at the root of |fs| where
// dropped databases will be moved until they are permanently removed.
func newDroppedDatabaseManager(fs filesys.Filesys) *droppedDatabaseManager {
return &droppedDatabaseManager{
fs: fs,
}
}
// DropDatabase will move the database directory for the database named |name| at the location |dropDbLoc| to the
// dolt_dropped_database directory where it can later be "undropped" to restore it. If any problems are encountered
// moving the database directory, an error is returned.
func (dd *droppedDatabaseManager) DropDatabase(ctx *sql.Context, name string, dropDbLoc string) error {
rootDbLoc, err := dd.fs.Abs("")
if err != nil {
return err
}
isRootDatabase := false
// 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 {
doltDirExists, _ := dd.fs.Exists(dbfactory.DoltDir)
if !doltDirExists {
return sql.ErrDatabaseNotFound.New(name)
}
dropDbLoc = filepath.Join(dropDbLoc, dbfactory.DoltDir)
isRootDatabase = true
}
if err = dd.initializeDeletedDatabaseDirectory(); err != nil {
return fmt.Errorf("unable to drop database %s: %w", name, err)
}
// 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 {
// For a root database, first create the subdirectory before we copy over the .dolt directory
newSubdirectory := filepath.Join(droppedDatabaseDirectoryName, name)
if err := dd.fs.MkDirs(newSubdirectory); err != nil {
return err
}
destinationDirectory = filepath.Join(newSubdirectory, file)
} else {
destinationDirectory = filepath.Join(droppedDatabaseDirectoryName, file)
}
// Add the final directory segment and convert any invalid chars so that the physical directory
// name matches the current logical/SQL name of the database.
dir, base := filepath.Split(destinationDirectory)
base = dbfactory.DirToDBName(file)
destinationDirectory = filepath.Join(dir, base)
if err := dd.prepareToMoveDroppedDatabase(ctx, destinationDirectory); err != nil {
return err
}
return dd.fs.MoveDir(dropDbLoc, destinationDirectory)
}
// UndropDatabase will restore the database named |name| by moving it from the dolt_dropped_database directory, back
// into the root of the filesystem where database directories are managed. This function returns the new location of
// the database directory and the exact name (case-sensitive) of the database. If any errors are encountered while
// attempting to undrop the database, an error is returned and other return parameters should be ignored.
func (dd *droppedDatabaseManager) UndropDatabase(ctx *sql.Context, name string) (filesys.Filesys, string, error) {
sourcePath, destinationPath, exactCaseName, err := dd.validateUndropDatabase(ctx, name)
if err != nil {
return nil, "", err
}
err = dd.fs.MoveDir(sourcePath, destinationPath)
if err != nil {
return nil, "", err
}
newFs, err := dd.fs.WithWorkingDir(exactCaseName)
if err != nil {
return nil, "", err
}
return newFs, exactCaseName, nil
}
// PurgeAllDroppedDatabases permanently removes all dropped databases that are being held in the dolt_dropped_database
// holding directory. Once dropped databases are purged, they can no longer be restored, so this method should be used
// with caution.
func (dd *droppedDatabaseManager) PurgeAllDroppedDatabases(_ *sql.Context) error {
// If the dropped database holding directory doesn't exist, then there's nothing to purge
if exists, _ := dd.fs.Exists(droppedDatabaseDirectoryName); !exists {
return nil
}
var err error
callback := func(path string, size int64, isDir bool) (stop bool) {
// Sanity check that the path we're about to delete is under the dropped database holding directory
if strings.Contains(path, droppedDatabaseDirectoryName) == false {
err = fmt.Errorf("path of database to purge isn't under dropped database holding directory: %s", path)
return true
}
// Attempt to permanently delete the dropped database and stop execution if we hit an error
err = dd.fs.Delete(path, true)
return err != nil
}
iterErr := dd.fs.Iter(droppedDatabaseDirectoryName, false, callback)
if iterErr != nil {
return iterErr
}
return err
}
// 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 (dd *droppedDatabaseManager) initializeDeletedDatabaseDirectory() error {
exists, isDir := dd.fs.Exists(droppedDatabaseDirectoryName)
if exists && !isDir {
return fmt.Errorf("%s exists, but is not a directory", droppedDatabaseDirectoryName)
}
if exists {
return nil
}
return dd.fs.MkDirs(droppedDatabaseDirectoryName)
}
func (dd *droppedDatabaseManager) ListDroppedDatabases(_ *sql.Context) ([]string, error) {
if err := dd.initializeDeletedDatabaseDirectory(); err != nil {
return nil, fmt.Errorf("unable to list undroppable database: %w", err)
}
databaseNames := make([]string, 0, 5)
callback := func(path string, size int64, isDir bool) (stop bool) {
// When we move a database to the dropped database directory, we normalize the physical directory
// name to be the same as the logical SQL name, so there's no need to do any name mapping here.
databaseNames = append(databaseNames, filepath.Base(path))
return false
}
if err := dd.fs.Iter(droppedDatabaseDirectoryName, 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 (dd *droppedDatabaseManager) validateUndropDatabase(ctx *sql.Context, name string) (sourcePath, destinationPath, exactCaseName string, err error) {
availableDatabases, err := dd.ListDroppedDatabases(ctx)
if err != nil {
return "", "", "", err
}
found, exactCaseName := hasCaseInsensitiveMatch(availableDatabases, name)
if !found {
return "", "", "", fmt.Errorf("no database named '%s' found to undrop. %s",
name, errors.CreateUndropErrorMessage(availableDatabases))
}
// Check to see if the destination directory for restoring the database already exists (case-insensitive match)
destinationPath, err = dd.fs.Abs(exactCaseName)
if err != nil {
return "", "", "", err
}
if ok, err := hasCaseInsensitivePath(dd.fs, destinationPath); err != nil {
return "", "", "", err
} else if ok {
return "", "", "", fmt.Errorf("unable to undrop database '%s'; "+
"another database already exists with the same case-insensitive name", exactCaseName)
}
sourcePath = filepath.Join(droppedDatabaseDirectoryName, exactCaseName)
return sourcePath, destinationPath, exactCaseName, nil
}
// hasCaseInsensitivePath returns true if the specified path |target| already exists on the filesystem |fs|, with
// a case-insensitive match on the final component of the path. Note that only the final component of the path is
// checked in a case-insensitive match the other components of the path must be a case-sensitive match.
func hasCaseInsensitivePath(fs filesys.Filesys, target string) (bool, error) {
found := false
err := fs.Iter(filepath.Dir(target), false, func(path string, size int64, isDir bool) (stop bool) {
if strings.EqualFold(filepath.Base(path), filepath.Base(target)) {
found = true
}
return found
})
if err != nil {
return false, err
}
return found, nil
}
// hasCaseInsensitiveMatch tests to see if any of |candidates| are a case-insensitive match for |target| and if so,
// returns true along with the exact candidate string that matched. If there was not a match, false and the empty
// string are returned.
func hasCaseInsensitiveMatch(candidates []string, target string) (bool, string) {
found := false
exactCaseName := ""
for _, s := range candidates {
if strings.EqualFold(target, s) {
exactCaseName = s
found = true
break
}
}
return found, exactCaseName
}
// prepareToMoveDroppedDatabase checks the specified |targetPath| to make sure there is not already a dropped database
// there, and if so, the existing dropped database will be renamed with a unique suffix. If any problems are encountered,
// such as not being able to rename an existing dropped database, this function will return an error.
func (dd *droppedDatabaseManager) prepareToMoveDroppedDatabase(_ *sql.Context, targetPath string) error {
if exists, _ := dd.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().UnixMilli())
if exists, _ := dd.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 := dd.fs.MoveDir(targetPath, newPath); err != nil {
return fmt.Errorf("unable to move existing dropped database out of the way: %w", err)
}
return nil
}