Skip to content

Commit d5a5172

Browse files
committed
feat: --schema flag & auto-format detection
1 parent 72e2d9c commit d5a5172

3 files changed

Lines changed: 116 additions & 13 deletions

File tree

cmd/snapshot.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var (
2121
snapshotInput string
2222
snapshotClean bool
2323
snapshotBuildFixtures []string
24+
snapshotBuildSchema string
2425
snapshotBuildVerbose bool
2526

2627
snapshotCmd = &cobra.Command{
@@ -88,6 +89,7 @@ Examples:
8889
Examples:
8990
regresql snapshot build
9091
regresql snapshot build --fixtures users,products,orders
92+
regresql snapshot build --schema schema.sql --fixtures seed_data
9193
regresql snapshot build --output snapshots/test_data.dump --verbose`,
9294
Run: func(cmd *cobra.Command, args []string) {
9395
if err := checkDirectory(snapshotCwd); err != nil {
@@ -145,6 +147,7 @@ func init() {
145147

146148
snapshotBuildCmd.Flags().StringVarP(&snapshotOutput, "output", "o", "", "Output file path")
147149
snapshotBuildCmd.Flags().StringVarP(&snapshotFormat, "format", "f", "", "Dump format: custom, plain, or directory")
150+
snapshotBuildCmd.Flags().StringVar(&snapshotBuildSchema, "schema", "", "Schema SQL file to apply before fixtures")
148151
snapshotBuildCmd.Flags().StringSliceVar(&snapshotBuildFixtures, "fixtures", nil, "Fixture names to apply")
149152
snapshotBuildCmd.Flags().BoolVarP(&snapshotBuildVerbose, "verbose", "v", false, "Print detailed progress")
150153
}
@@ -376,17 +379,32 @@ func runSnapshotBuild() error {
376379
return fmt.Errorf("pguri not configured in regress.yaml")
377380
}
378381

382+
// Resolve and validate schema path
383+
var schemaPath string
384+
if snapshotBuildSchema != "" {
385+
schemaPath = snapshotBuildSchema
386+
if !filepath.IsAbs(schemaPath) {
387+
schemaPath = filepath.Join(snapshotCwd, schemaPath)
388+
}
389+
if _, err := os.Stat(schemaPath); err != nil {
390+
return fmt.Errorf("schema file not found: %s", schemaPath)
391+
}
392+
}
393+
379394
fixtures := snapshotBuildFixtures
380395
if len(fixtures) == 0 {
381396
fixtures = regresql.GetSnapshotFixtures(cfg.Snapshot)
382397
}
383398

384-
if len(fixtures) == 0 {
399+
// Allow build with just schema (no fixtures)
400+
if len(fixtures) == 0 && schemaPath == "" {
385401
return fmt.Errorf("no fixtures specified. Use --fixtures flag or configure snapshot.fixtures in regress.yaml")
386402
}
387403

388-
if err := regresql.FixturesExist(snapshotCwd, fixtures); err != nil {
389-
return err
404+
if len(fixtures) > 0 {
405+
if err := regresql.FixturesExist(snapshotCwd, fixtures); err != nil {
406+
return err
407+
}
390408
}
391409

392410
outputPath := snapshotOutput
@@ -407,12 +425,18 @@ func runSnapshotBuild() error {
407425
fmt.Printf(" Database: %s\n", maskConnectionString(cfg.PgUri))
408426
fmt.Printf(" Output: %s\n", outputPath)
409427
fmt.Printf(" Format: %s\n", format)
410-
fmt.Printf(" Fixtures: %v\n", fixtures)
428+
if schemaPath != "" {
429+
fmt.Printf(" Schema: %s\n", schemaPath)
430+
}
431+
if len(fixtures) > 0 {
432+
fmt.Printf(" Fixtures: %v\n", fixtures)
433+
}
411434
fmt.Println()
412435

413436
result, err := regresql.BuildSnapshot(cfg.PgUri, snapshotCwd, regresql.SnapshotBuildOptions{
414437
OutputPath: outputPath,
415438
Format: format,
439+
SchemaPath: schemaPath,
416440
Fixtures: fixtures,
417441
Verbose: snapshotBuildVerbose,
418442
})
@@ -429,7 +453,12 @@ func runSnapshotBuild() error {
429453
fmt.Printf(" Size: %s\n", regresql.FormatBytes(result.Info.SizeBytes))
430454
fmt.Printf(" Hash: %s\n", result.Info.Hash)
431455
fmt.Printf(" Duration: %s\n", result.Duration.Round(time.Millisecond))
432-
fmt.Printf(" Fixtures: %d applied\n", len(result.FixturesUsed))
456+
if result.Info.SchemaHash != "" {
457+
fmt.Printf(" Schema: %s\n", result.Info.SchemaHash[:20]+"...")
458+
}
459+
if len(result.FixturesUsed) > 0 {
460+
fmt.Printf(" Fixtures: %d applied\n", len(result.FixturesUsed))
461+
}
433462

434463
return nil
435464
}
@@ -454,6 +483,13 @@ func runSnapshotInfo() error {
454483
fmt.Printf(" Created: %s\n", info.Created.Format("2006-01-02 15:04:05 UTC"))
455484
fmt.Printf(" Hash: %s\n", info.Hash)
456485

486+
if info.SchemaPath != "" {
487+
fmt.Println()
488+
fmt.Println("Schema:")
489+
fmt.Printf(" Path: %s\n", info.SchemaPath)
490+
fmt.Printf(" Hash: %s\n", info.SchemaHash)
491+
}
492+
457493
if len(info.FixturesUsed) > 0 {
458494
fmt.Println()
459495
fmt.Println("Fixtures used:")

regresql/snapshot.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ type (
2525
Created time.Time `yaml:"created"`
2626
SizeBytes int64 `yaml:"size_bytes"`
2727
Format string `yaml:"format"`
28+
SchemaPath string `yaml:"schema_path,omitempty"`
29+
SchemaHash string `yaml:"schema_hash,omitempty"`
2830
FixturesUsed []string `yaml:"fixtures_used,omitempty"`
2931
}
3032

regresql/snapshot_build.go

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"database/sql"
55
"fmt"
66
"os"
7+
"os/exec"
78
"path/filepath"
89
"strings"
910
"time"
@@ -13,6 +14,7 @@ type (
1314
SnapshotBuildOptions struct {
1415
OutputPath string
1516
Format SnapshotFormat
17+
SchemaPath string
1618
Fixtures []string
1719
Verbose bool
1820
}
@@ -31,8 +33,15 @@ func BuildSnapshot(basePgUri string, root string, opts SnapshotBuildOptions) (*s
3133
return nil, err
3234
}
3335

34-
if len(opts.Fixtures) == 0 {
35-
return nil, fmt.Errorf("no fixtures specified for snapshot build")
36+
// Check pg_restore is available for non-plain schema files
37+
if opts.SchemaPath != "" && DetectSnapshotFormat(opts.SchemaPath) != FormatPlain {
38+
if err := CheckPgTool("pg_restore", root); err != nil {
39+
return nil, err
40+
}
41+
}
42+
43+
if len(opts.Fixtures) == 0 && opts.SchemaPath == "" {
44+
return nil, fmt.Errorf("no schema or fixtures specified for snapshot build")
3645
}
3746

3847
if opts.Verbose {
@@ -62,17 +71,34 @@ func BuildSnapshot(basePgUri string, root string, opts SnapshotBuildOptions) (*s
6271
}
6372
defer db.Close()
6473

65-
if opts.Verbose {
66-
fmt.Printf("Applying %d fixture(s)...\n", len(opts.Fixtures))
74+
// Apply schema first if provided
75+
var schemaHash string
76+
if opts.SchemaPath != "" {
77+
if opts.Verbose {
78+
format := DetectSnapshotFormat(opts.SchemaPath)
79+
fmt.Printf("Applying schema: %s (format: %s)\n", opts.SchemaPath, format)
80+
}
81+
if err := applySchemaFile(tempDB.PgUri, opts.SchemaPath); err != nil {
82+
return nil, fmt.Errorf("schema %q: %w", opts.SchemaPath, err)
83+
}
84+
schemaHash, err = computeSchemaHash(opts.SchemaPath)
85+
if err != nil {
86+
return nil, fmt.Errorf("failed to compute schema hash: %w", err)
87+
}
6788
}
6889

69-
fixturesUsed, err := applyFixtures(db, root, opts.Fixtures, opts.Verbose)
70-
if err != nil {
71-
return nil, err
90+
var fixturesUsed []string
91+
if len(opts.Fixtures) > 0 {
92+
if opts.Verbose {
93+
fmt.Printf("Applying %d fixture(s)...\n", len(opts.Fixtures))
94+
}
95+
fixturesUsed, err = applyFixtures(db, root, opts.Fixtures, opts.Verbose)
96+
if err != nil {
97+
return nil, err
98+
}
7299
}
73100

74101
if opts.Verbose {
75-
fmt.Printf("Fixtures applied successfully\n")
76102
fmt.Printf("Capturing snapshot with pg_dump...\n")
77103
}
78104

@@ -84,6 +110,8 @@ func BuildSnapshot(basePgUri string, root string, opts SnapshotBuildOptions) (*s
84110
return nil, fmt.Errorf("failed to capture snapshot: %w", err)
85111
}
86112

113+
info.SchemaPath = opts.SchemaPath
114+
info.SchemaHash = schemaHash
87115
info.FixturesUsed = fixturesUsed
88116

89117
return &snapshotBuildResult{
@@ -153,6 +181,43 @@ func execSQLFile(db *sql.DB, path string) error {
153181
return nil
154182
}
155183

184+
func computeSchemaHash(schemaPath string) (string, error) {
185+
format := DetectSnapshotFormat(schemaPath)
186+
return computeFileHash(schemaPath, format)
187+
}
188+
189+
func applySchemaFile(pguri, schemaPath string) error {
190+
format := DetectSnapshotFormat(schemaPath)
191+
192+
if format == FormatPlain {
193+
db, err := OpenDB(pguri)
194+
if err != nil {
195+
return err
196+
}
197+
defer db.Close()
198+
return execSQLFile(db, schemaPath)
199+
}
200+
201+
// Custom or Directory format - use pg_restore --schema-only
202+
args := []string{
203+
"--dbname", pguri,
204+
"--schema-only",
205+
"--no-owner",
206+
"--no-acl",
207+
}
208+
if format == FormatDirectory {
209+
args = append(args, "--format=directory")
210+
}
211+
args = append(args, schemaPath)
212+
213+
cmd := exec.Command("pg_restore", args...)
214+
cmd.Stderr = os.Stderr
215+
if err := cmd.Run(); err != nil {
216+
return fmt.Errorf("pg_restore failed: %w", err)
217+
}
218+
return nil
219+
}
220+
156221
func isSQLFixture(name string) bool {
157222
return strings.HasSuffix(strings.ToLower(name), ".sql")
158223
}

0 commit comments

Comments
 (0)