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