This is an automated email from the ASF dual-hosted git repository.
MisterRaindrop pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudberry-backup.git
The following commit(s) were added to refs/heads/main by this push:
new 682202ca Add database filters to gpbackman commands. (#113)
682202ca is described below
commit 682202ca778ea3f098d8a0108012ae2f5475f144
Author: Anton Kurochkin <[email protected]>
AuthorDate: Tue Sep 1 07:07:44 2026 +0300
Add database filters to gpbackman commands. (#113)
Add the --database option to backup-clean, history-clean, and backup-info.
Database names are matched exactly and case-sensitively against backup
history. When the option is omitted, the existing all-database behavior
is preserved.
Disallow combining --database with --timestamp in backup-info, and add
unit tests, end-to-end coverage, and command documentation.
---
end_to_end/end_to_end_suite_test.go | 30 +++-
end_to_end/gpbackman_test.go | 114 ++++++++++++-
gpbackman/COMMANDS.md | 62 ++++++-
gpbackman/cmd/backup_clean.go | 34 +++-
gpbackman/cmd/backup_clean_test.go | 132 +++++++++++++++
gpbackman/cmd/backup_info.go | 41 ++++-
gpbackman/cmd/backup_info_test.go | 292 ++++++++++++++++++++++++++++++++
gpbackman/cmd/constants.go | 1 +
gpbackman/cmd/history_clean.go | 22 ++-
gpbackman/cmd/history_clean_test.go | 182 ++++++++++++++++++++
gpbackman/cmd/history_sync_test.go | 20 +++
gpbackman/cmd/wrappers_test.go | 10 ++
gpbackman/gpbckpconfig/utils_db.go | 48 ++++--
gpbackman/gpbckpconfig/utils_db_test.go | 78 ++++++++-
14 files changed, 1018 insertions(+), 48 deletions(-)
diff --git a/end_to_end/end_to_end_suite_test.go
b/end_to_end/end_to_end_suite_test.go
index 75d60a32..4986c3c4 100644
--- a/end_to_end/end_to_end_suite_test.go
+++ b/end_to_end/end_to_end_suite_test.go
@@ -78,6 +78,7 @@ const (
TOTAL_RELATIONS = 37
TOTAL_RELATIONS_AFTER_EXCLUDE = 21
TOTAL_CREATE_STATEMENTS = 9
+ gpbackmanFilterDatabase = "gpbackman_filter_db"
)
// This function is run automatically by ginkgo before any tests are run.
@@ -90,14 +91,18 @@ func init() {
* to allow checking its output.
*/
func gpbackup(gpbackupPath string, backupHelperPath string, args ...string)
[]byte {
- return runGpbackup(gpbackupPath, backupHelperPath, true, args...)
+ return gpbackupForDatabase("testdb", gpbackupPath, backupHelperPath,
args...)
}
func gpbackupWithHistoryStandbySync(gpbackupPath string, backupHelperPath
string, args ...string) []byte {
- return runGpbackup(gpbackupPath, backupHelperPath, false, args...)
+ return runGpbackupForDatabase("testdb", gpbackupPath, backupHelperPath,
false, args...)
}
-func runGpbackup(gpbackupPath string, backupHelperPath string,
disableHistoryStandbySync bool, args ...string) []byte {
+func gpbackupForDatabase(databaseName string, gpbackupPath string,
backupHelperPath string, args ...string) []byte {
+ return runGpbackupForDatabase(databaseName, gpbackupPath,
backupHelperPath, true, args...)
+}
+
+func runGpbackupForDatabase(databaseName string, gpbackupPath string,
backupHelperPath string, disableHistoryStandbySync bool, args ...string) []byte
{
if useOldBackupVersion {
_ = os.Chdir("..")
command := exec.Command("make", "install",
fmt.Sprintf("helper_path=%s", backupHelperPath))
@@ -107,7 +112,7 @@ func runGpbackup(gpbackupPath string, backupHelperPath
string, disableHistorySta
if disableHistoryStandbySync && !useOldBackupVersion &&
!hasCommandArgument(args, "--no-history-sync-standby") {
args = append(args, "--no-history-sync-standby")
}
- args = append([]string{"--verbose", "--dbname", "testdb"}, args...)
+ args = append([]string{"--verbose", "--dbname", databaseName}, args...)
command := exec.Command(gpbackupPath, args...)
return mustRunCommand(command)
}
@@ -744,6 +749,23 @@ func end_to_end_teardown() {
_ = os.RemoveAll(backupDir)
}
+func setupGpbackmanFilterDatabase() {
+ _ = exec.Command("dropdb", gpbackmanFilterDatabase).Run()
+ Expect(exec.Command("createdb",
gpbackmanFilterDatabase).Run()).To(Succeed())
+
+ filterConn := testutils.SetupTestDbConn(gpbackmanFilterDatabase)
+ defer filterConn.Close()
+ testhelper.AssertQueryRuns(filterConn, `
+ CREATE TABLE public.e2e_data (id integer, value text)
DISTRIBUTED BY (id);
+ INSERT INTO public.e2e_data (id, value)
+ SELECT i, 'e2e-value-' || i FROM generate_series(1, 100) AS i;
+ `)
+}
+
+func teardownGpbackmanFilterDatabase() {
+ _ = exec.Command("dropdb", gpbackmanFilterDatabase).Run()
+}
+
var _ = Describe("backup and restore end to end tests", func() {
BeforeEach(func() {
end_to_end_setup()
diff --git a/end_to_end/gpbackman_test.go b/end_to_end/gpbackman_test.go
index cabf46c1..f4ce0781 100644
--- a/end_to_end/gpbackman_test.go
+++ b/end_to_end/gpbackman_test.go
@@ -76,6 +76,7 @@ var _ = Describe("gpbackman end to end tests", func() {
BeforeEach(func() {
end_to_end_setup()
+ setupGpbackmanFilterDatabase()
historyDB = getHistoryDBPathForCluster()
timestampMap = make(map[string]string)
@@ -109,9 +110,15 @@ var _ = Describe("gpbackman end to end tests", func() {
"--backup-dir", backupDir,
"--metadata-only")
timestampMap["metadata_only"] =
getBackupTimestamp(string(output))
+
+ output = gpbackupForDatabase(gpbackmanFilterDatabase,
gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--include-table", "public.e2e_data")
+ timestampMap["filter_database"] =
getBackupTimestamp(string(output))
})
AfterEach(func() {
+ teardownGpbackmanFilterDatabase()
end_to_end_teardown()
})
@@ -121,9 +128,35 @@ var _ = Describe("gpbackman end to end tests", func() {
"--history-db", historyDB,
)
lines := countBackupInfoLines(output)
- Expect(lines).To(BeNumerically(">=", 5),
- fmt.Sprintf("Expected at least 5 backup
entries, got %d.\nOutput:\n%s",
+ Expect(lines).To(BeNumerically(">=", 6),
+ fmt.Sprintf("Expected at least 6 backup
entries, got %d.\nOutput:\n%s",
lines, string(output)))
+ Expect(string(output)).To(ContainSubstring("testdb"))
+
Expect(string(output)).To(ContainSubstring(gpbackmanFilterDatabase))
+ })
+
+ It("filters by database and composes with detail and table
filters", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ "--database", gpbackmanFilterDatabase,
+ "--type", "full",
+ "--table", "public.e2e_data",
+ "--detail",
+ )
+ Expect(countBackupInfoLines(output)).To(Equal(1))
+
Expect(string(output)).To(ContainSubstring(timestampMap["filter_database"]))
+
Expect(string(output)).To(ContainSubstring(gpbackmanFilterDatabase))
+ Expect(string(output)).To(ContainSubstring("e2e_data"))
+ })
+
+ It("returns no rows for an unknown database", func() {
+ output := gpbackman(
+ "backup-info",
+ "--history-db", historyDB,
+ "--database", "gpbackman_unknown_db",
+ )
+ Expect(countBackupInfoLines(output)).To(Equal(0))
})
It("filters by type full", func() {
@@ -226,6 +259,17 @@ var _ = Describe("gpbackman end to end tests", func() {
Expect(err).To(HaveOccurred())
})
+ It("rejects incompatible flags --timestamp with --database",
func() {
+ _, err := gpbackmanWithError(
+ "backup-info",
+ "--history-db", historyDB,
+ "--timestamp", timestampMap["filter_database"],
+ "--database", gpbackmanFilterDatabase,
+ "--detail",
+ )
+ Expect(err).To(HaveOccurred())
+ })
+
It("rejects invalid timestamp format", func() {
_, err := gpbackmanWithError(
"backup-info",
@@ -614,6 +658,32 @@ var _ = Describe("gpbackman end to end tests", func() {
)
// Success if no error was thrown
})
+
+ It("cleans only the selected database's eligible backups",
func() {
+ setupGpbackmanFilterDatabase()
+ DeferCleanup(teardownGpbackmanFilterDatabase)
+
+ primaryOutput := gpbackup(gpbackupPath,
backupHelperPath,
+ "--backup-dir", backupDir)
+ primaryTimestamp :=
getBackupTimestamp(string(primaryOutput))
+ filterOutput :=
gpbackupForDatabase(gpbackmanFilterDatabase, gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir)
+ filterTimestamp :=
getBackupTimestamp(string(filterOutput))
+
+ gpbackman(
+ "backup-clean",
+ "--history-db", historyDB,
+ "--before-timestamp", "99991231235959",
+ "--database", gpbackmanFilterDatabase,
+ )
+
+ primaryActive := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM backups WHERE
timestamp = '%s' AND date_deleted = ''", primaryTimestamp))
+ Expect(primaryActive).To(Equal("1"), "backup for the
default database should remain active")
+ filterDeleted := queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM backups WHERE
timestamp = '%s' AND date_deleted != ''", filterTimestamp))
+ Expect(filterDeleted).To(Equal("1"), "backup for the
selected database should be deleted")
+ })
})
// ------------------------------------------------------------------ //
@@ -716,6 +786,46 @@ var _ = Describe("gpbackman end to end tests", func() {
Expect(count2).To(Equal("1"),
"Non-deleted backup should remain in history")
})
+
+ It("cleans deleted history and related rows only for the
selected database", func() {
+ setupGpbackmanFilterDatabase()
+ DeferCleanup(teardownGpbackmanFilterDatabase)
+
+ primaryOutput := gpbackup(gpbackupPath,
backupHelperPath,
+ "--backup-dir", backupDir)
+ primaryTimestamp :=
getBackupTimestamp(string(primaryOutput))
+ filterOutput :=
gpbackupForDatabase(gpbackmanFilterDatabase, gpbackupPath, backupHelperPath,
+ "--backup-dir", backupDir,
+ "--include-table", "public.e2e_data")
+ filterTimestamp :=
getBackupTimestamp(string(filterOutput))
+
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", primaryTimestamp,
+ )
+ gpbackman(
+ "backup-delete",
+ "--history-db", historyDB,
+ "--timestamp", filterTimestamp,
+ )
+ Expect(queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM
include_relations WHERE timestamp = '%s'", filterTimestamp))).To(Equal("1"))
+
+ gpbackman(
+ "history-clean",
+ "--history-db", historyDB,
+ "--before-timestamp", "99991231235959",
+ "--database", gpbackmanFilterDatabase,
+ )
+
+ Expect(queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM backups WHERE
timestamp = '%s'", primaryTimestamp))).To(Equal("1"))
+ Expect(queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM backups WHERE
timestamp = '%s'", filterTimestamp))).To(Equal("0"))
+ Expect(queryHistoryDB(historyDB,
+ fmt.Sprintf("SELECT count(*) FROM
include_relations WHERE timestamp = '%s'", filterTimestamp))).To(Equal("0"))
+ })
})
// ------------------------------------------------------------------ //
diff --git a/gpbackman/COMMANDS.md b/gpbackman/COMMANDS.md
index 715536b9..7f827f4f 100644
--- a/gpbackman/COMMANDS.md
+++ b/gpbackman/COMMANDS.md
@@ -65,6 +65,11 @@ To delete backup sets older than the given number of days,
use the --older-than-
To delete backup sets newer than the given timestamp, use the
--after-timestamp option.
Only --older-than-days, --before-timestamp or --after-timestamp option must be
specified.
+Use --database to clean backup sets only for the specified database. Without
--database,
+cleanup includes backup sets for all databases in the history database.
+Database names are matched exactly and case-sensitively against backup history.
+For database names that require quoting, include the double quotes in the
--database value.
+
By default, the existence of dependent backups is checked and deletion process
is not performed,
unless the --cascade option is passed in.
@@ -99,6 +104,7 @@ Flags:
--backup-dir string the full path to backup directory for local
backups
--before-timestamp string delete backup sets older than the given
timestamp
--cascade delete all dependent backups
+ --database string delete backup sets only for the specified
database
-h, --help help for backup-clean
--history-sync-standby-timeout int shared rsync and remote install
timeout in seconds; must be an integer between 1 and 86400 (default 300)
--no-history-sync-standby skip automatic gpbackup_history.db sync to
standby coordinator after this command
@@ -124,6 +130,20 @@ Delete backups older than a timestamp:
--cascade
```
+Delete local backups only for database `analytics`:
+```bash
+./gpbackman backup-clean \
+ --before-timestamp 20240701100000 \
+ --database analytics
+```
+
+For database `Sales DB`, include double quotes in the flag value:
+```bash
+./gpbackman backup-clean \
+ --before-timestamp 20240701100000 \
+ --database '"Sales DB"'
+```
+
Delete backups older than a number of days with multiple parallel processes:
```bash
./gpbackman backup-clean \
@@ -251,6 +271,12 @@ To display all backups, use --deleted and --failed options
together.
To display backups of a specific type, use the --type option.
+Without the --database option, backups for all databases are displayed.
+To display backups only for a specific database, use the --database option.
+Database names are matched exactly and case-sensitively against backup history.
+The --database value is used without transformation. For database names that
require quoting,
+include the double quotes in the flag value, for example: --database '"Sales
DB"'.
+
To display backups that include the specified table, use the --table option.
The formatting rules for <schema>.<table> match those of the --include-table
option in gpbackup.
@@ -273,7 +299,7 @@ To display a backup chain for a specific backup, use the
--timestamp option.
In this mode, the backup with the specified timestamp and all of its dependent
backups will be displayed.
The deleted and failed backups are always included in this mode.
To display object filtering details in this mode, use the --detail option.
-When --timestamp is set, the following options cannot be used: --type,
--table, --schema, --exclude, --failed, --deleted.
+When --timestamp is set, the following options cannot be used: --database,
--type, --table, --schema, --exclude, --failed, --deleted.
To display the "object filtering details" column for all backups without using
--timestamp, use the --detail option.
@@ -285,6 +311,7 @@ Usage:
gpbackman backup-info [flags]
Flags:
+ --database string show backups only for the specified database
(exact, case-sensitive match)
--deleted show deleted backups
--detail show object filtering details
--exclude show backups that exclude the specific table
(format <schema>.<table>) or schema
@@ -378,6 +405,19 @@ Display info for active full backups from
`gpbackup_history.db`:
20230523101115 | Tue May 23 2023 10:11:15 | Success | demo | full |
include-schema | gpbackup_s3_plugin | 01:01:00 |
```
+Display active backups only for database `analytics`:
+```bash
+./gpbackman backup-info \
+ --database analytics
+```
+
+Display active full backups only for database `analytics`:
+```bash
+./gpbackman backup-info \
+ --database analytics \
+ --type full
+```
+
Find all backups, including deleted ones, containing the `test1` schema.
```bash
./gpbackman backup-info \
@@ -477,6 +517,11 @@ To delete information about backups older than the given
timestamp, use the --be
To delete information about backups older than the given number of days, use
the --older-than-day option.
Only --older-than-days or --before-timestamp option must be specified, not
both.
+Use --database to clean history only for the specified database. Without
--database,
+cleanup includes deleted backup history for all databases in the history
database.
+Database names are matched exactly and case-sensitively against backup history.
+For database names that require quoting, include the double quotes in the
--database value.
+
The gpbackup_history.db file location can be set using the --history-db option.
Can be specified only once. The full path to the file is required.
If the --history-db option is not specified, the history database is looked
for in the current directory. To resolve it from $COORDINATOR_DATA_DIRECTORY
instead, pass the --auto-load-history-db flag.
@@ -486,6 +531,7 @@ Usage:
Flags:
--before-timestamp string delete information about backups older than
the given timestamp
+ --database string delete backup history only for the specified
database
-h, --help help for history-clean
--history-sync-standby-timeout int shared rsync and remote install
timeout in seconds; must be an integer between 1 and 86400 (default 300)
--no-history-sync-standby skip automatic gpbackup_history.db sync to
standby coordinator after this command
@@ -514,6 +560,20 @@ Delete information about deleted backups from history
database older than timest
--before-timestamp 20240101100000
```
+Delete deleted backup history only for database `analytics`:
+```bash
+./gpbackman history-clean \
+ --before-timestamp 20240101100000 \
+ --database analytics
+```
+
+For database `Sales DB`, include double quotes in the flag value:
+```bash
+./gpbackman history-clean \
+ --before-timestamp 20240101100000 \
+ --database '"Sales DB"'
+```
+
# Sync the history database to the standby coordinator (`history-sync`)
Available options for `history-sync` command and their description:
diff --git a/gpbackman/cmd/backup_clean.go b/gpbackman/cmd/backup_clean.go
index 5435c2c8..7222a555 100644
--- a/gpbackman/cmd/backup_clean.go
+++ b/gpbackman/cmd/backup_clean.go
@@ -42,6 +42,7 @@ var (
backupCleanParallelProcesses int
backupCleanCascade bool
backupCleanNoHistorySyncStandby bool
+ backupCleanDatabase string
)
var backupCleanCmd = &cobra.Command{
@@ -54,6 +55,11 @@ To delete backup sets older than the given number of days,
use the --older-than-
To delete backup sets newer than the given timestamp, use the
--after-timestamp option.
Only --older-than-days, --before-timestamp or --after-timestamp option must be
specified.
+Use --database to clean backups only for the specified database. Without
--database,
+cleanup includes backups for all databases in the history database.
+Database names are matched exactly and case-sensitively against backup history.
+For database names that require quoting, include the double quotes in the
--database value.
+
By default, the existence of dependent backups is checked and deletion process
is not performed,
unless the --cascade option is passed in.
@@ -89,6 +95,12 @@ If the --history-db option is not specified, the history
database is looked for
func init() {
rootCmd.AddCommand(backupCleanCmd)
+ backupCleanCmd.Flags().StringVar(
+ &backupCleanDatabase,
+ databaseFlagName,
+ "",
+ "delete backup sets only for the specified database",
+ )
backupCleanCmd.PersistentFlags().StringVar(
&backupCleanPluginConfigFile,
pluginConfigFileFlagName,
@@ -149,6 +161,10 @@ func init() {
// These flag checks are applied only for backup-clean command.
func doCleanBackupFlagValidation(flags *pflag.FlagSet) {
var err error
+ if flags.Changed(databaseFlagName) && backupCleanDatabase == "" {
+ gplog.Error("%s",
textmsg.ErrorTextUnableValidateFlag(backupCleanDatabase, databaseFlagName,
textmsg.ErrorEmptyDatabase()))
+ execOSExit(exitErrorCode)
+ }
// If before-timestamp flag is specified and have correct values.
if flags.Changed(beforeTimestampFlagName) {
err = gpbckpconfig.CheckTimestamp(backupCleanBeforeTimestamp)
@@ -232,12 +248,12 @@ func cleanBackup() error {
gplog.Error("%s",
textmsg.ErrorTextUnableReadPluginConfigFile(err))
return err
}
- err = backupCleanDBPlugin(backupCleanCascade, beforeTimestamp,
afterTimestamp, backupCleanPluginConfigFile, pluginConfig, hDB)
+ err = backupCleanDBPlugin(backupCleanCascade, beforeTimestamp,
afterTimestamp, backupCleanDatabase, backupCleanPluginConfigFile, pluginConfig,
hDB)
if err != nil {
return err
}
} else {
- err := backupCleanDBLocal(backupCleanCascade, beforeTimestamp,
afterTimestamp, backupCleanBackupDir, backupCleanParallelProcesses, hDB)
+ err := backupCleanDBLocal(backupCleanCascade, beforeTimestamp,
afterTimestamp, backupCleanDatabase, backupCleanBackupDir,
backupCleanParallelProcesses, hDB)
if err != nil {
return err
}
@@ -245,8 +261,8 @@ func cleanBackup() error {
return nil
}
-func backupCleanDBPlugin(deleteCascade bool, cutOffTimestamp,
cutOffAfterTimestamp, pluginConfigPath string, pluginConfig
*utils.PluginConfig, hDB *sql.DB) error {
- backupList, err := fetchBackupNamesForDeletion(cutOffTimestamp,
cutOffAfterTimestamp, hDB)
+func backupCleanDBPlugin(deleteCascade bool, cutOffTimestamp,
cutOffAfterTimestamp, databaseName, pluginConfigPath string, pluginConfig
*utils.PluginConfig, hDB *sql.DB) error {
+ backupList, err := fetchBackupNamesForDeletion(cutOffTimestamp,
cutOffAfterTimestamp, databaseName, hDB)
if err != nil {
gplog.Error("%s", textmsg.ErrorTextUnableReadHistoryDB(err))
return err
@@ -266,8 +282,8 @@ func backupCleanDBPlugin(deleteCascade bool,
cutOffTimestamp, cutOffAfterTimesta
return nil
}
-func backupCleanDBLocal(deleteCascade bool, cutOffTimestamp,
cutOffAfterTimestamp, backupDir string, maxParallelProcesses int, hDB *sql.DB)
error {
- backupList, err := fetchBackupNamesForDeletion(cutOffTimestamp,
cutOffAfterTimestamp, hDB)
+func backupCleanDBLocal(deleteCascade bool, cutOffTimestamp,
cutOffAfterTimestamp, databaseName, backupDir string, maxParallelProcesses int,
hDB *sql.DB) error {
+ backupList, err := fetchBackupNamesForDeletion(cutOffTimestamp,
cutOffAfterTimestamp, databaseName, hDB)
if err != nil {
gplog.Error("%s", textmsg.ErrorTextUnableReadHistoryDB(err))
return err
@@ -285,17 +301,17 @@ func backupCleanDBLocal(deleteCascade bool,
cutOffTimestamp, cutOffAfterTimestam
}
// Get the list of backup names for deletion.
-func fetchBackupNamesForDeletion(cutOffTimestamp, cutOffAfterTimestamp string,
hDB *sql.DB) ([]string, error) {
+func fetchBackupNamesForDeletion(cutOffTimestamp, cutOffAfterTimestamp,
databaseName string, hDB *sql.DB) ([]string, error) {
var backupList []string
var err error
if cutOffTimestamp != "" {
- backupList, err =
gpbckpconfig.GetBackupNamesBeforeTimestamp(cutOffTimestamp, hDB)
+ backupList, err =
gpbckpconfig.GetBackupNamesBeforeTimestamp(cutOffTimestamp, databaseName, hDB)
if err != nil {
return nil, err
}
}
if cutOffAfterTimestamp != "" {
- backupList, err =
gpbckpconfig.GetBackupNamesAfterTimestamp(cutOffAfterTimestamp, hDB)
+ backupList, err =
gpbckpconfig.GetBackupNamesAfterTimestamp(cutOffAfterTimestamp, databaseName,
hDB)
if err != nil {
return nil, err
}
diff --git a/gpbackman/cmd/backup_clean_test.go
b/gpbackman/cmd/backup_clean_test.go
new file mode 100644
index 00000000..ef7f4d9f
--- /dev/null
+++ b/gpbackman/cmd/backup_clean_test.go
@@ -0,0 +1,132 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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 cmd
+
+import (
+ "database/sql"
+ "path/filepath"
+
+ "github.com/spf13/pflag"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("backup-clean database filter", func() {
+ It("registers a command-local database flag and documents its
behavior", func() {
+ flag := backupCleanCmd.Flags().Lookup(databaseFlagName)
+ Expect(flag).NotTo(BeNil())
+ Expect(flag.DefValue).To(Equal(""))
+ Expect(flag.Usage).To(ContainSubstring("specified database"))
+ Expect(backupCleanCmd.Long).To(ContainSubstring("Without
--database"))
+
Expect(backupCleanCmd.Long).To(ContainSubstring("case-sensitively"))
+ })
+
+ It("requires a value when the database flag is supplied", func() {
+ rootCmd.SetArgs([]string{"backup-clean", "--" +
databaseFlagName})
+ DeferCleanup(func() { rootCmd.SetArgs(nil) })
+
+ err := rootCmd.Execute()
+ Expect(err).To(MatchError(ContainSubstring("flag needs an
argument")))
+ })
+
+ DescribeTable("validates explicit empty database values",
+ func(database string, setDatabase, wantExit bool) {
+ oldDatabase := backupCleanDatabase
+ oldCleanBeforeTimestamp := backupCleanBeforeTimestamp
+ oldBeforeTimestamp := beforeTimestamp
+ oldAfterTimestamp := afterTimestamp
+ oldExecOSExit := execOSExit
+ DeferCleanup(func() {
+ backupCleanDatabase = oldDatabase
+ backupCleanBeforeTimestamp =
oldCleanBeforeTimestamp
+ beforeTimestamp = oldBeforeTimestamp
+ afterTimestamp = oldAfterTimestamp
+ execOSExit = oldExecOSExit
+ })
+
+ backupCleanDatabase = database
+ backupCleanBeforeTimestamp = "20240101120000"
+ beforeTimestamp = ""
+ afterTimestamp = ""
+ flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
+ flags.String(beforeTimestampFlagName, "", "")
+ flags.String(databaseFlagName, "", "")
+ Expect(flags.Set(beforeTimestampFlagName,
backupCleanBeforeTimestamp)).To(Succeed())
+ if setDatabase {
+ Expect(flags.Set(databaseFlagName,
database)).To(Succeed())
+ }
+
+ exited := false
+ execOSExit = func(code int) {
+ Expect(code).To(Equal(exitErrorCode))
+ exited = true
+ }
+ doCleanBackupFlagValidation(flags)
+ Expect(exited).To(Equal(wantExit))
+ },
+ Entry("when absent", "", false, false),
+ Entry("when non-empty", `"Customer's DB"`, true, false),
+ Entry("when explicitly empty", "", true, true),
+ )
+
+ Describe("backup selection", func() {
+ var historyDB *sql.DB
+
+ BeforeEach(func() {
+ var err error
+ historyDB, err = sql.Open("sqlite3",
"file:"+filepath.Join(GinkgoT().TempDir(), "history.db")+"?mode=rwc")
+ Expect(err).NotTo(HaveOccurred())
+ _, err = historyDB.Exec(`CREATE TABLE backups
(timestamp TEXT, database_name TEXT, status TEXT, date_deleted TEXT)`)
+ Expect(err).NotTo(HaveOccurred())
+ for _, backup := range [][]string{
+ {"20240101090000", "demo", "Success", ""},
+ {"20240101100000", `"Customer's DB"`,
"Success", ""},
+ {"20240101110000", `"customer's db"`,
"Success", ""},
+ {"20240101130000", `"Customer's DB"`,
"Success", ""},
+ } {
+ _, err = historyDB.Exec(`INSERT INTO backups
(timestamp, database_name, status, date_deleted) VALUES (?, ?, ?, ?)`,
backup[0], backup[1], backup[2], backup[3])
+ Expect(err).NotTo(HaveOccurred())
+ }
+ })
+
+ AfterEach(func() {
+ Expect(historyDB.Close()).To(Succeed())
+ })
+
+ DescribeTable("filters before and after timestamp selections
exactly",
+ func(before, after, database string, expected []string)
{
+ actual, err :=
fetchBackupNamesForDeletion(before, after, database, historyDB)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(actual).To(Equal(expected))
+ },
+ Entry("before timestamp exact database",
"20240101120000", "", `"Customer's DB"`, []string{"20240101100000"}),
+ Entry("after timestamp exact database", "",
"20240101120000", `"Customer's DB"`, []string{"20240101130000"}),
+ Entry("quoted case mismatch", "20240101120000", "",
`"customer's db"`, []string{"20240101110000"}),
+ Entry("unknown database", "20240101120000", "",
"unknown", nil),
+ Entry("without filter", "20240101120000", "", "",
[]string{"20240101110000", "20240101100000", "20240101090000"}),
+ )
+
+ It("does not invoke local or plugin cleanup for an unknown
database", func() {
+ Expect(backupCleanDBLocal(false, "20240101120000", "",
"unknown", "", 1, historyDB)).To(Succeed())
+ Expect(backupCleanDBPlugin(false, "20240101120000", "",
"unknown", "", nil, historyDB)).To(Succeed())
+ })
+ })
+})
diff --git a/gpbackman/cmd/backup_info.go b/gpbackman/cmd/backup_info.go
index 96674b48..073a35eb 100644
--- a/gpbackman/cmd/backup_info.go
+++ b/gpbackman/cmd/backup_info.go
@@ -41,6 +41,7 @@ var (
backupInfoTableNameFilter string
backupInfoSchemaNameFilter string
backupInfoExcludeFilter bool
+ backupInfoDatabase string
backupInfoTimestamp string
backupInfoShowDetails bool
)
@@ -53,6 +54,7 @@ type BackupInfoOptions struct {
TableNameFilter string
SchemaNameFilter string
ExcludeFilter bool
+ DatabaseFilter string
Timestamp string
ShowDetails bool
}
@@ -70,6 +72,12 @@ To display all backups, use --deleted and --failed options
together.
To display backups of a specific type, use the --type option.
+Without the --database option, backups for all databases are displayed.
+To display backups only for a specific database, use the --database option.
+Database names are matched exactly and case-sensitively against backup history.
+The --database value is used without transformation. For database names that
require quoting,
+include the double quotes in the flag value, for example: --database '"Sales
DB"'.
+
To display backups that include the specified table, use the --table option.
The formatting rules for <schema>.<table> match those of the --include-table
option in gpbackup.
@@ -91,8 +99,8 @@ The details are presented as follows, depending on the active
filtering type:
To display a backup chain for a specific backup, use the --timestamp option.
In this mode, the backup with the specified timestamp and all of its dependent
backups will be displayed.
The deleted and failed backups are always included in this mode.
-To display object filtering details in this mode, use the --detail option.
-When --timestamp is set, the following options cannot be used: --type,
--table, --schema, --exclude, --failed, --deleted.
+The --detail option can be used with --timestamp to display object filtering
details in this mode.
+When --timestamp is set, the following options cannot be used: --database,
--type, --table, --schema, --exclude, --failed, --deleted.
To display the "object filtering details" column for all backups without using
--timestamp, use the --detail option.
@@ -151,6 +159,12 @@ func init() {
false,
"show backups that exclude the specific table (format
<schema>.<table>) or schema",
)
+ backupInfoCmd.Flags().StringVar(
+ &backupInfoDatabase,
+ databaseFlagName,
+ "",
+ "show backups only for the specified database (exact,
case-sensitive match)",
+ )
backupInfoCmd.Flags().BoolVar(
&backupInfoShowDetails,
detailFlagName,
@@ -162,17 +176,21 @@ func init() {
// These flag checks are applied only for backup-info commands.
func doBackupInfoFlagValidation(flags *pflag.FlagSet) {
var err error
+ if flags.Changed(databaseFlagName) && backupInfoDatabase == "" {
+ gplog.Error("%s",
textmsg.ErrorTextUnableValidateFlag(backupInfoDatabase, databaseFlagName,
textmsg.ErrorEmptyDatabase()))
+ execOSExit(exitErrorCode)
+ }
if flags.Changed(timestampFlagName) {
err = gpbckpconfig.CheckTimestamp(backupInfoTimestamp)
if err != nil {
gplog.Error("%s",
textmsg.ErrorTextUnableValidateFlag(backupInfoTimestamp, timestampFlagName,
err))
execOSExit(exitErrorCode)
}
- // --timestamp is not compatible with --type, --table,
--schema, --exclude, --failed, --deleted
+ // --timestamp is not compatible with --database, --type,
--table, --schema, --exclude, --failed, --deleted
err = checkCompatibleFlags(flags, timestampFlagName,
- typeFlagName, tableFlagName, schemaFlagName,
excludeFlagName, failedFlagName, deletedFlagName)
+ databaseFlagName, typeFlagName, tableFlagName,
schemaFlagName, excludeFlagName, failedFlagName, deletedFlagName)
if err != nil {
- gplog.Error("%s",
textmsg.ErrorTextUnableCompatibleFlags(err, timestampFlagName, typeFlagName,
tableFlagName, schemaFlagName, excludeFlagName, failedFlagName,
deletedFlagName))
+ gplog.Error("%s",
textmsg.ErrorTextUnableCompatibleFlags(err, timestampFlagName,
databaseFlagName, typeFlagName, tableFlagName, schemaFlagName, excludeFlagName,
failedFlagName, deletedFlagName))
execOSExit(exitErrorCode)
}
}
@@ -221,6 +239,7 @@ func backupInfo() error {
TableNameFilter: backupInfoTableNameFilter,
SchemaNameFilter: backupInfoSchemaNameFilter,
ExcludeFilter: backupInfoExcludeFilter,
+ DatabaseFilter: backupInfoDatabase,
Timestamp: backupInfoTimestamp,
ShowDetails: backupInfoShowDetails,
}
@@ -259,7 +278,7 @@ func backupInfoDB(opts BackupInfoOptions, hDB *sql.DB, t
*tablewriter.Table) err
gplog.Error("%s",
textmsg.ErrorTextUnableGetBackupInfo(backupName, err))
return err
}
- addBackupToTable(opts.BackupTypeFilter,
opts.TableNameFilter, opts.SchemaNameFilter, opts.ExcludeFilter,
opts.ShowDetails, backupData, t)
+ addBackupToTable(opts.BackupTypeFilter,
opts.TableNameFilter, opts.SchemaNameFilter, opts.DatabaseFilter,
opts.ExcludeFilter, opts.ShowDetails, backupData, t)
}
return nil
}
@@ -270,7 +289,7 @@ func backupInfoDB(opts BackupInfoOptions, hDB *sql.DB, t
*tablewriter.Table) err
gplog.Error("%s",
textmsg.ErrorTextUnableGetBackupInfo(opts.Timestamp, err))
return err
}
- addBackupToTable("", "", "", false, opts.ShowDetails, baseBackupData, t)
+ addBackupToTable("", "", "", "", false, opts.ShowDetails,
baseBackupData, t)
backupDependenciesList, err :=
gpbckpconfig.GetBackupDependencies(opts.Timestamp, hDB)
if err != nil {
gplog.Error("%s", textmsg.ErrorTextUnableReadHistoryDB(err))
@@ -282,7 +301,7 @@ func backupInfoDB(opts BackupInfoOptions, hDB *sql.DB, t
*tablewriter.Table) err
gplog.Error("%s",
textmsg.ErrorTextUnableGetBackupInfo(depTimestamp, err))
return err
}
- addBackupToTable("", "", "", false, opts.ShowDetails,
backupData, t)
+ addBackupToTable("", "", "", "", false, opts.ShowDetails,
backupData, t)
}
return nil
}
@@ -311,7 +330,11 @@ func initTable(t *tablewriter.Table, includeDetails bool) {
// If errors occur, they are logged, but they are not returned.
// The main idea is to show the maximum available information and display all
errors that occur.
// But do not fall when errors occur. So, display anyway.
-func addBackupToTable(backupTypeFilter, backupTableFilter, backupSchemaFilter
string, backupExcludeFilter, includeDetails bool, backupData
*history.BackupConfig, t *tablewriter.Table) {
+func addBackupToTable(backupTypeFilter, backupTableFilter, backupSchemaFilter,
backupDatabaseFilter string, backupExcludeFilter, includeDetails bool,
backupData *history.BackupConfig, t *tablewriter.Table) {
+ if backupDatabaseFilter != "" && backupDatabaseFilter !=
backupData.DatabaseName {
+ return
+ }
+
var matchToObjectFilter bool
backupDate, err := gpbckpconfig.GetBackupDate(backupData)
if err != nil {
diff --git a/gpbackman/cmd/backup_info_test.go
b/gpbackman/cmd/backup_info_test.go
new file mode 100644
index 00000000..a9405840
--- /dev/null
+++ b/gpbackman/cmd/backup_info_test.go
@@ -0,0 +1,292 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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 cmd
+
+import (
+ "database/sql"
+ "path/filepath"
+
+ "github.com/apache/cloudberry-backup/history"
+ "github.com/olekukonko/tablewriter"
+ "github.com/spf13/pflag"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("backup-info database filter", func() {
+ It("registers and documents the command-local database flag", func() {
+ flag := backupInfoCmd.Flags().Lookup(databaseFlagName)
+ Expect(flag).NotTo(BeNil())
+ Expect(flag.DefValue).To(Equal(""))
+ Expect(flag.Usage).To(ContainSubstring("specified database"))
+ Expect(backupInfoCmd.Long).To(ContainSubstring("Without the
--database option, backups for all databases are displayed."))
+ Expect(backupInfoCmd.Long).To(ContainSubstring("Database names
are matched exactly and case-sensitively against backup history."))
+ Expect(backupInfoCmd.Long).To(ContainSubstring("include the
double quotes in the flag value"))
+ Expect(backupInfoCmd.Long).To(ContainSubstring("The --detail
option can be used with --timestamp"))
+ Expect(backupInfoCmd.Long).To(ContainSubstring("--database,
--type, --table, --schema, --exclude, --failed, --deleted"))
+ Expect(backupInfoCmd.UsageString()).To(ContainSubstring("--" +
databaseFlagName + " string"))
+ })
+
+ It("requires a value when the database flag is supplied", func() {
+ rootCmd.SetArgs([]string{"backup-info", "--" +
databaseFlagName})
+ DeferCleanup(func() { rootCmd.SetArgs(nil) })
+
+ Expect(rootCmd.Execute()).To(MatchError(ContainSubstring("flag
needs an argument")))
+ })
+
+ DescribeTable("validates explicit database values",
+ func(database string, setDatabase, wantExit bool) {
+ oldDatabase := backupInfoDatabase
+ oldTimestamp := backupInfoTimestamp
+ oldExecOSExit := execOSExit
+ DeferCleanup(func() {
+ backupInfoDatabase = oldDatabase
+ backupInfoTimestamp = oldTimestamp
+ execOSExit = oldExecOSExit
+ })
+
+ backupInfoDatabase = database
+ backupInfoTimestamp = ""
+ flags := pflag.NewFlagSet("backup-info",
pflag.ContinueOnError)
+ flags.String(databaseFlagName, "", "")
+ if setDatabase {
+ Expect(flags.Set(databaseFlagName,
database)).To(Succeed())
+ }
+
+ exited := false
+ execOSExit = func(code int) {
+ Expect(code).To(Equal(exitErrorCode))
+ exited = true
+ }
+ doBackupInfoFlagValidation(flags)
+ Expect(exited).To(Equal(wantExit))
+ },
+ Entry("when absent", "", false, false),
+ Entry("when non-empty", `"Customer's DB"`, true, false),
+ Entry("when explicitly empty", "", true, true),
+ )
+
+ DescribeTable("preserves timestamp and detail compatibility",
+ func(setDatabase, setTimestamp, setDetail, wantExit bool) {
+ oldDatabase := backupInfoDatabase
+ oldTimestamp := backupInfoTimestamp
+ oldExecOSExit := execOSExit
+ DeferCleanup(func() {
+ backupInfoDatabase = oldDatabase
+ backupInfoTimestamp = oldTimestamp
+ execOSExit = oldExecOSExit
+ })
+
+ backupInfoDatabase = `"Customer's DB"`
+ backupInfoTimestamp = "20240101120000"
+ flags := pflag.NewFlagSet("backup-info",
pflag.ContinueOnError)
+ flags.String(databaseFlagName, "", "")
+ flags.String(timestampFlagName, "", "")
+ flags.Bool(detailFlagName, false, "")
+ if setDatabase {
+ Expect(flags.Set(databaseFlagName,
backupInfoDatabase)).To(Succeed())
+ }
+ if setTimestamp {
+ Expect(flags.Set(timestampFlagName,
backupInfoTimestamp)).To(Succeed())
+ }
+ if setDetail {
+ Expect(flags.Set(detailFlagName,
"true")).To(Succeed())
+ }
+
+ exited := false
+ execOSExit = func(code int) {
+ Expect(code).To(Equal(exitErrorCode))
+ exited = true
+ }
+ doBackupInfoFlagValidation(flags)
+ Expect(exited).To(Equal(wantExit))
+ },
+ Entry("database with detail", true, false, true, false),
+ Entry("timestamp with detail", false, true, true, false),
+ Entry("database with timestamp", true, true, false, true),
+ Entry("database with timestamp and detail", true, true, true,
true),
+ )
+
+ DescribeTable("matches database names exactly before other filters",
+ func(databaseFilter, backupDatabase string, wantRows int) {
+ t := tablewriter.NewWriter(GinkgoWriter)
+ backupData := backupInfoTestConfig("20240101000000",
backupDatabase)
+ addBackupToTable("", "", "", databaseFilter, false,
false, &backupData, t)
+ Expect(t.NumLines()).To(Equal(wantRows))
+ },
+ Entry("without a filter", "", "demo", 1),
+ Entry("with an exact match", "demo", "demo", 1),
+ Entry("with a case mismatch", "Demo", "demo", 0),
+ Entry("with a quoted name", `"Customer's DB"`, `"Customer's
DB"`, 1),
+ Entry("with an unknown database", "unknown", "demo", 0),
+ )
+
+ It("displays matching backups when their derived fields are invalid",
func() {
+ t := tablewriter.NewWriter(GinkgoWriter)
+ backupData := backupInfoTestConfig("invalid", "demo")
+ backupData.EndTime = "also-invalid"
+ backupData.Incremental = true
+ backupData.DataOnly = true
+ backupData.IncludeSchemaFiltered = true
+ backupData.IncludeTableFiltered = true
+ backupData.DateDeleted = "invalid"
+
+ addBackupToTable("", "", "", "demo", false, false, &backupData,
t)
+ Expect(t.NumLines()).To(Equal(1))
+ })
+
+ It("composes database filtering with type, table, schema, and detail
filters", func() {
+ matchingTable := backupInfoTestConfig("20240101000000", "demo")
+ matchingTable.IncludeTableFiltered = true
+ matchingTable.IncludeRelations = []string{"public.orders"}
+ matchingSchema := backupInfoTestConfig("20240101000001", "demo")
+ matchingSchema.IncludeSchemaFiltered = true
+ matchingSchema.IncludeSchemas = []string{"public"}
+ wrongType := backupInfoTestConfig("20240101000002", "demo")
+ wrongType.Incremental = true
+ wrongObject := backupInfoTestConfig("20240101000003", "demo")
+ wrongObject.IncludeTableFiltered = true
+ wrongObject.IncludeRelations = []string{"public.customers"}
+ otherDatabase := backupInfoTestConfig("20240101000004", "other")
+ otherDatabase.IncludeTableFiltered = true
+ otherDatabase.IncludeRelations = []string{"public.orders"}
+ configs := []*history.BackupConfig{&matchingTable,
&matchingSchema, &wrongType, &wrongObject, &otherDatabase}
+
+ for _, test := range []struct {
+ name, typeFilter, tableFilter, schemaFilter string
+ includeDetail bool
+ wantRows int
+ }{
+ {"type", "full", "", "", false, 3},
+ {"table", "", "public.orders", "", false, 1},
+ {"schema", "", "", "public", false, 1},
+ {"database and type", "full", "", "", false, 3},
+ {"database and table", "", "public.orders", "", false,
1},
+ {"database and schema", "", "", "public", false, 1},
+ {"database and detail", "full", "", "", true, 3},
+ } {
+ t := tablewriter.NewWriter(GinkgoWriter)
+ for _, config := range configs {
+ addBackupToTable(test.typeFilter,
test.tableFilter, test.schemaFilter, "demo", false, test.includeDetail, config,
t)
+ }
+ Expect(t.NumLines()).To(Equal(test.wantRows), test.name)
+ }
+ })
+
+ It("filters database output and leaves timestamp chains unfiltered",
func() {
+ baseTimestamp := "20240101000000"
+ historyDB := createBackupInfoTestDB(
+ backupInfoTestConfig(baseTimestamp, "demo"),
+ backupInfoTestConfig("20240101000001", "other"),
+ backupInfoTestConfigWithDependency("20240101000002",
"demo", baseTimestamp),
+ backupInfoTestConfigWithDependency("20240101000003",
`"Customer's DB"`, baseTimestamp),
+ )
+
+ filtered := tablewriter.NewWriter(GinkgoWriter)
+ Expect(backupInfoDB(BackupInfoOptions{DatabaseFilter: "demo"},
historyDB, filtered)).To(Succeed())
+ Expect(filtered.NumLines()).To(Equal(2))
+
+ unknown := tablewriter.NewWriter(GinkgoWriter)
+ Expect(backupInfoDB(BackupInfoOptions{DatabaseFilter:
"unknown"}, historyDB, unknown)).To(Succeed())
+ Expect(unknown.NumLines()).To(Equal(0))
+
+ chain := tablewriter.NewWriter(GinkgoWriter)
+ Expect(backupInfoDB(BackupInfoOptions{Timestamp: baseTimestamp,
ShowDetails: true}, historyDB, chain)).To(Succeed())
+ Expect(chain.NumLines()).To(Equal(3))
+ })
+
+ It("composes database filtering with exclude, deleted, and failed
filters", func() {
+ configs := make([]history.BackupConfig, 0, 6)
+ for _, database := range []struct {
+ name string
+ timestamps [3]string
+ }{
+ {"demo", [3]string{"20240101000000", "20240101000001",
"20240101000002"}},
+ {"other", [3]string{"20240101000003", "20240101000004",
"20240101000005"}},
+ } {
+ active := backupInfoTestConfig(database.timestamps[0],
database.name)
+ active.ExcludeTableFiltered = true
+ active.ExcludeRelations = []string{"public.orders"}
+ configs = append(configs, active)
+
+ deleted := backupInfoTestConfig(database.timestamps[1],
database.name)
+ deleted.ExcludeTableFiltered = true
+ deleted.ExcludeRelations = []string{"public.orders"}
+ deleted.DateDeleted = "20240102000000"
+ configs = append(configs, deleted)
+
+ failed := backupInfoTestConfig(database.timestamps[2],
database.name)
+ failed.ExcludeTableFiltered = true
+ failed.ExcludeRelations = []string{"public.orders"}
+ failed.Status = history.BackupStatusFailed
+ configs = append(configs, failed)
+ }
+ historyDB := createBackupInfoTestDB(configs...)
+
+ for _, test := range []struct {
+ name string
+ showDeleted bool
+ showFailed bool
+ wantRows int
+ }{
+ {"exclude", false, false, 1},
+ {"exclude and deleted", true, false, 2},
+ {"exclude and failed", false, true, 2},
+ {"exclude, deleted, and failed", true, true, 3},
+ } {
+ t := tablewriter.NewWriter(GinkgoWriter)
+ err := backupInfoDB(BackupInfoOptions{
+ ShowDeleted: test.showDeleted,
+ ShowFailed: test.showFailed,
+ TableNameFilter: "public.orders",
+ ExcludeFilter: true,
+ DatabaseFilter: "demo",
+ }, historyDB, t)
+ Expect(err).To(Succeed(), test.name)
+ Expect(t.NumLines()).To(Equal(test.wantRows), test.name)
+ }
+ })
+})
+
+func backupInfoTestConfig(timestamp, databaseName string) history.BackupConfig
{
+ return history.BackupConfig{
+ Timestamp: timestamp,
+ EndTime: timestamp,
+ DatabaseName: databaseName,
+ Status: history.BackupStatusSucceed,
+ }
+}
+
+func backupInfoTestConfigWithDependency(timestamp, databaseName, baseTimestamp
string) history.BackupConfig {
+ config := backupInfoTestConfig(timestamp, databaseName)
+ config.RestorePlan = []history.RestorePlanEntry{{Timestamp:
baseTimestamp}}
+ return config
+}
+
+func createBackupInfoTestDB(configs ...history.BackupConfig) *sql.DB {
+ historyDB, err :=
history.InitializeHistoryDatabase(filepath.Join(GinkgoT().TempDir(),
historyDBNameConst))
+ Expect(err).NotTo(HaveOccurred())
+ DeferCleanup(func() { Expect(historyDB.Close()).To(Succeed()) })
+ for i := range configs {
+ Expect(history.StoreBackupHistory(historyDB,
&configs[i])).To(Succeed())
+ }
+ return historyDB
+}
diff --git a/gpbackman/cmd/constants.go b/gpbackman/cmd/constants.go
index 3d00a615..f068e9e2 100644
--- a/gpbackman/cmd/constants.go
+++ b/gpbackman/cmd/constants.go
@@ -55,6 +55,7 @@ const (
excludeFlagName = "exclude"
backupDirFlagName = "backup-dir"
parallelProcessesFlagName = "parallel-processes"
+ databaseFlagName = "database"
ignoreErrorsFlagName = "ignore-errors"
noHistorySyncStandbyFlagName = "no-history-sync-standby"
historySyncStandbyTimeoutFlagName = "history-sync-standby-timeout"
diff --git a/gpbackman/cmd/history_clean.go b/gpbackman/cmd/history_clean.go
index b8012339..3ad318b4 100644
--- a/gpbackman/cmd/history_clean.go
+++ b/gpbackman/cmd/history_clean.go
@@ -35,6 +35,7 @@ var (
historyCleanBeforeTimestamp string
historyCleanOlderThanDays uint
historyCleanNoHistorySyncStandby bool
+ historyCleanDatabase string
)
var historyCleanCmd = &cobra.Command{
@@ -49,6 +50,11 @@ To delete information about backups older than the given
timestamp, use the --be
To delete information about backups older than the given number of days, use
the --older-than-day option.
Only --older-than-days or --before-timestamp option must be specified, not
both.
+Use --database to clean history only for the specified database. Without
--database,
+cleanup includes deleted backup history for all databases in the history
database.
+Database names are matched exactly and case-sensitively against backup history.
+For database names that require quoting, include the double quotes in the
--database value.
+
The gpbackup_history.db file location can be set using the --history-db option.
Can be specified only once. The full path to the file is required.
If the --history-db option is not specified, the history database is looked
for in the current directory. To resolve it from $COORDINATOR_DATA_DIRECTORY
instead, pass the --auto-load-history-db flag.`,
@@ -62,6 +68,12 @@ If the --history-db option is not specified, the history
database is looked for
func init() {
rootCmd.AddCommand(historyCleanCmd)
+ historyCleanCmd.Flags().StringVar(
+ &historyCleanDatabase,
+ databaseFlagName,
+ "",
+ "delete backup history only for the specified database",
+ )
historyCleanCmd.PersistentFlags().UintVar(
&historyCleanOlderThanDays,
olderThanDaysFlagName,
@@ -92,6 +104,10 @@ func init() {
// These flag checks are applied only for history-clean command.
func doCleanHistoryFlagValidation(flags *pflag.FlagSet) {
var err error
+ if flags.Changed(databaseFlagName) && historyCleanDatabase == "" {
+ gplog.Error("%s",
textmsg.ErrorTextUnableValidateFlag(historyCleanDatabase, databaseFlagName,
textmsg.ErrorEmptyDatabase()))
+ execOSExit(exitErrorCode)
+ }
// If before-timestamp are specified and have correct values.
if flags.Changed(beforeTimestampFlagName) {
err = gpbckpconfig.CheckTimestamp(historyCleanBeforeTimestamp)
@@ -127,15 +143,15 @@ func cleanHistory() error {
gplog.Error("%s",
textmsg.ErrorTextUnableActionHistoryDB("close", closeErr))
}
}()
- err = historyCleanDB(beforeTimestamp, hDB)
+ err = historyCleanDB(beforeTimestamp, historyCleanDatabase, hDB)
if err != nil {
return err
}
return nil
}
-func historyCleanDB(cutOffTimestamp string, hDB *sql.DB) error {
- backupList, err :=
gpbckpconfig.GetBackupNamesForCleanBeforeTimestamp(cutOffTimestamp, hDB)
+func historyCleanDB(cutOffTimestamp, databaseName string, hDB *sql.DB) error {
+ backupList, err :=
gpbckpconfig.GetBackupNamesForCleanBeforeTimestamp(cutOffTimestamp,
databaseName, hDB)
if err != nil {
gplog.Error("%s", textmsg.ErrorTextUnableReadHistoryDB(err))
return err
diff --git a/gpbackman/cmd/history_clean_test.go
b/gpbackman/cmd/history_clean_test.go
new file mode 100644
index 00000000..9edda0fb
--- /dev/null
+++ b/gpbackman/cmd/history_clean_test.go
@@ -0,0 +1,182 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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 cmd
+
+import (
+ "database/sql"
+ "path/filepath"
+
+ "github.com/apache/cloudberry-backup/history"
+ "github.com/spf13/pflag"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("history-clean database filter", func() {
+ It("registers a command-local database flag and documents its
behavior", func() {
+ flag := historyCleanCmd.Flags().Lookup(databaseFlagName)
+ Expect(flag).NotTo(BeNil())
+ Expect(flag.DefValue).To(Equal(""))
+ Expect(flag.Usage).To(ContainSubstring("specified database"))
+ Expect(historyCleanCmd.Long).To(ContainSubstring("Without
--database"))
+
Expect(historyCleanCmd.Long).To(ContainSubstring("case-sensitively"))
+ })
+
+ It("requires a value when the database flag is supplied", func() {
+ rootCmd.SetArgs([]string{"history-clean", "--" +
databaseFlagName})
+ DeferCleanup(func() { rootCmd.SetArgs(nil) })
+
+ err := rootCmd.Execute()
+ Expect(err).To(MatchError(ContainSubstring("flag needs an
argument")))
+ })
+
+ DescribeTable("validates explicit empty database values",
+ func(database string, setDatabase, olderThan, wantExit bool) {
+ oldDatabase := historyCleanDatabase
+ oldCleanBeforeTimestamp := historyCleanBeforeTimestamp
+ oldCleanOlderThanDays := historyCleanOlderThanDays
+ oldBeforeTimestamp := beforeTimestamp
+ oldExecOSExit := execOSExit
+ DeferCleanup(func() {
+ historyCleanDatabase = oldDatabase
+ historyCleanBeforeTimestamp =
oldCleanBeforeTimestamp
+ historyCleanOlderThanDays =
oldCleanOlderThanDays
+ beforeTimestamp = oldBeforeTimestamp
+ execOSExit = oldExecOSExit
+ })
+
+ historyCleanDatabase = database
+ historyCleanBeforeTimestamp = "20240101120000"
+ historyCleanOlderThanDays = 1
+ beforeTimestamp = ""
+ flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
+ flags.String(beforeTimestampFlagName, "", "")
+ flags.Uint(olderThanDaysFlagName, 0, "")
+ flags.String(databaseFlagName, "", "")
+ if olderThan {
+ Expect(flags.Set(olderThanDaysFlagName,
"1")).To(Succeed())
+ } else {
+ Expect(flags.Set(beforeTimestampFlagName,
historyCleanBeforeTimestamp)).To(Succeed())
+ }
+ if setDatabase {
+ Expect(flags.Set(databaseFlagName,
database)).To(Succeed())
+ }
+
+ exited := false
+ execOSExit = func(code int) {
+ Expect(code).To(Equal(exitErrorCode))
+ exited = true
+ }
+ doCleanHistoryFlagValidation(flags)
+ Expect(exited).To(Equal(wantExit))
+ if olderThan {
+ Expect(beforeTimestamp).NotTo(BeEmpty())
+ }
+ },
+ Entry("when absent", "", false, false, false),
+ Entry("when non-empty", `"Customer's DB"`, true, false, false),
+ Entry("when explicitly empty", "", true, false, true),
+ Entry("with older-than-days", "demo", true, true, false),
+ )
+
+ It("cleans only the selected database and its related history rows",
func() {
+ historyDB := createHistoryCleanTestDB()
+ selectedDeleted := historyCleanTestConfig("20240101000000",
`"Customer's DB"`, "20240102000000")
+ otherDeleted := historyCleanTestConfig("20240101010000",
`"customer's db"`, "20240102000000")
+ selectedNew := historyCleanTestConfig("20240103000000",
`"Customer's DB"`, "20240104000000")
+ selectedActive := historyCleanTestConfig("20240101020000",
`"Customer's DB"`, "")
+ selectedFailed := historyCleanTestConfig("20240101030000",
`"Customer's DB"`, "")
+ selectedFailed.Status = history.BackupStatusFailed
+ storeHistoryCleanConfigs(historyDB, selectedDeleted,
otherDeleted, selectedNew, selectedActive, selectedFailed)
+
+ Expect(historyCleanDB("20240102000000", `"Customer's DB"`,
historyDB)).To(Succeed())
+
+ assertHistoryCleanTimestampExists(historyDB,
selectedDeleted.Timestamp, false)
+ for _, backup := range []history.BackupConfig{otherDeleted,
selectedNew, selectedActive, selectedFailed} {
+ assertHistoryCleanTimestampExists(historyDB,
backup.Timestamp, true)
+ }
+ })
+
+ It("leaves unknown databases untouched and cleans all databases without
a filter", func() {
+ historyDB := createHistoryCleanTestDB()
+ first := historyCleanTestConfig("20240101000000", "demo",
"20240102000000")
+ second := historyCleanTestConfig("20240101010000", "other",
"20240102000000")
+ storeHistoryCleanConfigs(historyDB, first, second)
+
+ Expect(historyCleanDB("20240102000000", "unknown",
historyDB)).To(Succeed())
+ assertHistoryCleanTimestampExists(historyDB, first.Timestamp,
true)
+ assertHistoryCleanTimestampExists(historyDB, second.Timestamp,
true)
+
+ Expect(historyCleanDB("20240102000000", "",
historyDB)).To(Succeed())
+ assertHistoryCleanTimestampExists(historyDB, first.Timestamp,
false)
+ assertHistoryCleanTimestampExists(historyDB, second.Timestamp,
false)
+ })
+})
+
+var historyCleanAuxiliaryTables = []string{
+ "backups",
+ "restore_plans",
+ "restore_plan_tables",
+ "exclude_relations",
+ "exclude_schemas",
+ "include_relations",
+ "include_schemas",
+}
+
+func createHistoryCleanTestDB() *sql.DB {
+ historyDB, err :=
history.InitializeHistoryDatabase(filepath.Join(GinkgoT().TempDir(),
"gpbackup_history.db"))
+ Expect(err).NotTo(HaveOccurred())
+ _, err = historyDB.Exec("PRAGMA foreign_keys = OFF;")
+ Expect(err).NotTo(HaveOccurred())
+ DeferCleanup(func() { Expect(historyDB.Close()).To(Succeed()) })
+ return historyDB
+}
+
+func historyCleanTestConfig(timestamp, databaseName, dateDeleted string)
history.BackupConfig {
+ return history.BackupConfig{
+ Timestamp: timestamp,
+ DatabaseName: databaseName,
+ DateDeleted: dateDeleted,
+ Status: history.BackupStatusSucceed,
+ ExcludeRelations: []string{"public.excluded_relation"},
+ ExcludeSchemas: []string{"excluded_schema"},
+ IncludeRelations: []string{"public.included_relation"},
+ IncludeSchemas: []string{"included_schema"},
+ RestorePlan: []history.RestorePlanEntry{{
+ Timestamp: "20230101000000",
+ TableFQNs: []string{"public.restored_table"},
+ }},
+ }
+}
+
+func storeHistoryCleanConfigs(historyDB *sql.DB, configs
...history.BackupConfig) {
+ for i := range configs {
+ Expect(history.StoreBackupHistory(historyDB,
&configs[i])).To(Succeed())
+ }
+}
+
+func assertHistoryCleanTimestampExists(historyDB *sql.DB, timestamp string,
want bool) {
+ for _, table := range historyCleanAuxiliaryTables {
+ var count int
+ Expect(historyDB.QueryRow("SELECT COUNT(*) FROM "+table+" WHERE
timestamp = ?", timestamp).Scan(&count)).To(Succeed())
+ Expect(count > 0).To(Equal(want), table)
+ }
+}
diff --git a/gpbackman/cmd/history_sync_test.go
b/gpbackman/cmd/history_sync_test.go
index acebdda3..9617ea6f 100644
--- a/gpbackman/cmd/history_sync_test.go
+++ b/gpbackman/cmd/history_sync_test.go
@@ -299,6 +299,26 @@ var _ = Describe("history sync command", func() {
Expect(disabledValues).To(Equal([]bool{true, false,
true}))
})
+
+ It("keeps standby synchronization enabled for filtered cleanup
commands", func() {
+ disabledValues := make([]bool, 0)
+ originalBackupCleanDatabase := backupCleanDatabase
+ originalHistoryCleanDatabase := historyCleanDatabase
+ runHistoryMutationWithStandbySync = func(work func()
error, disabled bool) {
+ disabledValues = append(disabledValues,
disabled)
+ }
+ backupCleanDatabase = `"Customer's DB"`
+ historyCleanDatabase = `"Customer's DB"`
+ DeferCleanup(func() {
+ backupCleanDatabase =
originalBackupCleanDatabase
+ historyCleanDatabase =
originalHistoryCleanDatabase
+ })
+
+ doCleanBackup()
+ doCleanHistory()
+
+ Expect(disabledValues).To(Equal([]bool{false, false}))
+ })
})
})
diff --git a/gpbackman/cmd/wrappers_test.go b/gpbackman/cmd/wrappers_test.go
index 12342ae5..f892cd08 100644
--- a/gpbackman/cmd/wrappers_test.go
+++ b/gpbackman/cmd/wrappers_test.go
@@ -33,6 +33,16 @@ import (
)
var _ = Describe("wrappers tests", func() {
+ Describe("database flag scope", func() {
+ It("registers the database filter only on supported commands",
func() {
+
Expect(rootCmd.PersistentFlags().Lookup(databaseFlagName)).To(BeNil())
+ for _, command := range rootCmd.Commands() {
+ want := command == backupCleanCmd || command ==
backupInfoCmd || command == historyCleanCmd
+ Expect(command.Flags().Lookup(databaseFlagName)
!= nil).To(Equal(want), command.Name())
+ }
+ })
+ })
+
Describe("getHistoryDBPath", func() {
// Save and restore env vars so these cases don't leak into the
rest
// of the suite when run with --randomize-all.
diff --git a/gpbackman/gpbckpconfig/utils_db.go
b/gpbackman/gpbckpconfig/utils_db.go
index 48cc28d7..cd022feb 100644
--- a/gpbackman/gpbckpconfig/utils_db.go
+++ b/gpbackman/gpbckpconfig/utils_db.go
@@ -73,16 +73,16 @@ func GetBackupDependencies(backupName string, historyDB
*sql.DB) ([]string, erro
return execQueryFunc(getBackupDependenciesQuery(backupName), historyDB)
}
-func GetBackupNamesBeforeTimestamp(timestamp string, historyDB *sql.DB)
([]string, error) {
- return execQueryFunc(getBackupNameBeforeTimestampQuery(timestamp),
historyDB)
+func GetBackupNamesBeforeTimestamp(timestamp, databaseName string, historyDB
*sql.DB) ([]string, error) {
+ return
execBackupNamesQuery(getBackupNameBeforeTimestampQuery(timestamp,
databaseName), databaseName, historyDB)
}
-func GetBackupNamesAfterTimestamp(timestamp string, historyDB *sql.DB)
([]string, error) {
- return execQueryFunc(getBackupNameAfterTimestampQuery(timestamp),
historyDB)
+func GetBackupNamesAfterTimestamp(timestamp, databaseName string, historyDB
*sql.DB) ([]string, error) {
+ return execBackupNamesQuery(getBackupNameAfterTimestampQuery(timestamp,
databaseName), databaseName, historyDB)
}
-func GetBackupNamesForCleanBeforeTimestamp(timestamp string, historyDB
*sql.DB) ([]string, error) {
- return
execQueryFunc(getBackupNameForCleanBeforeTimestampQuery(timestamp), historyDB)
+func GetBackupNamesForCleanBeforeTimestamp(timestamp, databaseName string,
historyDB *sql.DB) ([]string, error) {
+ return
execBackupNamesQuery(getBackupNameForCleanBeforeTimestampQuery(timestamp,
databaseName), databaseName, historyDB)
}
func getBackupNameQuery(showD, showF bool) string {
@@ -111,36 +111,50 @@ ORDER BY timestamp DESC;
`, backupName, backupName)
}
-func getBackupNameBeforeTimestampQuery(timestamp string) string {
- return fmt.Sprintf(`
+func getBackupNameBeforeTimestampQuery(timestamp, databaseName string) string {
+ query := fmt.Sprintf(`
SELECT timestamp
FROM backups
WHERE timestamp < '%s'
AND status != '%s'
AND date_deleted IN ('', '%s', '%s')
-ORDER BY timestamp DESC;
`, timestamp, history.BackupStatusInProgress, DateDeletedPluginFailed,
DateDeletedLocalFailed)
+ return addDatabaseNamePredicate(query, databaseName) + "ORDER BY
timestamp DESC;\n"
}
-func getBackupNameAfterTimestampQuery(timestamp string) string {
- return fmt.Sprintf(`
+func getBackupNameAfterTimestampQuery(timestamp, databaseName string) string {
+ query := fmt.Sprintf(`
SELECT timestamp
FROM backups
WHERE timestamp > '%s'
AND status != '%s'
AND date_deleted IN ('', '%s', '%s')
-ORDER BY timestamp DESC;
`, timestamp, history.BackupStatusInProgress, DateDeletedPluginFailed,
DateDeletedLocalFailed)
+ return addDatabaseNamePredicate(query, databaseName) + "ORDER BY
timestamp DESC;\n"
}
-func getBackupNameForCleanBeforeTimestampQuery(timestamp string) string {
- return fmt.Sprintf(`
+func getBackupNameForCleanBeforeTimestampQuery(timestamp, databaseName string)
string {
+ query := fmt.Sprintf(`
SELECT timestamp
FROM backups
WHERE timestamp < '%s'
AND date_deleted NOT IN ('', '%s', '%s', '%s')
-ORDER BY timestamp DESC;
`, timestamp, DateDeletedPluginFailed, DateDeletedLocalFailed,
DateDeletedInProgress)
+ return addDatabaseNamePredicate(query, databaseName) + "ORDER BY
timestamp DESC;\n"
+}
+
+func addDatabaseNamePredicate(query, databaseName string) string {
+ if databaseName == "" {
+ return query
+ }
+ return query + "\tAND database_name = ?\n"
+}
+
+func execBackupNamesQuery(query, databaseName string, historyDB *sql.DB)
([]string, error) {
+ if databaseName == "" {
+ return execQueryFunc(query, historyDB)
+ }
+ return execQueryFunc(query, historyDB, databaseName)
}
// UpdateDeleteStatus updates the date_deleted column in the history database.
@@ -197,8 +211,8 @@ func updateDeleteStatusQuery(timestamp, status string)
string {
return fmt.Sprintf(`UPDATE backups SET date_deleted = '%s' WHERE
timestamp = '%s';`, status, timestamp)
}
-func execQueryFunc(query string, historyDB *sql.DB) ([]string, error) {
- sqlRow, err := historyDB.Query(query)
+func execQueryFunc(query string, historyDB *sql.DB, args ...any) ([]string,
error) {
+ sqlRow, err := historyDB.Query(query, args...)
if err != nil {
return nil, err
}
diff --git a/gpbackman/gpbckpconfig/utils_db_test.go
b/gpbackman/gpbckpconfig/utils_db_test.go
index a89e93e9..8582a842 100644
--- a/gpbackman/gpbckpconfig/utils_db_test.go
+++ b/gpbackman/gpbckpconfig/utils_db_test.go
@@ -128,7 +128,7 @@ WHERE timestamp < '20240101120000'
AND date_deleted IN ('', 'Plugin Backup Delete Failed', 'Local Delete
Failed')
ORDER BY timestamp DESC;
`, history.BackupStatusInProgress)
-
Expect(getBackupNameBeforeTimestampQuery("20240101120000")).To(Equal(want))
+
Expect(getBackupNameBeforeTimestampQuery("20240101120000", "")).To(Equal(want))
})
})
@@ -142,7 +142,7 @@ WHERE timestamp > '20240101120000'
AND date_deleted IN ('', 'Plugin Backup Delete Failed', 'Local Delete
Failed')
ORDER BY timestamp DESC;
`, history.BackupStatusInProgress)
-
Expect(getBackupNameAfterTimestampQuery("20240101120000")).To(Equal(want))
+
Expect(getBackupNameAfterTimestampQuery("20240101120000", "")).To(Equal(want))
})
})
@@ -155,10 +155,82 @@ WHERE timestamp < '20240101120000'
AND date_deleted NOT IN ('', 'Plugin Backup Delete Failed', 'Local
Delete Failed', 'In progress')
ORDER BY timestamp DESC;
`
-
Expect(getBackupNameForCleanBeforeTimestampQuery("20240101120000")).To(Equal(want))
+
Expect(getBackupNameForCleanBeforeTimestampQuery("20240101120000",
"")).To(Equal(want))
})
})
+ Describe("database-filtered backup name queries", func() {
+ var historyDB *sql.DB
+
+ BeforeEach(func() {
+ var err error
+ historyDB, err = sql.Open("sqlite3",
"file:"+filepath.Join(GinkgoT().TempDir(), "history.db")+"?mode=rwc")
+ Expect(err).NotTo(HaveOccurred())
+ _, err = historyDB.Exec(`CREATE TABLE backups
(timestamp TEXT, database_name TEXT, status TEXT, date_deleted TEXT)`)
+ Expect(err).NotTo(HaveOccurred())
+ for _, backup := range [][]string{
+ {"20240101110000", "customer", "Success", ""},
+ {"20240101100000", "Customer", "Success", ""},
+ {"20240101090000", `"quoted db"`, "Success",
""},
+ {"20240101080000", "customer's db", "Success",
""},
+ {"20240102110000", "customer", "Success", ""},
+ {"20240102100000", "Customer", "Success", ""},
+ {"20240102090000", `"quoted db"`, "Success",
""},
+ {"20240102080000", "customer's db", "Success",
""},
+ {"20240101110000-clean", "customer", "Success",
"20240103000000"},
+ {"20240101100000-clean", "Customer", "Success",
"20240103000000"},
+ {"20240101090000-clean", `"quoted db"`,
"Success", "20240103000000"},
+ {"20240101080000-clean", "customer's db",
"Success", "20240103000000"},
+ } {
+ _, err = historyDB.Exec(`INSERT INTO backups
(timestamp, database_name, status, date_deleted) VALUES (?, ?, ?, ?)`,
backup[0], backup[1], backup[2], backup[3])
+ Expect(err).NotTo(HaveOccurred())
+ }
+ })
+
+ AfterEach(func() {
+ Expect(historyDB.Close()).To(Succeed())
+ })
+
+ It("adds a bound database predicate only when a database is
supplied", func() {
+ unfiltered :=
getBackupNameBeforeTimestampQuery("20240101120000", "")
+ filtered :=
getBackupNameBeforeTimestampQuery("20240101120000", "customer's db")
+
Expect(unfiltered).NotTo(ContainSubstring("database_name"))
+ Expect(filtered).To(ContainSubstring("AND database_name
= ?"))
+ Expect(filtered).NotTo(ContainSubstring("customer's
db"))
+ })
+
+ It("returns a scan error from a backup-name query", func() {
+ _, err := execQueryFunc("SELECT NULL", historyDB)
+ Expect(err).To(HaveOccurred())
+ })
+
+ DescribeTable("returns only exact database matches while
retaining unfiltered selection",
+ func(query func(string, string, *sql.DB) ([]string,
error), timestamp string, expectedAll []string, exact, quoted, apostrophe
string) {
+ for database, expected := range
map[string][]string{
+ "": expectedAll,
+ "customer": {exact},
+ "CUSTOMER": nil,
+ `"quoted db"`: {quoted},
+ "customer's db": {apostrophe},
+ "does-not-exist": nil,
+ } {
+ actual, err := query(timestamp,
database, historyDB)
+ Expect(err).NotTo(HaveOccurred(),
database)
+ Expect(actual).To(Equal(expected),
database)
+ }
+ },
+ Entry("before timestamp",
GetBackupNamesBeforeTimestamp, "20240101120000",
+ []string{"20240101110000", "20240101100000",
"20240101090000", "20240101080000"},
+ "20240101110000", "20240101090000",
"20240101080000"),
+ Entry("after timestamp", GetBackupNamesAfterTimestamp,
"20240101120000",
+ []string{"20240102110000", "20240102100000",
"20240102090000", "20240102080000"},
+ "20240102110000", "20240102090000",
"20240102080000"),
+ Entry("history clean",
GetBackupNamesForCleanBeforeTimestamp, "20240101120000",
+ []string{"20240101110000-clean",
"20240101100000-clean", "20240101090000-clean", "20240101080000-clean"},
+ "20240101110000-clean", "20240101090000-clean",
"20240101080000-clean"),
+ )
+ })
+
Describe("deleteBackupsFormTableQuery", func() {
It("returns correct query", func() {
got := deleteBackupsFormTableQuery("TestBackup",
"'20220401102430', '20220401102430'")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]