github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/env/multi_repo_env.go (about)

     1  // Copyright 2020 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package env
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"os"
    21  	"path/filepath"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/sirupsen/logrus"
    26  
    27  	"github.com/dolthub/dolt/go/libraries/doltcore/dbfactory"
    28  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    29  	"github.com/dolthub/dolt/go/libraries/utils/config"
    30  	"github.com/dolthub/dolt/go/libraries/utils/filesys"
    31  	"github.com/dolthub/dolt/go/libraries/utils/set"
    32  	"github.com/dolthub/dolt/go/store/types"
    33  )
    34  
    35  // EnvNameAndPath is a simple tuple of the name of an environment and the path to where it is on disk
    36  type EnvNameAndPath struct {
    37  	// Name is the name of the environment and is used as the identifier when accessing a given environment
    38  	Name string
    39  	// Path is the path on disk to where the environment lives
    40  	Path string
    41  }
    42  
    43  type NamedEnv struct {
    44  	name string
    45  	env  *DoltEnv
    46  }
    47  
    48  // MultiRepoEnv is a type used to store multiple environments which can be retrieved by name
    49  type MultiRepoEnv struct {
    50  	envs         []NamedEnv
    51  	fs           filesys.Filesys
    52  	cfg          config.ReadWriteConfig
    53  	dialProvider dbfactory.GRPCDialProvider
    54  }
    55  
    56  // NewMultiEnv returns a new MultiRepoEnv instance dirived from a root DoltEnv instance.
    57  func MultiEnvForSingleEnv(ctx context.Context, env *DoltEnv) (*MultiRepoEnv, error) {
    58  	return MultiEnvForDirectory(ctx, env.Config.WriteableConfig(), env.FS, env.Version, env)
    59  }
    60  
    61  // MultiEnvForDirectory returns a MultiRepoEnv for the directory rooted at the file system given. The doltEnv from the
    62  // invoking context is included. If it's non-nil and valid, it will be included in the returned MultiRepoEnv, and will
    63  // be the first database in all iterations.
    64  func MultiEnvForDirectory(
    65  	ctx context.Context,
    66  	config config.ReadWriteConfig,
    67  	dataDirFS filesys.Filesys,
    68  	version string,
    69  	dEnv *DoltEnv,
    70  ) (*MultiRepoEnv, error) {
    71  	// Load current dataDirFS and put into mr env
    72  	var dbName string = "dolt"
    73  	var newDEnv *DoltEnv = dEnv
    74  
    75  	// InMemFS is used only for testing.
    76  	// All other FS Types should get a newly created Environment which will serve as the primary env in the MultiRepoEnv
    77  	if _, ok := dataDirFS.(*filesys.InMemFS); !ok {
    78  		path, err := dataDirFS.Abs("")
    79  		if err != nil {
    80  			return nil, err
    81  		}
    82  		envName := getRepoRootDir(path, string(os.PathSeparator))
    83  		dbName = dbfactory.DirToDBName(envName)
    84  
    85  		newDEnv = Load(ctx, GetCurrentUserHomeDir, dataDirFS, doltdb.LocalDirDoltDB, version)
    86  	}
    87  
    88  	mrEnv := &MultiRepoEnv{
    89  		envs:         make([]NamedEnv, 0),
    90  		fs:           dataDirFS,
    91  		cfg:          config,
    92  		dialProvider: NewGRPCDialProviderFromDoltEnv(newDEnv),
    93  	}
    94  
    95  	envSet := map[string]*DoltEnv{}
    96  	if newDEnv.Valid() {
    97  		envSet[dbName] = newDEnv
    98  	}
    99  
   100  	// If there are other directories in the directory, try to load them as additional databases
   101  	dataDirFS.Iter(".", false, func(path string, size int64, isDir bool) (stop bool) {
   102  		if !isDir {
   103  			return false
   104  		}
   105  
   106  		dir := filepath.Base(path)
   107  
   108  		newFs, err := dataDirFS.WithWorkingDir(dir)
   109  		if err != nil {
   110  			return false
   111  		}
   112  
   113  		// TODO: get rid of version altogether
   114  		version := ""
   115  		if dEnv != nil {
   116  			version = dEnv.Version
   117  		}
   118  
   119  		newEnv := Load(ctx, GetCurrentUserHomeDir, newFs, doltdb.LocalDirDoltDB, version)
   120  		if newEnv.Valid() {
   121  			envSet[dbfactory.DirToDBName(dir)] = newEnv
   122  		} else {
   123  			dbErr := newEnv.DBLoadError
   124  			if dbErr != nil {
   125  				if !errors.Is(dbErr, doltdb.ErrMissingDoltDataDir) {
   126  					logrus.Warnf("failed to load database at %s with error: %s", path, dbErr.Error())
   127  				}
   128  			}
   129  			cfgErr := newEnv.CfgLoadErr
   130  			if cfgErr != nil {
   131  				logrus.Warnf("failed to load database configuration at %s with error: %s", path, cfgErr.Error())
   132  			}
   133  		}
   134  		return false
   135  	})
   136  
   137  	enforceSingleFormat(envSet)
   138  
   139  	// if the current directory database is in our set, add it first so it will be the current database
   140  	if env, ok := envSet[dbName]; ok && env.Valid() {
   141  		mrEnv.addEnv(dbName, env)
   142  		delete(envSet, dbName)
   143  	}
   144  
   145  	// get the keys from the envSet keys as a sorted list
   146  	sortedKeys := make([]string, 0, len(envSet))
   147  	for k := range envSet {
   148  		sortedKeys = append(sortedKeys, k)
   149  	}
   150  	sort.Strings(sortedKeys)
   151  	for _, dbName := range sortedKeys {
   152  		mrEnv.addEnv(dbName, envSet[dbName])
   153  	}
   154  
   155  	return mrEnv, nil
   156  }
   157  
   158  func (mrEnv *MultiRepoEnv) FileSystem() filesys.Filesys {
   159  	return mrEnv.fs
   160  }
   161  
   162  func (mrEnv *MultiRepoEnv) RemoteDialProvider() dbfactory.GRPCDialProvider {
   163  	return mrEnv.dialProvider
   164  }
   165  
   166  func (mrEnv *MultiRepoEnv) Config() config.ReadWriteConfig {
   167  	return mrEnv.cfg
   168  }
   169  
   170  // addEnv adds an environment to the MultiRepoEnv by name
   171  func (mrEnv *MultiRepoEnv) addEnv(name string, dEnv *DoltEnv) {
   172  	mrEnv.envs = append(mrEnv.envs, NamedEnv{
   173  		name: name,
   174  		env:  dEnv,
   175  	})
   176  }
   177  
   178  // GetEnv returns the env with the name given, or nil if no such env exists
   179  func (mrEnv *MultiRepoEnv) GetEnv(name string) *DoltEnv {
   180  	var found *DoltEnv
   181  	mrEnv.Iter(func(n string, dEnv *DoltEnv) (stop bool, err error) {
   182  		if n == name {
   183  			found = dEnv
   184  			return true, nil
   185  		}
   186  		return false, nil
   187  	})
   188  	return found
   189  }
   190  
   191  // Iter iterates over all environments in the MultiRepoEnv
   192  func (mrEnv *MultiRepoEnv) Iter(cb func(name string, dEnv *DoltEnv) (stop bool, err error)) error {
   193  	for _, e := range mrEnv.envs {
   194  		stop, err := cb(e.name, e.env)
   195  
   196  		if err != nil {
   197  			return err
   198  		}
   199  
   200  		if stop {
   201  			break
   202  		}
   203  	}
   204  
   205  	return nil
   206  }
   207  
   208  // GetFirstDatabase returns the name of the first database in the MultiRepoEnv. This will be the database in the
   209  // current working directory if applicable, or the first database alphabetically otherwise.
   210  func (mrEnv *MultiRepoEnv) GetFirstDatabase() string {
   211  	var currentDb string
   212  	_ = mrEnv.Iter(func(name string, _ *DoltEnv) (stop bool, err error) {
   213  		currentDb = name
   214  		return true, nil
   215  	})
   216  
   217  	return currentDb
   218  }
   219  
   220  func getRepoRootDir(path, pathSeparator string) string {
   221  	if pathSeparator != "/" {
   222  		path = strings.ReplaceAll(path, pathSeparator, "/")
   223  	}
   224  
   225  	// filepath.Clean does not work with cross platform paths.  So can't test a windows path on a mac
   226  	tokens := strings.Split(path, "/")
   227  
   228  	for i := len(tokens) - 1; i >= 0; i-- {
   229  		if tokens[i] == "" {
   230  			tokens = append(tokens[:i], tokens[i+1:]...)
   231  		}
   232  	}
   233  
   234  	if len(tokens) == 0 {
   235  		return ""
   236  	}
   237  
   238  	if tokens[len(tokens)-1] == dbfactory.DataDir && tokens[len(tokens)-2] == dbfactory.DoltDir {
   239  		tokens = tokens[:len(tokens)-2]
   240  	}
   241  
   242  	if len(tokens) == 0 {
   243  		return ""
   244  	}
   245  
   246  	name := tokens[len(tokens)-1]
   247  
   248  	// handles drive letters. fine with a folder containing a colon having the default name
   249  	if strings.IndexRune(name, ':') != -1 {
   250  		return ""
   251  	}
   252  
   253  	return name
   254  }
   255  
   256  // enforceSingleFormat enforces that constraint that all databases in
   257  // a multi-database environment have the same NomsBinFormat.
   258  // Databases are removed from the MultiRepoEnv to ensure this is true.
   259  func enforceSingleFormat(envSet map[string]*DoltEnv) {
   260  	formats := set.NewEmptyStrSet()
   261  	for _, dEnv := range envSet {
   262  		formats.Add(dEnv.DoltDB.Format().VersionString())
   263  	}
   264  
   265  	var nbf string
   266  	// if present, prefer types.Format_Default
   267  	if ok := formats.Contains(types.Format_Default.VersionString()); ok {
   268  		nbf = types.Format_Default.VersionString()
   269  	} else {
   270  		// otherwise, pick an arbitrary format
   271  		for _, dEnv := range envSet {
   272  			nbf = dEnv.DoltDB.Format().VersionString()
   273  		}
   274  	}
   275  
   276  	template := "incompatible format for database '%s'; expected '%s', found '%s'"
   277  	for name, dEnv := range envSet {
   278  		found := dEnv.DoltDB.Format().VersionString()
   279  		if found != nbf {
   280  			logrus.Infof(template, name, nbf, found)
   281  			delete(envSet, name)
   282  		}
   283  	}
   284  }