Skip to content
Merged
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
103 changes: 103 additions & 0 deletions cmd/dump/multifile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,109 @@ func TestCreateMultiFileOutput(t *testing.T) {
}
}

// TestMultiFileIncludeOrderDeterministic verifies that \i include lines in main.sql
// are deterministic across multiple runs (GitHub issue #299).
func TestMultiFileIncludeOrderDeterministic(t *testing.T) {
// Create multiple tables so the map iteration order matters
diffs := []diff.Diff{
{
Statements: []diff.SQLStatement{{SQL: "CREATE TABLE accounts ();", CanRunInTransaction: true}},
Type: diff.DiffTypeTable,
Operation: diff.DiffOperationCreate,
Path: "public.accounts",
Source: &ir.Table{Name: "accounts"},
},
{
Statements: []diff.SQLStatement{{SQL: "CREATE TABLE orders ();", CanRunInTransaction: true}},
Type: diff.DiffTypeTable,
Operation: diff.DiffOperationCreate,
Path: "public.orders",
Source: &ir.Table{Name: "orders"},
},
{
Statements: []diff.SQLStatement{{SQL: "CREATE TABLE products ();", CanRunInTransaction: true}},
Type: diff.DiffTypeTable,
Operation: diff.DiffOperationCreate,
Path: "public.products",
Source: &ir.Table{Name: "products"},
},
{
Statements: []diff.SQLStatement{{SQL: "CREATE TABLE users ();", CanRunInTransaction: true}},
Type: diff.DiffTypeTable,
Operation: diff.DiffOperationCreate,
Path: "public.users",
Source: &ir.Table{Name: "users"},
},
{
Statements: []diff.SQLStatement{{SQL: "CREATE FUNCTION alpha() RETURNS void AS $$ BEGIN END; $$;", CanRunInTransaction: true}},
Type: diff.DiffTypeFunction,
Operation: diff.DiffOperationCreate,
Path: "public.alpha",
Source: &ir.Function{Name: "alpha"},
},
{
Statements: []diff.SQLStatement{{SQL: "CREATE FUNCTION beta() RETURNS void AS $$ BEGIN END; $$;", CanRunInTransaction: true}},
Type: diff.DiffTypeFunction,
Operation: diff.DiffOperationCreate,
Path: "public.beta",
Source: &ir.Function{Name: "beta"},
},
}

formatter := dump.NewDumpFormatter("PostgreSQL 17.0", "public", false)

// Run multiple times and verify output is always the same
var firstOutput string
for i := 0; i < 20; i++ {
tmpDir := t.TempDir()
outputPath := filepath.Join(tmpDir, "main.sql")

err := formatter.FormatMultiFile(diffs, outputPath)
if err != nil {
t.Fatalf("FormatMultiFile failed on iteration %d: %v", i, err)
}

content, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("Failed to read main file on iteration %d: %v", i, err)
}

output := string(content)
if i == 0 {
firstOutput = output

// Verify includes are sorted within each directory group
lines := strings.Split(output, "\n")
var includes []string
for _, line := range lines {
if strings.HasPrefix(line, "\\i ") {
includes = append(includes, line)
}
}

// Expected sorted order: functions/alpha, functions/beta, tables/accounts, tables/orders, tables/products, tables/users
expectedOrder := []string{
"\\i functions/alpha.sql",
"\\i functions/beta.sql",
"\\i tables/accounts.sql",
"\\i tables/orders.sql",
"\\i tables/products.sql",
"\\i tables/users.sql",
}
if len(includes) != len(expectedOrder) {
t.Fatalf("Expected %d includes, got %d: %v", len(expectedOrder), len(includes), includes)
}
for j, expected := range expectedOrder {
if includes[j] != expected {
t.Errorf("Include[%d]: expected %q, got %q", j, expected, includes[j])
}
}
} else if output != firstOutput {
t.Fatalf("Non-deterministic output detected on iteration %d.\nFirst:\n%s\nGot:\n%s", i, firstOutput, output)
}
}
}

func TestDumpFormatterHelpers(t *testing.T) {
// Create a formatter instance for testing helper methods
formatter := dump.NewDumpFormatter("PostgreSQL 17.0", "public", false)
Expand Down
11 changes: 10 additions & 1 deletion internal/dump/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"regexp"
"sort"
"strings"

"github.com/pgplex/pgschema/internal/diff"
Expand Down Expand Up @@ -117,8 +118,16 @@ func (f *DumpFormatter) FormatMultiFile(diffs []diff.Diff, outputPath string) er
return fmt.Errorf("failed to create directory %s: %w", dirPath, err)
}

// Sort object names for deterministic output (Go maps have random iteration order)
objNames := make([]string, 0, len(objects))
for objName := range objects {
objNames = append(objNames, objName)
}
sort.Strings(objNames)

// Create files for each object
for objName, objSteps := range objects {
for _, objName := range objNames {
objSteps := objects[objName]
fileName := f.sanitizeFileName(objName) + ".sql"
filePath := filepath.Join(dirPath, fileName)
relativePath := filepath.Join(dir, fileName)
Expand Down