diff --git a/cmd/relspec/scripts.go b/cmd/relspec/scripts.go index e3fe554..f90dd0e 100644 --- a/cmd/relspec/scripts.go +++ b/cmd/relspec/scripts.go @@ -14,10 +14,11 @@ import ( ) var ( - scriptsDir string - scriptsConn string - scriptsSchemaName string - scriptsDBName string + scriptsDir string + scriptsConn string + scriptsSchemaName string + scriptsDBName string + scriptsIgnoreErrors bool ) var scriptsCmd = &cobra.Command{ @@ -62,7 +63,7 @@ var scriptsExecuteCmd = &cobra.Command{ Long: `Execute SQL scripts from a directory against a PostgreSQL database. Scripts are executed in order: Priority (ascending), Sequence (ascending), Name (alphabetical). -Execution stops immediately on the first error. +By default, execution stops immediately on the first error. Use --ignore-errors to continue execution. The directory is scanned recursively for all subdirectories and files matching the patterns: {priority}_{sequence}_{name}.sql or .pgsql (underscore format) @@ -86,7 +87,12 @@ Examples: # Execute with SSL disabled relspec scripts execute --dir ./sql \ - --conn "postgres://user:pass@localhost/db?sslmode=disable"`, + --conn "postgres://user:pass@localhost/db?sslmode=disable" + + # Continue executing even if errors occur + relspec scripts execute --dir ./migrations \ + --conn "postgres://localhost/mydb" \ + --ignore-errors`, RunE: runScriptsExecute, } @@ -105,6 +111,7 @@ func init() { scriptsExecuteCmd.Flags().StringVar(&scriptsConn, "conn", "", "PostgreSQL connection string (required)") scriptsExecuteCmd.Flags().StringVar(&scriptsSchemaName, "schema", "public", "Schema name (optional, default: public)") scriptsExecuteCmd.Flags().StringVar(&scriptsDBName, "database", "database", "Database name (optional, default: database)") + scriptsExecuteCmd.Flags().BoolVar(&scriptsIgnoreErrors, "ignore-errors", false, "Continue executing scripts even if errors occur") err = scriptsExecuteCmd.MarkFlagRequired("dir") if err != nil { @@ -250,17 +257,39 @@ func runScriptsExecute(cmd *cobra.Command, args []string) error { writer := sqlexec.NewWriter(&writers.WriterOptions{ Metadata: map[string]any{ "connection_string": scriptsConn, + "ignore_errors": scriptsIgnoreErrors, }, }) if err := writer.WriteSchema(schema); err != nil { fmt.Fprintf(os.Stderr, "\n") - return fmt.Errorf("execution failed: %w", err) + return fmt.Errorf("script execution failed: %w", err) + } + + // Get execution results from writer metadata + totalCount := len(schema.Scripts) + successCount := totalCount + failedCount := 0 + + opts := writer.Options() + if total, exists := opts.Metadata["execution_total"].(int); exists { + totalCount = total + } + if success, exists := opts.Metadata["execution_success"].(int); exists { + successCount = success + } + if failed, exists := opts.Metadata["execution_failed"].(int); exists { + failedCount = failed } fmt.Fprintf(os.Stderr, "\n=== Execution Complete ===\n") fmt.Fprintf(os.Stderr, "Completed at: %s\n", getCurrentTimestamp()) - fmt.Fprintf(os.Stderr, "Successfully executed %d script(s)\n\n", len(schema.Scripts)) + fmt.Fprintf(os.Stderr, "Total scripts: %d\n", totalCount) + fmt.Fprintf(os.Stderr, "Successful: %d\n", successCount) + if failedCount > 0 { + fmt.Fprintf(os.Stderr, "Failed: %d\n", failedCount) + } + fmt.Fprintf(os.Stderr, "\n") return nil } diff --git a/pkg/writers/sqlexec/writer.go b/pkg/writers/sqlexec/writer.go index e7c41df..35feb9a 100644 --- a/pkg/writers/sqlexec/writer.go +++ b/pkg/writers/sqlexec/writer.go @@ -23,6 +23,11 @@ func NewWriter(options *writers.WriterOptions) *Writer { } } +// Options returns the writer options (useful for reading execution results) +func (w *Writer) Options() *writers.WriterOptions { + return w.options +} + // WriteDatabase executes all scripts from all schemas in the database func (w *Writer) WriteDatabase(db *models.Database) error { if db == nil { @@ -92,6 +97,22 @@ func (w *Writer) executeScripts(ctx context.Context, conn *pgx.Conn, scripts []* return nil } + // Check if we should ignore errors + ignoreErrors := false + if val, ok := w.options.Metadata["ignore_errors"].(bool); ok { + ignoreErrors = val + } + + // Track failed scripts and execution counts + var failedScripts []struct { + name string + priority int + sequence uint + err error + } + successCount := 0 + totalCount := 0 + // Sort scripts by Priority (ascending), Sequence (ascending), then Name (ascending) sortedScripts := make([]*models.Script, len(scripts)) copy(sortedScripts, scripts) @@ -111,18 +132,49 @@ func (w *Writer) executeScripts(ctx context.Context, conn *pgx.Conn, scripts []* continue } + totalCount++ fmt.Printf("Executing script: %s (Priority=%d, Sequence=%d)\n", script.Name, script.Priority, script.Sequence) // Execute the SQL script _, err := conn.Exec(ctx, script.SQL) if err != nil { - return fmt.Errorf("failed to execute script %s (Priority=%d, Sequence=%d): %w", + if ignoreErrors { + fmt.Printf("⚠ Error executing %s: %v (continuing due to --ignore-errors)\n", script.Name, err) + failedScripts = append(failedScripts, struct { + name string + priority int + sequence uint + err error + }{ + name: script.Name, + priority: script.Priority, + sequence: script.Sequence, + err: err, + }) + continue + } + return fmt.Errorf("script %s (Priority=%d, Sequence=%d): %w", script.Name, script.Priority, script.Sequence, err) } + successCount++ fmt.Printf("✓ Successfully executed: %s\n", script.Name) } + // Store execution results in metadata for caller + w.options.Metadata["execution_total"] = totalCount + w.options.Metadata["execution_success"] = successCount + w.options.Metadata["execution_failed"] = len(failedScripts) + + // Print summary of failed scripts if any + if len(failedScripts) > 0 { + fmt.Printf("\n⚠ Failed Scripts Summary (%d failed):\n", len(failedScripts)) + for i, failed := range failedScripts { + fmt.Printf(" %d. %s (Priority=%d, Sequence=%d)\n Error: %v\n", + i+1, failed.name, failed.priority, failed.sequence, failed.err) + } + } + return nil }