Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion cmd/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,23 @@ 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)
}
Comment on lines +271 to +274
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schema auto-detection ignores errors from DetectSchemaFromFile. If header parsing fails, apply will silently continue using the default schema, which can reintroduce the cross-schema risk this change is meant to prevent. Consider warning on detection errors or requiring --schema when detection fails.

Suggested change
if detected, err := util.DetectSchemaFromFile(applyFile); err == nil && detected != "" {
effectiveSchema = detected
fmt.Fprintf(os.Stderr, "Auto-detected schema '%s' from dump file\n", detected)
}
detected, err := util.DetectSchemaFromFile(applyFile)
if err != nil {
return fmt.Errorf("failed to auto-detect schema from dump file %q: %w (please specify --schema explicitly)", applyFile, err)
}
if detected == "" {
return fmt.Errorf("schema could not be auto-detected from dump file %q; please specify --schema explicitly", applyFile)
}
effectiveSchema = detected
fmt.Fprintf(os.Stderr, "Auto-detected schema '%s' from dump file\n", detected)

Copilot uses AI. Check for mistakes.
}

// Build configuration
config := &ApplyConfig{
Host: applyHost,
Port: applyPort,
DB: applyDB,
User: applyUser,
Password: finalPassword,
Schema: applySchema,
Schema: effectiveSchema,
AutoApprove: applyAutoApprove,
NoColor: applyNoColor,
LockTimeout: applyLockTimeout,
Expand Down
9 changes: 5 additions & 4 deletions cmd/dump/dump_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 10 additions & 1 deletion cmd/plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,23 @@ 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)
}
}
Comment on lines +104 to +111
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schema auto-detection ignores errors from DetectSchemaFromFile. If header parsing fails (e.g., due to a scanning error), this silently falls back to the default schema, which undermines the safety goal of preventing cross-schema operations. Consider surfacing a warning to stderr when err != nil, or failing fast with a message instructing the user to pass --schema explicitly.

Copilot uses AI. Check for mistakes.

// Create plan configuration
config := &PlanConfig{
Host: planHost,
Port: planPort,
DB: planDB,
User: planUser,
Password: finalPassword,
Schema: planSchema,
Schema: effectiveSchema,
File: planFile,
ApplicationName: "pgschema",
// Plan database configuration
Expand Down
36 changes: 36 additions & 0 deletions cmd/util/schema_detect.go
Original file line number Diff line number Diff line change
@@ -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: <name>" 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()
Comment on lines +27 to +35
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detectSchemaFromReader uses bufio.Scanner without increasing the buffer size. Scanner will return ErrTooLong if it encounters a line >64K, which can happen in SQL files (e.g., large function bodies or comments on a single line), causing schema detection to error. Consider increasing the scanner buffer (scanner.Buffer) or using a bufio.Reader to read lines so header parsing is robust.

Copilot uses AI. Check for mistakes.
}
87 changes: 87 additions & 0 deletions cmd/util/schema_detect_test.go
Original file line number Diff line number Diff line change
@@ -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",
},
Comment on lines +41 to +52
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case is labeled "quoted schema name" but the content uses an unquoted identifier (my_schema). Rename the case to reflect what it actually covers, or update the fixture to include a truly quoted schema name (e.g., "MySchema") and assert the intended behavior.

Copilot uses AI. Check for mistakes.
{
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)
}
})
}
}
24 changes: 0 additions & 24 deletions internal/diff/header.go
Original file line number Diff line number Diff line change
@@ -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()
}
1 change: 1 addition & 0 deletions internal/dump/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down