Files
relspecgo/cmd/relspec/scripts.go
Hein adfe126758
Some checks failed
CI / Test (1.24) (push) Successful in -25m17s
CI / Test (1.25) (push) Successful in -25m15s
CI / Build (push) Successful in -25m45s
CI / Lint (push) Successful in -25m31s
Integration Tests / Integration Tests (push) Failing after -25m58s
Added a scripts execution ability
2025-12-31 00:44:14 +02:00

264 lines
8.1 KiB
Go

package main
import (
"fmt"
"os"
"sort"
"github.com/spf13/cobra"
"git.warky.dev/wdevs/relspecgo/pkg/readers"
"git.warky.dev/wdevs/relspecgo/pkg/readers/sqldir"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
"git.warky.dev/wdevs/relspecgo/pkg/writers/sqlexec"
)
var (
scriptsDir string
scriptsConn string
scriptsSchemaName string
scriptsDBName string
)
var scriptsCmd = &cobra.Command{
Use: "scripts",
Short: "Manage and execute SQL migration scripts",
Long: `Manage and execute SQL migration scripts from a directory.
Scripts must follow the naming pattern (both separators supported):
{priority}_{sequence}_{name}.sql or .pgsql
{priority}-{sequence}-{name}.sql or .pgsql
Example filenames (underscore format):
1_001_create_users.sql # Priority 1, Sequence 1
1_002_create_posts.sql # Priority 1, Sequence 2
2_001_add_indexes.pgsql # Priority 2, Sequence 1
Example filenames (hyphen format):
1-001-create-users.sql # Priority 1, Sequence 1
1-002-create-posts.sql # Priority 1, Sequence 2
10-10-create-newid.pgsql # Priority 10, Sequence 10
Both formats can be mixed in the same directory.
Scripts are executed in order: Priority (ascending), then Sequence (ascending).`,
}
var scriptsListCmd = &cobra.Command{
Use: "list",
Short: "List SQL scripts from a directory",
Long: `List SQL scripts from a directory and show their execution order.
The scripts are read from the specified directory and displayed in the order
they would be executed (Priority ascending, then Sequence ascending).
Example:
relspec scripts list --dir ./migrations`,
RunE: runScriptsList,
}
var scriptsExecuteCmd = &cobra.Command{
Use: "execute",
Short: "Execute SQL scripts against a database",
Long: `Execute SQL scripts from a directory against a PostgreSQL database.
Scripts are executed in order: Priority (ascending), then Sequence (ascending).
Execution stops immediately on the first error.
The directory is scanned recursively for files matching the patterns:
{priority}_{sequence}_{name}.sql or .pgsql (underscore format)
{priority}-{sequence}-{name}.sql or .pgsql (hyphen format)
PostgreSQL Connection String Examples:
postgres://username:password@localhost:5432/database_name
postgres://username:password@localhost/database_name
postgresql://user:pass@host:5432/dbname?sslmode=disable
postgresql://user:pass@host/dbname?sslmode=require
Examples:
# Execute migration scripts
relspec scripts execute --dir ./migrations \
--conn "postgres://user:pass@localhost:5432/mydb"
# Execute with custom schema name
relspec scripts execute --dir ./migrations \
--conn "postgres://localhost/mydb" \
--schema public
# Execute with SSL disabled
relspec scripts execute --dir ./sql \
--conn "postgres://user:pass@localhost/db?sslmode=disable"`,
RunE: runScriptsExecute,
}
func init() {
// List command flags
scriptsListCmd.Flags().StringVar(&scriptsDir, "dir", "", "Directory containing SQL scripts (required)")
scriptsListCmd.Flags().StringVar(&scriptsSchemaName, "schema", "public", "Schema name (optional, default: public)")
scriptsListCmd.Flags().StringVar(&scriptsDBName, "database", "database", "Database name (optional, default: database)")
err := scriptsListCmd.MarkFlagRequired("dir")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marking dir flag as required: %v\n", err)
}
// Execute command flags
scriptsExecuteCmd.Flags().StringVar(&scriptsDir, "dir", "", "Directory containing SQL scripts (required)")
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)")
err = scriptsExecuteCmd.MarkFlagRequired("dir")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marking dir flag as required: %v\n", err)
}
err = scriptsExecuteCmd.MarkFlagRequired("conn")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marking conn flag as required: %v\n", err)
}
// Add subcommands to scripts command
scriptsCmd.AddCommand(scriptsListCmd)
scriptsCmd.AddCommand(scriptsExecuteCmd)
}
func runScriptsList(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, "\n=== SQL Scripts List ===\n")
fmt.Fprintf(os.Stderr, "Directory: %s\n\n", scriptsDir)
// Read scripts from directory
reader := sqldir.NewReader(&readers.ReaderOptions{
FilePath: scriptsDir,
Metadata: map[string]any{
"schema_name": scriptsSchemaName,
"database_name": scriptsDBName,
},
})
db, err := reader.ReadDatabase()
if err != nil {
return fmt.Errorf("failed to read scripts: %w", err)
}
if len(db.Schemas) == 0 {
fmt.Fprintf(os.Stderr, "No schemas found\n")
return nil
}
schema := db.Schemas[0]
if len(schema.Scripts) == 0 {
fmt.Fprintf(os.Stderr, "No SQL scripts found matching pattern {priority}_{sequence}_{name}.sql\n")
return nil
}
// Sort scripts by Priority then Sequence
sortedScripts := make([]*struct {
name string
priority int
sequence uint
sqlLines int
}, len(schema.Scripts))
for i, script := range schema.Scripts {
// Count non-empty lines in SQL
sqlLines := 0
for _, line := range []byte(script.SQL) {
if line == '\n' {
sqlLines++
}
}
if len(script.SQL) > 0 {
sqlLines++ // Count last line if no trailing newline
}
sortedScripts[i] = &struct {
name string
priority int
sequence uint
sqlLines int
}{
name: script.Name,
priority: script.Priority,
sequence: script.Sequence,
sqlLines: sqlLines,
}
}
sort.Slice(sortedScripts, func(i, j int) bool {
if sortedScripts[i].priority != sortedScripts[j].priority {
return sortedScripts[i].priority < sortedScripts[j].priority
}
return sortedScripts[i].sequence < sortedScripts[j].sequence
})
fmt.Fprintf(os.Stderr, "Found %d script(s) in execution order:\n\n", len(sortedScripts))
fmt.Fprintf(os.Stderr, "%-4s %-10s %-8s %-30s %s\n", "No.", "Priority", "Sequence", "Name", "Lines")
fmt.Fprintf(os.Stderr, "%-4s %-10s %-8s %-30s %s\n", "----", "--------", "--------", "------------------------------", "-----")
for i, script := range sortedScripts {
fmt.Fprintf(os.Stderr, "%-4d %-10d %-8d %-30s %d\n",
i+1,
script.priority,
script.sequence,
script.name,
script.sqlLines,
)
}
fmt.Fprintf(os.Stderr, "\n")
return nil
}
func runScriptsExecute(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, "\n=== SQL Scripts Execution ===\n")
fmt.Fprintf(os.Stderr, "Started at: %s\n", getCurrentTimestamp())
fmt.Fprintf(os.Stderr, "Directory: %s\n", scriptsDir)
fmt.Fprintf(os.Stderr, "Database: %s\n\n", maskPassword(scriptsConn))
// Step 1: Read scripts from directory
fmt.Fprintf(os.Stderr, "[1/2] Reading SQL scripts...\n")
reader := sqldir.NewReader(&readers.ReaderOptions{
FilePath: scriptsDir,
Metadata: map[string]any{
"schema_name": scriptsSchemaName,
"database_name": scriptsDBName,
},
})
db, err := reader.ReadDatabase()
if err != nil {
return fmt.Errorf("failed to read scripts: %w", err)
}
if len(db.Schemas) == 0 {
return fmt.Errorf("no schemas found")
}
schema := db.Schemas[0]
if len(schema.Scripts) == 0 {
fmt.Fprintf(os.Stderr, " No scripts found. Nothing to execute.\n\n")
return nil
}
fmt.Fprintf(os.Stderr, " ✓ Found %d script(s)\n\n", len(schema.Scripts))
// Step 2: Execute scripts
fmt.Fprintf(os.Stderr, "[2/2] Executing scripts in order (Priority → Sequence)...\n\n")
writer := sqlexec.NewWriter(&writers.WriterOptions{
Metadata: map[string]any{
"connection_string": scriptsConn,
},
})
if err := writer.WriteSchema(schema); err != nil {
fmt.Fprintf(os.Stderr, "\n")
return fmt.Errorf("execution failed: %w", err)
}
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))
return nil
}