264 lines
8.1 KiB
Go
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
|
|
}
|