fix: address audit execution failures and unsafe type conversions

This commit is contained in:
2026-10-12 19:53:19 +02:00
parent bfe9852826
commit 2fa48335d3
29 changed files with 1630 additions and 421 deletions
+29 -30
View File
@@ -117,50 +117,49 @@ func (c *statementCollector) Write(p []byte) (int, error) {
// against the SQLite database file at dbPath.
func (w *Writer) executeDatabaseSQL(db *models.Database, dbPath string) error {
collector := &statementCollector{}
saved := w.writer
w.writer = collector
defer func() { w.writer = saved }()
if err := w.writeContent(db); err != nil {
return fmt.Errorf("failed to generate SQL statements: %w", err)
}
conn, err := sql.Open("sqlite", dbPath)
if err != nil {
return fmt.Errorf("failed to open sqlite database %q: %w", dbPath, err)
return writers.FailExecution(w.options, "SQLite", writers.StatementOperations(collector.statements), fmt.Errorf("failed to open sqlite database %q: %w", dbPath, err))
}
defer conn.Close()
// Pin one connection so connection-local settings (including foreign_keys) persist.
ctx := context.Background()
ignoreErrors := false
if val, ok := w.options.Metadata["ignore_errors"].(bool); ok {
ignoreErrors = val
session, err := conn.Conn(ctx)
if err != nil {
return writers.FailExecution(w.options, "SQLite", writers.StatementOperations(collector.statements), err)
}
total, executed := 0, 0
var execErrors []string
for _, stmt := range collector.statements {
if strings.HasPrefix(stmt, "--") {
continue
}
total++
if _, err := conn.ExecContext(ctx, stmt); err != nil {
execErrors = append(execErrors, fmt.Sprintf("statement %d (%s): %v", total, truncateStatement(stmt), err))
if !ignoreErrors {
break
defer session.Close()
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
}
continue
}
executed++
_, err := session.ExecContext(ctx, stmt)
return err
},
Begin: func(ctx context.Context) error {
// SQLite ignores foreign_keys changes inside transactions.
if _, err := session.ExecContext(ctx, "PRAGMA foreign_keys=ON"); err != nil {
return err
}
var err error
tx, err = session.BeginTx(ctx, nil)
return err
},
Commit: func(context.Context) error { return tx.Commit() },
Rollback: func(context.Context) error { return tx.Rollback() },
}
w.options.Metadata["execution_total"] = total
w.options.Metadata["execution_success"] = executed
w.options.Metadata["execution_failed"] = len(execErrors)
if len(execErrors) > 0 {
return fmt.Errorf("failed to execute %d/%d statement(s) against %q:\n%s", len(execErrors), total, dbPath, strings.Join(execErrors, "\n"))
}
return nil
return writers.RunExecution(ctx, w.options, &writers.ExecutionReport{Dialect: "SQLite"}, writers.StatementOperations(collector.statements), backend)
}
// truncateStatement shortens a SQL statement for error messages.
+35 -1
View File
@@ -2,6 +2,7 @@ package sqlite
import (
"database/sql"
"errors"
"os"
"path/filepath"
"strings"
@@ -182,7 +183,7 @@ func TestExecute_StopsOnErrorUnlessIgnored(t *testing.T) {
path := prepare(t)
opts := &writers.WriterOptions{Metadata: map[string]any{"connection_string": path}}
err := NewWriter(opts).WriteDatabase(shopDB())
if err == nil || !strings.Contains(err.Error(), "failed to execute") {
if err == nil || !strings.Contains(err.Error(), "execution failed") {
t.Fatalf("expected failure, got %v", err)
}
if opts.Metadata["execution_failed"] != 1 {
@@ -248,3 +249,36 @@ func TestDBMLFixtureExecutes(t *testing.T) {
t.Errorf("nothing executed: %+v", opts.Metadata)
}
}
func TestExecuteAtomicRollback(t *testing.T) {
path := filepath.Join(t.TempDir(), "atomic.db")
opts := &writers.WriterOptions{AtomicExecution: true, Metadata: map[string]any{"connection_string": path}}
db := models.InitDatabase("atomic")
schema := models.InitSchema("main")
for _, name := range []string{"first", "bad", "last"} {
table := models.InitTable(name, "main")
col := models.InitColumn("id", name, "main")
col.Type = "integer"
table.Columns[col.Name] = col
schema.Tables = append(schema.Tables, table)
}
// A duplicate column creates an invalid table definition in the middle.
bad := schema.Tables[1]
bad.Columns["duplicate"] = models.InitColumn("id", "bad", "main")
bad.Columns["duplicate"].Type = "integer"
db.Schemas = append(db.Schemas, schema)
err := NewWriter(opts).WriteDatabase(db)
var failure *writers.ExecutionFailure
if !errors.As(err, &failure) || failure.Report.Outcome != "rolled_back" || failure.Report.RolledBackStatements == 0 || failure.Report.ExecutedStatements != 0 {
t.Fatalf("rollback report: %v", err)
}
conn, err := sql.Open("sqlite", path)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
var count int
if err := conn.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table'").Scan(&count); err != nil || count != 0 {
t.Fatalf("tables survived rollback: %d %v", count, err)
}
}