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 }