Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d57c947cd | ||
|
|
cdd066dafe | ||
|
|
5c25f333b7 | ||
|
|
77e6e72f5a | ||
|
|
4d4bc09b86 |
@@ -23,10 +23,16 @@ jobs:
|
|||||||
- name: go vet
|
- name: go vet
|
||||||
run: go vet ./...
|
run: go vet ./...
|
||||||
|
|
||||||
- name: gofumpt (golangci-lint fmt)
|
- name: Install lint tools
|
||||||
run: |
|
run: |
|
||||||
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||||
diff=$(golangci-lint fmt --diff 2>&1)
|
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: gofumpt (golangci-lint fmt)
|
||||||
|
run: |
|
||||||
|
diff=$(golangci-lint fmt --diff)
|
||||||
if [ -n "$diff" ]; then
|
if [ -n "$diff" ]; then
|
||||||
echo "$diff"
|
echo "$diff"
|
||||||
echo "Formatting issues found. Run: make fmt"
|
echo "Formatting issues found. Run: make fmt"
|
||||||
@@ -34,14 +40,10 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: staticcheck
|
- name: staticcheck
|
||||||
run: |
|
run: staticcheck ./...
|
||||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
|
||||||
staticcheck ./...
|
|
||||||
|
|
||||||
- name: govulncheck
|
- name: govulncheck
|
||||||
run: |
|
run: govulncheck ./...
|
||||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
||||||
govulncheck ./...
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ GOGET=$(GOCMD) get
|
|||||||
GOMOD=$(GOCMD) mod
|
GOMOD=$(GOCMD) mod
|
||||||
GOCLEAN=$(GOCMD) clean
|
GOCLEAN=$(GOCMD) clean
|
||||||
|
|
||||||
# Resolve Go tool binaries (GOPATH/bin may not be on PATH in CI)
|
# Tool versions (compiled on demand via `go run` so they match the local toolchain)
|
||||||
GOBIN_DIR := $(shell $(GOCMD) env GOPATH)/bin
|
GOLANGCI_LINT = go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||||
TOOL = $(if $(shell command -v $(1) 2>/dev/null),$(1),$(GOBIN_DIR)/$(1))
|
STATICCHECK = go run honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
|
GOVULNCHECK = go run golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
|
||||||
# Version information
|
# Version information
|
||||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||||
@@ -53,24 +54,20 @@ vet: ## Run go vet
|
|||||||
|
|
||||||
fmt: ## Format code (gofumpt + goimports via golangci-lint)
|
fmt: ## Format code (gofumpt + goimports via golangci-lint)
|
||||||
@echo "Formatting..."
|
@echo "Formatting..."
|
||||||
@command -v golangci-lint > /dev/null || test -x $(GOBIN_DIR)/golangci-lint || $(GOCMD) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
$(GOLANGCI_LINT) fmt --config=.golangci.json
|
||||||
$(call TOOL,golangci-lint) fmt --config=.golangci.json
|
|
||||||
|
|
||||||
fmt-check: ## Check formatting (gofumpt + goimports via golangci-lint)
|
fmt-check: ## Check formatting (gofumpt + goimports via golangci-lint)
|
||||||
@echo "Checking formatting..."
|
@echo "Checking formatting..."
|
||||||
@command -v golangci-lint > /dev/null || test -x $(GOBIN_DIR)/golangci-lint || $(GOCMD) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
@diff=$$($(GOLANGCI_LINT) fmt --diff --config=.golangci.json); \
|
||||||
@diff=$$($(call TOOL,golangci-lint) fmt --diff --config=.golangci.json 2>&1); \
|
|
||||||
if [ -n "$$diff" ]; then echo "$$diff"; echo "Run: make fmt"; exit 1; fi
|
if [ -n "$$diff" ]; then echo "$$diff"; echo "Run: make fmt"; exit 1; fi
|
||||||
|
|
||||||
staticcheck: ## Run staticcheck
|
staticcheck: ## Run staticcheck
|
||||||
@echo "Running staticcheck..."
|
@echo "Running staticcheck..."
|
||||||
@command -v staticcheck > /dev/null || test -x $(GOBIN_DIR)/staticcheck || $(GOCMD) install honnef.co/go/tools/cmd/staticcheck@latest
|
$(STATICCHECK) ./...
|
||||||
$(call TOOL,staticcheck) ./...
|
|
||||||
|
|
||||||
govulncheck: ## Run govulncheck
|
govulncheck: ## Run govulncheck
|
||||||
@echo "Running govulncheck..."
|
@echo "Running govulncheck..."
|
||||||
@command -v govulncheck > /dev/null || test -x $(GOBIN_DIR)/govulncheck || $(GOCMD) install golang.org/x/vuln/cmd/govulncheck@latest
|
$(GOVULNCHECK) ./...
|
||||||
$(call TOOL,govulncheck) ./...
|
|
||||||
|
|
||||||
build: deps ## Build the binary
|
build: deps ## Build the binary
|
||||||
@echo "Building $(BINARY_NAME) $(VERSION)..."
|
@echo "Building $(BINARY_NAME) $(VERSION)..."
|
||||||
|
|||||||
+61
-1
@@ -16,7 +16,9 @@ import (
|
|||||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||||
"git.warky.dev/wdevs/relspecgo/pkg/readers/sqldir"
|
"git.warky.dev/wdevs/relspecgo/pkg/readers/sqldir"
|
||||||
|
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||||
wpgsql "git.warky.dev/wdevs/relspecgo/pkg/writers/pgsql"
|
wpgsql "git.warky.dev/wdevs/relspecgo/pkg/writers/pgsql"
|
||||||
|
wtemplate "git.warky.dev/wdevs/relspecgo/pkg/writers/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -54,7 +56,7 @@ inputs, output and options:
|
|||||||
logfile: .relspec/log/build-schema.log
|
logfile: .relspec/log/build-schema.log
|
||||||
|
|
||||||
Rules and guarantees:
|
Rules and guarantees:
|
||||||
- command is a closed allow-list (convert, merge, scripts-list). Arbitrary
|
- command is a closed allow-list (convert, merge, scripts-list, templ). Arbitrary
|
||||||
shell strings are never executed.
|
shell strings are never executed.
|
||||||
- Every path is relative to the directory holding the job file and may not
|
- Every path is relative to the directory holding the job file and may not
|
||||||
escape it. Absolute and home-relative paths are rejected.
|
escape it. Absolute and home-relative paths are rejected.
|
||||||
@@ -215,6 +217,7 @@ type resolvedJob struct {
|
|||||||
outputConn string // resolved connection string (secret)
|
outputConn string // resolved connection string (secret)
|
||||||
outputConnEnv string
|
outputConnEnv string
|
||||||
logPath string
|
logPath string
|
||||||
|
templatePath string
|
||||||
secrets []string // resolved secret values to redact from logs
|
secrets []string // resolved secret values to redact from logs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +239,17 @@ func preflightJob(j *jobs.Job) (*resolvedJob, error) {
|
|||||||
}
|
}
|
||||||
rj.logPath = p
|
rj.logPath = p
|
||||||
}
|
}
|
||||||
|
if j.Template != "" {
|
||||||
|
p, err := jobs.SafeJoin(root, j.Template)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("template: %w", err)
|
||||||
|
}
|
||||||
|
info, err := os.Stat(p)
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return nil, fmt.Errorf("template %q: not found or is a directory", j.Template)
|
||||||
|
}
|
||||||
|
rj.templatePath = p
|
||||||
|
}
|
||||||
|
|
||||||
for i, in := range j.Inputs {
|
for i, in := range j.Inputs {
|
||||||
ri := resolvedInput{format: strings.ToLower(in.Format)}
|
ri := resolvedInput{format: strings.ToLower(in.Format)}
|
||||||
@@ -348,6 +362,8 @@ func executeResolvedJob(rj *resolvedJob) (err error) {
|
|||||||
err = runMergeJob(rj, lg)
|
err = runMergeJob(rj, lg)
|
||||||
case jobs.CommandScriptsList:
|
case jobs.CommandScriptsList:
|
||||||
err = runScriptsListJob(rj, lg)
|
err = runScriptsListJob(rj, lg)
|
||||||
|
case jobs.CommandTempl:
|
||||||
|
err = runTemplJob(rj, lg)
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("unsupported command %q", rj.job.Command)
|
err = fmt.Errorf("unsupported command %q", rj.job.Command)
|
||||||
}
|
}
|
||||||
@@ -359,6 +375,50 @@ func executeResolvedJob(rj *resolvedJob) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runTemplJob(rj *resolvedJob, lg *jobLogger) error {
|
||||||
|
db, err := readJobInputs(rj, lg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if schema := rj.job.Options.Schema; schema != "" {
|
||||||
|
found := false
|
||||||
|
for _, s := range db.Schemas {
|
||||||
|
if s.Name == schema {
|
||||||
|
db.Schemas = []*models.Schema{s}
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("schema not found: %s", schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mode := rj.job.Mode
|
||||||
|
if mode == "" {
|
||||||
|
mode = "database"
|
||||||
|
}
|
||||||
|
pattern := rj.job.FilenamePattern
|
||||||
|
if pattern == "" {
|
||||||
|
pattern = "{{.Name}}.txt"
|
||||||
|
}
|
||||||
|
writer, err := wtemplate.NewWriter(&writers.WriterOptions{
|
||||||
|
OutputPath: rj.outputPath,
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
"template_path": rj.templatePath,
|
||||||
|
"mode": mode,
|
||||||
|
"filename_pattern": pattern,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create template writer: %w", err)
|
||||||
|
}
|
||||||
|
lg.logf("applying template: %s (mode %s)", rj.templatePath, mode)
|
||||||
|
if err := writer.WriteDatabase(db); err != nil {
|
||||||
|
return fmt.Errorf("execute template: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func runConvertJob(rj *resolvedJob, lg *jobLogger) error {
|
func runConvertJob(rj *resolvedJob, lg *jobLogger) error {
|
||||||
db, err := readJobInputs(rj, lg)
|
db, err := readJobInputs(rj, lg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -375,3 +375,30 @@ jobs:
|
|||||||
t.Fatal("job list output not deterministic")
|
t.Fatal("job list output not deterministic")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJobRun_TemplDatabaseMode(t *testing.T) {
|
||||||
|
dir := jobFixture(t, `version: 1
|
||||||
|
jobs:
|
||||||
|
docs:
|
||||||
|
command: templ
|
||||||
|
inputs:
|
||||||
|
- path: schema/core.dbml
|
||||||
|
format: dbml
|
||||||
|
template: templates/schema.tmpl
|
||||||
|
output:
|
||||||
|
path: build/schema.txt
|
||||||
|
overwrite: true
|
||||||
|
`)
|
||||||
|
writeFile(t, filepath.Join(dir, "templates", "schema.tmpl"), "{{range .Database.Schemas}}{{range .Tables}}{{.Name}} {{end}}{{end}}")
|
||||||
|
set := mustLoadSet(t, filepath.Join(dir, "relspec.yml"))
|
||||||
|
if err := executeJobPlan(set, "docs", false, false, &bytes.Buffer{}); err != nil {
|
||||||
|
t.Fatalf("execute templ job: %v", err)
|
||||||
|
}
|
||||||
|
out, err := os.ReadFile(filepath.Join(dir, "build", "schema.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read templ output: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(out), "users") {
|
||||||
|
t.Fatalf("templ output missing users table: %s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+10
-1
@@ -25,9 +25,10 @@ means adding a vetted adapter in the RelSpec source.
|
|||||||
| `convert` | read one or more input schemas, additively merge them, write one output |
|
| `convert` | read one or more input schemas, additively merge them, write one output |
|
||||||
| `merge` | like `convert` but requires ≥2 inputs and exposes `skip_*` merge options |
|
| `merge` | like `convert` but requires ≥2 inputs and exposes `skip_*` merge options |
|
||||||
| `scripts-list` | deterministically list SQL scripts across one or more directories |
|
| `scripts-list` | deterministically list SQL scripts across one or more directories |
|
||||||
|
| `templ` | apply a custom Go text template to one or more input schemas |
|
||||||
|
|
||||||
Deferred (documented, not implemented here): `scripts` execution against a live
|
Deferred (documented, not implemented here): `scripts` execution against a live
|
||||||
database, `split`, `inspect`, `diff`, `templ`, job-to-job output wiring,
|
database, `split`, `inspect`, `diff`, job-to-job output wiring,
|
||||||
log rotation/retention. Live SQL execution already exists as
|
log rotation/retention. Live SQL execution already exists as
|
||||||
`relspec scripts execute`; wiring it into the job runner is a follow-up because
|
`relspec scripts execute`; wiring it into the job runner is a follow-up because
|
||||||
it needs live database credentials and cannot be covered by offline tests.
|
it needs live database credentials and cannot be covered by offline tests.
|
||||||
@@ -123,6 +124,9 @@ jobs:
|
|||||||
script_dirs: # scripts-list (≥1)
|
script_dirs: # scripts-list (≥1)
|
||||||
- migrations/core
|
- migrations/core
|
||||||
- migrations/tenant
|
- migrations/tenant
|
||||||
|
template: templates/schema.tmpl # templ (required)
|
||||||
|
mode: table # templ: database/schema/script/table
|
||||||
|
filename_pattern: "{{.Name}}.go" # templ multi-output modes
|
||||||
output: # convert / merge (required)
|
output: # convert / merge (required)
|
||||||
format: pgsql
|
format: pgsql
|
||||||
path: build/schema.sql # file output, OR:
|
path: build/schema.sql # file output, OR:
|
||||||
@@ -141,6 +145,11 @@ jobs:
|
|||||||
logfile: .relspec/log/<job-name>.log # optional; appended to
|
logfile: .relspec/log/<job-name>.log # optional; appended to
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
### Supported input formats
|
### Supported input formats
|
||||||
|
|
||||||
`dbml`, `dctx`, `drawdb`, `graphql`, `json`, `yaml`, `gorm`, `bun`, `drizzle`,
|
`dbml`, `dctx`, `drawdb`, `graphql`, `json`, `yaml`, `gorm`, `bun`, `drizzle`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module git.warky.dev/wdevs/relspecgo
|
module git.warky.dev/wdevs/relspecgo
|
||||||
|
|
||||||
go 1.25.7
|
go 1.25.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gdamore/tcell/v2 v2.13.9
|
github.com/gdamore/tcell/v2 v2.13.9
|
||||||
|
|||||||
+72
-10
@@ -33,10 +33,11 @@ const (
|
|||||||
CommandConvert = "convert" // read one or more schema files, optionally merge, write one output
|
CommandConvert = "convert" // read one or more schema files, optionally merge, write one output
|
||||||
CommandMerge = "merge" // additive merge of two or more schema files into one output
|
CommandMerge = "merge" // additive merge of two or more schema files into one output
|
||||||
CommandScriptsList = "scripts-list" // deterministically list SQL scripts across one or more directories
|
CommandScriptsList = "scripts-list" // deterministically list SQL scripts across one or more directories
|
||||||
|
CommandTempl = "templ" // apply a custom Go text template to one or more schemas
|
||||||
)
|
)
|
||||||
|
|
||||||
// SupportedCommands lists every accepted command, in help order.
|
// SupportedCommands lists every accepted command, in help order.
|
||||||
var SupportedCommands = []string{CommandConvert, CommandMerge, CommandScriptsList}
|
var SupportedCommands = []string{CommandConvert, CommandMerge, CommandScriptsList, CommandTempl}
|
||||||
|
|
||||||
// readerFormats are the file-based input formats a job may declare (path).
|
// readerFormats are the file-based input formats a job may declare (path).
|
||||||
var readerFormats = map[string]bool{
|
var readerFormats = map[string]bool{
|
||||||
@@ -72,14 +73,17 @@ type Job struct {
|
|||||||
Name string `yaml:"-"`
|
Name string `yaml:"-"`
|
||||||
SourceFile string `yaml:"-"`
|
SourceFile string `yaml:"-"`
|
||||||
|
|
||||||
Command string `yaml:"command"`
|
Command string `yaml:"command"`
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"`
|
||||||
DependsOn []string `yaml:"depends_on"`
|
DependsOn []string `yaml:"depends_on"`
|
||||||
Inputs []Input `yaml:"inputs"`
|
Inputs []Input `yaml:"inputs"`
|
||||||
ScriptDirs []string `yaml:"script_dirs"`
|
ScriptDirs []string `yaml:"script_dirs"`
|
||||||
Output *Output `yaml:"output"`
|
Template string `yaml:"template"`
|
||||||
Options Options `yaml:"options"`
|
Mode string `yaml:"mode"`
|
||||||
Logfile string `yaml:"logfile"`
|
FilenamePattern string `yaml:"filename_pattern"`
|
||||||
|
Output *Output `yaml:"output"`
|
||||||
|
Options Options `yaml:"options"`
|
||||||
|
Logfile string `yaml:"logfile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input is one declared input schema.
|
// Input is one declared input schema.
|
||||||
@@ -258,7 +262,7 @@ func (j *Job) validate() []string {
|
|||||||
var e []string
|
var e []string
|
||||||
|
|
||||||
switch j.Command {
|
switch j.Command {
|
||||||
case CommandConvert, CommandMerge, CommandScriptsList:
|
case CommandConvert, CommandMerge, CommandScriptsList, CommandTempl:
|
||||||
case "":
|
case "":
|
||||||
e = append(e, "missing command")
|
e = append(e, "missing command")
|
||||||
return e
|
return e
|
||||||
@@ -277,6 +281,7 @@ func (j *Job) validate() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkPath("logfile", j.Logfile)
|
checkPath("logfile", j.Logfile)
|
||||||
|
checkPath("template", j.Template)
|
||||||
for _, in := range j.Inputs {
|
for _, in := range j.Inputs {
|
||||||
checkPath("input path", in.Path)
|
checkPath("input path", in.Path)
|
||||||
}
|
}
|
||||||
@@ -317,6 +322,63 @@ func (j *Job) validate() []string {
|
|||||||
if j.Output != nil {
|
if j.Output != nil {
|
||||||
e = append(e, "output is not valid for command \"scripts-list\"")
|
e = append(e, "output is not valid for command \"scripts-list\"")
|
||||||
}
|
}
|
||||||
|
case CommandTempl:
|
||||||
|
if len(j.Inputs) < 1 {
|
||||||
|
e = append(e, "command \"templ\" requires at least 1 input")
|
||||||
|
}
|
||||||
|
for i, in := range j.Inputs {
|
||||||
|
e = append(e, validateTemplInput(i, in)...)
|
||||||
|
}
|
||||||
|
if j.Template == "" {
|
||||||
|
e = append(e, "command \"templ\" requires template")
|
||||||
|
}
|
||||||
|
mode := strings.ToLower(j.Mode)
|
||||||
|
if mode == "" {
|
||||||
|
mode = "database"
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case "database", "schema", "script", "table":
|
||||||
|
default:
|
||||||
|
e = append(e, fmt.Sprintf("command \"templ\" has unsupported mode %q (supported: database, schema, script, table)", j.Mode))
|
||||||
|
}
|
||||||
|
if len(j.ScriptDirs) > 0 {
|
||||||
|
e = append(e, "script_dirs is not valid for command \"templ\"")
|
||||||
|
}
|
||||||
|
if j.Output != nil && j.Output.ConnEnv != "" {
|
||||||
|
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\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTemplInput(i int, in Input) []string {
|
||||||
|
var e []string
|
||||||
|
if in.Format == "" {
|
||||||
|
return []string{fmt.Sprintf("input[%d]: missing format", i)}
|
||||||
|
}
|
||||||
|
f := strings.ToLower(in.Format)
|
||||||
|
if f == "pgsql" {
|
||||||
|
if in.ConnEnv == "" {
|
||||||
|
e = append(e, fmt.Sprintf("input[%d]: format %q requires conn_env (an environment variable name)", i, in.Format))
|
||||||
|
}
|
||||||
|
if in.Path != "" {
|
||||||
|
e = append(e, fmt.Sprintf("input[%d]: format %q takes conn_env, not path", i, in.Format))
|
||||||
|
}
|
||||||
|
} else if readerFormats[f] {
|
||||||
|
if in.Path == "" {
|
||||||
|
e = append(e, fmt.Sprintf("input[%d]: missing path", i))
|
||||||
|
}
|
||||||
|
if in.ConnEnv != "" {
|
||||||
|
e = append(e, fmt.Sprintf("input[%d]: format %q does not use conn_env", i, in.Format))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
e = append(e, fmt.Sprintf("input[%d]: unsupported templ input format %q", i, in.Format))
|
||||||
|
}
|
||||||
|
if looksLikeSecret(in.ConnEnv) {
|
||||||
|
e = append(e, fmt.Sprintf("input[%d]: conn_env must be an environment variable name, not a connection string", i))
|
||||||
}
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,21 +445,21 @@ type (
|
|||||||
SqlUUID = SqlNull[uuid.UUID]
|
SqlUUID = SqlNull[uuid.UUID]
|
||||||
)
|
)
|
||||||
|
|
||||||
// SqlTimeStamp - Timestamp with custom formatting (YYYY-MM-DDTHH:MM:SS).
|
// SqlTimeStamp - Timestamp serialized as RFC3339.
|
||||||
type SqlTimeStamp struct{ SqlNull[time.Time] }
|
type SqlTimeStamp struct{ SqlNull[time.Time] }
|
||||||
|
|
||||||
func (t SqlTimeStamp) MarshalJSON() ([]byte, error) {
|
func (t SqlTimeStamp) MarshalJSON() ([]byte, error) {
|
||||||
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
}
|
}
|
||||||
return fmt.Appendf(nil, `"%s"`, t.Val.Format("2006-01-02T15:04:05")), nil
|
return fmt.Appendf(nil, `"%s"`, t.Val.Format(time.RFC3339)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SqlTimeStamp) UnmarshalJSON(b []byte) error {
|
func (t *SqlTimeStamp) UnmarshalJSON(b []byte) error {
|
||||||
if err := t.SqlNull.UnmarshalJSON(b); err != nil {
|
if err := t.SqlNull.UnmarshalJSON(b); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if t.Valid && (t.Val.IsZero() || t.Val.Format("2006-01-02T15:04:05") == "0001-01-01T00:00:00") {
|
if t.Valid && (t.Val.IsZero() || t.Val.Format(time.RFC3339) == "0001-01-01T00:00:00Z") {
|
||||||
t.Valid = false
|
t.Valid = false
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -469,21 +469,21 @@ func (t SqlTimeStamp) Value() (driver.Value, error) {
|
|||||||
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return t.Val.Format("2006-01-02T15:04:05"), nil
|
return t.Val.Format(time.RFC3339), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t SqlTimeStamp) MarshalYAML() (any, error) {
|
func (t SqlTimeStamp) MarshalYAML() (any, error) {
|
||||||
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return t.Val.Format("2006-01-02T15:04:05"), nil
|
return t.Val.Format(time.RFC3339), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SqlTimeStamp) UnmarshalYAML(value *yaml.Node) error {
|
func (t *SqlTimeStamp) UnmarshalYAML(value *yaml.Node) error {
|
||||||
if err := t.SqlNull.UnmarshalYAML(value); err != nil {
|
if err := t.SqlNull.UnmarshalYAML(value); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if t.Valid && (t.Val.IsZero() || t.Val.Format("2006-01-02T15:04:05") == "0001-01-01T00:00:00") {
|
if t.Valid && (t.Val.IsZero() || t.Val.Format(time.RFC3339) == "0001-01-01T00:00:00Z") {
|
||||||
t.Valid = false
|
t.Valid = false
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -493,7 +493,7 @@ func (t SqlTimeStamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
|||||||
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0o002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||||
return e.EncodeElement("", start)
|
return e.EncodeElement("", start)
|
||||||
}
|
}
|
||||||
return e.EncodeElement(t.Val.Format("2006-01-02T15:04:05"), start)
|
return e.EncodeElement(t.Val.Format(time.RFC3339), start)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SqlTimeStamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (t *SqlTimeStamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
@@ -511,7 +511,7 @@ func (t *SqlTimeStamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.Val = tm
|
t.Val = tm
|
||||||
t.Valid = !tm.IsZero() && tm.Format("2006-01-02T15:04:05") != "0001-01-01T00:00:00"
|
t.Valid = !tm.IsZero() && tm.Format(time.RFC3339) != "0001-01-01T00:00:00Z"
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ func TestSqlTimeStamp_JSON(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Marshal failed: %v", err)
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
}
|
}
|
||||||
expected := `"2024-01-15T10:30:45"`
|
expected := `"2024-01-15T10:30:45Z"`
|
||||||
if string(data) != expected {
|
if string(data) != expected {
|
||||||
t.Errorf("expected %s, got %s", expected, string(data))
|
t.Errorf("expected %s, got %s", expected, string(data))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ func TestSqlTimeStamp_YAML(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Marshal failed: %v", err)
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
}
|
}
|
||||||
if string(data) != "2024-06-15T09:30:00\n" {
|
if string(data) != "\"2024-06-15T09:30:00Z\"\n" {
|
||||||
t.Errorf("unexpected YAML: %q", string(data))
|
t.Errorf("unexpected YAML: %q", string(data))
|
||||||
}
|
}
|
||||||
var ts2 SqlTimeStamp
|
var ts2 SqlTimeStamp
|
||||||
|
|||||||
Reference in New Issue
Block a user