feat(pgsql): add execution reporting for SQL statements
All checks were successful
CI / Test (1.24) (push) Successful in -25m29s
CI / Test (1.25) (push) Successful in -25m13s
CI / Lint (push) Successful in -26m13s
CI / Build (push) Successful in -26m27s
Integration Tests / Integration Tests (push) Successful in -26m11s
Release / Build and Release (push) Successful in -25m8s
All checks were successful
CI / Test (1.24) (push) Successful in -25m29s
CI / Test (1.25) (push) Successful in -25m13s
CI / Lint (push) Successful in -26m13s
CI / Build (push) Successful in -26m27s
Integration Tests / Integration Tests (push) Successful in -26m11s
Release / Build and Release (push) Successful in -25m8s
- Implemented ExecutionReport to track the execution status of SQL statements. - Added SchemaReport and TableReport to monitor execution per schema and table. - Enhanced WriteDatabase to execute SQL directly on a PostgreSQL database if a connection string is provided. - Included error handling and logging for failed statements during execution. - Added functionality to write execution reports to a JSON file. - Introduced utility functions to extract table names from CREATE TABLE statements and truncate long SQL statements for error messages.
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
@@ -13,8 +18,40 @@ import (
|
||||
|
||||
// Writer implements the Writer interface for PostgreSQL SQL output
|
||||
type Writer struct {
|
||||
options *writers.WriterOptions
|
||||
writer io.Writer
|
||||
options *writers.WriterOptions
|
||||
writer io.Writer
|
||||
executionReport *ExecutionReport
|
||||
}
|
||||
|
||||
// ExecutionReport tracks the execution status of SQL statements
|
||||
type ExecutionReport struct {
|
||||
TotalStatements int `json:"total_statements"`
|
||||
ExecutedStatements int `json:"executed_statements"`
|
||||
FailedStatements int `json:"failed_statements"`
|
||||
Schemas []SchemaReport `json:"schemas"`
|
||||
Errors []ExecutionError `json:"errors,omitempty"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
}
|
||||
|
||||
// SchemaReport tracks execution per schema
|
||||
type SchemaReport struct {
|
||||
Name string `json:"name"`
|
||||
Tables []TableReport `json:"tables"`
|
||||
}
|
||||
|
||||
// TableReport tracks execution per table
|
||||
type TableReport struct {
|
||||
Name string `json:"name"`
|
||||
Created bool `json:"created"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ExecutionError represents a failed statement
|
||||
type ExecutionError struct {
|
||||
StatementNumber int `json:"statement_number"`
|
||||
Statement string `json:"statement"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// NewWriter creates a new PostgreSQL SQL writer
|
||||
@@ -26,6 +63,11 @@ func NewWriter(options *writers.WriterOptions) *Writer {
|
||||
|
||||
// WriteDatabase writes the entire database schema as SQL
|
||||
func (w *Writer) WriteDatabase(db *models.Database) error {
|
||||
// Check if we should execute SQL directly on a database
|
||||
if connString, ok := w.options.Metadata["connection_string"].(string); ok && connString != "" {
|
||||
return w.executeDatabaseSQL(db, connString)
|
||||
}
|
||||
|
||||
var writer io.Writer
|
||||
var file *os.File
|
||||
var err error
|
||||
@@ -849,3 +891,195 @@ func extractSequenceName(defaultExpr string) string {
|
||||
}
|
||||
return fullName
|
||||
}
|
||||
|
||||
// executeDatabaseSQL executes SQL statements directly on a PostgreSQL database
|
||||
func (w *Writer) executeDatabaseSQL(db *models.Database, connString string) error {
|
||||
// Initialize execution report
|
||||
w.executionReport = &ExecutionReport{
|
||||
StartTime: getCurrentTimestamp(),
|
||||
Schemas: make([]SchemaReport, 0),
|
||||
Errors: make([]ExecutionError, 0),
|
||||
}
|
||||
|
||||
// Generate SQL statements
|
||||
statements, err := w.GenerateDatabaseStatements(db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate SQL statements: %w", err)
|
||||
}
|
||||
|
||||
w.executionReport.TotalStatements = len(statements)
|
||||
|
||||
// Connect to database
|
||||
ctx := context.Background()
|
||||
conn, err := pgx.Connect(ctx, connString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
defer conn.Close(ctx)
|
||||
|
||||
// Track schemas and tables
|
||||
schemaMap := make(map[string]*SchemaReport)
|
||||
currentSchema := ""
|
||||
|
||||
// Execute each statement
|
||||
for i, stmt := range statements {
|
||||
stmtTrimmed := strings.TrimSpace(stmt)
|
||||
|
||||
// Skip comments
|
||||
if strings.HasPrefix(stmtTrimmed, "--") {
|
||||
// Check if this is a schema comment to track schema changes
|
||||
if strings.Contains(stmtTrimmed, "Schema:") {
|
||||
parts := strings.Split(stmtTrimmed, "Schema:")
|
||||
if len(parts) > 1 {
|
||||
currentSchema = strings.TrimSpace(parts[1])
|
||||
if _, exists := schemaMap[currentSchema]; !exists {
|
||||
schemaReport := SchemaReport{
|
||||
Name: currentSchema,
|
||||
Tables: make([]TableReport, 0),
|
||||
}
|
||||
schemaMap[currentSchema] = &schemaReport
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip empty statements
|
||||
if stmtTrimmed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Executing statement %d/%d...\n", i+1, len(statements))
|
||||
|
||||
_, execErr := conn.Exec(ctx, stmt)
|
||||
if execErr != nil {
|
||||
w.executionReport.FailedStatements++
|
||||
execError := ExecutionError{
|
||||
StatementNumber: i + 1,
|
||||
Statement: truncateStatement(stmt),
|
||||
Error: execErr.Error(),
|
||||
}
|
||||
w.executionReport.Errors = append(w.executionReport.Errors, execError)
|
||||
|
||||
// Track table creation failure
|
||||
if strings.Contains(strings.ToUpper(stmtTrimmed), "CREATE TABLE") && currentSchema != "" {
|
||||
tableName := extractTableNameFromCreate(stmtTrimmed)
|
||||
if tableName != "" && schemaMap[currentSchema] != nil {
|
||||
schemaMap[currentSchema].Tables = append(schemaMap[currentSchema].Tables, TableReport{
|
||||
Name: tableName,
|
||||
Created: false,
|
||||
Error: execErr.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with next statement instead of failing completely
|
||||
fmt.Fprintf(os.Stderr, "⚠ Warning: Statement %d failed: %v\n", i+1, execErr)
|
||||
continue
|
||||
}
|
||||
|
||||
w.executionReport.ExecutedStatements++
|
||||
|
||||
// Track successful table creation
|
||||
if strings.Contains(strings.ToUpper(stmtTrimmed), "CREATE TABLE") && currentSchema != "" {
|
||||
tableName := extractTableNameFromCreate(stmtTrimmed)
|
||||
if tableName != "" && schemaMap[currentSchema] != nil {
|
||||
schemaMap[currentSchema].Tables = append(schemaMap[currentSchema].Tables, TableReport{
|
||||
Name: tableName,
|
||||
Created: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert schema map to slice
|
||||
for _, schemaReport := range schemaMap {
|
||||
w.executionReport.Schemas = append(w.executionReport.Schemas, *schemaReport)
|
||||
}
|
||||
|
||||
w.executionReport.EndTime = getCurrentTimestamp()
|
||||
|
||||
// Write report if path is specified
|
||||
if reportPath, ok := w.options.Metadata["report_path"].(string); ok && reportPath != "" {
|
||||
if err := w.writeReport(reportPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠ Warning: Failed to write report: %v\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "✓ Report written to: %s\n", reportPath)
|
||||
}
|
||||
}
|
||||
|
||||
if w.executionReport.FailedStatements > 0 {
|
||||
fmt.Fprintf(os.Stderr, "⚠ Completed with %d errors out of %d statements\n",
|
||||
w.executionReport.FailedStatements, w.executionReport.TotalStatements)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "✓ Successfully executed %d statements\n", w.executionReport.ExecutedStatements)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeReport writes the execution report to a JSON file
|
||||
func (w *Writer) writeReport(reportPath string) error {
|
||||
file, err := os.Create(reportPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create report file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(w.executionReport); err != nil {
|
||||
return fmt.Errorf("failed to encode report: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractTableNameFromCreate extracts table name from CREATE TABLE statement
|
||||
func extractTableNameFromCreate(stmt string) string {
|
||||
// Match: CREATE TABLE [IF NOT EXISTS] schema.table_name or table_name
|
||||
upper := strings.ToUpper(stmt)
|
||||
idx := strings.Index(upper, "CREATE TABLE")
|
||||
if idx == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
rest := strings.TrimSpace(stmt[idx+12:]) // Skip "CREATE TABLE"
|
||||
|
||||
// Skip "IF NOT EXISTS"
|
||||
if strings.HasPrefix(strings.ToUpper(rest), "IF NOT EXISTS") {
|
||||
rest = strings.TrimSpace(rest[13:])
|
||||
}
|
||||
|
||||
// Get the table name (first token before '(' or whitespace)
|
||||
tokens := strings.FieldsFunc(rest, func(r rune) bool {
|
||||
return r == '(' || r == ' ' || r == '\n' || r == '\t'
|
||||
})
|
||||
|
||||
if len(tokens) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Handle schema.table format
|
||||
fullName := tokens[0]
|
||||
parts := strings.Split(fullName, ".")
|
||||
if len(parts) > 1 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
return fullName
|
||||
}
|
||||
|
||||
// truncateStatement truncates long SQL statements for error messages
|
||||
func truncateStatement(stmt string) string {
|
||||
const maxLen = 200
|
||||
if len(stmt) <= maxLen {
|
||||
return stmt
|
||||
}
|
||||
return stmt[:maxLen] + "..."
|
||||
}
|
||||
|
||||
// getCurrentTimestamp returns the current timestamp in a readable format
|
||||
func getCurrentTimestamp() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user