fix: address audit execution failures and unsafe type conversions
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user