github.com/cloudberrydb/gpbackup@v1.0.3-0.20240118031043-5410fd45eed6/restore/validate_test.go (about) 1 package restore_test 2 3 import ( 4 "strings" 5 6 "github.com/DATA-DOG/go-sqlmock" 7 "github.com/cloudberrydb/gp-common-go-libs/testhelper" 8 "github.com/cloudberrydb/gpbackup/history" 9 "github.com/cloudberrydb/gpbackup/options" 10 "github.com/cloudberrydb/gpbackup/restore" 11 "github.com/cloudberrydb/gpbackup/testutils" 12 "github.com/cloudberrydb/gpbackup/toc" 13 "github.com/cloudberrydb/gpbackup/utils" 14 "github.com/spf13/cobra" 15 16 . "github.com/onsi/ginkgo/v2" 17 . "github.com/onsi/gomega" 18 ) 19 20 var _ = Describe("restore/validate tests", func() { 21 var filterList []string 22 var tocfile *toc.TOC 23 var backupfile *utils.FileWithByteCount 24 AfterEach(func() { 25 filterList = []string{} 26 }) 27 Describe("ValidateSchemasInBackupSet", func() { 28 sequence := toc.StatementWithType{ObjectType: "SEQUENCE", Statement: "CREATE SEQUENCE schema.somesequence"} 29 sequenceLen := uint64(len(sequence.Statement)) 30 table1 := toc.StatementWithType{ObjectType: "TABLE", Statement: "CREATE TABLE schema1.table1"} 31 table1Len := uint64(len(table1.Statement)) 32 table2 := toc.StatementWithType{ObjectType: "TABLE", Statement: "CREATE TABLE schema2.table2"} 33 table2Len := uint64(len(table2.Statement)) 34 BeforeEach(func() { 35 tocfile, backupfile = testutils.InitializeTestTOC(buffer, "predata") 36 backupfile.ByteCount = table1Len 37 tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema1", Name: "table1", ObjectType: "TABLE"}, 0, backupfile.ByteCount) 38 tocfile.AddCoordinatorDataEntry("schema1", "table1", 1, "(i)", 0, "", "") 39 backupfile.ByteCount += table2Len 40 tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema2", Name: "table2", ObjectType: "TABLE"}, table1Len, backupfile.ByteCount) 41 tocfile.AddCoordinatorDataEntry("schema2", "table2", 2, "(j)", 0, "", "") 42 backupfile.ByteCount += sequenceLen 43 tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema", Name: "somesequence", ObjectType: "SEQUENCE"}, table1Len+table2Len, backupfile.ByteCount) 44 restore.SetTOC(tocfile) 45 }) 46 It("passes when schema exists in normal backup", func() { 47 restore.SetBackupConfig(&history.BackupConfig{}) 48 filterList = []string{"schema1"} 49 restore.ValidateIncludeSchemasInBackupSet(filterList) 50 }) 51 It("panics when schema does not exist in normal backup", func() { 52 restore.SetBackupConfig(&history.BackupConfig{}) 53 filterList = []string{"schema3"} 54 defer testhelper.ShouldPanicWithMessage("Could not find the following schema(s) in the backup set: schema3") 55 restore.ValidateIncludeSchemasInBackupSet(filterList) 56 }) 57 It("passes when schema exists in data-only backup", func() { 58 restore.SetBackupConfig(&history.BackupConfig{DataOnly: true}) 59 filterList = []string{"schema1"} 60 restore.ValidateIncludeSchemasInBackupSet(filterList) 61 }) 62 It("panics when schema does not exist in data-only backup", func() { 63 restore.SetBackupConfig(&history.BackupConfig{DataOnly: true}) 64 filterList = []string{"schema3"} 65 defer testhelper.ShouldPanicWithMessage("Could not find the following schema(s) in the backup set: schema3") 66 restore.ValidateIncludeSchemasInBackupSet(filterList) 67 }) 68 It("generates warning when exclude-schema does not exist in backup and noFatal is true", func() { 69 _, _, logfile = testhelper.SetupTestLogger() 70 restore.SetBackupConfig(&history.BackupConfig{}) 71 filterList = []string{"schema3"} 72 restore.ValidateExcludeSchemasInBackupSet(filterList) 73 testhelper.ExpectRegexp(logfile, "[WARNING]:-Could not find the following excluded schema(s) in the backup set: schema3") 74 }) 75 }) 76 Describe("GenerateRestoreRelationList", func() { 77 var opts *options.Options 78 BeforeEach(func() { 79 tocfile, _ = testutils.InitializeTestTOC(buffer, "metadata") 80 tocfile.AddCoordinatorDataEntry("s1", "table1", 1, "(j)", 0, "", "") 81 tocfile.AddCoordinatorDataEntry("s1", "table2", 2, "(j)", 0, "", "") 82 tocfile.AddCoordinatorDataEntry("s2", "table1", 3, "(j)", 0, "", "") 83 tocfile.AddCoordinatorDataEntry("s2", "table2", 4, "(j)", 0, "", "") 84 restore.SetTOC(tocfile) 85 86 opts = &options.Options{} 87 }) 88 It("returns all tables if no filtering is used", func() { 89 opts.IncludedRelations = []string{"s1.table1", "s1.table2", "s2.table1", "s2.table2"} 90 91 resultRelations := restore.GenerateRestoreRelationList(*opts) 92 93 expectedRelations := []string{"s1.table1", "s1.table2", "s2.table1", "s2.table2"} 94 Expect(resultRelations).To(ConsistOf(expectedRelations)) 95 }) 96 It("filters on include relations", func() { 97 opts.IncludedRelations = []string{"s1.table1", "s1.table2"} 98 99 resultRelations := restore.GenerateRestoreRelationList(*opts) 100 101 expectedRelations := []string{"s1.table1", "s1.table2"} 102 Expect(resultRelations).To(ConsistOf(expectedRelations)) 103 }) 104 It("filters on exclude relations", func() { 105 opts.ExcludedRelations = []string{"s1.table2", "s2.table1"} 106 107 resultRelations := restore.GenerateRestoreRelationList(*opts) 108 109 expectedRelations := []string{"s1.table1", "s2.table2"} 110 Expect(resultRelations).To(ConsistOf(expectedRelations)) 111 }) 112 It("filters on include schema", func() { 113 opts.IncludedSchemas = []string{"s1"} 114 115 resultRelations := restore.GenerateRestoreRelationList(*opts) 116 117 expectedRelations := []string{"s1.table1", "s1.table2"} 118 Expect(resultRelations).To(ConsistOf(expectedRelations)) 119 }) 120 It("filters on exclude schema", func() { 121 opts.ExcludedSchemas = []string{"s2"} 122 123 resultRelations := restore.GenerateRestoreRelationList(*opts) 124 125 expectedRelations := []string{"s1.table1", "s1.table2"} 126 Expect(resultRelations).To(ConsistOf(expectedRelations)) 127 }) 128 It("filters on include schema with exclude relation", func() { 129 opts.IncludedSchemas = []string{"s1"} 130 opts.ExcludedRelations = []string{"s1.table1"} 131 132 resultRelations := restore.GenerateRestoreRelationList(*opts) 133 134 expectedRelations := []string{"s1.table2"} 135 Expect(resultRelations).To(ConsistOf(expectedRelations)) 136 }) 137 }) 138 Describe("ValidateRelationsInRestoreDatabase", func() { 139 BeforeEach(func() { 140 restore.SetBackupConfig(&history.BackupConfig{DataOnly: false}) 141 _ = cmdFlags.Set(options.DATA_ONLY, "false") 142 }) 143 Context("data-only restore", func() { 144 BeforeEach(func() { 145 _ = cmdFlags.Set(options.DATA_ONLY, "true") 146 }) 147 It("panics if all tables missing from database", func() { 148 noTableRows := sqlmock.NewRows([]string{"string"}) 149 mock.ExpectQuery("SELECT (.*)").WillReturnRows(noTableRows) 150 filterList = []string{"public.table2"} 151 defer testhelper.ShouldPanicWithMessage("Relation public.table2 must exist for data-only restore") 152 restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList) 153 }) 154 It("panics if some tables missing from database", func() { 155 singleTableRow := sqlmock.NewRows([]string{"string"}). 156 AddRow("public.table1") 157 mock.ExpectQuery("SELECT (.*)").WillReturnRows(singleTableRow) 158 filterList = []string{"public.table1", "public.table2"} 159 defer testhelper.ShouldPanicWithMessage("Relation public.table2 must exist for data-only restore") 160 restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList) 161 }) 162 It("passes if all tables are present in database", func() { 163 twoTableRows := sqlmock.NewRows([]string{"string"}). 164 AddRow("public.table1").AddRow("public.table2") 165 mock.ExpectQuery("SELECT (.*)").WillReturnRows(twoTableRows) 166 filterList = []string{"public.table1", "public.table2"} 167 restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList) 168 }) 169 }) 170 Context("restore includes metadata", func() { 171 It("passes if table is not present in database", func() { 172 noTableRows := sqlmock.NewRows([]string{"string"}) 173 mock.ExpectQuery("SELECT (.*)").WillReturnRows(noTableRows) 174 filterList = []string{"public.table2"} 175 restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList) 176 }) 177 It("panics if single table is present in database", func() { 178 singleTableRow := sqlmock.NewRows([]string{"string"}). 179 AddRow("public.table1") 180 mock.ExpectQuery("SELECT (.*)").WillReturnRows(singleTableRow) 181 filterList = []string{"public.table1", "public.table2"} 182 defer testhelper.ShouldPanicWithMessage("Relation public.table1 already exists") 183 restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList) 184 }) 185 It("panics if multiple tables are present in database", func() { 186 twoTableRows := sqlmock.NewRows([]string{"string"}). 187 AddRow("public.table1").AddRow("public.view1") 188 mock.ExpectQuery("SELECT (.*)").WillReturnRows(twoTableRows) 189 filterList = []string{"public.table1", "public.view1"} 190 defer testhelper.ShouldPanicWithMessage("Relation public.table1 already exists") 191 restore.ValidateRelationsInRestoreDatabase(connectionPool, filterList) 192 }) 193 }) 194 }) 195 Describe("ValidateRelationsInBackupSet", func() { 196 var tocfile *toc.TOC 197 var backupfile *utils.FileWithByteCount 198 BeforeEach(func() { 199 tocfile, backupfile = testutils.InitializeTestTOC(buffer, "predata") 200 tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema1", Name: "table1", ObjectType: "TABLE"}, 0, backupfile.ByteCount) 201 tocfile.AddCoordinatorDataEntry("schema1", "table1", 1, "(i)", 0, "", "") 202 203 tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema2", Name: "table2", ObjectType: "TABLE"}, 0, backupfile.ByteCount) 204 tocfile.AddCoordinatorDataEntry("schema2", "table2", 2, "(j)", 0, "", "") 205 206 tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema1", Name: "somesequence", ObjectType: "SEQUENCE"}, 0, backupfile.ByteCount) 207 tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema1", Name: "someview", ObjectType: "VIEW"}, 0, backupfile.ByteCount) 208 tocfile.AddMetadataEntry("predata", toc.MetadataEntry{Schema: "schema1", Name: "somefunction", ObjectType: "FUNCTION"}, 0, backupfile.ByteCount) 209 210 restore.SetTOC(tocfile) 211 }) 212 It("passes when table exists in normal backup", func() { 213 restore.SetBackupConfig(&history.BackupConfig{}) 214 filterList = []string{"schema1.table1"} 215 restore.ValidateIncludeRelationsInBackupSet(filterList) 216 }) 217 It("panics when table does not exist in normal backup", func() { 218 restore.SetBackupConfig(&history.BackupConfig{}) 219 filterList = []string{"schema1.table3"} 220 defer testhelper.ShouldPanicWithMessage("Could not find the following relation(s) in the backup set: schema1.table3") 221 restore.ValidateIncludeRelationsInBackupSet(filterList) 222 }) 223 It("passes when sequence exists in normal backup", func() { 224 restore.SetBackupConfig(&history.BackupConfig{}) 225 filterList = []string{"schema1.somesequence"} 226 restore.ValidateIncludeRelationsInBackupSet(filterList) 227 }) 228 It("generates a warning if the exclude-schema is not in the backup set and noFatal is true", func() { 229 _, _, logfile = testhelper.SetupTestLogger() 230 restore.SetBackupConfig(&history.BackupConfig{}) 231 filterList = []string{"schema1.table3"} 232 restore.ValidateExcludeRelationsInBackupSet(filterList) 233 testhelper.ExpectRegexp(logfile, "[WARNING]:-Could not find the following excluded relation(s) in the backup set: schema1.table3") 234 }) 235 It("passes when view exists in normal backup", func() { 236 restore.SetBackupConfig(&history.BackupConfig{}) 237 filterList = []string{"schema1.someview"} 238 restore.ValidateIncludeRelationsInBackupSet(filterList) 239 }) 240 It("passes when table exists in data-only backup", func() { 241 restore.SetBackupConfig(&history.BackupConfig{DataOnly: true}) 242 filterList = []string{"schema1.table1"} 243 restore.ValidateIncludeRelationsInBackupSet(filterList) 244 }) 245 It("panics when relation does not exist in backup but function with same name does", func() { 246 restore.SetBackupConfig(&history.BackupConfig{}) 247 filterList = []string{"schema1.somefunction"} 248 defer testhelper.ShouldPanicWithMessage("Could not find the following relation(s) in the backup set: schema1.somefunction") 249 restore.ValidateIncludeRelationsInBackupSet(filterList) 250 }) 251 It("table does not exist in data-only backup", func() { 252 restore.SetBackupConfig(&history.BackupConfig{DataOnly: true}) 253 filterList = []string{"schema1.table3"} 254 defer testhelper.ShouldPanicWithMessage("Could not find the following relation(s) in the backup set: schema1.table3") 255 restore.ValidateIncludeRelationsInBackupSet(filterList) 256 }) 257 It("passes when table exists in most recent restore plan entry", func() { 258 restore.SetBackupConfig(&history.BackupConfig{RestorePlan: []history.RestorePlanEntry{{TableFQNs: []string{"schema1.table1_part_1"}}}}) 259 filterList = []string{"schema1.table1_part_1"} 260 restore.ValidateIncludeRelationsInBackupSet(filterList) 261 }) 262 It("passes when table exists in previous restore plan entry", func() { 263 restore.SetBackupConfig(&history.BackupConfig{RestorePlan: []history.RestorePlanEntry{{TableFQNs: []string{"schema1.random_table"}}, {TableFQNs: []string{"schema1.table1_part_1"}}}}) 264 filterList = []string{"schema1.table1_part_1"} 265 restore.ValidateIncludeRelationsInBackupSet(filterList) 266 }) 267 }) 268 Describe("ValidateDatabaseExistence", func() { 269 It("panics if createdb passed when db exists", func() { 270 dbExists := sqlmock.NewRows([]string{"string"}). 271 AddRow("true") 272 mock.ExpectQuery("SELECT (.*)").WillReturnRows(dbExists) 273 defer testhelper.ShouldPanicWithMessage(`Database "testdb" already exists.`) 274 restore.ValidateDatabaseExistence("testdb", true, false) 275 }) 276 It("passes if db exists and --create-db not passed", func() { 277 dbExists := sqlmock.NewRows([]string{"string"}). 278 AddRow("true") 279 mock.ExpectQuery("SELECT (.*)").WillReturnRows(dbExists) 280 restore.ValidateDatabaseExistence("testdb", false, false) 281 }) 282 It("panics and tells user to manually create db when db does not exist and filtered", func() { 283 dbExists := sqlmock.NewRows([]string{"string"}). 284 AddRow("false") 285 mock.ExpectQuery("SELECT (.*)").WillReturnRows(dbExists) 286 defer testhelper.ShouldPanicWithMessage(`Database "testdb" must be created manually`) 287 restore.ValidateDatabaseExistence("testdb", true, true) 288 }) 289 It("panics and tells user to pass --create-db when db does not exist, not filtered, and no --create-db", func() { 290 dbExists := sqlmock.NewRows([]string{"string"}). 291 AddRow("false") 292 mock.ExpectQuery("SELECT (.*)").WillReturnRows(dbExists) 293 defer testhelper.ShouldPanicWithMessage(`Database "testdb" does not exist. Use the --create-db flag`) 294 restore.ValidateDatabaseExistence("testdb", false, false) 295 }) 296 }) 297 Describe("Validate various flag combinations that are required or exclusive", func() { 298 DescribeTable("Validate various flag combinations that are required or exclusive", 299 func(argString string, valid bool) { 300 testCmd := &cobra.Command{ 301 Use: "flag validation", 302 Args: cobra.NoArgs, 303 Run: func(cmd *cobra.Command, args []string) { 304 restore.ValidateFlagCombinations(cmd.Flags()) 305 }} 306 testCmd.SetArgs(strings.Split(argString, " ")) 307 restore.SetCmdFlags(testCmd.Flags()) 308 309 if !valid { 310 defer testhelper.ShouldPanicWithMessage("CRITICAL") 311 } 312 313 err := testCmd.Execute() 314 if err != nil && valid { 315 Fail("Valid flag combination failed validation check") 316 } 317 }, 318 Entry("--backup-dir combo", "--backup-dir /tmp --plugin-config /tmp/config", false), 319 320 /* 321 * Below are all the different filter combinations 322 */ 323 // --exclude-schema combinations with other filters 324 Entry("--exclude-schema combos", "--exclude-schema schema1 --include-table schema.table2", false), 325 Entry("--exclude-schema combos", "--exclude-schema schema1 --include-table-file /tmp/file2", false), 326 Entry("--exclude-schema combos", "--exclude-schema schema1 --include-schema schema2", false), 327 Entry("--exclude-schema combos", "--exclude-schema schema1 --include-schema-file /tmp/file2", true), // TODO: Verify this. 328 Entry("--exclude-schema combos", "--exclude-schema schema1 --exclude-table schema.table2", false), 329 Entry("--exclude-schema combos", "--exclude-schema schema1 --exclude-table-file /tmp/file2", false), 330 Entry("--exclude-schema combos", "--exclude-schema schema1 --exclude-schema schema2", true), 331 Entry("--exclude-schema combos", "--exclude-schema schema1 --exclude-schema-file /tmp/file2", true), // TODO: Verify this. 332 333 // --exclude-schema-file combinations with other filters 334 Entry("--exclude-schema-file combos", "--exclude-schema-file /tmp/file --include-table schema.table2", true), // TODO: Verify this. 335 Entry("--exclude-schema-file combos", "--exclude-schema-file /tmp/file --include-table-file /tmp/file2", true), // TODO: Verify this. 336 Entry("--exclude-schema-file combos", "--exclude-schema-file /tmp/file --include-schema schema2", true), // TODO: Verify this. 337 Entry("--exclude-schema-file combos", "--exclude-schema-file /tmp/file --include-schema-file /tmp/file2", true), // TODO: Verify this. 338 Entry("--exclude-schema-file combos", "--exclude-schema-file /tmp/file --exclude-table schema.table2", true), // TODO: Verify this. 339 Entry("--exclude-schema-file combos", "--exclude-schema-file /tmp/file --exclude-table-file /tmp/file2", true), // TODO: Verify this. 340 341 // --exclude-table combinations with other filters 342 Entry("--exclude-table combos", "--exclude-table schema.table --include-table schema.table2", false), 343 Entry("--exclude-table combos", "--exclude-table schema.table --include-table-file /tmp/file2", false), 344 Entry("--exclude-table combos", "--exclude-table schema.table --include-schema schema2", true), 345 Entry("--exclude-table combos", "--exclude-table schema.table --include-schema-file /tmp/file2", true), 346 Entry("--exclude-table combos", "--exclude-table schema.table --exclude-table schema.table2", true), 347 Entry("--exclude-table combos", "--exclude-table schema.table --exclude-table-file /tmp/file2", false), 348 349 // --exclude-table-file combinations with other filters 350 Entry("--exclude-table-file combos", "--exclude-table-file /tmp/file --include-table schema.table2", false), 351 Entry("--exclude-table-file combos", "--exclude-table-file /tmp/file --include-table-file /tmp/file2", false), 352 Entry("--exclude-table-file combos", "--exclude-table-file /tmp/file --include-schema schema2", true), 353 Entry("--exclude-table-file combos", "--exclude-table-file /tmp/file --include-schema-file /tmp/file2", true), 354 355 // --include-schema combinations with other filters 356 Entry("--include-schema combos", "--include-schema schema1 --include-table schema.table2", false), 357 Entry("--include-schema combos", "--include-schema schema1 --include-table-file /tmp/file2", false), 358 Entry("--include-schema combos", "--include-schema schema1 --include-schema schema2", true), 359 Entry("--include-schema combos", "--include-schema schema1 --include-schema-file /tmp/file2", true), // TODO: Verify this. 360 361 // --include-schema-file combinations with other filters 362 Entry("--include-schema-file combos", "--include-schema-file /tmp/file --include-table schema.table2", true), // TODO: Verify this. 363 Entry("--include-schema-file combos", "--include-schema-file /tmp/file --include-table-file /tmp/file2", true), // TODO: Verify this. 364 365 // --include-table combinations with other filters 366 Entry("--include-table combos", "--include-table schema.table --include-table schema.table2", true), 367 Entry("--include-table combos", "--include-table schema.table --include-table-file /tmp/file2", false), 368 369 /* 370 * Below are various different incremental combinations 371 */ 372 Entry("incremental combos", "--incremental", false), 373 Entry("incremental combos", "--incremental --data-only", true), 374 375 /* 376 * Below are various different truncate combinations 377 */ 378 Entry("truncate combos", "--truncate-table", false), 379 Entry("truncate combos", "--truncate-table --include-table schema.table2", true), 380 Entry("truncate combos", "--truncate-table --include-table-file /tmp/file2", true), 381 Entry("truncate combos", "--truncate-table --include-table schema.table2 --redirect-db foodb", true), 382 Entry("truncate combos", "--truncate-table --include-table schema.table2 --redirect-schema schema2", false), 383 384 /* 385 * Below are various different redirect-schema combinations 386 */ 387 Entry("--redirect-schema combos", "--redirect-schema schema1", false), 388 Entry("--redirect-schema combos", "--redirect-schema schema1 --include-table schema.table2", true), 389 Entry("--redirect-schema combos", "--redirect-schema schema1 --include-table-file /tmp/file2", true), 390 Entry("--redirect-schema combos", "--redirect-schema schema1 --include-schema schema2", true), 391 Entry("--redirect-schema combos", "--redirect-schema schema1 --include-schema-file /tmp/file2", true), 392 Entry("--redirect-schema combos", "--redirect-schema schema1 --exclude-table schema.table2", false), 393 Entry("--redirect-schema combos", "--redirect-schema schema1 --exclude-table-file /tmp/file2", false), 394 Entry("--redirect-schema combos", "--redirect-schema schema1 --exclude-schema schema2", false), 395 Entry("--redirect-schema combos", "--redirect-schema schema1 --exclude-schema-file /tmp/file2", false), 396 Entry("--redirect-schema combos", "--redirect-schema schema1 --include-table schema.table2 --metadata-only", true), 397 Entry("--redirect-schema combos", "--redirect-schema schema1 --include-table schema.table2 --data-only", true), 398 ) 399 }) 400 Describe("ValidateBackupFlagCombinations", func() { 401 It("restore with copy-queue-size should fatal if backup was not taken with single-data-file", func() { 402 restore.SetBackupConfig(&history.BackupConfig{SingleDataFile: false}) 403 testCmd := &cobra.Command{ 404 Use: "flag validation", 405 Args: cobra.NoArgs, 406 Run: func(cmd *cobra.Command, args []string) { 407 restore.ValidateBackupFlagCombinations() 408 }} 409 testCmd.SetArgs([]string{"--copy-queue-size", "4"}) 410 restore.SetCmdFlags(testCmd.Flags()) 411 412 defer testhelper.ShouldPanicWithMessage("CRITICAL") 413 err := testCmd.Execute() 414 if err == nil { 415 Fail("invalid flag combination passed validation check") 416 } 417 }) 418 }) 419 })