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
+1
View File
@@ -68,6 +68,7 @@ func init() {
batchCmd.Flags().StringVar(&batchNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm)")
batchCmd.Flags().StringVar(&batchNullableArrays, "array-nullable", "", "Nullable array representation for the Bun writer")
batchCmd.Flags().BoolVar(&batchContinueOnError, "continue-on-error", false, "Prepend \\set ON_ERROR_STOP off to generated SQL (pgsql output only)")
batchCmd.Flags().BoolVar(&batchContinueOnError, "coe", false, "Alias for --continue-on-error")
batchCmd.Flags().BoolVar(&batchKeepGoing, "keep-going", false, "Process remaining inputs after a failure; still exits non-zero")
batchCmd.Flags().BoolVar(&batchDryRun, "dry-run", false, "Read and validate every input and print the plan without writing any output")
+1
View File
@@ -193,6 +193,7 @@ func init() {
convertCmd.Flags().StringVar(&convertNullableTypes, "types", "", "Nullable type package for code-gen writers (bun/gorm): 'baselib' (default, Go pointer types), 'stdlib' (database/sql), or 'sqltypes'")
convertCmd.Flags().StringVar(&convertNullableArrays, "array-nullable", "", "Nullable PostgreSQL array representation for the Bun writer in stdlib/baselib --types mode: 'slice' (default, plain slice) or 'pointer_slice' (*[]T, distinguishes NULL from '{}')")
convertCmd.Flags().BoolVar(&convertContinueOnError, "continue-on-error", false, "Prepend \\set ON_ERROR_STOP off to generated SQL so psql continues past errors (pgsql output only)")
convertCmd.Flags().BoolVar(&convertContinueOnError, "coe", false, "Alias for --continue-on-error")
convertCmd.Flags().StringVar(&convertExtraFields, "extra-fields", "", "Path to JSON file containing extra Bun model fields to inject (bun output only); fields support target_table, name, type, bun_tag, json_tag, comment")
convertCmd.Flags().BoolVar(&convertDryRun, "dry-run", false, "Read and validate the input and print the plan without writing any output")
+4 -1
View File
@@ -777,6 +777,8 @@ func runScriptsExecJob(rj *resolvedJob, lg *jobLogger) error {
lg.logf("executing %d script(s) against database env:%s", len(combined.Scripts), rj.outputConnEnv)
writer := sqlexec.NewWriter(&writers.WriterOptions{
ContinueOnError: rj.job.Options.ContinueOnError,
AtomicExecution: rj.job.Options.AtomicExecution,
Metadata: map[string]any{
"connection_string": rj.outputConn,
"ignore_errors": rj.job.Options.ContinueOnError,
@@ -791,7 +793,7 @@ func runScriptsExecJob(rj *resolvedJob, lg *jobLogger) error {
success, _ := opts.Metadata["execution_success"].(int)
failed, _ := opts.Metadata["execution_failed"].(int)
lg.logf("executed %d script(s): %d succeeded, %d failed", total, success, failed)
if failed > 0 && !rj.job.Options.ContinueOnError {
if failed > 0 {
return fmt.Errorf("%d script(s) failed", failed)
}
return nil
@@ -844,6 +846,7 @@ func writeJobOutput(rj *resolvedJob, db *models.Database, lg *jobLogger) error {
}
lg.logf("writing output to database env:%s", rj.outputConnEnv)
writerOpts := newWriterOptions("", o.Package, o.FlattenSchema, o.Types, o.ArrayNullable, o.ContinueOnError)
writerOpts.AtomicExecution = o.AtomicExecution
writerOpts.Metadata = map[string]interface{}{"connection_string": rj.outputConn, "full_ddl": o.FullDDL}
return wpgsql.NewWriter(writerOpts).WriteDatabase(db)
}
+105
View File
@@ -2,11 +2,15 @@ package main
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/spf13/cobra"
"git.warky.dev/wdevs/relspecgo/pkg/jobs"
@@ -741,3 +745,104 @@ jobs:
t.Fatalf("expected templ format rejection, got %v", err)
}
}
func TestLive_JobPostgresFailureStopsDependents(t *testing.T) {
connString := os.Getenv("RELSPEC_TEST_PG_CONN")
if connString == "" {
t.Skip("RELSPEC_TEST_PG_CONN not set")
}
t.Setenv("RELSPEC_TEST_JOB_OUTPUT_CONN", connString)
for _, continueOnError := range []string{"false", "true"} {
t.Run("continue="+continueOnError, func(t *testing.T) {
manifest := `version: 1
jobs:
apply:
command: convert
inputs:
- path: schema/core.dbml
format: dbml
output:
format: pgsql
conn_env: RELSPEC_TEST_JOB_OUTPUT_CONN
options:
full_ddl: true
continue_on_error: ` + continueOnError + `
logfile: .relspec/apply.log
dependent:
command: convert
depends_on: [apply]
inputs:
- path: schema/core.dbml
format: dbml
output:
format: json
path: build/dependent.json
logfile: .relspec/dependent.log
`
dir := jobFixture(t, manifest)
schema := fmt.Sprintf("job_audit_%d", time.Now().UnixNano())
conn, err := pgx.Connect(context.Background(), connString)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_, _ = conn.Exec(context.Background(), "DROP SCHEMA IF EXISTS "+schema+" CASCADE")
_ = conn.Close(context.Background())
})
// An invalid column type prevents table creation in this isolated schema.
writeFile(t, filepath.Join(dir, "schema", "core.dbml"), "Table "+schema+".users {\n id no_such_type_audit_xyz\n}\n")
set := mustLoadSet(t, filepath.Join(dir, "relspec.yml"))
err = executeJobPlan(set, "dependent", false, false, &bytes.Buffer{})
if err == nil || !strings.Contains(err.Error(), `job "apply" failed`) {
t.Fatalf("expected database failure propagation, got %v", err)
}
for _, path := range []string{"build/dependent.json", ".relspec/dependent.log"} {
if _, err := os.Stat(filepath.Join(dir, path)); !os.IsNotExist(err) {
t.Fatalf("dependent must not start: path=%s err=%v", path, err)
}
}
logData, err := os.ReadFile(filepath.Join(dir, ".relspec/apply.log"))
if err != nil || !strings.Contains(string(logData), "FAILED") {
t.Fatalf("failed application must be logged: %s err=%v", logData, err)
}
})
}
}
func TestLive_JobScriptContinuationStillStopsDependents(t *testing.T) {
connection := os.Getenv("RELSPEC_TEST_PG_CONN")
if connection == "" {
t.Skip("RELSPEC_TEST_PG_CONN not set")
}
t.Setenv("RELSPEC_TEST_EXEC_CONN", connection)
dir := jobFixture(t, `version: 1
jobs:
migrate:
command: scripts-exec
script_dirs: [migrations]
output:
conn_env: RELSPEC_TEST_EXEC_CONN
options:
continue_on_error: true
logfile: .relspec/migrate.log
dependent:
command: convert
depends_on: [migrate]
inputs:
- path: schema/core.dbml
format: dbml
output:
format: json
path: build/dependent.json
`)
writeFile(t, filepath.Join(dir, "migrations", "1_001_bad.sql"), "SELECT 1 / 0;\n")
writeFile(t, filepath.Join(dir, "migrations", "1_002_good.sql"), "SELECT 1;\n")
set := mustLoadSet(t, filepath.Join(dir, "relspec.yml"))
err := executeJobPlan(set, "dependent", false, false, &bytes.Buffer{})
if err == nil || !strings.Contains(err.Error(), `job "migrate" failed`) {
t.Fatalf("failed scripts must fail job: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, "build", "dependent.json")); !os.IsNotExist(err) {
t.Fatalf("dependent ran: %v", err)
}
}
+3 -1
View File
@@ -63,7 +63,7 @@ var scriptsExecuteCmd = &cobra.Command{
Long: `Execute SQL scripts from a directory against a PostgreSQL database.
Scripts are executed in order: Priority (ascending), Sequence (ascending), Name (alphabetical).
By default, execution stops immediately on the first error. Use --ignore-errors to continue execution.
By default, execution stops immediately on the first error. Use --ignore-errors (or --continue-on-error / --coe) to attempt later scripts; any failure still exits nonzero.
The directory is scanned recursively for all subdirectories and files matching the patterns:
{priority}_{sequence}_{name}.sql or .pgsql (underscore format)
@@ -112,6 +112,8 @@ func init() {
scriptsExecuteCmd.Flags().StringVar(&scriptsSchemaName, "schema", "public", "Schema name (optional, default: public)")
scriptsExecuteCmd.Flags().StringVar(&scriptsDBName, "database", "database", "Database name (optional, default: database)")
scriptsExecuteCmd.Flags().BoolVar(&scriptsIgnoreErrors, "ignore-errors", false, "Continue executing scripts even if errors occur")
scriptsExecuteCmd.Flags().BoolVar(&scriptsIgnoreErrors, "continue-on-error", false, "Attempt later scripts after failures; still exit nonzero")
scriptsExecuteCmd.Flags().BoolVar(&scriptsIgnoreErrors, "coe", false, "Alias for --continue-on-error")
err = scriptsExecuteCmd.MarkFlagRequired("dir")
if err != nil {