fix: address audit execution failures and unsafe type conversions
This commit is contained in:
+16
-16
@@ -502,30 +502,30 @@ func (w *Writer) executeDatabaseSQL(db *models.Database, connString string) erro
|
||||
// Connect to database
|
||||
dbConn, err := sql.Open("mssql", connString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
return writers.FailExecution(w.options, "SQL Server", writers.StatementOperations(statements), fmt.Errorf("failed to connect to database: %w", err))
|
||||
}
|
||||
defer dbConn.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
if err = dbConn.PingContext(ctx); err != nil {
|
||||
return fmt.Errorf("failed to ping database: %w", err)
|
||||
return writers.FailExecution(w.options, "SQL Server", writers.StatementOperations(statements), fmt.Errorf("failed to ping database: %w", err))
|
||||
}
|
||||
|
||||
// Execute statements
|
||||
executedCount := 0
|
||||
for i, stmt := range statements {
|
||||
fmt.Fprintf(os.Stderr, "Executing statement %d/%d...\n", i+1, len(statements))
|
||||
|
||||
if _, execErr := dbConn.ExecContext(ctx, stmt); execErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠ Warning: Statement failed: %v\n", execErr)
|
||||
continue
|
||||
}
|
||||
|
||||
executedCount++
|
||||
var tx *sql.Tx
|
||||
backend := writers.ExecutionBackend{
|
||||
Exec: func(ctx context.Context, _ int, stmt string) error {
|
||||
if tx != nil {
|
||||
_, err := tx.ExecContext(ctx, stmt)
|
||||
return err
|
||||
}
|
||||
_, err := dbConn.ExecContext(ctx, stmt)
|
||||
return err
|
||||
},
|
||||
Begin: func(ctx context.Context) error { var err error; tx, err = dbConn.BeginTx(ctx, nil); return err },
|
||||
Commit: func(context.Context) error { return tx.Commit() },
|
||||
Rollback: func(context.Context) error { return tx.Rollback() },
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "✓ Successfully executed %d statements\n", executedCount)
|
||||
return nil
|
||||
return writers.RunExecution(ctx, w.options, &writers.ExecutionReport{Dialect: "SQL Server"}, writers.StatementOperations(statements), backend)
|
||||
}
|
||||
|
||||
// generateStatements renders the same script WriteDatabase would produce and
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package mssql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
)
|
||||
|
||||
func TestLive_ExecutionFailureModes(t *testing.T) {
|
||||
connection := os.Getenv("RELSPEC_TEST_MSSQL_CONN")
|
||||
if connection == "" {
|
||||
t.Skip("RELSPEC_TEST_MSSQL_CONN not set")
|
||||
}
|
||||
conn, err := sql.Open("mssql", connection)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
for _, mode := range []string{"stop", "continue", "atomic"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
prefix := fmt.Sprintf("audit_%d_", time.Now().UnixNano())
|
||||
db := models.InitDatabase("audit")
|
||||
schema := models.InitSchema("dbo")
|
||||
for _, name := range []string{"first", "bad", "last"} {
|
||||
table := models.InitTable(prefix+name, "dbo")
|
||||
col := models.InitColumn("id", table.Name, "dbo")
|
||||
col.Type = "integer"
|
||||
if name == "bad" {
|
||||
duplicate := models.InitColumn("id", table.Name, "dbo")
|
||||
duplicate.Type = "integer"
|
||||
table.Columns["duplicate"] = duplicate
|
||||
}
|
||||
table.Columns[col.Name] = col
|
||||
schema.Tables = append(schema.Tables, table)
|
||||
}
|
||||
db.Schemas = append(db.Schemas, schema)
|
||||
t.Cleanup(func() {
|
||||
for _, name := range []string{"first", "bad", "last"} {
|
||||
_, _ = conn.Exec("DROP TABLE IF EXISTS dbo." + prefix + name)
|
||||
}
|
||||
})
|
||||
opts := &writers.WriterOptions{ContinueOnError: mode == "continue", AtomicExecution: mode == "atomic", Metadata: map[string]interface{}{"connection_string": connection}}
|
||||
err := NewWriter(opts).WriteDatabase(db)
|
||||
var failure *writers.ExecutionFailure
|
||||
if !errors.As(err, &failure) || failure.Report.FailedStatements == 0 {
|
||||
t.Fatalf("expected structured error: %v", err)
|
||||
}
|
||||
for _, name := range []string{"first", "last"} {
|
||||
var count int
|
||||
if err := conn.QueryRow("SELECT count(*) FROM sys.tables WHERE name=?", prefix+name).Scan(&count); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := mode != "atomic" && (name == "first" || mode == "continue")
|
||||
if (count == 1) != want {
|
||||
t.Fatalf("%s exists=%v want=%v", name, count == 1, want)
|
||||
}
|
||||
}
|
||||
if mode == "atomic" && (failure.Report.Outcome != "rolled_back" || failure.Report.RolledBackStatements == 0 || failure.Report.ExecutedStatements != 0) {
|
||||
t.Fatalf("incorrect rollback report: %+v", failure.Report)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user