diff --git a/cmd/apply/apply.go b/cmd/apply/apply.go index 657cb3af..d8916a9f 100644 --- a/cmd/apply/apply.go +++ b/cmd/apply/apply.go @@ -265,6 +265,15 @@ func RunApply(cmd *cobra.Command, args []string) error { } } + // Auto-detect schema from dump file if --schema was not explicitly set + effectiveSchema := applySchema + if !cmd.Flags().Changed("schema") && applyFile != "" { + if detected, err := util.DetectSchemaFromFile(applyFile); err == nil && detected != "" { + effectiveSchema = detected + fmt.Fprintf(os.Stderr, "Auto-detected schema '%s' from dump file\n", detected) + } + } + // Build configuration config := &ApplyConfig{ Host: applyHost, @@ -272,7 +281,7 @@ func RunApply(cmd *cobra.Command, args []string) error { DB: applyDB, User: applyUser, Password: finalPassword, - Schema: applySchema, + Schema: effectiveSchema, AutoApprove: applyAutoApprove, NoColor: applyNoColor, LockTimeout: applyLockTimeout, diff --git a/cmd/dump/dump_integration_test.go b/cmd/dump/dump_integration_test.go index 8c2011cf..073313a1 100644 --- a/cmd/dump/dump_integration_test.go +++ b/cmd/dump/dump_integration_test.go @@ -356,16 +356,17 @@ func runTenantSchemaTest(t *testing.T, testDataDir string) { } } -// normalizeSchemaOutput removes version-specific lines for comparison. -// This allows comparing dumps across different PostgreSQL versions. +// normalizeSchemaOutput removes version-specific and metadata lines for comparison. +// This allows comparing dumps across different PostgreSQL versions and schema contexts. func normalizeSchemaOutput(output string) string { lines := strings.Split(output, "\n") var normalizedLines []string for _, line := range lines { - // Skip version-related lines + // Skip version-related and metadata lines if strings.Contains(line, "-- Dumped by pgschema version") || - strings.Contains(line, "-- Dumped from database version") { + strings.Contains(line, "-- Dumped from database version") || + strings.Contains(line, "-- Dumped from schema:") { continue } normalizedLines = append(normalizedLines, line) diff --git a/cmd/plan/plan.go b/cmd/plan/plan.go index 2e172755..e2dcf17a 100644 --- a/cmd/plan/plan.go +++ b/cmd/plan/plan.go @@ -101,6 +101,15 @@ func runPlan(cmd *cobra.Command, args []string) error { } } + // Auto-detect schema from dump file if --schema was not explicitly set + effectiveSchema := planSchema + if !cmd.Flags().Changed("schema") && planFile != "" { + if detected, err := util.DetectSchemaFromFile(planFile); err == nil && detected != "" { + effectiveSchema = detected + fmt.Fprintf(os.Stderr, "Auto-detected schema '%s' from dump file\n", detected) + } + } + // Create plan configuration config := &PlanConfig{ Host: planHost, @@ -108,7 +117,7 @@ func runPlan(cmd *cobra.Command, args []string) error { DB: planDB, User: planUser, Password: finalPassword, - Schema: planSchema, + Schema: effectiveSchema, File: planFile, ApplicationName: "pgschema", // Plan database configuration diff --git a/cmd/util/schema_detect.go b/cmd/util/schema_detect.go new file mode 100644 index 00000000..dca73d20 --- /dev/null +++ b/cmd/util/schema_detect.go @@ -0,0 +1,36 @@ +package util + +import ( + "bufio" + "io" + "os" + "strings" +) + +const schemaHeaderPrefix = "-- Dumped from schema: " + +// DetectSchemaFromFile reads the header of a SQL dump file and extracts the +// schema name from the "-- Dumped from schema: " metadata line. +// Returns empty string if the header is not found. +func DetectSchemaFromFile(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + return detectSchemaFromReader(f) +} + +// detectSchemaFromReader reads from an io.Reader and extracts the schema name +// from the pgschema dump header. Only scans the first 20 lines (header area). +func detectSchemaFromReader(r io.Reader) (string, error) { + scanner := bufio.NewScanner(r) + for i := 0; i < 20 && scanner.Scan(); i++ { + line := scanner.Text() + if strings.HasPrefix(line, schemaHeaderPrefix) { + return strings.TrimSpace(line[len(schemaHeaderPrefix):]), nil + } + } + return "", scanner.Err() +} diff --git a/cmd/util/schema_detect_test.go b/cmd/util/schema_detect_test.go new file mode 100644 index 00000000..37d8c11c --- /dev/null +++ b/cmd/util/schema_detect_test.go @@ -0,0 +1,87 @@ +package util + +import ( + "strings" + "testing" +) + +func TestDetectSchemaFromReader(t *testing.T) { + tests := []struct { + name string + content string + expected string + }{ + { + name: "public schema", + content: `-- +-- pgschema database dump +-- + +-- Dumped from database version PostgreSQL 17.7 +-- Dumped by pgschema version 1.7.1 +-- Dumped from schema: public + +`, + expected: "public", + }, + { + name: "non-public schema", + content: `-- +-- pgschema database dump +-- + +-- Dumped from database version PostgreSQL 17.7 +-- Dumped by pgschema version 1.7.1 +-- Dumped from schema: vehicle + +`, + expected: "vehicle", + }, + { + name: "quoted schema name", + content: `-- +-- pgschema database dump +-- + +-- Dumped from database version PostgreSQL 17.7 +-- Dumped by pgschema version 1.7.1 +-- Dumped from schema: my_schema + +`, + expected: "my_schema", + }, + { + name: "no schema header (old dump format)", + content: `-- +-- pgschema database dump +-- + +-- Dumped from database version PostgreSQL 17.7 +-- Dumped by pgschema version 1.7.1 + + +CREATE TABLE IF NOT EXISTS users ( + id integer NOT NULL +); +`, + expected: "", + }, + { + name: "empty file", + content: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := detectSchemaFromReader(strings.NewReader(tt.content)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != tt.expected { + t.Errorf("got %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/internal/diff/header.go b/internal/diff/header.go index ea655dee..f8689a22 100644 --- a/internal/diff/header.go +++ b/internal/diff/header.go @@ -1,25 +1 @@ package diff - -import ( - "fmt" - "strings" - - "github.com/pgplex/pgschema/internal/version" - "github.com/pgplex/pgschema/ir" -) - -// GenerateDumpHeader generates the header for database dumps with metadata -func GenerateDumpHeader(schemaIR *ir.IR) string { - var header strings.Builder - - header.WriteString("--\n") - header.WriteString("-- pgschema database dump\n") - header.WriteString("--\n") - header.WriteString("\n") - - header.WriteString(fmt.Sprintf("-- Dumped from database version %s\n", schemaIR.Metadata.DatabaseVersion)) - header.WriteString(fmt.Sprintf("-- Dumped by pgschema version %s\n", version.App())) - header.WriteString("\n") - header.WriteString("\n") - return header.String() -} diff --git a/internal/dump/formatter.go b/internal/dump/formatter.go index 6d1524cf..5d67c311 100644 --- a/internal/dump/formatter.go +++ b/internal/dump/formatter.go @@ -179,6 +179,7 @@ func (f *DumpFormatter) generateDumpHeader() string { header.WriteString(fmt.Sprintf("-- Dumped from database version %s\n", f.dbVersion)) header.WriteString(fmt.Sprintf("-- Dumped by pgschema version %s\n", version.App())) + header.WriteString(fmt.Sprintf("-- Dumped from schema: %s\n", f.targetSchema)) header.WriteString("\n") header.WriteString("\n") return header.String()