From ade71b75fa5a4b5e37e3e824ae027163348be1d9 Mon Sep 17 00:00:00 2001 From: SG Command Date: Sun, 20 Sep 2026 23:16:41 +0200 Subject: [PATCH] fix(jobs): preserve writer and template output options --- cmd/relspec/job.go | 4 +-- cmd/relspec/job_test.go | 59 +++++++++++++++++++++++++++++++ docs/JOB_FILES.md | 8 +++-- examples/jobs/relspec.reports.yml | 1 + pkg/jobs/jobs.go | 14 +++++--- 5 files changed, 78 insertions(+), 8 deletions(-) diff --git a/cmd/relspec/job.go b/cmd/relspec/job.go index 816d6a2..cbacd6b 100644 --- a/cmd/relspec/job.go +++ b/cmd/relspec/job.go @@ -841,7 +841,7 @@ func writeJobOutput(rj *resolvedJob, db *models.Database, lg *jobLogger) error { return fmt.Errorf("database output is only supported for pgsql (got %q)", rj.job.Output.Format) } lg.logf("writing output to database env:%s", rj.outputConnEnv) - writerOpts := newWriterOptions("", o.Package, o.FlattenSchema, "", "", o.ContinueOnError) + writerOpts := newWriterOptions("", o.Package, o.FlattenSchema, o.Types, o.ArrayNullable, o.ContinueOnError) writerOpts.Metadata = map[string]interface{}{"connection_string": rj.outputConn} return wpgsql.NewWriter(writerOpts).WriteDatabase(db) } @@ -852,7 +852,7 @@ func writeJobOutput(rj *resolvedJob, db *models.Database, lg *jobLogger) error { lg.logf("writing output: %s (%s)", rj.outputPath, format) write := func(target string) error { - return writeDatabase(db, format, target, o.Package, o.Schema, o.FlattenSchema, "", "", o.ContinueOnError, "") + return writeDatabase(db, format, target, o.Package, o.Schema, o.FlattenSchema, o.Types, o.ArrayNullable, o.ContinueOnError, "") } // Single-file formats are written to a temp file and renamed into place so // a failure never leaves a partial or truncated output. Directory-emitting diff --git a/cmd/relspec/job_test.go b/cmd/relspec/job_test.go index fc7938d..38484bf 100644 --- a/cmd/relspec/job_test.go +++ b/cmd/relspec/job_test.go @@ -666,6 +666,7 @@ jobs: format: dbml template: templates/schema.tmpl output: + format: text path: build/schema.txt overwrite: true `) @@ -682,3 +683,61 @@ jobs: t.Fatalf("templ output missing users table: %s", out) } } + +func TestJobRun_BunSqlTypesOption(t *testing.T) { + dir := jobFixture(t, `version: 1 +jobs: + models: + command: convert + inputs: + - path: schema/core.dbml + format: dbml + output: + format: bun + path: build/models + options: + package: models + types: sqltypes +`) + set := mustLoadSet(t, filepath.Join(dir, "relspec.yml")) + if err := executeJobPlan(set, "models", false, false, &bytes.Buffer{}); err != nil { + t.Fatalf("execute Bun job: %v", err) + } + generated, err := os.ReadFile(filepath.Join(dir, "build", "models")) + if err != nil { + t.Fatalf("read Bun output: %v", err) + } + if !strings.Contains(string(generated), "git.warky.dev/wdevs/relspecgo/pkg/sqltypes") { + t.Fatalf("Bun output did not preserve options.types: %s", generated) + } +} + +func TestJobValidation_TemplTextFormat(t *testing.T) { + validDir := jobFixture(t, `version: 1 +jobs: + docs: + command: templ + inputs: [{path: schema/core.dbml, format: dbml}] + template: schema.tmpl + output: {format: text, path: build/docs.txt} +`) + valid := mustLoadSet(t, filepath.Join(validDir, "relspec.yml")) + if err := valid.Validate(); err != nil { + t.Fatalf("text output should be valid for templ: %v", err) + } + invalidDir := jobFixture(t, `version: 1 +jobs: + docs: + command: templ + inputs: [{path: schema/core.dbml, format: dbml}] + template: schema.tmpl + output: {format: json, path: build/docs.txt} +`) + invalid, err := jobs.Load([]string{filepath.Join(invalidDir, "relspec.yml")}) + if err != nil { + t.Fatalf("load invalid manifest: %v", err) + } + if err := invalid.Validate(); err == nil || !strings.Contains(err.Error(), "output.format: text") { + t.Fatalf("expected templ format rejection, got %v", err) + } +} diff --git a/docs/JOB_FILES.md b/docs/JOB_FILES.md index 4f911be..1a776ab 100644 --- a/docs/JOB_FILES.md +++ b/docs/JOB_FILES.md @@ -192,6 +192,8 @@ jobs: flatten_schema: false schema: public package: models # for gorm/bun output + types: sqltypes # Bun/GORM nullable types: baselib|stdlib|sqltypes + array_nullable: pointer_slice # Bun: slice|pointer_slice continue_on_error: false # pgsql / scripts-exec output skip_relations: false # merge only skip_enums: false @@ -205,8 +207,10 @@ jobs: For `templ`, `inputs` use the same file or `pgsql`/`conn_env` source forms as schema conversion. `output` is optional (empty means stdout); when present it -contains only `path` and `overwrite`, because templates do not select a schema -writer format. +contains `path` and `overwrite`, plus the explicit `format: text` marker. The +marker is templ-only: it describes arbitrary text produced by a Go template, +not a database schema format. It is optional for compatibility with older +manifests and has no effect on template execution. A `from_job` input takes no `path`, `format` or `conn_env`: it resolves to the named job's `output.path` and inherits its format, and implies a dependency on diff --git a/examples/jobs/relspec.reports.yml b/examples/jobs/relspec.reports.yml index 16b53e8..55b8a2d 100644 --- a/examples/jobs/relspec.reports.yml +++ b/examples/jobs/relspec.reports.yml @@ -15,6 +15,7 @@ jobs: mode: table filename_pattern: "{{.Name}}.ts" output: + format: text path: "${VST}" overwrite: true diff --git a/pkg/jobs/jobs.go b/pkg/jobs/jobs.go index c57f771..b81dc7e 100644 --- a/pkg/jobs/jobs.go +++ b/pkg/jobs/jobs.go @@ -277,9 +277,13 @@ func parseHumanSize(s string) (int64, error) { // Options carries the subset of command flags a job file may set. type Options struct { - FlattenSchema bool `yaml:"flatten_schema"` - Schema string `yaml:"schema"` - Package string `yaml:"package"` + FlattenSchema bool `yaml:"flatten_schema"` + Schema string `yaml:"schema"` + Package string `yaml:"package"` + // Types and ArrayNullable mirror the existing convert/split writer flags. + // They are passed through only to writers that already support them. + Types string `yaml:"types"` + ArrayNullable string `yaml:"array_nullable"` ContinueOnError bool `yaml:"continue_on_error"` SkipRelations bool `yaml:"skip_relations"` SkipEnums bool `yaml:"skip_enums"` @@ -569,7 +573,9 @@ func (j *Job) validate() []string { e = append(e, "command \"templ\" does not support database output") } if j.Output != nil && j.Output.Format != "" { - e = append(e, "output.format is not valid for command \"templ\"") + if strings.ToLower(j.Output.Format) != "text" { + e = append(e, "command \"templ\" accepts only output.format: text") + } } case CommandSplit: if len(j.Inputs) < 1 {