Skip to content

Commit 6aac8d0

Browse files
Sarumyanclaude
andcommitted
feat: add built-in databaseRename option for snapshot jobs
Add native databaseRename configuration to physicalSnapshot and logicalSnapshot jobs, eliminating the need for custom preprocessing scripts for this common operation. The feature spins up a temporary container and runs ALTER DATABASE RENAME for each configured mapping. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0ea3188 commit 6aac8d0

7 files changed

Lines changed: 247 additions & 1 deletion

File tree

engine/configs/config.example.logical_generic.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ retrieval: # Data retrieval: initial sync and ongoing updates. Two methods:
135135
options:
136136
<<: *db_configs # Adjust PostgreSQL configuration
137137
preprocessingScript: "" # Pre-processing script for data scrubbing/masking; e.g., "/tmp/scripts/custom.sh"
138-
138+
databaseRename: # Rename databases before finalizing snapshot; runs after preprocessingScript; default: empty (disabled)
139+
# mydb_prod: mydb_dblab # Rename "mydb_prod" to "mydb_dblab"
140+
139141
dataPatching: # Pre-processing SQL queries for data patching
140142
<<: *db_container
141143
queryPreprocessing:

engine/configs/config.example.physical_walg.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ retrieval: # Data retrieval: initial sync and ongoing updates. Two methods:
104104
# recovery_target_timeline: 'latest'
105105

106106
preprocessingScript: "" # Shell script path to execute before finalizing snapshot; example: "/tmp/scripts/custom.sh"; default: "" (disabled)
107+
databaseRename: # Rename databases before finalizing snapshot; runs after preprocessingScript; default: empty (disabled)
108+
# example_production: example_dblab # Rename "example_production" to "example_dblab"
109+
# analytics_prod: analytics_dblab
107110
scheduler: # Snapshot scheduling and retention policy configuration
108111
snapshot: # Snapshot creation scheduling
109112
timetable: "0 */6 * * *" # Cron expression defining snapshot schedule: https://en.wikipedia.org/wiki/Cron#Overview

engine/internal/retrieval/engine/postgres/snapshot/logical.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type LogicalInitial struct {
6363
type LogicalOptions struct {
6464
DataPatching DataPatching `yaml:"dataPatching"`
6565
PreprocessingScript string `yaml:"preprocessingScript"`
66+
DatabaseRename map[string]string `yaml:"databaseRename"`
6667
Configs map[string]string `yaml:"configs"`
6768
Schedule Scheduler `yaml:"schedule"`
6869
}
@@ -127,6 +128,12 @@ func (s *LogicalInitial) Run(ctx context.Context) error {
127128
}
128129
}
129130

131+
if len(s.options.DatabaseRename) > 0 {
132+
if err := runDatabaseRename(ctx, s.dockerClient, s.engineProps, s.globalCfg, s.fsPool.DataDir(), s.options.DatabaseRename); err != nil {
133+
return errors.Wrap(err, "failed to rename databases")
134+
}
135+
}
136+
130137
if err := s.touchConfigFiles(); err != nil {
131138
return errors.Wrap(err, "failed to create PostgreSQL configuration files")
132139
}

engine/internal/retrieval/engine/postgres/snapshot/physical.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ type PhysicalOptions struct {
114114
SkipStartSnapshot bool `yaml:"skipStartSnapshot"`
115115
Promotion Promotion `yaml:"promotion"`
116116
PreprocessingScript string `yaml:"preprocessingScript"`
117+
DatabaseRename map[string]string `yaml:"databaseRename"`
117118
Configs map[string]string `yaml:"configs"`
118119
Sysctls map[string]string `yaml:"sysctls"`
119120
Envs map[string]string `yaml:"envs"`
@@ -388,6 +389,12 @@ func (p *PhysicalInitial) run(ctx context.Context) (err error) {
388389
}
389390
}
390391

392+
if len(p.options.DatabaseRename) > 0 {
393+
if err := runDatabaseRename(ctx, p.dockerClient, p.engineProps, p.globalCfg, cloneDataDir, p.options.DatabaseRename); err != nil {
394+
return errors.Wrap(err, "failed to rename databases")
395+
}
396+
}
397+
391398
// Mark database data.
392399
if err := p.markDatabaseData(); err != nil {
393400
return errors.Wrap(err, "failed to mark the prepared data")
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
2024 © Postgres.ai
3+
*/
4+
5+
package snapshot
6+
7+
import (
8+
"context"
9+
"fmt"
10+
11+
"github.com/docker/docker/api/types/container"
12+
"github.com/docker/docker/api/types/filters"
13+
"github.com/docker/docker/client"
14+
"github.com/pkg/errors"
15+
16+
"gitlab.com/postgres-ai/database-lab/v3/internal/diagnostic"
17+
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools"
18+
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools/cont"
19+
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools/health"
20+
"gitlab.com/postgres-ai/database-lab/v3/pkg/config/global"
21+
"gitlab.com/postgres-ai/database-lab/v3/pkg/log"
22+
)
23+
24+
const renameContainerPrefix = "dblab_rename_"
25+
26+
// runDatabaseRename renames databases using ALTER DATABASE in a temporary container.
27+
func runDatabaseRename(
28+
ctx context.Context,
29+
dockerClient *client.Client,
30+
engineProps *global.EngineProps,
31+
globalCfg *global.Config,
32+
dataDir string,
33+
renames map[string]string,
34+
) error {
35+
if len(renames) == 0 {
36+
return nil
37+
}
38+
39+
connDB := globalCfg.Database.Name()
40+
41+
if err := validateDatabaseRenames(renames, connDB); err != nil {
42+
return err
43+
}
44+
45+
pgVersion, err := tools.DetectPGVersion(dataDir)
46+
if err != nil {
47+
return errors.Wrap(err, "failed to detect postgres version")
48+
}
49+
50+
image := fmt.Sprintf("postgresai/extended-postgres:%g", pgVersion)
51+
52+
if err := tools.PullImage(ctx, dockerClient, image); err != nil {
53+
return errors.Wrap(err, "failed to pull image for database rename")
54+
}
55+
56+
pwd, err := tools.GeneratePassword()
57+
if err != nil {
58+
return errors.Wrap(err, "failed to generate password")
59+
}
60+
61+
hostConfig, err := cont.BuildHostConfig(ctx, dockerClient, dataDir, nil)
62+
if err != nil {
63+
return errors.Wrap(err, "failed to build host config")
64+
}
65+
66+
containerName := renameContainerPrefix + engineProps.InstanceID
67+
68+
containerID, err := tools.CreateContainerIfMissing(ctx, dockerClient, containerName,
69+
&container.Config{
70+
Labels: map[string]string{
71+
cont.DBLabControlLabel: cont.DBLabRenameLabel,
72+
cont.DBLabInstanceIDLabel: engineProps.InstanceID,
73+
cont.DBLabEngineNameLabel: engineProps.ContainerName,
74+
},
75+
Env: []string{
76+
"PGDATA=" + dataDir,
77+
"POSTGRES_PASSWORD=" + pwd,
78+
},
79+
Image: image,
80+
Healthcheck: health.GetConfig(
81+
globalCfg.Database.User(),
82+
connDB,
83+
),
84+
},
85+
hostConfig,
86+
)
87+
if err != nil {
88+
return fmt.Errorf("failed to create rename container: %w", err)
89+
}
90+
91+
defer tools.RemoveContainer(ctx, dockerClient, containerID, cont.StopPhysicalTimeout)
92+
93+
defer func() {
94+
if err != nil {
95+
tools.PrintContainerLogs(ctx, dockerClient, containerName)
96+
tools.PrintLastPostgresLogs(ctx, dockerClient, containerName, dataDir)
97+
98+
filterArgs := filters.NewArgs(
99+
filters.KeyValuePair{Key: "label",
100+
Value: fmt.Sprintf("%s=%s", cont.DBLabControlLabel, cont.DBLabRenameLabel)})
101+
102+
if diagErr := diagnostic.CollectDiagnostics(ctx, dockerClient, filterArgs, containerName, dataDir); diagErr != nil {
103+
log.Err("failed to collect rename container diagnostics", diagErr)
104+
}
105+
}
106+
}()
107+
108+
log.Msg(fmt.Sprintf("Running rename container: %s. ID: %v", containerName, containerID))
109+
110+
if err = dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
111+
return errors.Wrap(err, "failed to start rename container")
112+
}
113+
114+
log.Msg("Waiting for rename container readiness")
115+
log.Msg(fmt.Sprintf("View logs using the command: %s %s", tools.ViewLogsCmd, containerName))
116+
117+
if err = tools.CheckContainerReadiness(ctx, dockerClient, containerID); err != nil {
118+
return errors.Wrap(err, "rename container readiness check failed")
119+
}
120+
121+
for oldName, newName := range renames {
122+
log.Msg(fmt.Sprintf("Renaming database %q to %q", oldName, newName))
123+
124+
cmd := buildRenameCommand(globalCfg.Database.User(), connDB, oldName, newName)
125+
126+
output, execErr := tools.ExecCommandWithOutput(ctx, dockerClient, containerID, container.ExecOptions{Cmd: cmd})
127+
if execErr != nil {
128+
err = errors.Wrapf(execErr, "failed to rename database %q to %q", oldName, newName)
129+
return err
130+
}
131+
132+
log.Msg("Rename result: ", output)
133+
}
134+
135+
if err = tools.RunCheckpoint(ctx, dockerClient, containerID, globalCfg.Database.User(), connDB); err != nil {
136+
return errors.Wrap(err, "failed to run checkpoint after rename")
137+
}
138+
139+
if err = tools.StopPostgres(ctx, dockerClient, containerID, dataDir, tools.DefaultStopTimeout); err != nil {
140+
return errors.Wrap(err, "failed to stop postgres after rename")
141+
}
142+
143+
return nil
144+
}
145+
146+
func buildRenameCommand(username, connDB, oldName, newName string) []string {
147+
return []string{
148+
"psql",
149+
"-U", username,
150+
"-d", connDB,
151+
"-XAtc", fmt.Sprintf(`ALTER DATABASE "%s" RENAME TO "%s"`, oldName, newName),
152+
}
153+
}
154+
155+
func validateDatabaseRenames(renames map[string]string, connDB string) error {
156+
for oldName := range renames {
157+
if oldName == connDB {
158+
return fmt.Errorf("cannot rename database %q: it is used as the connection database", oldName)
159+
}
160+
}
161+
162+
return nil
163+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package snapshot
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestValidateDatabaseRenames(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
renames map[string]string
14+
connDB string
15+
wantErr bool
16+
}{
17+
{name: "empty map", renames: map[string]string{}, connDB: "postgres", wantErr: false},
18+
{name: "valid renames", renames: map[string]string{"prod_db": "dblab_db"}, connDB: "postgres", wantErr: false},
19+
{name: "multiple valid renames", renames: map[string]string{"db1": "db1_new", "db2": "db2_new"}, connDB: "postgres", wantErr: false},
20+
{name: "rename matches connDB", renames: map[string]string{"postgres": "pg_renamed"}, connDB: "postgres", wantErr: true},
21+
{name: "one of multiple matches connDB", renames: map[string]string{"safe_db": "new_safe", "postgres": "renamed"}, connDB: "postgres", wantErr: true},
22+
}
23+
24+
for _, tt := range tests {
25+
t.Run(tt.name, func(t *testing.T) {
26+
err := validateDatabaseRenames(tt.renames, tt.connDB)
27+
if tt.wantErr {
28+
require.Error(t, err)
29+
assert.Contains(t, err.Error(), "connection database")
30+
} else {
31+
require.NoError(t, err)
32+
}
33+
})
34+
}
35+
}
36+
37+
func TestBuildRenameCommand(t *testing.T) {
38+
tests := []struct {
39+
name string
40+
username string
41+
connDB string
42+
oldName string
43+
newName string
44+
expected []string
45+
}{
46+
{
47+
name: "simple rename", username: "postgres", connDB: "postgres", oldName: "prod_db", newName: "dblab_db",
48+
expected: []string{"psql", "-U", "postgres", "-d", "postgres", "-XAtc", `ALTER DATABASE "prod_db" RENAME TO "dblab_db"`},
49+
},
50+
{
51+
name: "special characters in name", username: "admin", connDB: "management", oldName: "my-db", newName: "my_db",
52+
expected: []string{"psql", "-U", "admin", "-d", "management", "-XAtc", `ALTER DATABASE "my-db" RENAME TO "my_db"`},
53+
},
54+
}
55+
56+
for _, tt := range tests {
57+
t.Run(tt.name, func(t *testing.T) {
58+
result := buildRenameCommand(tt.username, tt.connDB, tt.oldName, tt.newName)
59+
assert.Equal(t, tt.expected, result)
60+
})
61+
}
62+
}

engine/internal/retrieval/engine/postgres/tools/cont/container.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ const (
5858
DBLabEmbeddedUILabel = "dblab_embedded_ui"
5959
// DBLabFoundationLabel defines a label value to mark foundation containers.
6060
DBLabFoundationLabel = "dblab_foundation"
61+
// DBLabRenameLabel defines a label value for database rename containers.
62+
DBLabRenameLabel = "dblab_rename"
6163

6264
// DBLabRunner defines a label to mark runner containers.
6365
DBLabRunner = "dblab_runner"

0 commit comments

Comments
 (0)