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
39 changes: 26 additions & 13 deletions cmd/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type ApplyConfig struct {
Plan *plan.Plan // Pre-generated plan (optional, alternative to File)
AutoApprove bool
NoColor bool
Quiet bool // Suppress plan display and progress messages (useful for tests)
LockTimeout string
ApplicationName string
}
Expand Down Expand Up @@ -157,8 +158,10 @@ func ApplyMigration(config *ApplyConfig, provider postgres.DesiredStateProvider)
return nil
}

// Display the plan
fmt.Print(migrationPlan.HumanColored(!config.NoColor))
// Display the plan (unless quiet mode is enabled)
if !config.Quiet {
fmt.Print(migrationPlan.HumanColored(!config.NoColor))
}

// Prompt for approval if not auto-approved
if !config.AutoApprove {
Expand All @@ -177,7 +180,9 @@ func ApplyMigration(config *ApplyConfig, provider postgres.DesiredStateProvider)
}

// Apply the changes
fmt.Println("\nApplying changes...")
if !config.Quiet {
fmt.Println("\nApplying changes...")
}

// Build database connection for applying changes
connConfig := &util.ConnectionConfig{
Expand Down Expand Up @@ -227,15 +232,19 @@ func ApplyMigration(config *ApplyConfig, provider postgres.DesiredStateProvider)

// Execute by groups with wait directive support
for i, group := range migrationPlan.Groups {
fmt.Printf("\nExecuting group %d/%d...\n", i+1, len(migrationPlan.Groups))
if !config.Quiet {
fmt.Printf("\nExecuting group %d/%d...\n", i+1, len(migrationPlan.Groups))
}

err = executeGroup(ctx, conn, group, i+1)
err = executeGroup(ctx, conn, group, i+1, config.Quiet)
if err != nil {
return err
}
}

fmt.Println("Changes applied successfully!")
if !config.Quiet {
fmt.Println("Changes applied successfully!")
}
return nil
}

Expand Down Expand Up @@ -368,7 +377,7 @@ func validateSchemaFingerprint(migrationPlan *plan.Plan, host string, port int,
}

// executeGroup executes all steps in a group, handling directives separately from SQL statements
func executeGroup(ctx context.Context, conn *sql.DB, group plan.ExecutionGroup, groupNum int) error {
func executeGroup(ctx context.Context, conn *sql.DB, group plan.ExecutionGroup, groupNum int, quiet bool) error {
// Check if this group has directives
hasDirectives := false

Expand All @@ -381,15 +390,15 @@ func executeGroup(ctx context.Context, conn *sql.DB, group plan.ExecutionGroup,

if !hasDirectives {
// No directives - concatenate all SQL and execute in implicit transaction
return executeGroupConcatenated(ctx, conn, group, groupNum)
return executeGroupConcatenated(ctx, conn, group, groupNum, quiet)
} else {
// Has directives - execute statements individually
return executeGroupIndividually(ctx, conn, group, groupNum)
return executeGroupIndividually(ctx, conn, group, groupNum, quiet)
}
}

// executeGroupConcatenated concatenates all SQL statements and executes them in an implicit transaction
func executeGroupConcatenated(ctx context.Context, conn *sql.DB, group plan.ExecutionGroup, groupNum int) error {
func executeGroupConcatenated(ctx context.Context, conn *sql.DB, group plan.ExecutionGroup, groupNum int, quiet bool) error {
var sqlStatements []string

// Collect all SQL statements
Expand All @@ -400,7 +409,9 @@ func executeGroupConcatenated(ctx context.Context, conn *sql.DB, group plan.Exec
// Concatenate all SQL statements
concatenatedSQL := strings.Join(sqlStatements, ";\n") + ";"

fmt.Printf(" Executing %d statements in implicit transaction\n", len(sqlStatements))
if !quiet {
fmt.Printf(" Executing %d statements in implicit transaction\n", len(sqlStatements))
}

// Execute all statements in a single call (implicit transaction)
_, err := conn.ExecContext(ctx, concatenatedSQL)
Expand All @@ -412,7 +423,7 @@ func executeGroupConcatenated(ctx context.Context, conn *sql.DB, group plan.Exec
}

// executeGroupIndividually executes statements individually without transactions
func executeGroupIndividually(ctx context.Context, conn *sql.DB, group plan.ExecutionGroup, groupNum int) error {
func executeGroupIndividually(ctx context.Context, conn *sql.DB, group plan.ExecutionGroup, groupNum int, quiet bool) error {
for stepIdx, step := range group.Steps {
if step.Directive != nil {
// Handle directive execution
Expand All @@ -422,7 +433,9 @@ func executeGroupIndividually(ctx context.Context, conn *sql.DB, group plan.Exec
}
} else {
// Execute regular SQL statement
fmt.Printf(" Executing: %s\n", truncateSQL(step.SQL, 80))
if !quiet {
fmt.Printf(" Executing: %s\n", truncateSQL(step.SQL, 80))
}

_, err := conn.ExecContext(ctx, step.SQL)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions cmd/apply/apply_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ func TestApplyCommand_TransactionRollback(t *testing.T) {
Plan: migrationPlan, // Use pre-generated plan with injected failure
AutoApprove: true,
NoColor: false,
Quiet: true, // Suppress output in tests
LockTimeout: "",
ApplicationName: "pgschema",
}
Expand Down Expand Up @@ -460,6 +461,7 @@ func TestApplyCommand_CreateIndexConcurrently(t *testing.T) {
Plan: migrationPlan, // Use pre-generated plan
AutoApprove: true,
NoColor: false,
Quiet: true, // Suppress output in tests
LockTimeout: "",
ApplicationName: "pgschema",
}
Expand Down Expand Up @@ -650,6 +652,7 @@ func TestApplyCommand_WithPlanFile(t *testing.T) {
Plan: migrationPlan, // Use pre-generated plan
AutoApprove: true,
NoColor: false,
Quiet: true, // Suppress output in tests
LockTimeout: "",
ApplicationName: "pgschema",
}
Expand Down Expand Up @@ -880,6 +883,7 @@ func TestApplyCommand_FingerprintMismatch(t *testing.T) {
Plan: migrationPlan, // Use pre-generated plan with old fingerprint
AutoApprove: true,
NoColor: false,
Quiet: true, // Suppress output in tests
LockTimeout: "",
ApplicationName: "pgschema",
}
Expand Down Expand Up @@ -1048,6 +1052,7 @@ func TestApplyCommand_WaitDirective(t *testing.T) {
Plan: migrationPlan, // Use pre-generated plan
AutoApprove: true,
NoColor: false,
Quiet: true, // Suppress output in tests
LockTimeout: "",
ApplicationName: "pgschema",
}
Expand Down Expand Up @@ -1176,6 +1181,7 @@ CREATE TABLE employees (
File: schemaFile,
AutoApprove: true, // Auto-approve for testing
NoColor: true,
Quiet: true, // Suppress output in tests
ApplicationName: "pgschema-test",
}

Expand Down Expand Up @@ -1296,6 +1302,7 @@ CREATE TABLE users (
Schema: "public",
AutoApprove: true,
NoColor: true,
Quiet: true, // Suppress output in tests
ApplicationName: "pgschema-test",
}

Expand Down
2 changes: 0 additions & 2 deletions cmd/dump/dump_permission_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,6 @@ func testProcedureAndFunctionSourceAccess(t *testing.T, ctx context.Context, con
if !strings.Contains(procedureDef, "Procedure source visibility test") {
t.Errorf("Expected procedure body content, got: %s", procedureDef)
}
t.Logf("Success: pg_get_functiondef returned procedure: %s", procedureDef)
}

// Test 4: Check that pg_get_functiondef() works for functions despite NULL routine_definition
Expand All @@ -417,7 +416,6 @@ func testProcedureAndFunctionSourceAccess(t *testing.T, ctx context.Context, con
if !strings.Contains(functionDef, "Function source visibility test") {
t.Errorf("Expected function body content, got: %s", functionDef)
}
t.Logf("Success: pg_get_functiondef returned function: %s", functionDef)
}

// Test 5: Try to dump schema as regular_user (should succeed with pg_get_functiondef)
Expand Down
27 changes: 14 additions & 13 deletions cmd/include_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package cmd
// the same organized file structure.

import (
"bytes"
"database/sql"
"fmt"
"os"
Expand Down Expand Up @@ -82,6 +83,11 @@ func applyIncludeSchema(t *testing.T, containerInfo *struct {
// Add the apply command as a subcommand
rootCmd.AddCommand(apply.ApplyCmd)

// Capture stdout and stderr to suppress verbose output
var stdout, stderr bytes.Buffer
rootCmd.SetOut(&stdout)
rootCmd.SetErr(&stderr)

// Set command arguments for apply
args := []string{
"apply",
Expand All @@ -98,10 +104,8 @@ func applyIncludeSchema(t *testing.T, containerInfo *struct {
// Execute the root command with apply subcommand
err := rootCmd.Execute()
if err != nil {
t.Fatalf("Failed to apply include schema: %v", err)
t.Fatalf("Failed to apply include schema: %v\nStdout: %s\nStderr: %s", err, stdout.String(), stderr.String())
}

t.Logf("✓ Successfully applied include-based schema using apply command")
}

// executeMultiFileDump runs pgschema dump --multi-file using the CLI command
Expand All @@ -121,6 +125,11 @@ func executeMultiFileDump(t *testing.T, containerInfo *struct {
// Add the dump command as a subcommand
rootCmd.AddCommand(dump.DumpCmd)

// Capture stdout and stderr to suppress verbose output
var stdout, stderr bytes.Buffer
rootCmd.SetOut(&stdout)
rootCmd.SetErr(&stderr)

// Set command arguments for dump
args := []string{
"dump",
Expand All @@ -138,10 +147,8 @@ func executeMultiFileDump(t *testing.T, containerInfo *struct {
// Execute the root command with dump subcommand
err := rootCmd.Execute()
if err != nil {
t.Fatalf("Failed to execute multi-file dump using pgschema dump: %v", err)
t.Fatalf("Failed to execute multi-file dump: %v\nStdout: %s\nStderr: %s", err, stdout.String(), stderr.String())
}

t.Logf("✓ Successfully executed multi-file dump using pgschema dump to %s", filepath.Dir(outputPath))
}

// compareIncludeFiles compares dumped files with original include files using direct comparison
Expand All @@ -150,8 +157,6 @@ func compareIncludeFiles(t *testing.T, dumpDir string) {

// Compare the entire directory structure and contents
compareDirectoryLayout(t, sourceDir, dumpDir)

t.Logf("✓ Include file comparison completed")
}

// compareDirectoryLayout compares the complete directory layout between source and dump
Expand Down Expand Up @@ -193,8 +198,6 @@ func compareDirectoryLayout(t *testing.T, sourceDir, dumpDir string) {
continue
}

t.Logf("✓ Directory exists: %s", dirName)

// Compare the contents of this directory
sourceDirPath := filepath.Join(sourceDir, dirName)
dumpDirPath := filepath.Join(dumpDir, dirName)
Expand Down Expand Up @@ -280,9 +283,7 @@ func compareFileContents(t *testing.T, sourceFilePath, dumpFilePath, displayName
return
}

if string(sourceContent) == string(dumpContent) {
t.Logf("✓ Content match for %s", displayName)
} else {
if string(sourceContent) != string(dumpContent) {
t.Errorf("Content mismatch for %s", displayName)
t.Logf("\n\nExpected:\n%s\n\n", string(sourceContent))
t.Logf("\n\nActual:\n%s\n\n", string(dumpContent))
Expand Down
13 changes: 1 addition & 12 deletions cmd/migrate_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,12 @@ func runPlanAndApplyTest(t *testing.T, ctx context.Context, container *struct {
dbName = dbName[:63]
}

t.Logf("=== PLAN AND APPLY TEST: %s → %s (DB: %s) ===", filepath.Base(tc.oldFile), filepath.Base(tc.newFile), dbName)

// Create test-specific database
if err := createDatabase(ctx, containerHost, portMapped, dbName); err != nil {
t.Fatalf("Failed to create test database %s: %v", dbName, err)
}

// STEP 1: Apply old.sql to initialize database state
t.Logf("--- Applying old.sql to initialize database state ---")
oldContent, err := os.ReadFile(tc.oldFile)
if err != nil {
t.Fatalf("Failed to read %s: %v", tc.oldFile, err)
Expand All @@ -233,37 +230,28 @@ func runPlanAndApplyTest(t *testing.T, ctx context.Context, container *struct {
if err := executeSQL(ctx, containerHost, portMapped, dbName, string(oldContent)); err != nil {
t.Fatalf("Failed to execute old.sql: %v", err)
}
t.Logf("Applied old.sql to initialize database state")
}

// STEP 2: Test plan command with new.sql as target
t.Logf("--- Testing plan command outputs ---")
testPlanOutputs(t, container, dbName, tc.newFile, tc.planSQLFile, tc.planJSONFile, tc.planTXTFile)

if !*generate {
// STEP 3: Apply the migration using apply command
t.Logf("--- Applying migration using apply command ---")
err = applySchemaChanges(containerHost, portMapped, dbName, container.User, container.Password, "public", tc.newFile)
if err != nil {
t.Fatalf("Failed to apply schema changes using pgschema apply: %v", err)
}
t.Logf("Applied migration successfully")

// STEP 4: Test idempotency - plan should produce no changes
t.Logf("--- Testing idempotency ---")
secondPlanOutput, err := generatePlanSQLFormatted(containerHost, portMapped, dbName, container.User, container.Password, "public", tc.newFile)
if err != nil {
t.Fatalf("Failed to generate plan SQL for idempotency check: %v", err)
}

if secondPlanOutput != "" {
t.Errorf("Expected no changes when applying schema twice, but got SQL output:\n%s", secondPlanOutput)
} else {
t.Logf("Idempotency verified: no changes detected on second apply")
}
}

t.Logf("=== PLAN AND APPLY TEST COMPLETED ===")
}

// testPlanOutputs tests all plan output formats against expected files
Expand Down Expand Up @@ -413,6 +401,7 @@ func applySchemaChanges(host string, port int, database, user, password, schema,
File: schemaFile,
AutoApprove: true,
NoColor: true,
Quiet: true, // Suppress plan display and progress messages in tests
LockTimeout: "",
ApplicationName: "pgschema",
}
Expand Down
Loading