Skip to content

Commit 72e2d9c

Browse files
committed
feat: introduce --sections flag to control pre-data/data/post-data
correctly
1 parent 02b62bb commit 72e2d9c

3 files changed

Lines changed: 165 additions & 13 deletions

File tree

cmd/snapshot.go

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import (
1313
var (
1414
snapshotCwd string
1515
snapshotOutput string
16+
snapshotOutputDir string
1617
snapshotFormat string
1718
snapshotSchemaOnly bool
1819
snapshotSection string
20+
snapshotSections bool
1921
snapshotInput string
2022
snapshotClean bool
2123
snapshotBuildFixtures []string
@@ -36,13 +38,21 @@ Examples:
3638
regresql snapshot capture
3739
regresql snapshot capture --output snapshots/mydata.dump
3840
regresql snapshot capture --schema-only
39-
regresql snapshot capture --format plain --output snapshots/schema.sql`,
41+
regresql snapshot capture --format plain --output snapshots/schema.sql
42+
regresql snapshot capture --section pre-data --output snapshots/pre-data.sql
43+
regresql snapshot capture --sections --output-dir snapshots/`,
4044
Run: func(cmd *cobra.Command, args []string) {
4145
if err := checkDirectory(snapshotCwd); err != nil {
4246
fmt.Print(err.Error())
4347
os.Exit(1)
4448
}
45-
if err := runSnapshotCapture(); err != nil {
49+
var err error
50+
if snapshotSections {
51+
err = runSnapshotCaptureSections()
52+
} else {
53+
err = runSnapshotCapture()
54+
}
55+
if err != nil {
4656
fmt.Printf("Error: %s\n", err.Error())
4757
os.Exit(1)
4858
}
@@ -123,9 +133,11 @@ func init() {
123133
snapshotCmd.PersistentFlags().StringVarP(&snapshotCwd, "cwd", "C", ".", "Change to directory")
124134

125135
snapshotCaptureCmd.Flags().StringVarP(&snapshotOutput, "output", "o", "", "Output file path")
136+
snapshotCaptureCmd.Flags().StringVar(&snapshotOutputDir, "output-dir", "", "Output directory for sectioned capture")
126137
snapshotCaptureCmd.Flags().StringVarP(&snapshotFormat, "format", "f", "", "Dump format: custom, plain, or directory")
127138
snapshotCaptureCmd.Flags().BoolVar(&snapshotSchemaOnly, "schema-only", false, "Dump only schema, no data")
128139
snapshotCaptureCmd.Flags().StringVar(&snapshotSection, "section", "", "Dump specific section: pre-data, data, or post-data")
140+
snapshotCaptureCmd.Flags().BoolVar(&snapshotSections, "sections", false, "Capture all sections to separate SQL files")
129141

130142
snapshotRestoreCmd.Flags().StringVar(&snapshotInput, "from", "", "Input file path")
131143
snapshotRestoreCmd.Flags().StringVarP(&snapshotFormat, "format", "f", "", "Snapshot format: custom, plain, or directory")
@@ -137,18 +149,26 @@ func init() {
137149
snapshotBuildCmd.Flags().BoolVarP(&snapshotBuildVerbose, "verbose", "v", false, "Print detailed progress")
138150
}
139151

152+
func validateSnapshotPrereqs(pguri string) error {
153+
if pguri == "" {
154+
return fmt.Errorf("pguri not configured in regress.yaml")
155+
}
156+
if err := regresql.TestConnectionString(pguri); err != nil {
157+
return fmt.Errorf("database connection failed: %w", err)
158+
}
159+
if err := regresql.CheckPgTool("pg_dump", snapshotCwd); err != nil {
160+
return err
161+
}
162+
return nil
163+
}
164+
140165
func runSnapshotCapture() error {
141166
cfg, err := regresql.ReadConfig(snapshotCwd)
142167
if err != nil {
143168
return fmt.Errorf("failed to read config: %w (have you run 'regresql init'?)", err)
144169
}
145-
146-
if cfg.PgUri == "" {
147-
return fmt.Errorf("pguri not configured in regress.yaml")
148-
}
149-
150-
if err := regresql.TestConnectionString(cfg.PgUri); err != nil {
151-
return fmt.Errorf("database connection failed: %w", err)
170+
if err := validateSnapshotPrereqs(cfg.PgUri); err != nil {
171+
return err
152172
}
153173

154174
outputPath := snapshotOutput
@@ -165,10 +185,6 @@ func runSnapshotCapture() error {
165185
format = regresql.GetSnapshotFormat(cfg.Snapshot)
166186
}
167187

168-
if err := regresql.CheckPgTool("pg_dump", snapshotCwd); err != nil {
169-
return err
170-
}
171-
172188
opts := regresql.SnapshotOptions{
173189
OutputPath: outputPath,
174190
Format: format,
@@ -206,6 +222,52 @@ func runSnapshotCapture() error {
206222
return nil
207223
}
208224

225+
func runSnapshotCaptureSections() error {
226+
cfg, err := regresql.ReadConfig(snapshotCwd)
227+
if err != nil {
228+
return fmt.Errorf("failed to read config: %w (have you run 'regresql init'?)", err)
229+
}
230+
if err := validateSnapshotPrereqs(cfg.PgUri); err != nil {
231+
return err
232+
}
233+
234+
outputDir := snapshotOutputDir
235+
if outputDir == "" {
236+
outputDir = regresql.GetSnapshotsDir(snapshotCwd)
237+
} else if !filepath.IsAbs(outputDir) {
238+
outputDir = filepath.Join(snapshotCwd, outputDir)
239+
}
240+
241+
fmt.Printf("Capturing database sections...\n")
242+
fmt.Printf(" Database: %s\n", maskConnectionString(cfg.PgUri))
243+
fmt.Printf(" Output dir: %s\n", outputDir)
244+
fmt.Printf(" Sections: pre-data, data, post-data\n")
245+
fmt.Println()
246+
247+
result, err := regresql.CaptureSections(cfg.PgUri, regresql.SectionsOptions{
248+
OutputDir: outputDir,
249+
})
250+
if err != nil {
251+
return err
252+
}
253+
254+
fmt.Printf("Sections captured successfully.\n")
255+
fmt.Printf(" Time: %s\n", result.Created.Format("2006-01-02 15:04:05 UTC"))
256+
fmt.Println()
257+
258+
var totalSize int64
259+
for _, s := range result.Sections {
260+
fmt.Printf(" %s.sql\n", s.Section)
261+
fmt.Printf(" Size: %s\n", regresql.FormatBytes(s.SizeBytes))
262+
fmt.Printf(" Hash: %s\n", s.Hash)
263+
totalSize += s.SizeBytes
264+
}
265+
fmt.Println()
266+
fmt.Printf(" Total: %s\n", regresql.FormatBytes(totalSize))
267+
268+
return nil
269+
}
270+
209271
func maskConnectionString(pguri string) string {
210272
masked := pguri
211273
if idx := findPasswordEnd(pguri); idx > 0 {

regresql/snapshot.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ type (
3737
Section string
3838
}
3939

40+
SectionsOptions struct {
41+
OutputDir string
42+
}
43+
44+
SectionInfo struct {
45+
Section string
46+
Path string
47+
Hash string
48+
SizeBytes int64
49+
}
50+
51+
SectionsResult struct {
52+
Sections []SectionInfo
53+
Created time.Time
54+
}
55+
4056
RestoreOptions struct {
4157
InputPath string
4258
Format SnapshotFormat
@@ -171,6 +187,68 @@ func CaptureSnapshot(pguri string, opts SnapshotOptions) (*SnapshotInfo, error)
171187
return info, nil
172188
}
173189

190+
// CaptureSections captures all three database sections (pre-data, data, post-data)
191+
// to separate plain SQL files for git-friendly version control.
192+
func CaptureSections(pguri string, opts SectionsOptions) (*SectionsResult, error) {
193+
if err := os.MkdirAll(opts.OutputDir, 0o755); err != nil {
194+
return nil, fmt.Errorf("failed to create output directory: %w", err)
195+
}
196+
197+
sections := []string{"pre-data", "data", "post-data"}
198+
result := &SectionsResult{
199+
Created: time.Now().UTC(),
200+
}
201+
202+
for _, section := range sections {
203+
outputPath := filepath.Join(opts.OutputDir, section+".sql")
204+
205+
info, err := captureSection(pguri, section, outputPath)
206+
if err != nil {
207+
return nil, fmt.Errorf("failed to capture %s section: %w", section, err)
208+
}
209+
210+
result.Sections = append(result.Sections, *info)
211+
}
212+
213+
return result, nil
214+
}
215+
216+
func captureSection(pguri, section, outputPath string) (*SectionInfo, error) {
217+
args := []string{"--dbname", pguri, "--format=plain", "--section", section}
218+
219+
cmd := exec.Command("pg_dump", args...)
220+
cmd.Stderr = os.Stderr
221+
222+
outFile, err := os.Create(outputPath)
223+
if err != nil {
224+
return nil, fmt.Errorf("failed to create output file: %w", err)
225+
}
226+
defer outFile.Close()
227+
228+
cmd.Stdout = outFile
229+
230+
if err := cmd.Run(); err != nil {
231+
return nil, fmt.Errorf("pg_dump failed: %w", err)
232+
}
233+
234+
stat, err := os.Stat(outputPath)
235+
if err != nil {
236+
return nil, fmt.Errorf("failed to stat output file: %w", err)
237+
}
238+
239+
hash, err := computeSingleFileHash(outputPath)
240+
if err != nil {
241+
return nil, fmt.Errorf("failed to compute hash: %w", err)
242+
}
243+
244+
return &SectionInfo{
245+
Section: section,
246+
Path: outputPath,
247+
Hash: hash,
248+
SizeBytes: stat.Size(),
249+
}, nil
250+
}
251+
174252
func buildPgDumpArgs(pguri string, opts SnapshotOptions) []string {
175253
args := []string{"--dbname", pguri}
176254

regresql/snapshot_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,3 +455,15 @@ func TestParseToolVersionsFileNotFound(t *testing.T) {
455455
t.Errorf("parseToolVersions() = %d, want 0 for missing file", got)
456456
}
457457
}
458+
459+
func TestCaptureSectionsCreatesDirectory(t *testing.T) {
460+
tmpDir := t.TempDir()
461+
outputDir := filepath.Join(tmpDir, "new", "nested", "dir")
462+
463+
// pg_dump will fail but directory creation happens first
464+
_, _ = CaptureSections("postgres://invalid", SectionsOptions{OutputDir: outputDir})
465+
466+
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
467+
t.Error("CaptureSections should create output directory before calling pg_dump")
468+
}
469+
}

0 commit comments

Comments
 (0)