diff --git a/.env.example b/.env.example
new file mode 100644
index 00000000..d2cedd98
--- /dev/null
+++ b/.env.example
@@ -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
\ No newline at end of file
diff --git a/cmd/apply/apply.go b/cmd/apply/apply.go
index 6d9538f0..597b7d06 100644
--- a/cmd/apply/apply.go
+++ b/cmd/apply/apply.go
@@ -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
@@ -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")
diff --git a/cmd/dotenv_test.go b/cmd/dotenv_test.go
new file mode 100644
index 00000000..ac55c0a2
--- /dev/null
+++ b/cmd/dotenv_test.go
@@ -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)
+ }
+ })
+}
diff --git a/cmd/dump/dump.go b/cmd/dump/dump.go
index fa701eaa..3f38aaf3 100644
--- a/cmd/dump/dump.go
+++ b/cmd/dump/dump.go
@@ -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 {
diff --git a/cmd/dump/dump_test.go b/cmd/dump/dump_test.go
index 61c0b136..0db477fb 100644
--- a/cmd/dump/dump_test.go
+++ b/cmd/dump/dump_test.go
@@ -1,8 +1,10 @@
package dump
import (
+ "os"
"testing"
+ "github.com/pgschema/pgschema/cmd/util"
"github.com/spf13/cobra"
)
@@ -74,3 +76,193 @@ func TestDumpCommand_ErrorHandling(t *testing.T) {
t.Error("Expected error with unreachable database, but got nil")
}
}
+
+func TestDumpCommand_PasswordPriority(t *testing.T) {
+ // Store original values
+ originalHost := host
+ originalPort := port
+ originalDb := db
+ originalUser := user
+ originalPassword := password
+
+ defer func() {
+ host = originalHost
+ port = originalPort
+ db = originalDb
+ user = originalUser
+ password = originalPassword
+ os.Unsetenv("PGPASSWORD")
+ }()
+
+ t.Run("PasswordFromFlag", func(t *testing.T) {
+ // Clear environment variable
+ os.Unsetenv("PGPASSWORD")
+
+ // Set flag values
+ host = "localhost"
+ port = 9999 // Use non-existent port to avoid actual connection
+ db = "test"
+ user = "test"
+ password = "flag_password"
+
+ // The password resolution happens in runDump when it calls:
+ // finalPassword := password
+ // if finalPassword == "" {
+ // if envPassword := os.Getenv("PGPASSWORD"); envPassword != "" {
+ // finalPassword = envPassword
+ // }
+ // }
+ // We can't easily test this without refactoring, but we can test the flag is set
+ if password != "flag_password" {
+ t.Errorf("Expected password flag to be 'flag_password', got '%s'", password)
+ }
+ })
+
+ t.Run("PasswordFromEnvVar", func(t *testing.T) {
+ // Set environment variable
+ os.Setenv("PGPASSWORD", "env_password")
+
+ // Clear flag
+ password = ""
+
+ // Set other required values
+ host = "localhost"
+ port = 9999
+ db = "test"
+ user = "test"
+
+ // Verify environment variable is set
+ envPassword := os.Getenv("PGPASSWORD")
+ if envPassword != "env_password" {
+ t.Errorf("Expected PGPASSWORD env var to be 'env_password', got '%s'", envPassword)
+ }
+
+ // Verify flag is empty (so env var should be used)
+ if password != "" {
+ t.Errorf("Expected password flag to be empty, got '%s'", password)
+ }
+ })
+
+ t.Run("FlagOverridesEnvVar", func(t *testing.T) {
+ // Set both environment variable and flag
+ os.Setenv("PGPASSWORD", "env_password")
+ password = "flag_password"
+
+ // Set other required values
+ host = "localhost"
+ port = 9999
+ db = "test"
+ user = "test"
+
+ // Flag should take precedence
+ if password != "flag_password" {
+ t.Errorf("Expected password flag to be 'flag_password' (should override env var), got '%s'", password)
+ }
+
+ // Environment variable should still be set
+ envPassword := os.Getenv("PGPASSWORD")
+ if envPassword != "env_password" {
+ t.Errorf("Expected PGPASSWORD env var to be 'env_password', got '%s'", envPassword)
+ }
+ })
+
+ t.Run("NoPasswordProvided", func(t *testing.T) {
+ // Clear both flag and environment variable
+ os.Unsetenv("PGPASSWORD")
+ password = ""
+
+ // Set other required values
+ host = "localhost"
+ port = 9999
+ db = "test"
+ user = "test"
+
+ // Both should be empty
+ if password != "" {
+ t.Errorf("Expected password flag to be empty, got '%s'", password)
+ }
+
+ envPassword := os.Getenv("PGPASSWORD")
+ if envPassword != "" {
+ t.Errorf("Expected PGPASSWORD env var to be empty, got '%s'", envPassword)
+ }
+ })
+}
+
+func TestDumpCommand_EnvironmentVariables(t *testing.T) {
+ // Store original values
+ originalHost := host
+ originalPort := port
+ originalDb := db
+ originalUser := user
+
+ defer func() {
+ host = originalHost
+ port = originalPort
+ db = originalDb
+ user = originalUser
+ // Clean up environment variables
+ os.Unsetenv("PGHOST")
+ os.Unsetenv("PGPORT")
+ os.Unsetenv("PGDATABASE")
+ os.Unsetenv("PGUSER")
+ os.Unsetenv("PGAPPNAME")
+ }()
+
+ t.Run("EnvironmentVariablesAsDefaults", func(t *testing.T) {
+ // Set environment variables
+ os.Setenv("PGHOST", "env-host")
+ os.Setenv("PGPORT", "9999")
+ os.Setenv("PGDATABASE", "env-db")
+ os.Setenv("PGUSER", "env-user")
+
+ // Reinitialize the command flags to pick up env vars
+ // We can't easily do this without recreating the command, but we can test the helper functions
+ if util.GetEnvWithDefault("PGHOST", "localhost") != "env-host" {
+ t.Errorf("Expected PGHOST env var to be 'env-host', got '%s'", util.GetEnvWithDefault("PGHOST", "localhost"))
+ }
+
+ if util.GetEnvIntWithDefault("PGPORT", 5432) != 9999 {
+ t.Errorf("Expected PGPORT env var to be 9999, got %d", util.GetEnvIntWithDefault("PGPORT", 5432))
+ }
+
+ if util.GetEnvWithDefault("PGDATABASE", "") != "env-db" {
+ t.Errorf("Expected PGDATABASE env var to be 'env-db', got '%s'", util.GetEnvWithDefault("PGDATABASE", ""))
+ }
+
+ if util.GetEnvWithDefault("PGUSER", "") != "env-user" {
+ t.Errorf("Expected PGUSER env var to be 'env-user', got '%s'", util.GetEnvWithDefault("PGUSER", ""))
+ }
+ })
+
+ t.Run("EnvVarHelperFunctions", func(t *testing.T) {
+ // Test string helper
+ os.Setenv("TEST_STRING", "test-value")
+ if util.GetEnvWithDefault("TEST_STRING", "default") != "test-value" {
+ t.Errorf("Expected GetEnvWithDefault to return 'test-value', got '%s'", util.GetEnvWithDefault("TEST_STRING", "default"))
+ }
+
+ // Test with missing env var
+ os.Unsetenv("MISSING_VAR")
+ if util.GetEnvWithDefault("MISSING_VAR", "default") != "default" {
+ t.Errorf("Expected GetEnvWithDefault to return 'default', got '%s'", util.GetEnvWithDefault("MISSING_VAR", "default"))
+ }
+
+ // Test int helper
+ os.Setenv("TEST_INT", "12345")
+ if util.GetEnvIntWithDefault("TEST_INT", 0) != 12345 {
+ t.Errorf("Expected GetEnvIntWithDefault to return 12345, got %d", util.GetEnvIntWithDefault("TEST_INT", 0))
+ }
+
+ // Test int with invalid value (should return default)
+ os.Setenv("TEST_INVALID_INT", "not-a-number")
+ if util.GetEnvIntWithDefault("TEST_INVALID_INT", 999) != 999 {
+ t.Errorf("Expected GetEnvIntWithDefault to return default 999, got %d", util.GetEnvIntWithDefault("TEST_INVALID_INT", 999))
+ }
+
+ // Cleanup
+ os.Unsetenv("TEST_STRING")
+ os.Unsetenv("TEST_INT")
+ os.Unsetenv("TEST_INVALID_INT")
+ })
+}
diff --git a/cmd/plan/plan.go b/cmd/plan/plan.go
index c11b4910..3db6c6ea 100644
--- a/cmd/plan/plan.go
+++ b/cmd/plan/plan.go
@@ -28,21 +28,23 @@ var (
planNoColor bool
)
+
var PlanCmd = &cobra.Command{
Use: "plan",
Short: "Generate migration plan for a specific schema",
Long: "Generate a migration plan to apply a desired schema state to a target database schema. Compares the desired state (from --file) with the current state of a specific schema (specified by --schema, defaults to 'public').",
RunE: runPlan,
SilenceUsage: true,
+ PreRunE: util.PreRunEWithEnvVars(&planDB, &planUser),
}
func init() {
// Target database connection flags
- PlanCmd.Flags().StringVar(&planHost, "host", "localhost", "Database server host")
- PlanCmd.Flags().IntVar(&planPort, "port", 5432, "Database server port")
- PlanCmd.Flags().StringVar(&planDB, "db", "", "Database name (required)")
- PlanCmd.Flags().StringVar(&planUser, "user", "", "Database user name (required)")
- PlanCmd.Flags().StringVar(&planPassword, "password", "", "Database password (optional)")
+ PlanCmd.Flags().StringVar(&planHost, "host", util.GetEnvWithDefault("PGHOST", "localhost"), "Database server host (env: PGHOST)")
+ PlanCmd.Flags().IntVar(&planPort, "port", util.GetEnvIntWithDefault("PGPORT", 5432), "Database server port (env: PGPORT)")
+ PlanCmd.Flags().StringVar(&planDB, "db", "", "Database name (required) (env: PGDATABASE)")
+ PlanCmd.Flags().StringVar(&planUser, "user", "", "Database user name (required) (env: PGUSER)")
+ PlanCmd.Flags().StringVar(&planPassword, "password", "", "Database password (optional, can also use PGPASSWORD env var)")
PlanCmd.Flags().StringVar(&planSchema, "schema", "public", "Schema name")
// Desired state schema file flag
@@ -54,9 +56,6 @@ func init() {
PlanCmd.Flags().StringVar(&outputSQL, "output-sql", "", "Output SQL format to stdout or file path")
PlanCmd.Flags().BoolVar(&planNoColor, "no-color", false, "Disable colored output")
- // Mark required flags
- PlanCmd.MarkFlagRequired("db")
- PlanCmd.MarkFlagRequired("user")
PlanCmd.MarkFlagRequired("file")
}
diff --git a/cmd/root.go b/cmd/root.go
index afe715c6..731247a4 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -83,6 +83,7 @@ func platform() string {
return runtime.GOOS + "/" + runtime.GOARCH
}
+
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
diff --git a/cmd/util/env.go b/cmd/util/env.go
new file mode 100644
index 00000000..010470cb
--- /dev/null
+++ b/cmd/util/env.go
@@ -0,0 +1,51 @@
+package util
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+
+ "github.com/spf13/cobra"
+)
+
+// GetEnvWithDefault returns the value of an environment variable or a default value if not set
+func GetEnvWithDefault(envVar, defaultValue string) string {
+ if value := os.Getenv(envVar); value != "" {
+ return value
+ }
+ return defaultValue
+}
+
+// GetEnvIntWithDefault returns the value of an environment variable as int or a default value if not set
+func GetEnvIntWithDefault(envVar string, defaultValue int) int {
+ if value := os.Getenv(envVar); value != "" {
+ if intValue, err := strconv.Atoi(value); err == nil {
+ return intValue
+ }
+ }
+ return defaultValue
+}
+
+// PreRunEWithEnvVars creates a PreRunE function that validates required database connection parameters
+// It checks environment variables if the corresponding flags weren't explicitly set
+func PreRunEWithEnvVars(dbPtr, userPtr *string) func(*cobra.Command, []string) error {
+ return func(cmd *cobra.Command, args []string) error {
+ // Check if required values are available from environment variables
+ if GetEnvWithDefault("PGDATABASE", "") != "" && !cmd.Flags().Changed("db") {
+ *dbPtr = GetEnvWithDefault("PGDATABASE", "")
+ }
+ if GetEnvWithDefault("PGUSER", "") != "" && !cmd.Flags().Changed("user") {
+ *userPtr = GetEnvWithDefault("PGUSER", "")
+ }
+
+ // Now validate that we have the required values
+ if *dbPtr == "" {
+ return fmt.Errorf("database name is required (use --db flag or PGDATABASE environment variable)")
+ }
+ if *userPtr == "" {
+ return fmt.Errorf("database user is required (use --user flag or PGUSER environment variable)")
+ }
+
+ return nil
+ }
+}
\ No newline at end of file
diff --git a/docs/cli/apply.mdx b/docs/cli/apply.mdx
index 1059aa16..e68145c5 100644
--- a/docs/cli/apply.mdx
+++ b/docs/cli/apply.mdx
@@ -37,28 +37,39 @@ pgschema apply --host localhost --db myapp --user postgres --password mypassword
## Connection Options
- Database server host
+ Database server host (env: PGHOST)
- Database server port
+ Database server port (env: PGPORT)
- Database name
+ Database name (required) (env: PGDATABASE)
- Database user name
+ Database user name (required) (env: PGUSER)
- Database password (can also use PGPASSWORD environment variable)
+ Database password (optional, can also use PGPASSWORD env var)
- You can provide the password in two ways:
+ You can provide the password in multiple ways:
- ```bash Environment Variable (Recommended)
+ ```bash .env File (Recommended)
+ # Create .env file with:
+ # PGHOST=localhost
+ # PGPORT=5432
+ # PGDATABASE=myapp
+ # PGUSER=postgres
+ # PGPASSWORD=mypassword
+
+ pgschema apply --file schema.sql
+ ```
+
+ ```bash Environment Variable
PGPASSWORD=mypassword pgschema apply \
--host localhost \
--db myapp \
@@ -75,6 +86,8 @@ pgschema apply --host localhost --db myapp --user postgres --password mypassword
--file schema.sql
```
+
+ See [dotenv (.env)](/cli/dotenv) for detailed configuration options.
@@ -115,8 +128,8 @@ pgschema apply --host localhost --db myapp --user postgres --password mypassword
- Application name for database connection (visible in pg_stat_activity)
-
+ Application name for database connection (visible in pg_stat_activity) (env: PGAPPNAME)
+
See [PostgreSQL application_name documentation](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-APPLICATION-NAME).
diff --git a/docs/cli/dotenv.mdx b/docs/cli/dotenv.mdx
new file mode 100644
index 00000000..63a688a9
--- /dev/null
+++ b/docs/cli/dotenv.mdx
@@ -0,0 +1,67 @@
+---
+title: "dotenv (.env)"
+---
+
+`pgschema` supports loading configuration from environment variables and dotenv `.env` files.
+
+## Overview
+
+pgschema automatically loads configuration from:
+1. `.env` file in the current directory (if present)
+2. System environment variables
+3. Command-line flags (highest priority)
+
+The precedence order is: **CLI flags > environment variables > defaults**
+
+## Supported Environment Variables
+
+pgschema supports all standard PostgreSQL environment variables:
+
+
+ Database server host
+
+
+
+ Database server port
+
+
+
+ Database name (required for all commands)
+
+
+
+ Database user name (required for all commands)
+
+
+
+ Database password
+
+
+
+ Application name visible in `pg_stat_activity`
+
+
+## .env Setup
+
+Create a `.env` file in your project directory:
+
+```bash
+# Database connection settings
+PGHOST=localhost
+PGPORT=5432
+PGDATABASE=myapp
+PGUSER=postgres
+PGPASSWORD=secretpassword
+
+# Optional: Custom application name
+PGAPPNAME=pgschema
+```
+
+**Security Note:** Add `.env` to your `.gitignore` file to prevent committing sensitive credentials:
+
+```gitignore
+# Environment files
+.env
+.env.local
+.env.*.local
+```
\ No newline at end of file
diff --git a/docs/cli/dump.mdx b/docs/cli/dump.mdx
index d1b96935..c73542fc 100644
--- a/docs/cli/dump.mdx
+++ b/docs/cli/dump.mdx
@@ -40,35 +40,46 @@ pgschema dump --host prod-host --db myapp --user postgres --schema public > curr
# 3. Plan changes against staging
pgschema plan --host staging-host --db myapp --user postgres --file current.sql
-# 4. Apply changes if plan looks good
+# 4. Apply changes if plan looks good
pgschema apply --host staging-host --db myapp --user postgres --file current.sql
```
## Connection Options
- Database server host
+ Database server host (env: PGHOST)
- Database server port
+ Database server port (env: PGPORT)
- Database name
+ Database name (required) (env: PGDATABASE)
- Database user name
+ Database user name (required) (env: PGUSER)
- Database password (can also use PGPASSWORD environment variable)
+ Database password (optional, can also use PGPASSWORD env var)
- You can provide the password in two ways:
+ You can provide the password in multiple ways:
- ```bash Environment Variable (Recommended)
+ ```bash .env File (Recommended)
+ # Create .env file with:
+ # PGHOST=localhost
+ # PGPORT=5432
+ # PGDATABASE=myapp
+ # PGUSER=postgres
+ # PGPASSWORD=mypassword
+
+ pgschema dump
+ ```
+
+ ```bash Environment Variable
PGPASSWORD=mypassword pgschema dump \
--host localhost \
--db myapp \
@@ -83,6 +94,8 @@ pgschema apply --host staging-host --db myapp --user postgres --file current.sql
--password mypassword
```
+
+ See [dotenv (.env)](/cli/dotenv) for detailed configuration options.
@@ -117,7 +130,7 @@ pgschema apply --host staging-host --db myapp --user postgres --file current.sql
# Dump default schema (public)
pgschema dump --host localhost --db myapp --user postgres
-# Dump specific schema
+# Dump specific schema
pgschema dump --host localhost --db myapp --user postgres --schema analytics
```
diff --git a/docs/cli/plan.mdx b/docs/cli/plan.mdx
index 6d7e947c..d8058cae 100644
--- a/docs/cli/plan.mdx
+++ b/docs/cli/plan.mdx
@@ -39,28 +39,39 @@ pgschema plan --host localhost --db myapp --user postgres --password mypassword
## Connection Options
- Database server host
+ Database server host (env: PGHOST)
- Database server port
+ Database server port (env: PGPORT)
- Database name
+ Database name (required) (env: PGDATABASE)
- Database user name
+ Database user name (required) (env: PGUSER)
- Database password (can also use PGPASSWORD environment variable)
+ Database password (optional, can also use PGPASSWORD env var)
- You can provide the password in two ways:
+ You can provide the password in multiple ways:
- ```bash Environment Variable (Recommended)
+ ```bash .env File (Recommended)
+ # Create .env file with:
+ # PGHOST=localhost
+ # PGPORT=5432
+ # PGDATABASE=myapp
+ # PGUSER=postgres
+ # PGPASSWORD=mypassword
+
+ pgschema plan --file schema.sql
+ ```
+
+ ```bash Environment Variable
PGPASSWORD=mypassword pgschema plan \
--host localhost \
--db myapp \
@@ -77,6 +88,8 @@ pgschema plan --host localhost --db myapp --user postgres --password mypassword
--file schema.sql
```
+
+ See [dotenv (.env)](/cli/dotenv) for detailed configuration options.
diff --git a/docs/docs.json b/docs/docs.json
index 400f954a..0391b5cf 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -40,7 +40,7 @@
},
{
"group": "CLI Reference",
- "pages": ["cli/dump", "cli/plan", "cli/apply"]
+ "pages": ["cli/dump", "cli/plan", "cli/apply", "cli/dotenv"]
}
]
},
diff --git a/go.mod b/go.mod
index af3207e5..b4ddbf11 100644
--- a/go.mod
+++ b/go.mod
@@ -40,6 +40,7 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
diff --git a/go.sum b/go.sum
index 0605986e..dc067afc 100644
--- a/go.sum
+++ b/go.sum
@@ -60,6 +60,8 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
diff --git a/main.go b/main.go
index b1446a8d..74b6c82b 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,13 @@
package main
import (
+ "github.com/joho/godotenv"
"github.com/pgschema/pgschema/cmd"
)
func main() {
+ // Load .env file if it exists (silently ignore errors)
+ _ = godotenv.Load()
+
cmd.Execute()
}