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
22 changes: 22 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# PostgreSQL Connection Configuration
# These environment variables are automatically used by pgschema commands
# when the corresponding CLI flags are not provided

# Database server host (default: localhost)
PGHOST=localhost

# Database server port (default: 5432)
PGPORT=5432

# Database name
PGDATABASE=your_database_name

# Database user name
PGUSER=your_username

# Database password
PGPASSWORD=your_password_here

# Application name for database connection (default: pgschema)
# This appears in pg_stat_activity and can help identify connections
PGAPPNAME=pgschema
18 changes: 8 additions & 10 deletions cmd/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,23 @@ var (
applyApplicationName string
)


var ApplyCmd = &cobra.Command{
Use: "apply",
Short: "Apply migration plan to update a database schema",
Long: "Apply a migration plan to update a database schema. Either provide a desired state file (--file) to generate and apply a plan, or provide a pre-generated plan file (--plan) to execute directly.",
RunE: RunApply,
SilenceUsage: true,
PreRunE: util.PreRunEWithEnvVars(&applyDB, &applyUser),
}

func init() {
// Target database connection flags
ApplyCmd.Flags().StringVar(&applyHost, "host", "localhost", "Database server host")
ApplyCmd.Flags().IntVar(&applyPort, "port", 5432, "Database server port")
ApplyCmd.Flags().StringVar(&applyDB, "db", "", "Database name (required)")
ApplyCmd.Flags().StringVar(&applyUser, "user", "", "Database user name (required)")
ApplyCmd.Flags().StringVar(&applyPassword, "password", "", "Database password (optional)")
ApplyCmd.Flags().StringVar(&applyHost, "host", util.GetEnvWithDefault("PGHOST", "localhost"), "Database server host (env: PGHOST)")
ApplyCmd.Flags().IntVar(&applyPort, "port", util.GetEnvIntWithDefault("PGPORT", 5432), "Database server port (env: PGPORT)")
ApplyCmd.Flags().StringVar(&applyDB, "db", "", "Database name (required) (env: PGDATABASE)")
ApplyCmd.Flags().StringVar(&applyUser, "user", "", "Database user name (required) (env: PGUSER)")
ApplyCmd.Flags().StringVar(&applyPassword, "password", "", "Database password (optional, can also use PGPASSWORD env var)")
ApplyCmd.Flags().StringVar(&applySchema, "schema", "public", "Schema name")

// Desired state schema file flag
Expand All @@ -59,11 +61,7 @@ func init() {
ApplyCmd.Flags().BoolVar(&applyAutoApprove, "auto-approve", false, "Apply changes without prompting for approval")
ApplyCmd.Flags().BoolVar(&applyNoColor, "no-color", false, "Disable colored output")
ApplyCmd.Flags().StringVar(&applyLockTimeout, "lock-timeout", "", "Maximum time to wait for database locks (e.g., 30s, 5m, 1h)")
ApplyCmd.Flags().StringVar(&applyApplicationName, "application-name", "pgschema", "Application name for database connection (visible in pg_stat_activity)")

// Mark required flags
ApplyCmd.MarkFlagRequired("db")
ApplyCmd.MarkFlagRequired("user")
ApplyCmd.Flags().StringVar(&applyApplicationName, "application-name", util.GetEnvWithDefault("PGAPPNAME", "pgschema"), "Application name for database connection (visible in pg_stat_activity) (env: PGAPPNAME)")

// Mark file and plan as mutually exclusive
ApplyCmd.MarkFlagsMutuallyExclusive("file", "plan")
Expand Down
184 changes: 184 additions & 0 deletions cmd/dotenv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package cmd

import (
"os"
"testing"

"github.com/joho/godotenv"
)

func TestDotenvLoading(t *testing.T) {
// Create a temporary directory for test
tmpDir := t.TempDir()
originalDir, _ := os.Getwd()

// Change to temp directory
err := os.Chdir(tmpDir)
if err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}

// Restore original directory after test
defer func() {
os.Chdir(originalDir)
}()

// Test 1: Load .env file with PGPASSWORD
t.Run("LoadEnvFile", func(t *testing.T) {
// Clean environment first
os.Unsetenv("PGPASSWORD")

// Create .env file
envContent := "PGPASSWORD=test_password_123\n"
err := os.WriteFile(".env", []byte(envContent), 0644)
if err != nil {
t.Fatalf("Failed to create .env file: %v", err)
}

// Load .env file
err = godotenv.Load()
if err != nil {
t.Fatalf("Failed to load .env file: %v", err)
}

// Verify PGPASSWORD is set
password := os.Getenv("PGPASSWORD")
if password != "test_password_123" {
t.Errorf("Expected PGPASSWORD='test_password_123', got '%s'", password)
}

// Cleanup
os.Remove(".env")
os.Unsetenv("PGPASSWORD")
})

// Test 2: Missing .env file should not cause errors
t.Run("MissingEnvFile", func(t *testing.T) {
// Clean environment first
os.Unsetenv("PGPASSWORD")

// Ensure no .env file exists
os.Remove(".env")

// Load .env file (should not error)
err := godotenv.Load()
if err == nil {
t.Error("Expected error when loading non-existent .env file, but got nil")
}

// PGPASSWORD should be empty
password := os.Getenv("PGPASSWORD")
if password != "" {
t.Errorf("Expected PGPASSWORD to be empty, got '%s'", password)
}
})

// Test 3: Environment variable priority
t.Run("EnvVarPriority", func(t *testing.T) {
// Set PGPASSWORD in environment first
os.Setenv("PGPASSWORD", "env_password")

// Create .env file with different password
envContent := "PGPASSWORD=dotenv_password\n"
err := os.WriteFile(".env", []byte(envContent), 0644)
if err != nil {
t.Fatalf("Failed to create .env file: %v", err)
}

// Load .env file - should NOT override existing env var
err = godotenv.Load()
if err != nil {
t.Fatalf("Failed to load .env file: %v", err)
}

// Should still have the original environment value
password := os.Getenv("PGPASSWORD")
if password != "env_password" {
t.Errorf("Expected PGPASSWORD='env_password' (existing env var should take precedence), got '%s'", password)
}

// Cleanup
os.Remove(".env")
os.Unsetenv("PGPASSWORD")
})

// Test 4: .env overrides when no existing env var
t.Run("DotenvOverridesWhenNoEnvVar", func(t *testing.T) {
// Clean environment first
os.Unsetenv("PGPASSWORD")

// Create .env file
envContent := "PGPASSWORD=dotenv_only_password\n"
err := os.WriteFile(".env", []byte(envContent), 0644)
if err != nil {
t.Fatalf("Failed to create .env file: %v", err)
}

// Load .env file
err = godotenv.Load()
if err != nil {
t.Fatalf("Failed to load .env file: %v", err)
}

// Should have the dotenv value
password := os.Getenv("PGPASSWORD")
if password != "dotenv_only_password" {
t.Errorf("Expected PGPASSWORD='dotenv_only_password', got '%s'", password)
}

// Cleanup
os.Remove(".env")
os.Unsetenv("PGPASSWORD")
})

// Test 5: All PostgreSQL environment variables
t.Run("AllPostgreSQLEnvVars", func(t *testing.T) {
// Clean environment first
envVars := []string{"PGHOST", "PGPORT", "PGDATABASE", "PGUSER", "PGPASSWORD", "PGAPPNAME"}
for _, envVar := range envVars {
os.Unsetenv(envVar)
}

// Create .env file with all variables
envContent := `PGHOST=test.example.com
PGPORT=5433
PGDATABASE=testdb
PGUSER=testuser
PGPASSWORD=testpass
PGAPPNAME=test-pgschema
`
err := os.WriteFile(".env", []byte(envContent), 0644)
if err != nil {
t.Fatalf("Failed to create .env file: %v", err)
}

// Load .env file
err = godotenv.Load()
if err != nil {
t.Fatalf("Failed to load .env file: %v", err)
}

// Verify all variables are loaded
expectedValues := map[string]string{
"PGHOST": "test.example.com",
"PGPORT": "5433",
"PGDATABASE": "testdb",
"PGUSER": "testuser",
"PGPASSWORD": "testpass",
"PGAPPNAME": "test-pgschema",
}

for envVar, expected := range expectedValues {
actual := os.Getenv(envVar)
if actual != expected {
t.Errorf("Expected %s='%s', got '%s'", envVar, expected, actual)
}
}

// Cleanup
os.Remove(".env")
for _, envVar := range envVars {
os.Unsetenv(envVar)
}
})
}
12 changes: 6 additions & 6 deletions cmd/dump/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,25 @@ var (
file string
)


var DumpCmd = &cobra.Command{
Use: "dump",
Short: "Dump database schema for a specific schema",
Long: "Dump and output database schema information for a specific schema. Uses the --schema flag to target a particular schema (defaults to 'public').",
RunE: runDump,
SilenceUsage: true,
PreRunE: util.PreRunEWithEnvVars(&db, &user),
}

func init() {
DumpCmd.Flags().StringVar(&host, "host", "localhost", "Database server host")
DumpCmd.Flags().IntVar(&port, "port", 5432, "Database server port")
DumpCmd.Flags().StringVar(&db, "db", "", "Database name (required)")
DumpCmd.Flags().StringVar(&user, "user", "", "Database user name (required)")
DumpCmd.Flags().StringVar(&host, "host", util.GetEnvWithDefault("PGHOST", "localhost"), "Database server host (env: PGHOST)")
DumpCmd.Flags().IntVar(&port, "port", util.GetEnvIntWithDefault("PGPORT", 5432), "Database server port (env: PGPORT)")
DumpCmd.Flags().StringVar(&db, "db", "", "Database name (required) (env: PGDATABASE)")
DumpCmd.Flags().StringVar(&user, "user", "", "Database user name (required) (env: PGUSER)")
DumpCmd.Flags().StringVar(&password, "password", "", "Database password (optional, can also use PGPASSWORD env var)")
DumpCmd.Flags().StringVar(&schema, "schema", "public", "Schema name to dump (default: public)")
DumpCmd.Flags().BoolVar(&multiFile, "multi-file", false, "Output schema to multiple files organized by object type")
DumpCmd.Flags().StringVar(&file, "file", "", "Output file path (required when --multi-file is used)")
DumpCmd.MarkFlagRequired("db")
DumpCmd.MarkFlagRequired("user")
}

func runDump(cmd *cobra.Command, args []string) error {
Expand Down
Loading