github.com/cloudberrydb/gpbackup@v1.0.3-0.20240118031043-5410fd45eed6/restore/validate.go (about)

     1  package restore
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  
     8  	"github.com/cloudberrydb/gp-common-go-libs/dbconn"
     9  	"github.com/cloudberrydb/gp-common-go-libs/gplog"
    10  	"github.com/cloudberrydb/gpbackup/options"
    11  	"github.com/cloudberrydb/gpbackup/utils"
    12  	"github.com/pkg/errors"
    13  	"github.com/spf13/pflag"
    14  )
    15  
    16  /*
    17   * This file contains functions related to validating user input.
    18   */
    19  
    20  func validateFilterListsInBackupSet() {
    21  	ValidateIncludeSchemasInBackupSet(opts.IncludedSchemas)
    22  	ValidateExcludeSchemasInBackupSet(opts.ExcludedSchemas)
    23  	ValidateIncludeRelationsInBackupSet(opts.IncludedRelations)
    24  	ValidateExcludeRelationsInBackupSet(opts.ExcludedRelations)
    25  }
    26  
    27  func ValidateIncludeSchemasInBackupSet(schemaList []string) {
    28  	if keys := getFilterSchemasInBackupSet(schemaList); len(keys) != 0 {
    29  		gplog.Fatal(errors.Errorf("Could not find the following schema(s) in the backup set: %s", strings.Join(keys, ", ")), "")
    30  	}
    31  }
    32  
    33  func ValidateExcludeSchemasInBackupSet(schemaList []string) {
    34  	if keys := getFilterSchemasInBackupSet(schemaList); len(keys) != 0 {
    35  		gplog.Warn("Could not find the following excluded schema(s) in the backup set: %s", strings.Join(keys, ", "))
    36  	}
    37  }
    38  
    39  /* This only checks the globalTOC, but will still succesfully validate tables
    40   * in incremental backups since incremental backups will always take backups of
    41   * the metadata (--incremental and --data-only backup flags are not compatible)
    42   */
    43  func getFilterSchemasInBackupSet(schemaList []string) []string {
    44  	if len(schemaList) == 0 {
    45  		return []string{}
    46  	}
    47  	schemaMap := make(map[string]bool, len(schemaList))
    48  	for _, schema := range schemaList {
    49  		schemaMap[schema] = true
    50  	}
    51  	if !backupConfig.DataOnly {
    52  		for _, entry := range globalTOC.PredataEntries {
    53  			if _, ok := schemaMap[entry.Schema]; ok {
    54  				delete(schemaMap, entry.Schema)
    55  			}
    56  			if len(schemaMap) == 0 {
    57  				return []string{}
    58  			}
    59  		}
    60  	} else {
    61  		for _, entry := range globalTOC.DataEntries {
    62  			if _, ok := schemaMap[entry.Schema]; ok {
    63  				delete(schemaMap, entry.Schema)
    64  			}
    65  			if len(schemaMap) == 0 {
    66  				return []string{}
    67  			}
    68  		}
    69  	}
    70  
    71  	keys := make([]string, len(schemaMap))
    72  	i := 0
    73  	for k := range schemaMap {
    74  		keys[i] = k
    75  		i++
    76  	}
    77  	return keys
    78  }
    79  
    80  func GenerateRestoreRelationList(opts options.Options) []string {
    81  	includeRelations := opts.IncludedRelations
    82  	if len(includeRelations) > 0 {
    83  		return includeRelations
    84  	}
    85  
    86  	relationList := make([]string, 0)
    87  	includedSchemaSet := utils.NewIncludeSet(opts.IncludedSchemas)
    88  	excludedSchemaSet := utils.NewExcludeSet(opts.ExcludedSchemas)
    89  	excludedRelationsSet := utils.NewExcludeSet(opts.ExcludedRelations)
    90  
    91  	if len(globalTOC.DataEntries) == 0 {
    92  		return []string{}
    93  	}
    94  	for _, entry := range globalTOC.DataEntries {
    95  		fqn := utils.MakeFQN(entry.Schema, entry.Name)
    96  
    97  		if includedSchemaSet.MatchesFilter(entry.Schema) &&
    98  			excludedSchemaSet.MatchesFilter(entry.Schema) &&
    99  			excludedRelationsSet.MatchesFilter(fqn) {
   100  			relationList = append(relationList, fqn)
   101  		}
   102  	}
   103  	return relationList
   104  }
   105  func ValidateRelationsInRestoreDatabase(connectionPool *dbconn.DBConn, relationList []string) {
   106  	if len(relationList) == 0 {
   107  		return
   108  	}
   109  	quotedTablesStr := utils.SliceToQuotedString(relationList)
   110  	query := fmt.Sprintf(`
   111  SELECT
   112  	quote_ident(n.nspname) || '.' || quote_ident(c.relname) AS string
   113  FROM pg_namespace n
   114  JOIN pg_class c ON n.oid = c.relnamespace
   115  WHERE quote_ident(n.nspname) || '.' || quote_ident(c.relname) IN (%s)`, quotedTablesStr)
   116  	relationsInDB := dbconn.MustSelectStringSlice(connectionPool, query)
   117  
   118  	/*
   119  	 * For data-only we check that the relations we are planning to restore
   120  	 * are already defined in the database so we have somewhere to put the data.
   121  	 *
   122  	 * For non-data-only we check that the relations we are planning to restore
   123  	 * are not already in the database so we don't get duplicate data.
   124  	 */
   125  	var errMsg string
   126  	if backupConfig.DataOnly || MustGetFlagBool(options.DATA_ONLY) {
   127  		if len(relationsInDB) < len(relationList) {
   128  			dbRelationsSet := utils.NewSet(relationsInDB)
   129  			for _, restoreRelation := range relationList {
   130  				matches := dbRelationsSet.MatchesFilter(restoreRelation)
   131  				if !matches {
   132  					errMsg = fmt.Sprintf("Relation %s must exist for data-only restore", restoreRelation)
   133  				}
   134  			}
   135  		}
   136  	} else if len(relationsInDB) > 0 {
   137  		errMsg = fmt.Sprintf("Relation %s already exists", relationsInDB[0])
   138  	}
   139  	if errMsg != "" {
   140  		gplog.Fatal(nil, errMsg)
   141  	}
   142  }
   143  
   144  func ValidateRedirectSchema(connectionPool *dbconn.DBConn, redirectSchema string) {
   145  	query := fmt.Sprintf(`SELECT quote_ident(nspname) AS name FROM pg_namespace n WHERE n.nspname = '%s'`, redirectSchema)
   146  	schemaInDB := dbconn.MustSelectStringSlice(connectionPool, query)
   147  
   148  	if len(schemaInDB) == 0 {
   149  		gplog.Fatal(nil, fmt.Sprintf("Schema %s to redirect into does not exist", redirectSchema))
   150  	}
   151  }
   152  
   153  func ValidateIncludeRelationsInBackupSet(schemaList []string) {
   154  	if keys := getFilterRelationsInBackupSet(schemaList); len(keys) != 0 {
   155  		gplog.Fatal(errors.Errorf("Could not find the following relation(s) in the backup set: %s", strings.Join(keys, ", ")), "")
   156  	}
   157  }
   158  
   159  func ValidateExcludeRelationsInBackupSet(schemaList []string) {
   160  	if keys := getFilterRelationsInBackupSet(schemaList); len(keys) != 0 {
   161  		gplog.Warn("Could not find the following excluded relation(s) in the backup set: %s", strings.Join(keys, ", "))
   162  	}
   163  }
   164  
   165  func getFilterRelationsInBackupSet(relationList []string) []string {
   166  	if len(relationList) == 0 {
   167  		return []string{}
   168  	}
   169  	relationMap := make(map[string]bool, len(relationList))
   170  	for _, relation := range relationList {
   171  		relationMap[relation] = true
   172  	}
   173  	for _, entry := range globalTOC.PredataEntries {
   174  		if entry.ObjectType != "TABLE" && entry.ObjectType != "SEQUENCE" && entry.ObjectType != "VIEW" && entry.ObjectType != "MATERIALIZED VIEW" {
   175  			continue
   176  		}
   177  		fqn := utils.MakeFQN(entry.Schema, entry.Name)
   178  		if _, ok := relationMap[fqn]; ok {
   179  			delete(relationMap, fqn)
   180  		}
   181  		if len(relationMap) == 0 {
   182  			return []string{}
   183  		}
   184  	}
   185  
   186  	dataEntries := make([]string, 0)
   187  	for _, restorePlanEntry := range backupConfig.RestorePlan {
   188  		dataEntries = append(dataEntries, restorePlanEntry.TableFQNs...)
   189  	}
   190  	for _, fqn := range dataEntries {
   191  		if _, ok := relationMap[fqn]; ok {
   192  			delete(relationMap, fqn)
   193  		}
   194  		if len(relationMap) == 0 {
   195  			return []string{}
   196  		}
   197  	}
   198  
   199  	keys := make([]string, len(relationMap))
   200  	i := 0
   201  	for k := range relationMap {
   202  		keys[i] = k
   203  		i++
   204  	}
   205  	return keys
   206  }
   207  
   208  func ValidateDatabaseExistence(unquotedDBName string, createDatabase bool, isFiltered bool) {
   209  	qry := fmt.Sprintf(`
   210  SELECT CASE
   211  	WHEN EXISTS (SELECT 1 FROM pg_database WHERE datname='%s') THEN 'true'
   212  	ELSE 'false'
   213  END AS string;`, utils.EscapeSingleQuotes(unquotedDBName))
   214  	databaseExists, err := strconv.ParseBool(dbconn.MustSelectString(connectionPool, qry))
   215  	gplog.FatalOnError(err)
   216  	if !databaseExists {
   217  		if isFiltered {
   218  			gplog.Fatal(errors.Errorf(`Database "%s" must be created manually to restore table-filtered or data-only backups.`, unquotedDBName), "")
   219  		} else if !createDatabase {
   220  			gplog.Fatal(errors.Errorf(`Database "%s" does not exist. Use the --create-db flag to create "%s" as part of the restore process.`, unquotedDBName, unquotedDBName), "")
   221  		}
   222  	} else if createDatabase {
   223  		gplog.Fatal(errors.Errorf(`Database "%s" already exists. Run gprestore again without --create-db flag.`, unquotedDBName), "")
   224  	}
   225  }
   226  
   227  func ValidateBackupFlagCombinations() {
   228  	if backupConfig.SingleDataFile && MustGetFlagInt(options.JOBS) != 1 {
   229  		gplog.Fatal(errors.Errorf("Cannot use jobs flag when restoring backups with a single data file per segment."), "")
   230  	}
   231  	if (backupConfig.IncludeTableFiltered || backupConfig.DataOnly) && MustGetFlagBool(options.WITH_GLOBALS) {
   232  		gplog.Fatal(errors.Errorf("Global metadata is not backed up in table-filtered or data-only backups."), "")
   233  	}
   234  	if backupConfig.MetadataOnly && MustGetFlagBool(options.DATA_ONLY) {
   235  		gplog.Fatal(errors.Errorf("Cannot use data-only flag when restoring metadata-only backup"), "")
   236  	}
   237  	if backupConfig.DataOnly && MustGetFlagBool(options.METADATA_ONLY) {
   238  		gplog.Fatal(errors.Errorf("Cannot use metadata-only flag when restoring data-only backup"), "")
   239  	}
   240  	if !backupConfig.SingleDataFile && FlagChanged(options.COPY_QUEUE_SIZE) {
   241  		gplog.Fatal(errors.Errorf("The --copy-queue-size flag can only be used if the backup was taken with --single-data-file"), "")
   242  	}
   243  	validateBackupFlagPluginCombinations()
   244  }
   245  
   246  func validateBackupFlagPluginCombinations() {
   247  	if backupConfig.Plugin != "" && MustGetFlagString(options.PLUGIN_CONFIG) == "" {
   248  		gplog.Fatal(errors.Errorf("Backup was taken with plugin %s. The --plugin-config flag must be used to restore.", backupConfig.Plugin), "")
   249  	} else if backupConfig.Plugin == "" && MustGetFlagString(options.PLUGIN_CONFIG) != "" {
   250  		gplog.Fatal(errors.Errorf("The --plugin-config flag cannot be used to restore a backup taken without a plugin."), "")
   251  	}
   252  }
   253  
   254  func ValidateFlagCombinations(flags *pflag.FlagSet) {
   255  	options.CheckExclusiveFlags(flags, options.DATA_ONLY, options.WITH_GLOBALS)
   256  	options.CheckExclusiveFlags(flags, options.DATA_ONLY, options.CREATE_DB)
   257  	options.CheckExclusiveFlags(flags, options.DEBUG, options.QUIET, options.VERBOSE)
   258  
   259  	options.CheckExclusiveFlags(flags, options.INCLUDE_SCHEMA, options.INCLUDE_RELATION, options.INCLUDE_RELATION_FILE)
   260  	options.CheckExclusiveFlags(flags, options.EXCLUDE_SCHEMA, options.INCLUDE_SCHEMA)
   261  	options.CheckExclusiveFlags(flags, options.EXCLUDE_SCHEMA, options.EXCLUDE_RELATION, options.INCLUDE_RELATION, options.EXCLUDE_RELATION_FILE, options.INCLUDE_RELATION_FILE)
   262  
   263  	options.CheckExclusiveFlags(flags, options.METADATA_ONLY, options.DATA_ONLY)
   264  	options.CheckExclusiveFlags(flags, options.PLUGIN_CONFIG, options.BACKUP_DIR)
   265  	options.CheckExclusiveFlags(flags, options.TRUNCATE_TABLE, options.METADATA_ONLY, options.INCREMENTAL)
   266  	options.CheckExclusiveFlags(flags, options.TRUNCATE_TABLE, options.REDIRECT_SCHEMA)
   267  
   268  	if flags.Changed(options.REDIRECT_SCHEMA) {
   269  		// Redirect schema not compatible with any exclude flags
   270  		if flags.Changed(options.EXCLUDE_SCHEMA) || flags.Changed(options.EXCLUDE_SCHEMA_FILE) ||
   271  			flags.Changed(options.EXCLUDE_RELATION) || flags.Changed(options.EXCLUDE_RELATION_FILE) {
   272  			gplog.Fatal(errors.Errorf("Cannot use --redirect-schema with exclude flags"), "")
   273  		}
   274  		// Redirect schema requires an include flag
   275  		if !(flags.Changed(options.INCLUDE_RELATION) || flags.Changed(options.INCLUDE_RELATION_FILE) ||
   276  			flags.Changed(options.INCLUDE_SCHEMA) || flags.Changed(options.INCLUDE_SCHEMA_FILE)) {
   277  			gplog.Fatal(errors.Errorf("Cannot use --redirect-schema without --include-table, --include-table-file, --include-schema, or --include-schema-file"), "")
   278  		}
   279  	}
   280  	if flags.Changed(options.TRUNCATE_TABLE) &&
   281  		!(flags.Changed(options.INCLUDE_RELATION) || flags.Changed(options.INCLUDE_RELATION_FILE)) &&
   282  		!flags.Changed(options.DATA_ONLY) {
   283  		gplog.Fatal(errors.Errorf("Cannot use --truncate-table without --include-table or --include-table-file and without --data-only"), "")
   284  	}
   285  	if flags.Changed(options.INCREMENTAL) && !flags.Changed(options.DATA_ONLY) {
   286  		gplog.Fatal(errors.Errorf("Cannot use --incremental without --data-only"), "")
   287  	}
   288  	options.CheckExclusiveFlags(flags, options.RUN_ANALYZE, options.WITH_STATS)
   289  }
   290  
   291  func ValidateSafeToResizeCluster() {
   292  	// If SegmentCount is 0, the backup was taken before the SegmentCount parameter was added, in which case we won't
   293  	// allow a restore to a different-size cluster.  Any backups that do have a SegmentCount will have that checked
   294  	// when attempting a normal restore, so that the user doesn't accidentally restore a different-size backup without
   295  	// using the --resize-cluster flag.
   296  	origSize, destSize, resizeCluster := GetResizeClusterInfo()
   297  
   298  	if resizeCluster {
   299  		if origSize == 0 {
   300  			timestamp := MustGetFlagString(options.TIMESTAMP)
   301  			gplog.Fatal(errors.Errorf("Segment count for backup with timestamp %s is unknown, cannot restore using --resize-cluster flag.", timestamp), "")
   302  		} else if origSize == destSize {
   303  			cmdFlags.Set(options.RESIZE_CLUSTER, "false")
   304  			gplog.Warn("Backup segment count matches restore segment count; the --resize-cluster flag is not needed.  Proceeding with a normal restore.")
   305  		} else {
   306  			gplog.Info("Resize restore specified, will restore a backup set from a %d-segment cluster to a %d-segment cluster", origSize, destSize)
   307  		}
   308  	} else {
   309  		if origSize != 0 && origSize != destSize {
   310  			gplog.Fatal(errors.New(fmt.Sprintf("Cannot restore a backup taken on a cluster with %d segments to a cluster with %d segments unless the --resize-cluster flag is used.", origSize, destSize)), "")
   311  		}
   312  	}
   313  }