sql writer
Some checks are pending
CI / Test (1.23) (push) Waiting to run
CI / Test (1.24) (push) Waiting to run
CI / Test (1.25) (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build (push) Waiting to run

This commit is contained in:
2025-12-17 20:44:02 +02:00
parent 40bc0be1cb
commit 5e1448dcdb
48 changed files with 4592 additions and 950 deletions

View File

@@ -2,4 +2,4 @@
description: Build the RelSpec binary
---
Build the RelSpec project by running `go build -o relspec ./cmd/relspec`. Report the build status and any errors encountered.
Build the RelSpec project by running `make build`. Report the build status and any errors encountered.

View File

@@ -15,11 +15,12 @@
"useTabs": true
},
"testing": {
"framework": "go test",
"framework": "make test",
"runOnChange": false
},
"build": {
"command": "go build -o relspec ./cmd/relspec",
"command": "make build",
"outputBinary": "relspec"
}
}

1
.gitignore vendored
View File

@@ -47,3 +47,4 @@ test_output/
# Build artifacts
dist/
build/
bin/

View File

@@ -2,11 +2,6 @@
## Core Infrastructure
- [ ] Define internal data model for database relations
- [ ] Implement relation types (one-to-one, one-to-many, many-to-many)
- [ ] Create validation framework for specifications
- [ ] Design plugin architecture for readers/writers
## Input Readers
- [ ] **Database Inspector**

165
go.mod
View File

@@ -3,25 +3,190 @@ module git.warky.dev/wdevs/relspecgo
go 1.25.5
require (
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // indirect
github.com/4meepo/tagalign v1.4.2 // indirect
github.com/Abirdcfly/dupword v0.1.3 // indirect
github.com/Antonboom/errname v1.0.0 // indirect
github.com/Antonboom/nilnil v1.0.1 // indirect
github.com/Antonboom/testifylint v1.5.2 // indirect
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/Crocmagnon/fatcontext v0.7.1 // indirect
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
github.com/alexkohler/nakedret/v2 v2.0.5 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/alingse/nilnesserr v0.1.2 // indirect
github.com/ashanbrown/forbidigo v1.6.0 // indirect
github.com/ashanbrown/makezero v1.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitechdev/ResolveSpec v0.0.108 // indirect
github.com/bkielbasa/cyclop v1.2.3 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.5.0 // indirect
github.com/breml/bidichk v0.3.2 // indirect
github.com/breml/errchkjson v0.4.0 // indirect
github.com/butuzov/ireturn v0.3.1 // indirect
github.com/butuzov/mirror v1.3.0 // indirect
github.com/catenacyber/perfsprint v0.8.2 // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charithe/durationcheck v0.0.10 // indirect
github.com/chavacava/garif v0.1.0 // indirect
github.com/ckaznocha/intrange v0.3.0 // indirect
github.com/curioswitch/go-reassign v0.3.0 // indirect
github.com/daixiang0/gci v0.13.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.5 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.9 // indirect
github.com/go-critic/go-critic v0.12.0 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
github.com/golangci/go-printf-func-name v0.1.0 // indirect
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
github.com/golangci/golangci-lint v1.64.8 // indirect
github.com/golangci/misspell v0.6.0 // indirect
github.com/golangci/plugin-module-register v0.1.1 // indirect
github.com/golangci/revgrep v0.8.0 // indirect
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.5.0 // indirect
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jgautheron/goconst v1.7.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jjti/go-spancheck v0.6.4 // indirect
github.com/julz/importas v0.2.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
github.com/kisielk/errcheck v1.9.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/kulti/thelper v0.6.3 // indirect
github.com/kunwardeep/paralleltest v1.0.10 // indirect
github.com/lasiar/canonicalheader v1.1.2 // indirect
github.com/ldez/exptostd v0.4.2 // indirect
github.com/ldez/gomoddirectives v0.6.1 // indirect
github.com/ldez/grignotin v0.9.0 // indirect
github.com/ldez/tagliatelle v0.7.1 // indirect
github.com/ldez/usetesting v0.4.2 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/macabu/inamedparam v0.1.3 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgechev/revive v1.7.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v1.7.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/raeperd/recvcheck v0.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/ryancurrah/gomodguard v1.3.5 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect
github.com/securego/gosec/v2 v2.22.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sivchari/tenv v1.12.1 // indirect
github.com/sonatard/noctx v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tdakkota/asciicheck v0.4.1 // indirect
github.com/tetafro/godot v1.5.0 // indirect
github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect
github.com/timonwong/loggercheck v0.10.1 // indirect
github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/ultraware/funlen v0.2.0 // indirect
github.com/ultraware/whitespace v0.2.0 // indirect
github.com/uudashr/gocognit v1.2.0 // indirect
github.com/uudashr/iface v1.3.1 // indirect
github.com/xen0n/gosmopolitan v1.2.2 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.3.0 // indirect
github.com/ykadowak/zerologlint v0.1.5 // indirect
gitlab.com/bosi/decorder v0.4.2 // indirect
go-simpler.org/musttag v0.13.0 // indirect
go-simpler.org/sloglint v0.9.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.35.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/tools v0.6.1 // indirect
mvdan.cc/gofumpt v0.7.0 // indirect
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
)

474
go.sum
View File

@@ -1,11 +1,169 @@
4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A=
4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=
github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw=
github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA=
github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI=
github.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs=
github.com/Antonboom/nilnil v1.0.1/go.mod h1:CH7pW2JsRNFgEh8B2UaPZTEPhCMuFowP/e8Udp9Nnb0=
github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk=
github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Crocmagnon/fatcontext v0.7.1 h1:SC/VIbRRZQeQWj/TcQBS6JmrXcfA+BU4OGSVUt54PjM=
github.com/Crocmagnon/fatcontext v0.7.1/go.mod h1:1wMvv3NXEBJucFGfwOJBxSVWcoIO6emV215SMkW9MFU=
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=
github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU=
github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU=
github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
github.com/alingse/nilnesserr v0.1.2 h1:Yf8Iwm3z2hUUrP4muWfW83DF4nE3r1xZ26fGWUKCZlo=
github.com/alingse/nilnesserr v0.1.2/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY=
github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU=
github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU=
github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitechdev/ResolveSpec v0.0.108 h1:0Asw4zt9SdBIDprNqtrGY67R4SovAPBmW2y1qRn/Wjw=
github.com/bitechdev/ResolveSpec v0.0.108/go.mod h1:/mtVcbXSBLNmWlTKeDnbQx18tmNqOnrpetpLOadLzqo=
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
github.com/bombsimon/wsl/v4 v4.5.0 h1:iZRsEvDdyhd2La0FVi5k6tYehpOR/R7qIUjmKk7N74A=
github.com/bombsimon/wsl/v4 v4.5.0/go.mod h1:NOQ3aLF4nD7N5YPXMruR6ZXDOAqLoM0GEpLwTdvmOSc=
github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs=
github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos=
github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk=
github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8=
github.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY=
github.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M=
github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=
github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=
github.com/catenacyber/perfsprint v0.8.2 h1:+o9zVmCSVa7M4MvabsWvESEhpsMkhfE7k0sHNGL95yw=
github.com/catenacyber/perfsprint v0.8.2/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM=
github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg=
github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4=
github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ=
github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=
github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd5ZY=
github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c=
github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA=
github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ=
github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w=
github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w=
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=
github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=
github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw=
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU=
github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
github.com/golangci/golangci-lint v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I=
github.com/golangci/golangci-lint v1.64.8/go.mod h1:5cEsUQBSr6zi8XI8OjmcY2Xmliqc4iYL7YoPrL+zLJ4=
github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=
github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=
github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc=
github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs=
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=
github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=
github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=
github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=
github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk=
github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A=
github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -14,40 +172,356 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk=
github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4=
github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs=
github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c=
github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc=
github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk=
github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI=
github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM=
github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M=
github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs=
github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I=
github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs=
github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY=
github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4=
github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=
github.com/ldez/exptostd v0.4.2 h1:l5pOzHBz8mFOlbcifTxzfyYbgEmoUqjxLFHZkjlbHXs=
github.com/ldez/exptostd v0.4.2/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ=
github.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc=
github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs=
github.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow=
github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk=
github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk=
github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I=
github.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA=
github.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ=
github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk=
github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I=
github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI=
github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE=
github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04=
github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc=
github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=
github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY=
github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg=
github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4=
github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L5TVMCnA=
github.com/polyfloyd/go-errorlint v1.7.1/go.mod h1:aXjNb1x2TNhoLsk26iv1yl7a+zTnXPhwEMtEXukiLR8=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo=
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI=
github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI=
github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU=
github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE=
github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=
github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ=
github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8=
github.com/securego/gosec/v2 v2.22.2 h1:IXbuI7cJninj0nRpZSLCUlotsj8jGusohfONMrHoF6g=
github.com/securego/gosec/v2 v2.22.2/go.mod h1:UEBGA+dSKb+VqM6TdehR7lnQtIIMorYJ4/9CW1KVQBE=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY=
github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw=
github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM=
github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4=
github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=
github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8=
github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw=
github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio=
github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 h1:y4mJRFlM6fUyPhoXuFg/Yu02fg/nIPFMOY8tOqppoFg=
github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg=
github.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8=
github.com/tomarrell/wrapcheck/v2 v2.10.0 h1:SzRCryzy4IrAH7bVGG4cK40tNUhmVmMDuJujy4XwYDg=
github.com/tomarrell/wrapcheck/v2 v2.10.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo=
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=
github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA=
github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g=
github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA=
github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU=
github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U=
github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg=
github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU=
github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg=
github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=
github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=
github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=
github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE=
go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM=
go-simpler.org/sloglint v0.9.0 h1:/40NQtjRx9txvsB/RN022KsUJU+zaaSb/9q9BSefSrE=
go-simpler.org/sloglint v0.9.0/go.mod h1:G/OrAF6uxj48sHahCzrbarVMptL2kjWTaUeC8+fOGww=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U=
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ=

View File

@@ -18,7 +18,7 @@ type Database struct {
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
DatabaseType DatabaseType `json:"database_type,omitempty" yaml:"database_type,omitempty" xml:"database_type,omitempty"`
DatabaseVersion string `json:"database_version,omitempty" yaml:"database_version,omitempty" xml:"database_version,omitempty"`
SourceFormat string `json:"source_format,omitempty" yaml:"source_format,omitempty" xml:"source_format,omitempty"` //Source Format of the database.
SourceFormat string `json:"source_format,omitempty" yaml:"source_format,omitempty" xml:"source_format,omitempty"` // Source Format of the database.
}
// SQLNamer returns the database name in lowercase
@@ -106,19 +106,19 @@ func (d *View) SQLName() string {
// Sequence represents a database sequence (auto-increment generator)
type Sequence struct {
Name string `json:"name" yaml:"name" xml:"name"`
Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"`
Schema string `json:"schema" yaml:"schema" xml:"schema"`
StartValue int64 `json:"start_value" yaml:"start_value" xml:"start_value"`
MinValue int64 `json:"min_value,omitempty" yaml:"min_value,omitempty" xml:"min_value,omitempty"`
MaxValue int64 `json:"max_value,omitempty" yaml:"max_value,omitempty" xml:"max_value,omitempty"`
IncrementBy int64 `json:"increment_by" yaml:"increment_by" xml:"increment_by"`
CacheSize int64 `json:"cache_size,omitempty" yaml:"cache_size,omitempty" xml:"cache_size,omitempty"`
Cycle bool `json:"cycle" yaml:"cycle" xml:"cycle"`
OwnedByTable string `json:"owned_by_table,omitempty" yaml:"owned_by_table,omitempty" xml:"owned_by_table,omitempty"`
OwnedByColumn string `json:"owned_by_column,omitempty" yaml:"owned_by_column,omitempty" xml:"owned_by_column,omitempty"`
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
Sequence uint `json:"sequence,omitempty" yaml:"sequence,omitempty" xml:"sequence,omitempty"`
Name string `json:"name" yaml:"name" xml:"name"`
Description string `json:"description,omitempty" yaml:"description,omitempty" xml:"description,omitempty"`
Schema string `json:"schema" yaml:"schema" xml:"schema"`
StartValue int64 `json:"start_value" yaml:"start_value" xml:"start_value"`
MinValue int64 `json:"min_value,omitempty" yaml:"min_value,omitempty" xml:"min_value,omitempty"`
MaxValue int64 `json:"max_value,omitempty" yaml:"max_value,omitempty" xml:"max_value,omitempty"`
IncrementBy int64 `json:"increment_by" yaml:"increment_by" xml:"increment_by"`
CacheSize int64 `json:"cache_size,omitempty" yaml:"cache_size,omitempty" xml:"cache_size,omitempty"`
Cycle bool `json:"cycle" yaml:"cycle" xml:"cycle"`
OwnedByTable string `json:"owned_by_table,omitempty" yaml:"owned_by_table,omitempty" xml:"owned_by_table,omitempty"`
OwnedByColumn string `json:"owned_by_column,omitempty" yaml:"owned_by_column,omitempty" xml:"owned_by_column,omitempty"`
Comment string `json:"comment,omitempty" yaml:"comment,omitempty" xml:"comment,omitempty"`
Sequence uint `json:"sequence,omitempty" yaml:"sequence,omitempty" xml:"sequence,omitempty"`
RefSchema *Schema `json:"ref_schema,omitempty" yaml:"ref_schema,omitempty" xml:"ref_schema,omitempty"`
}

View File

@@ -67,6 +67,15 @@ func (r *Reader) ReadTable() (*models.Table, error) {
return schema.Tables[0], nil
}
// stripQuotes removes surrounding quotes from an identifier
func stripQuotes(s string) string {
s = strings.TrimSpace(s)
if len(s) >= 2 && ((s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'')) {
return s[1 : len(s)-1]
}
return s
}
// parseDBML parses DBML content and returns a Database model
func (r *Reader) parseDBML(content string) (*models.Database, error) {
db := models.InitDatabase("database")
@@ -79,13 +88,14 @@ func (r *Reader) parseDBML(content string) (*models.Database, error) {
scanner := bufio.NewScanner(strings.NewReader(content))
schemaMap := make(map[string]*models.Schema)
pendingConstraints := []*models.Constraint{}
var currentTable *models.Table
var currentSchema string
var inIndexes bool
var inTable bool
tableRegex := regexp.MustCompile(`^Table\s+([a-zA-Z0-9_.]+)\s*{`)
tableRegex := regexp.MustCompile(`^Table\s+(.+?)\s*{`)
refRegex := regexp.MustCompile(`^Ref:\s+(.+)`)
for scanner.Scan() {
@@ -102,10 +112,11 @@ func (r *Reader) parseDBML(content string) (*models.Database, error) {
parts := strings.Split(tableName, ".")
if len(parts) == 2 {
currentSchema = parts[0]
tableName = parts[1]
currentSchema = stripQuotes(parts[0])
tableName = stripQuotes(parts[1])
} else {
currentSchema = "public"
tableName = stripQuotes(parts[0])
}
// Ensure schema exists
@@ -131,7 +142,7 @@ func (r *Reader) parseDBML(content string) (*models.Database, error) {
}
// Parse indexes section
if inTable && strings.HasPrefix(line, "indexes") {
if inTable && (strings.HasPrefix(line, "Indexes {") || strings.HasPrefix(line, "indexes {")) {
inIndexes = true
continue
}
@@ -161,10 +172,14 @@ func (r *Reader) parseDBML(content string) (*models.Database, error) {
// Parse column definition
if inTable && !inIndexes && currentTable != nil {
column := r.parseColumn(line, currentTable.Name, currentSchema)
column, constraint := r.parseColumn(line, currentTable.Name, currentSchema)
if column != nil {
currentTable.Columns[column.Name] = column
}
if constraint != nil {
// Add to pending list - will assign to tables at the end
pendingConstraints = append(pendingConstraints, constraint)
}
continue
}
@@ -186,6 +201,19 @@ func (r *Reader) parseDBML(content string) (*models.Database, error) {
}
}
// Assign pending constraints to their respective tables
for _, constraint := range pendingConstraints {
// Find the table this constraint belongs to
if schema, exists := schemaMap[constraint.Schema]; exists {
for _, table := range schema.Tables {
if table.Name == constraint.Table {
table.Constraints[constraint.Name] = constraint
break
}
}
}
}
// Add schemas to database
for _, schema := range schemaMap {
db.Schemas = append(db.Schemas, schema)
@@ -195,19 +223,21 @@ func (r *Reader) parseDBML(content string) (*models.Database, error) {
}
// parseColumn parses a DBML column definition
func (r *Reader) parseColumn(line, tableName, schemaName string) *models.Column {
func (r *Reader) parseColumn(line, tableName, schemaName string) (*models.Column, *models.Constraint) {
// Format: column_name type [attributes] // comment
parts := strings.Fields(line)
if len(parts) < 2 {
return nil
return nil, nil
}
columnName := parts[0]
columnType := parts[1]
columnName := stripQuotes(parts[0])
columnType := stripQuotes(parts[1])
column := models.InitColumn(columnName, tableName, schemaName)
column.Type = columnType
var constraint *models.Constraint
// Parse attributes in brackets
if strings.Contains(line, "[") && strings.Contains(line, "]") {
attrStart := strings.Index(line, "[")
@@ -230,7 +260,55 @@ func (r *Reader) parseColumn(line, tableName, schemaName string) *models.Column
defaultVal := strings.TrimSpace(strings.TrimPrefix(attr, "default:"))
column.Default = strings.Trim(defaultVal, "'\"")
} else if attr == "unique" {
// Could create a unique constraint here
// Create a unique constraint
uniqueConstraint := models.InitConstraint(
fmt.Sprintf("uq_%s", columnName),
models.UniqueConstraint,
)
uniqueConstraint.Schema = schemaName
uniqueConstraint.Table = tableName
uniqueConstraint.Columns = []string{columnName}
// Store it to be added later
if constraint == nil {
constraint = uniqueConstraint
}
} else if strings.HasPrefix(attr, "ref:") {
// Parse inline reference
// DBML semantics depend on context:
// - On FK column: ref: < target means "this FK references target"
// - On PK column: ref: < source means "source references this PK" (reverse notation)
refStr := strings.TrimSpace(strings.TrimPrefix(attr, "ref:"))
// Check relationship direction operator
refOp := strings.TrimSpace(refStr)
var isReverse bool
if strings.HasPrefix(refOp, "<") {
isReverse = column.IsPrimaryKey // < on PK means "is referenced by" (reverse)
} else if strings.HasPrefix(refOp, ">") {
isReverse = !column.IsPrimaryKey // > on FK means reverse
}
constraint = r.parseRef(refStr)
if constraint != nil {
if isReverse {
// Reverse: parsed ref is SOURCE, current column is TARGET
// Constraint should be ON the source table
constraint.Schema = constraint.ReferencedSchema
constraint.Table = constraint.ReferencedTable
constraint.Columns = constraint.ReferencedColumns
constraint.ReferencedSchema = schemaName
constraint.ReferencedTable = tableName
constraint.ReferencedColumns = []string{columnName}
} else {
// Forward: current column is SOURCE, parsed ref is TARGET
// Standard FK: constraint is ON current table
constraint.Schema = schemaName
constraint.Table = tableName
constraint.Columns = []string{columnName}
}
// Generate short constraint name based on the column
constraint.Name = fmt.Sprintf("fk_%s", constraint.Columns[0])
}
}
}
}
@@ -242,28 +320,41 @@ func (r *Reader) parseColumn(line, tableName, schemaName string) *models.Column
column.Comment = strings.TrimSpace(line[commentStart+2:])
}
return column
return column, constraint
}
// parseIndex parses a DBML index definition
func (r *Reader) parseIndex(line, tableName, schemaName string) *models.Index {
// Format: (columns) [attributes]
if !strings.Contains(line, "(") || !strings.Contains(line, ")") {
return nil
// Format: (columns) [attributes] OR columnname [attributes]
var columns []string
if strings.Contains(line, "(") && strings.Contains(line, ")") {
// Multi-column format: (col1, col2) [attributes]
colStart := strings.Index(line, "(")
colEnd := strings.Index(line, ")")
if colStart >= colEnd {
return nil
}
columnsStr := line[colStart+1 : colEnd]
for _, col := range strings.Split(columnsStr, ",") {
columns = append(columns, stripQuotes(strings.TrimSpace(col)))
}
} else {
// Single column format: columnname [attributes]
// Extract column name before the bracket
if strings.Contains(line, "[") {
colName := strings.TrimSpace(line[:strings.Index(line, "[")])
if colName != "" {
columns = []string{stripQuotes(colName)}
}
}
}
colStart := strings.Index(line, "(")
colEnd := strings.Index(line, ")")
if colStart >= colEnd {
if len(columns) == 0 {
return nil
}
columnsStr := line[colStart+1 : colEnd]
columns := strings.Split(columnsStr, ",")
for i := range columns {
columns[i] = strings.TrimSpace(columns[i])
}
index := models.InitIndex("")
index.Table = tableName
index.Schema = schemaName
@@ -304,9 +395,11 @@ func (r *Reader) parseIndex(line, tableName, schemaName string) *models.Index {
// parseRef parses a DBML Ref (foreign key relationship)
func (r *Reader) parseRef(refStr string) *models.Constraint {
// Format: schema.table.(columns) > schema.table.(columns) [actions]
// Or inline format: < schema.table.column (for inline column refs)
// Split by relationship operator (>, <, -, etc.)
var fromPart, toPart string
isInlineRef := false
for _, op := range []string{">", "<", "-"} {
if strings.Contains(refStr, op) {
@@ -314,30 +407,53 @@ func (r *Reader) parseRef(refStr string) *models.Constraint {
if len(parts) == 2 {
fromPart = strings.TrimSpace(parts[0])
toPart = strings.TrimSpace(parts[1])
// Check if this is an inline ref (operator at start)
if fromPart == "" {
isInlineRef = true
}
break
}
}
}
if fromPart == "" || toPart == "" {
// For inline refs, only toPart should be populated
if isInlineRef {
if toPart == "" {
return nil
}
} else if fromPart == "" || toPart == "" {
return nil
}
// Remove actions part if present
if strings.Contains(toPart, "[") {
toPart = strings.TrimSpace(toPart[:strings.Index(toPart, "[")])
if idx := strings.Index(toPart, "["); idx >= 0 {
toPart = strings.TrimSpace(toPart[:idx])
}
// Parse from table and column
fromSchema, fromTable, fromColumns := r.parseTableRef(fromPart)
// Parse references
var fromSchema, fromTable string
var fromColumns []string
toSchema, toTable, toColumns := r.parseTableRef(toPart)
if fromTable == "" || toTable == "" {
if !isInlineRef {
fromSchema, fromTable, fromColumns = r.parseTableRef(fromPart)
if fromTable == "" {
return nil
}
}
if toTable == "" {
return nil
}
// Generate short constraint name based on the source column
constraintName := fmt.Sprintf("fk_%s_%s", fromTable, toTable)
if len(fromColumns) > 0 {
constraintName = fmt.Sprintf("fk_%s", fromColumns[0])
}
constraint := models.InitConstraint(
fmt.Sprintf("fk_%s_%s", fromTable, toTable),
constraintName,
models.ForeignKeyConstraint,
)
@@ -371,29 +487,48 @@ func (r *Reader) parseRef(refStr string) *models.Constraint {
return constraint
}
// parseTableRef parses a table reference like "schema.table.(column1, column2)"
// parseTableRef parses a table reference like "schema.table.(column1, column2)" or "schema"."table"."column"
func (r *Reader) parseTableRef(ref string) (schema, table string, columns []string) {
// Extract columns if present
// Extract columns if present in parentheses format
hasParentheses := false
if strings.Contains(ref, "(") && strings.Contains(ref, ")") {
colStart := strings.Index(ref, "(")
colEnd := strings.Index(ref, ")")
if colStart < colEnd {
columnsStr := ref[colStart+1 : colEnd]
for _, col := range strings.Split(columnsStr, ",") {
columns = append(columns, strings.TrimSpace(col))
columns = append(columns, stripQuotes(strings.TrimSpace(col)))
}
hasParentheses = true
}
ref = ref[:colStart]
}
// Parse schema and table
// Parse schema, table, and optionally column
parts := strings.Split(strings.TrimSpace(ref), ".")
if len(parts) == 2 {
schema = parts[0]
table = parts[1]
if len(parts) == 3 {
// Format: "schema"."table"."column"
schema = stripQuotes(parts[0])
table = stripQuotes(parts[1])
if !hasParentheses {
columns = []string{stripQuotes(parts[2])}
}
} else if len(parts) == 2 {
// Could be "schema"."table" or "table"."column"
// If columns are already extracted from parentheses, this is schema.table
// If no parentheses, this is table.column
if hasParentheses {
schema = stripQuotes(parts[0])
table = stripQuotes(parts[1])
} else {
schema = "public"
table = stripQuotes(parts[0])
columns = []string{stripQuotes(parts[1])}
}
} else if len(parts) == 1 {
// Format: "table"
schema = "public"
table = parts[0]
table = stripQuotes(parts[0])
}
return

View File

@@ -80,14 +80,15 @@ func (r *Reader) convertToDatabase(dctx *DCTXDictionary) (*models.Database, erro
schema := models.InitSchema("public")
// Create GUID mappings for tables and keys
tableGuidMap := make(map[string]string) // GUID -> table name
keyGuidMap := make(map[string]*DCTXKey) // GUID -> key definition
keyTableMap := make(map[string]string) // key GUID -> table name
tableGuidMap := make(map[string]string) // GUID -> table name
keyGuidMap := make(map[string]*DCTXKey) // GUID -> key definition
keyTableMap := make(map[string]string) // key GUID -> table name
fieldGuidMaps := make(map[string]map[string]string) // table name -> field GUID -> field name
// First pass: build GUID mappings
for _, dctxTable := range dctx.Tables {
if !r.hasSQLOption(&dctxTable) {
for i := range dctx.Tables {
dctxTable := &dctx.Tables[i]
if !r.hasSQLOption(dctxTable) {
continue
}
@@ -102,12 +103,13 @@ func (r *Reader) convertToDatabase(dctx *DCTXDictionary) (*models.Database, erro
}
// Process tables - only include tables with SQL option enabled
for _, dctxTable := range dctx.Tables {
if !r.hasSQLOption(&dctxTable) {
for i := range dctx.Tables {
dctxTable := &dctx.Tables[i]
if !r.hasSQLOption(dctxTable) {
continue
}
table, fieldGuidMap, err := r.convertTable(&dctxTable)
table, fieldGuidMap, err := r.convertTable(dctxTable)
if err != nil {
return nil, fmt.Errorf("failed to convert table %s: %w", dctxTable.Name, err)
}
@@ -116,7 +118,7 @@ func (r *Reader) convertToDatabase(dctx *DCTXDictionary) (*models.Database, erro
schema.Tables = append(schema.Tables, table)
// Process keys (indexes, primary keys)
err = r.processKeys(&dctxTable, table, fieldGuidMap)
err = r.processKeys(dctxTable, table, fieldGuidMap)
if err != nil {
return nil, fmt.Errorf("failed to process keys for table %s: %w", dctxTable.Name, err)
}
@@ -208,7 +210,7 @@ func (r *Reader) convertField(dctxField *DCTXField, tableName string) ([]*models
}
// mapDataType maps Clarion data types to SQL types
func (r *Reader) mapDataType(clarionType string, size int) (string, int) {
func (r *Reader) mapDataType(clarionType string, size int) (sqlType string, precision int) {
switch strings.ToUpper(clarionType) {
case "LONG":
if size == 8 {
@@ -360,7 +362,8 @@ func (r *Reader) convertKey(dctxKey *DCTXKey, table *models.Table, fieldGuidMap
// processRelations processes DCTX relations and creates foreign keys
func (r *Reader) processRelations(dctx *DCTXDictionary, schema *models.Schema, tableGuidMap map[string]string, keyGuidMap map[string]*DCTXKey, fieldGuidMaps map[string]map[string]string) error {
for _, relation := range dctx.Relations {
for i := range dctx.Relations {
relation := &dctx.Relations[i]
// Get table names from GUIDs
primaryTableName := tableGuidMap[relation.PrimaryTable]
foreignTableName := tableGuidMap[relation.ForeignTable]

View File

@@ -357,15 +357,15 @@ func (r *Reader) queryForeignKeys(schemaName string) (map[string][]*models.Const
// First pass: collect all FK data
type fkData struct {
schema string
tableName string
constraintName string
foreignColumns []string
referencedSchema string
referencedTable string
referencedColumns []string
updateRule string
deleteRule string
schema string
tableName string
constraintName string
foreignColumns []string
referencedSchema string
referencedTable string
referencedColumns []string
updateRule string
deleteRule string
}
fkMap := make(map[string]*fkData)

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/jackc/pgx/v5"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/pgsql"
"git.warky.dev/wdevs/relspecgo/pkg/readers"
@@ -261,47 +262,47 @@ func (r *Reader) close() {
func (r *Reader) mapDataType(pgType, udtName string) string {
// Map common PostgreSQL types
typeMap := map[string]string{
"integer": "int",
"bigint": "int64",
"smallint": "int16",
"int": "int",
"int2": "int16",
"int4": "int",
"int8": "int64",
"serial": "int",
"bigserial": "int64",
"smallserial": "int16",
"numeric": "decimal",
"decimal": "decimal",
"real": "float32",
"double precision": "float64",
"float4": "float32",
"float8": "float64",
"money": "decimal",
"character varying": "string",
"varchar": "string",
"character": "string",
"char": "string",
"text": "string",
"boolean": "bool",
"bool": "bool",
"date": "date",
"time": "time",
"time without time zone": "time",
"time with time zone": "timetz",
"timestamp": "timestamp",
"integer": "int",
"bigint": "int64",
"smallint": "int16",
"int": "int",
"int2": "int16",
"int4": "int",
"int8": "int64",
"serial": "int",
"bigserial": "int64",
"smallserial": "int16",
"numeric": "decimal",
"decimal": "decimal",
"real": "float32",
"double precision": "float64",
"float4": "float32",
"float8": "float64",
"money": "decimal",
"character varying": "string",
"varchar": "string",
"character": "string",
"char": "string",
"text": "string",
"boolean": "bool",
"bool": "bool",
"date": "date",
"time": "time",
"time without time zone": "time",
"time with time zone": "timetz",
"timestamp": "timestamp",
"timestamp without time zone": "timestamp",
"timestamp with time zone": "timestamptz",
"timestamptz": "timestamptz",
"interval": "interval",
"uuid": "uuid",
"json": "json",
"jsonb": "jsonb",
"bytea": "bytea",
"inet": "inet",
"cidr": "cidr",
"macaddr": "macaddr",
"xml": "xml",
"timestamp with time zone": "timestamptz",
"timestamptz": "timestamptz",
"interval": "interval",
"uuid": "uuid",
"json": "json",
"jsonb": "jsonb",
"bytea": "bytea",
"inet": "inet",
"cidr": "cidr",
"macaddr": "macaddr",
"xml": "xml",
}
// Try mapped type first

View File

@@ -62,7 +62,7 @@ func PascalCaseToSnakeCase(s string) string {
// Add underscore before uppercase letter if:
// 1. Previous char was lowercase, OR
// 2. Next char is lowercase (end of acronym)
if !prevUpper || (nextUpper == false && i+1 < len(runes)) {
if !prevUpper || (!nextUpper && i+1 < len(runes)) {
result.WriteRune('_')
}
}
@@ -84,20 +84,20 @@ func capitalize(s string) string {
// Handle common acronyms
acronyms := map[string]bool{
"ID": true,
"UUID": true,
"GUID": true,
"URL": true,
"URI": true,
"HTTP": true,
"ID": true,
"UUID": true,
"GUID": true,
"URL": true,
"URI": true,
"HTTP": true,
"HTTPS": true,
"API": true,
"JSON": true,
"XML": true,
"SQL": true,
"HTML": true,
"CSS": true,
"RID": true,
"API": true,
"JSON": true,
"XML": true,
"SQL": true,
"HTML": true,
"CSS": true,
"RID": true,
}
if acronyms[upper] {
@@ -146,8 +146,8 @@ func Pluralize(s string) string {
// Words ending in s, x, z, ch, sh
if strings.HasSuffix(s, "s") || strings.HasSuffix(s, "x") ||
strings.HasSuffix(s, "z") || strings.HasSuffix(s, "ch") ||
strings.HasSuffix(s, "sh") {
strings.HasSuffix(s, "z") || strings.HasSuffix(s, "ch") ||
strings.HasSuffix(s, "sh") {
return s + "es"
}
@@ -220,8 +220,8 @@ func Singularize(s string) string {
// Words ending in ses, xes, zes, ches, shes
if strings.HasSuffix(s, "ses") || strings.HasSuffix(s, "xes") ||
strings.HasSuffix(s, "zes") || strings.HasSuffix(s, "ches") ||
strings.HasSuffix(s, "shes") {
strings.HasSuffix(s, "zes") || strings.HasSuffix(s, "ches") ||
strings.HasSuffix(s, "shes") {
return s[:len(s)-2]
}

View File

@@ -17,17 +17,17 @@ type TemplateData struct {
// ModelData represents a single model/struct in the template
type ModelData struct {
Name string
TableName string // schema.table format
SchemaName string
TableNameOnly string // just table name without schema
Comment string
Fields []*FieldData
Config *MethodConfig
PrimaryKeyField string // Name of the primary key field
PrimaryKeyIsSQL bool // Whether PK uses SQL type (needs .Int64() call)
IDColumnName string // Name of the ID column in database
Prefix string // 3-letter prefix
Name string
TableName string // schema.table format
SchemaName string
TableNameOnly string // just table name without schema
Comment string
Fields []*FieldData
Config *MethodConfig
PrimaryKeyField string // Name of the primary key field
PrimaryKeyIsSQL bool // Whether PK uses SQL type (needs .Int64() call)
IDColumnName string // Name of the ID column in database
Prefix string // 3-letter prefix
}
// FieldData represents a single field in a struct

View File

@@ -91,6 +91,7 @@ func (w *Writer) databaseToDrawDB(d *models.Database) *DrawDBSchema {
note += "\n"
}
note += schemaModel.Comment
_ = note // TODO: Add note/description field to DrawDBArea when supported
area := &DrawDBArea{
ID: areaID,
@@ -242,12 +243,12 @@ func (w *Writer) tableToDrawDB(table *models.Table) *DrawDBSchema {
}
// convertTableToDrawDB converts a table to DrawDB format and returns the table and next field ID
func (w *Writer) convertTableToDrawDB(table *models.Table, schemaName string, tableID, fieldID, tableIndex, tablesPerRow, gridX, gridY, colWidth, rowHeight, colorIndex int) (*DrawDBTable, int) {
func (w *Writer) convertTableToDrawDB(table *models.Table, schemaName string, tableID, fieldID, tableIndex, tablesPerRow, gridX, gridY, colWidth, rowHeight, colorIndex int) (drawTable *DrawDBTable, nextFieldID int) {
// Calculate position
x := gridX + (tableIndex%tablesPerRow)*colWidth
y := gridY + (tableIndex/tablesPerRow)*rowHeight
drawTable := &DrawDBTable{
drawTable = &DrawDBTable{
ID: tableID,
Name: table.Name,
Schema: schemaName,

View File

@@ -62,7 +62,7 @@ func PascalCaseToSnakeCase(s string) string {
// Add underscore before uppercase letter if:
// 1. Previous char was lowercase, OR
// 2. Next char is lowercase (end of acronym)
if !prevUpper || (nextUpper == false && i+1 < len(runes)) {
if !prevUpper || (!nextUpper && i+1 < len(runes)) {
result.WriteRune('_')
}
}
@@ -84,20 +84,20 @@ func capitalize(s string) string {
// Handle common acronyms
acronyms := map[string]bool{
"ID": true,
"UUID": true,
"GUID": true,
"URL": true,
"URI": true,
"HTTP": true,
"ID": true,
"UUID": true,
"GUID": true,
"URL": true,
"URI": true,
"HTTP": true,
"HTTPS": true,
"API": true,
"JSON": true,
"XML": true,
"SQL": true,
"HTML": true,
"CSS": true,
"RID": true,
"API": true,
"JSON": true,
"XML": true,
"SQL": true,
"HTML": true,
"CSS": true,
"RID": true,
}
if acronyms[upper] {
@@ -146,8 +146,8 @@ func Pluralize(s string) string {
// Words ending in s, x, z, ch, sh
if strings.HasSuffix(s, "s") || strings.HasSuffix(s, "x") ||
strings.HasSuffix(s, "z") || strings.HasSuffix(s, "ch") ||
strings.HasSuffix(s, "sh") {
strings.HasSuffix(s, "z") || strings.HasSuffix(s, "ch") ||
strings.HasSuffix(s, "sh") {
return s + "es"
}
@@ -220,8 +220,8 @@ func Singularize(s string) string {
// Words ending in ses, xes, zes, ches, shes
if strings.HasSuffix(s, "ses") || strings.HasSuffix(s, "xes") ||
strings.HasSuffix(s, "zes") || strings.HasSuffix(s, "ches") ||
strings.HasSuffix(s, "shes") {
strings.HasSuffix(s, "zes") || strings.HasSuffix(s, "ches") ||
strings.HasSuffix(s, "shes") {
return s[:len(s)-2]
}

View File

@@ -17,15 +17,15 @@ type TemplateData struct {
// ModelData represents a single model/struct in the template
type ModelData struct {
Name string
TableName string // schema.table format
TableName string // schema.table format
SchemaName string
TableNameOnly string // just table name without schema
TableNameOnly string // just table name without schema
Comment string
Fields []*FieldData
Config *MethodConfig
PrimaryKeyField string // Name of the primary key field
IDColumnName string // Name of the ID column in database
Prefix string // 3-letter prefix
PrimaryKeyField string // Name of the primary key field
IDColumnName string // Name of the ID column in database
Prefix string // 3-letter prefix
}
// FieldData represents a single field in a struct

View File

@@ -52,24 +52,24 @@ func (tm *TypeMapper) extractBaseType(sqlType string) string {
func (tm *TypeMapper) baseGoType(sqlType string) string {
typeMap := map[string]string{
// Integer types
"integer": "int32",
"int": "int32",
"int4": "int32",
"smallint": "int16",
"int2": "int16",
"bigint": "int64",
"int8": "int64",
"serial": "int32",
"bigserial": "int64",
"integer": "int32",
"int": "int32",
"int4": "int32",
"smallint": "int16",
"int2": "int16",
"bigint": "int64",
"int8": "int64",
"serial": "int32",
"bigserial": "int64",
"smallserial": "int16",
// String types
"text": "string",
"varchar": "string",
"char": "string",
"character": "string",
"citext": "string",
"bpchar": "string",
"text": "string",
"varchar": "string",
"char": "string",
"character": "string",
"citext": "string",
"bpchar": "string",
// Boolean
"boolean": "bool",
@@ -84,15 +84,15 @@ func (tm *TypeMapper) baseGoType(sqlType string) string {
"decimal": "float64",
// Date/Time types
"timestamp": "time.Time",
"timestamp without time zone": "time.Time",
"timestamp with time zone": "time.Time",
"timestamptz": "time.Time",
"date": "time.Time",
"time": "time.Time",
"time without time zone": "time.Time",
"time with time zone": "time.Time",
"timetz": "time.Time",
"timestamp": "time.Time",
"timestamp without time zone": "time.Time",
"timestamp with time zone": "time.Time",
"timestamptz": "time.Time",
"date": "time.Time",
"time": "time.Time",
"time without time zone": "time.Time",
"time with time zone": "time.Time",
"timetz": "time.Time",
// Binary
"bytea": "[]byte",
@@ -105,8 +105,8 @@ func (tm *TypeMapper) baseGoType(sqlType string) string {
"jsonb": "string",
// Network
"inet": "string",
"cidr": "string",
"inet": "string",
"cidr": "string",
"macaddr": "string",
// Other
@@ -125,24 +125,24 @@ func (tm *TypeMapper) baseGoType(sqlType string) string {
func (tm *TypeMapper) nullableGoType(sqlType string) string {
typeMap := map[string]string{
// Integer types
"integer": tm.sqlTypesAlias + ".SqlInt32",
"int": tm.sqlTypesAlias + ".SqlInt32",
"int4": tm.sqlTypesAlias + ".SqlInt32",
"smallint": tm.sqlTypesAlias + ".SqlInt16",
"int2": tm.sqlTypesAlias + ".SqlInt16",
"bigint": tm.sqlTypesAlias + ".SqlInt64",
"int8": tm.sqlTypesAlias + ".SqlInt64",
"serial": tm.sqlTypesAlias + ".SqlInt32",
"bigserial": tm.sqlTypesAlias + ".SqlInt64",
"integer": tm.sqlTypesAlias + ".SqlInt32",
"int": tm.sqlTypesAlias + ".SqlInt32",
"int4": tm.sqlTypesAlias + ".SqlInt32",
"smallint": tm.sqlTypesAlias + ".SqlInt16",
"int2": tm.sqlTypesAlias + ".SqlInt16",
"bigint": tm.sqlTypesAlias + ".SqlInt64",
"int8": tm.sqlTypesAlias + ".SqlInt64",
"serial": tm.sqlTypesAlias + ".SqlInt32",
"bigserial": tm.sqlTypesAlias + ".SqlInt64",
"smallserial": tm.sqlTypesAlias + ".SqlInt16",
// String types
"text": tm.sqlTypesAlias + ".SqlString",
"varchar": tm.sqlTypesAlias + ".SqlString",
"char": tm.sqlTypesAlias + ".SqlString",
"character": tm.sqlTypesAlias + ".SqlString",
"citext": tm.sqlTypesAlias + ".SqlString",
"bpchar": tm.sqlTypesAlias + ".SqlString",
"text": tm.sqlTypesAlias + ".SqlString",
"varchar": tm.sqlTypesAlias + ".SqlString",
"char": tm.sqlTypesAlias + ".SqlString",
"character": tm.sqlTypesAlias + ".SqlString",
"citext": tm.sqlTypesAlias + ".SqlString",
"bpchar": tm.sqlTypesAlias + ".SqlString",
// Boolean
"boolean": tm.sqlTypesAlias + ".SqlBool",
@@ -157,15 +157,15 @@ func (tm *TypeMapper) nullableGoType(sqlType string) string {
"decimal": tm.sqlTypesAlias + ".SqlFloat64",
// Date/Time types
"timestamp": tm.sqlTypesAlias + ".SqlTime",
"timestamp without time zone": tm.sqlTypesAlias + ".SqlTime",
"timestamp with time zone": tm.sqlTypesAlias + ".SqlTime",
"timestamptz": tm.sqlTypesAlias + ".SqlTime",
"date": tm.sqlTypesAlias + ".SqlDate",
"time": tm.sqlTypesAlias + ".SqlTime",
"time without time zone": tm.sqlTypesAlias + ".SqlTime",
"time with time zone": tm.sqlTypesAlias + ".SqlTime",
"timetz": tm.sqlTypesAlias + ".SqlTime",
"timestamp": tm.sqlTypesAlias + ".SqlTime",
"timestamp without time zone": tm.sqlTypesAlias + ".SqlTime",
"timestamp with time zone": tm.sqlTypesAlias + ".SqlTime",
"timestamptz": tm.sqlTypesAlias + ".SqlTime",
"date": tm.sqlTypesAlias + ".SqlDate",
"time": tm.sqlTypesAlias + ".SqlTime",
"time without time zone": tm.sqlTypesAlias + ".SqlTime",
"time with time zone": tm.sqlTypesAlias + ".SqlTime",
"timetz": tm.sqlTypesAlias + ".SqlTime",
// Binary
"bytea": "[]byte", // No nullable version needed
@@ -178,8 +178,8 @@ func (tm *TypeMapper) nullableGoType(sqlType string) string {
"jsonb": tm.sqlTypesAlias + ".SqlString",
// Network
"inet": tm.sqlTypesAlias + ".SqlString",
"cidr": tm.sqlTypesAlias + ".SqlString",
"inet": tm.sqlTypesAlias + ".SqlString",
"cidr": tm.sqlTypesAlias + ".SqlString",
"macaddr": tm.sqlTypesAlias + ".SqlString",
// Other

View File

@@ -1,258 +0,0 @@
# PostgreSQL Migration Writer
## Overview
The PostgreSQL Migration Writer implements database schema inspection and differential migration generation, following the same approach as the `pgsql_meta_upgrade` migration system. It compares a desired model (target schema) against the current database state and generates the necessary SQL migration scripts.
## Migration Phases
The migration writer follows a phased approach with specific priorities to ensure proper execution order:
### Phase 1: Drops (Priority 11-50)
- Drop changed constraints (Priority 11)
- Drop changed indexes (Priority 20)
- Drop changed foreign keys (Priority 50)
### Phase 2: Renames (Priority 60-90)
- Rename tables (Priority 60)
- Rename columns (Priority 90)
- *Note: Currently requires manual handling or metadata for rename detection*
### Phase 3: Tables & Columns (Priority 100-145)
- Create new tables (Priority 100)
- Add new columns (Priority 120)
- Alter column types (Priority 120)
- Alter column defaults (Priority 145)
### Phase 4: Indexes (Priority 160-180)
- Create primary keys (Priority 160)
- Create indexes (Priority 180)
### Phase 5: Foreign Keys (Priority 195)
- Create foreign key constraints
### Phase 6: Comments (Priority 200+)
- Add table and column comments
## Usage
### 1. Inspect Current Database
```go
import (
"git.warky.dev/wdevs/relspecgo/pkg/readers"
"git.warky.dev/wdevs/relspecgo/pkg/readers/pgsql"
)
// Create reader with connection string
options := &readers.ReaderOptions{
ConnectionString: "host=localhost port=5432 dbname=mydb user=postgres password=secret",
}
reader := pgsql.NewReader(options)
// Read current database state
currentDB, err := reader.ReadDatabase()
if err != nil {
log.Fatal(err)
}
```
### 2. Define Desired Model
```go
import "git.warky.dev/wdevs/relspecgo/pkg/models"
// Create desired model (could be loaded from DBML, JSON, etc.)
modelDB := models.InitDatabase("mydb")
schema := models.InitSchema("public")
// Define table
table := models.InitTable("users", "public")
table.Description = "User accounts"
// Add columns
idCol := models.InitColumn("id", "users", "public")
idCol.Type = "integer"
idCol.NotNull = true
idCol.IsPrimaryKey = true
table.Columns["id"] = idCol
nameCol := models.InitColumn("name", "users", "public")
nameCol.Type = "text"
nameCol.NotNull = true
table.Columns["name"] = nameCol
emailCol := models.InitColumn("email", "users", "public")
emailCol.Type = "text"
table.Columns["email"] = emailCol
// Add primary key constraint
pkConstraint := &models.Constraint{
Name: "pk_users",
Type: models.PrimaryKeyConstraint,
Columns: []string{"id"},
}
table.Constraints["pk_users"] = pkConstraint
// Add unique index
emailIndex := &models.Index{
Name: "uk_users_email",
Unique: true,
Columns: []string{"email"},
}
table.Indexes["uk_users_email"] = emailIndex
schema.Tables = append(schema.Tables, table)
modelDB.Schemas = append(modelDB.Schemas, schema)
```
### 3. Generate Migration
```go
import (
"git.warky.dev/wdevs/relspecgo/pkg/writers"
"git.warky.dev/wdevs/relspecgo/pkg/writers/pgsql"
)
// Create migration writer
writerOptions := &writers.WriterOptions{
OutputPath: "migration_001.sql",
}
migrationWriter := pgsql.NewMigrationWriter(writerOptions)
// Generate migration comparing model vs current
err = migrationWriter.WriteMigration(modelDB, currentDB)
if err != nil {
log.Fatal(err)
}
```
## Example Migration Output
```sql
-- PostgreSQL Migration Script
-- Generated by RelSpec
-- Source: mydb -> mydb
-- Priority: 11 | Type: drop constraint | Object: public.users.old_constraint
ALTER TABLE public.users DROP CONSTRAINT IF EXISTS old_constraint;
-- Priority: 100 | Type: create table | Object: public.orders
CREATE TABLE IF NOT EXISTS public.orders (
id integer NOT NULL,
user_id integer,
total numeric(10,2) DEFAULT 0.00,
created_at timestamp DEFAULT CURRENT_TIMESTAMP
);
-- Priority: 120 | Type: create column | Object: public.users.phone
ALTER TABLE public.users
ADD COLUMN IF NOT EXISTS phone text;
-- Priority: 120 | Type: alter column type | Object: public.users.age
ALTER TABLE public.users
ALTER COLUMN age TYPE integer;
-- Priority: 160 | Type: create primary key | Object: public.orders.pk_orders
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE table_schema = 'public'
AND table_name = 'orders'
AND constraint_name = 'pk_orders'
) THEN
ALTER TABLE public.orders
ADD CONSTRAINT pk_orders PRIMARY KEY (id);
END IF;
END;
$$;
-- Priority: 180 | Type: create index | Object: public.users.idx_users_email
CREATE INDEX IF NOT EXISTS idx_users_email
ON public.users USING btree (email);
-- Priority: 195 | Type: create foreign key | Object: public.orders.fk_orders_users
ALTER TABLE public.orders
DROP CONSTRAINT IF EXISTS fk_orders_users;
ALTER TABLE public.orders
ADD CONSTRAINT fk_orders_users
FOREIGN KEY (user_id)
REFERENCES public.users (id)
ON DELETE CASCADE
ON UPDATE CASCADE
DEFERRABLE;
-- Priority: 200 | Type: comment on table | Object: public.users
COMMENT ON TABLE public.users IS 'User accounts';
-- Priority: 200 | Type: comment on column | Object: public.users.email
COMMENT ON COLUMN public.users.email IS 'User email address';
```
## Migration Script Structure
Each migration script includes:
- **ObjectName**: Fully qualified name of the object being modified
- **ObjectType**: Type of operation (create table, alter column, etc.)
- **Schema**: Schema name
- **Priority**: Execution order priority (lower runs first)
- **Sequence**: Sub-ordering within same priority
- **Body**: The actual SQL statement
## Comparison Logic
The migration writer compares objects using:
### Tables
- Existence check by name (case-insensitive)
- New tables generate CREATE TABLE statements
### Columns
- Existence check within tables
- Type changes generate ALTER COLUMN TYPE
- Default value changes generate SET/DROP DEFAULT
- New columns generate ADD COLUMN
### Constraints
- Compared by type, columns, and referenced objects
- Changed constraints are dropped and recreated
### Indexes
- Compared by uniqueness and column list
- Changed indexes are dropped and recreated
### Foreign Keys
- Compared by columns, referenced table/columns, and actions
- Changed foreign keys are dropped and recreated
## Best Practices
1. **Always Review Generated Migrations**: Manually review SQL before execution
2. **Test on Non-Production First**: Apply migrations to development/staging environments first
3. **Backup Before Migration**: Create database backup before running migrations
4. **Use Transactions**: Wrap migrations in transactions when possible
5. **Handle Renames Carefully**: Column/table renames may appear as DROP + CREATE without metadata
6. **Consider Data Migration**: Generated SQL handles structure only; data migration may be needed
## Limitations
1. **Rename Detection**: Automatic rename detection not implemented; requires GUID or metadata matching
2. **Data Type Conversions**: Some type changes may require custom USING clauses
3. **Complex Constraints**: CHECK constraints with complex expressions may need manual handling
4. **Sequence Values**: Current sequence values not automatically synced
5. **Permissions**: Schema and object permissions not included in migrations
## Integration with Migration System
This implementation follows the same logic as the SQL migration system in `examples/pgsql_meta_upgrade`:
- `migration_inspect.sql` → Reader (pkg/readers/pgsql)
- `migration_build.sql` → MigrationWriter (pkg/writers/pgsql)
- `migration_run.sql` → External execution (psql, application code)
The phases, priorities, and script generation logic match the original migration system to ensure compatibility and consistency.

View File

@@ -0,0 +1,696 @@
# PostgreSQL Migration Templates
## Overview
The PostgreSQL migration writer uses Go text templates to generate SQL, making the code much more maintainable and customizable than hardcoded string concatenation.
## Architecture
```
pkg/writers/pgsql/
├── templates/ # Template files
│ ├── create_table.tmpl # CREATE TABLE
│ ├── add_column.tmpl # ALTER TABLE ADD COLUMN
│ ├── alter_column_type.tmpl # ALTER TABLE ALTER COLUMN TYPE
│ ├── alter_column_default.tmpl # ALTER TABLE ALTER COLUMN DEFAULT
│ ├── create_primary_key.tmpl # ADD CONSTRAINT PRIMARY KEY
│ ├── create_index.tmpl # CREATE INDEX
│ ├── create_foreign_key.tmpl # ADD CONSTRAINT FOREIGN KEY
│ ├── drop_constraint.tmpl # DROP CONSTRAINT
│ ├── drop_index.tmpl # DROP INDEX
│ ├── comment_table.tmpl # COMMENT ON TABLE
│ ├── comment_column.tmpl # COMMENT ON COLUMN
│ ├── audit_tables.tmpl # CREATE audit tables
│ ├── audit_function.tmpl # CREATE audit function
│ └── audit_trigger.tmpl # CREATE audit trigger
├── templates.go # Template executor and data structures
└── migration_writer_templated.go # Templated migration writer
```
## Using Templates
### Basic Usage
```go
// Create template executor
executor, err := pgsql.NewTemplateExecutor()
if err != nil {
log.Fatal(err)
}
// Prepare data
data := pgsql.CreateTableData{
SchemaName: "public",
TableName: "users",
Columns: []pgsql.ColumnData{
{Name: "id", Type: "integer", NotNull: true},
{Name: "name", Type: "text"},
},
}
// Execute template
sql, err := executor.ExecuteCreateTable(data)
if err != nil {
log.Fatal(err)
}
fmt.Println(sql)
```
### Using Templated Migration Writer
```go
// Create templated migration writer
writer, err := pgsql.NewTemplatedMigrationWriter(&writers.WriterOptions{
OutputPath: "migration.sql",
})
if err != nil {
log.Fatal(err)
}
// Generate migration (uses templates internally)
err = writer.WriteMigration(modelDB, currentDB)
if err != nil {
log.Fatal(err)
}
```
## Template Data Structures
### CreateTableData
For `create_table.tmpl`:
```go
type CreateTableData struct {
SchemaName string
TableName string
Columns []ColumnData
}
type ColumnData struct {
Name string
Type string
Default string
NotNull bool
}
```
Example:
```go
data := CreateTableData{
SchemaName: "public",
TableName: "products",
Columns: []ColumnData{
{Name: "id", Type: "serial", NotNull: true},
{Name: "name", Type: "text", NotNull: true},
{Name: "price", Type: "numeric(10,2)", Default: "0.00"},
},
}
```
### AddColumnData
For `add_column.tmpl`:
```go
type AddColumnData struct {
SchemaName string
TableName string
ColumnName string
ColumnType string
Default string
NotNull bool
}
```
### CreateIndexData
For `create_index.tmpl`:
```go
type CreateIndexData struct {
SchemaName string
TableName string
IndexName string
IndexType string // btree, hash, gin, gist
Columns string // comma-separated
Unique bool
}
```
### CreateForeignKeyData
For `create_foreign_key.tmpl`:
```go
type CreateForeignKeyData struct {
SchemaName string
TableName string
ConstraintName string
SourceColumns string // comma-separated
TargetSchema string
TargetTable string
TargetColumns string // comma-separated
OnDelete string // CASCADE, SET NULL, etc.
OnUpdate string
}
```
### AuditFunctionData
For `audit_function.tmpl`:
```go
type AuditFunctionData struct {
SchemaName string
FunctionName string
TableName string
TablePrefix string
PrimaryKey string
AuditSchema string
UserFunction string
AuditInsert bool
AuditUpdate bool
AuditDelete bool
UpdateCondition string
UpdateColumns []AuditColumnData
DeleteColumns []AuditColumnData
}
type AuditColumnData struct {
Name string
OldValue string // SQL expression for old value
NewValue string // SQL expression for new value
}
```
## Customizing Templates
### Modifying Existing Templates
Templates are embedded in the binary but can be modified at compile time:
1. **Edit template file** in `pkg/writers/pgsql/templates/`:
```go
// templates/create_table.tmpl
CREATE TABLE IF NOT EXISTS {{.SchemaName}}.{{.TableName}} (
{{- range $i, $col := .Columns}}
{{- if $i}},{{end}}
{{$col.Name}} {{$col.Type}}
{{- if $col.Default}} DEFAULT {{$col.Default}}{{end}}
{{- if $col.NotNull}} NOT NULL{{end}}
{{- end}}
);
-- Custom comment
COMMENT ON TABLE {{.SchemaName}}.{{.TableName}} IS 'Auto-generated by RelSpec';
```
2. **Rebuild** the application:
```bash
go build ./cmd/relspec
```
The new template is automatically embedded.
### Template Syntax Reference
#### Variables
```go
{{.FieldName}} // Access field
{{.SchemaName}} // String field
{{.NotNull}} // Boolean field
```
#### Conditionals
```go
{{if .NotNull}}
NOT NULL
{{end}}
{{if .Default}}
DEFAULT {{.Default}}
{{else}}
-- No default
{{end}}
```
#### Loops
```go
{{range $i, $col := .Columns}}
Column: {{$col.Name}} Type: {{$col.Type}}
{{end}}
```
#### Functions
```go
{{if eq .Type "CASCADE"}}
ON DELETE CASCADE
{{end}}
{{join .Columns ", "}} // Join string slice
```
### Creating New Templates
1. **Create template file** in `pkg/writers/pgsql/templates/`:
```go
// templates/custom_operation.tmpl
-- Custom operation for {{.TableName}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
{{.CustomOperation}};
```
2. **Define data structure** in `templates.go`:
```go
type CustomOperationData struct {
SchemaName string
TableName string
CustomOperation string
}
```
3. **Add executor method** in `templates.go`:
```go
func (te *TemplateExecutor) ExecuteCustomOperation(data CustomOperationData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "custom_operation.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute custom_operation template: %w", err)
}
return buf.String(), nil
}
```
4. **Use in migration writer**:
```go
sql, err := w.executor.ExecuteCustomOperation(CustomOperationData{
SchemaName: "public",
TableName: "users",
CustomOperation: "ADD COLUMN custom_field text",
})
```
## Template Examples
### Example 1: Custom Table Creation
Modify `create_table.tmpl` to add table options:
```sql
CREATE TABLE IF NOT EXISTS {{.SchemaName}}.{{.TableName}} (
{{- range $i, $col := .Columns}}
{{- if $i}},{{end}}
{{$col.Name}} {{$col.Type}}
{{- if $col.Default}} DEFAULT {{$col.Default}}{{end}}
{{- if $col.NotNull}} NOT NULL{{end}}
{{- end}}
) WITH (fillfactor = 90);
-- Add automatic comment
COMMENT ON TABLE {{.SchemaName}}.{{.TableName}}
IS 'Created: {{.CreatedDate}} | Version: {{.Version}}';
```
### Example 2: Custom Index with WHERE Clause
Add to `create_index.tmpl`:
```sql
CREATE {{if .Unique}}UNIQUE {{end}}INDEX IF NOT EXISTS {{.IndexName}}
ON {{.SchemaName}}.{{.TableName}}
USING {{.IndexType}} ({{.Columns}})
{{- if .Where}}
WHERE {{.Where}}
{{- end}}
{{- if .Include}}
INCLUDE ({{.Include}})
{{- end}};
```
Update data structure:
```go
type CreateIndexData struct {
SchemaName string
TableName string
IndexName string
IndexType string
Columns string
Unique bool
Where string // New field for partial indexes
Include string // New field for covering indexes
}
```
### Example 3: Enhanced Audit Function
Modify `audit_function.tmpl` to add custom logging:
```sql
CREATE OR REPLACE FUNCTION {{.SchemaName}}.{{.FunctionName}}()
RETURNS trigger AS
$body$
DECLARE
m_funcname text = '{{.FunctionName}}';
m_user text;
m_atevent integer;
m_application_name text;
BEGIN
-- Get current user and application
m_user := {{.UserFunction}}::text;
m_application_name := current_setting('application_name', true);
-- Custom logging
RAISE NOTICE 'Audit: % on %.% by % from %',
TG_OP, TG_TABLE_SCHEMA, TG_TABLE_NAME, m_user, m_application_name;
-- Rest of function...
...
```
## Best Practices
### 1. Keep Templates Simple
Templates should focus on SQL generation. Complex logic belongs in Go code:
**Good:**
```go
// In Go code
columns := buildColumnList(table)
// In template
{{range .Columns}}
{{.Name}} {{.Type}}
{{end}}
```
**Bad:**
```go
// Don't do complex transformations in templates
{{range .Columns}}
{{if eq .Type "integer"}}
{{.Name}} serial
{{else}}
{{.Name}} {{.Type}}
{{end}}
{{end}}
```
### 2. Use Descriptive Field Names
```go
// Good
type CreateTableData struct {
SchemaName string
TableName string
}
// Bad
type CreateTableData struct {
S string // What is S?
T string // What is T?
}
```
### 3. Document Template Data
Always document what data a template expects:
```go
// CreateTableData contains data for create table template.
// Used by templates/create_table.tmpl
type CreateTableData struct {
SchemaName string // Schema where table will be created
TableName string // Name of the table
Columns []ColumnData // List of columns to create
}
```
### 4. Handle SQL Injection
Always escape user input:
```go
// In Go code - escape before passing to template
data := CommentTableData{
SchemaName: schema,
TableName: table,
Comment: escapeQuote(userComment), // Escape quotes
}
```
### 5. Test Templates Thoroughly
```go
func TestTemplate_CreateTable(t *testing.T) {
executor, _ := NewTemplateExecutor()
data := CreateTableData{
SchemaName: "public",
TableName: "test",
Columns: []ColumnData{{Name: "id", Type: "integer"}},
}
sql, err := executor.ExecuteCreateTable(data)
if err != nil {
t.Fatal(err)
}
// Verify expected SQL patterns
if !strings.Contains(sql, "CREATE TABLE") {
t.Error("Missing CREATE TABLE")
}
}
```
## Benefits of Template-Based Approach
### Maintainability
**Before (string concatenation):**
```go
sql := fmt.Sprintf(`CREATE TABLE %s.%s (
%s %s%s%s
);`, schema, table, col, typ,
func() string {
if def != "" {
return " DEFAULT " + def
}
return ""
}(),
func() string {
if notNull {
return " NOT NULL"
}
return ""
}(),
)
```
**After (templates):**
```go
sql, _ := executor.ExecuteCreateTable(CreateTableData{
SchemaName: schema,
TableName: table,
Columns: columns,
})
```
### Customization
Users can modify templates without changing Go code:
- Edit template file
- Rebuild application
- New SQL generation logic active
### Testing
Templates can be tested independently:
```go
func TestAuditTemplate(t *testing.T) {
executor, _ := NewTemplateExecutor()
// Test with various data
for _, testCase := range testCases {
sql, err := executor.ExecuteAuditFunction(testCase.data)
// Verify output
}
}
```
### Readability
SQL templates are easier to read and review than Go string building code.
## Migration from Old Writer
To migrate from the old string-based writer to templates:
### Option 1: Use TemplatedMigrationWriter
```go
// Old
writer := pgsql.NewMigrationWriter(options)
// New
writer, err := pgsql.NewTemplatedMigrationWriter(options)
if err != nil {
log.Fatal(err)
}
// Same interface
writer.WriteMigration(model, current)
```
### Option 2: Keep Both
Both writers are available:
- `MigrationWriter` - Original string-based
- `TemplatedMigrationWriter` - New template-based
Choose based on your needs.
## Troubleshooting
### Template Not Found
```
Error: template: "my_template.tmpl" not defined
```
Solution: Ensure template file exists in `templates/` directory and rebuild.
### Template Execution Error
```
Error: template: create_table.tmpl:5:10: executing "create_table.tmpl"
at <.InvalidField>: can't evaluate field InvalidField
```
Solution: Check data structure has all fields used in template.
### Embedded Files Not Updating
If template changes aren't reflected:
1. Clean build cache: `go clean -cache`
2. Rebuild: `go build ./cmd/relspec`
3. Verify template file is in `templates/` directory
## Custom Template Functions
RelSpec provides a comprehensive library of template functions for SQL generation:
### String Manipulation
- `upper`, `lower` - Case conversion
- `snake_case`, `camelCase` - Naming convention conversion
- Usage: `{{upper .TableName}}``USERS`
### SQL Formatting
- `indent(spaces, text)` - Indent text
- `quote(string)` - Quote for SQL with escaping
- `escape(string)` - Escape special characters
- `safe_identifier(string)` - Make SQL-safe identifier
- Usage: `{{quote "O'Brien"}}``'O''Brien'`
### Type Conversion
- `goTypeToSQL(type)` - Convert Go type to PostgreSQL type
- `sqlTypeToGo(type)` - Convert PostgreSQL type to Go type
- `isNumeric(type)`, `isText(type)` - Type checking
- Usage: `{{goTypeToSQL "int64"}}``bigint`
### Collection Helpers
- `first(slice)`, `last(slice)` - Get elements
- `join_with(slice, sep)` - Join with custom separator
- Usage: `{{join_with .Columns ", "}}``id, name, email`
See [template_functions.go](template_functions.go) for full documentation.
## Template Inheritance and Composition
RelSpec supports Go template inheritance using `{{template}}` and `{{block}}`:
### Base Templates
- `base_ddl.tmpl` - Common DDL patterns
- `base_constraint.tmpl` - Constraint operations
- `fragments.tmpl` - Reusable fragments
### Using Fragments
```gotmpl
{{/* Use predefined fragments */}}
CREATE TABLE {{template "qualified_table" .}} (
{{range .Columns}}
{{template "column_definition" .}}
{{end}}
);
```
### Template Blocks
```gotmpl
{{/* Define with override capability */}}
{{define "table_options"}}
) {{block "storage_options" .}}WITH (fillfactor = 90){{end}};
{{end}}
```
See [TEMPLATE_INHERITANCE.md](TEMPLATE_INHERITANCE.md) for detailed guide.
## Visual Template Editor
A VS Code extension is available for visual template editing:
### Features
- **Live Preview** - See rendered SQL as you type
- **IntelliSense** - Auto-completion for functions
- **Validation** - Syntax checking and error highlighting
- **Scaffolding** - Quick template creation
- **Function Browser** - Browse available functions
### Installation
```bash
cd vscode-extension
npm install
npm run compile
code .
# Press F5 to launch
```
See [vscode-extension/README.md](../../vscode-extension/README.md) for full documentation.
## Future Enhancements
Completed:
- [x] Template inheritance/composition
- [x] Custom template functions library
- [x] Visual template editor (VS Code)
Potential future improvements:
- [ ] Parameterized templates (load from config)
- [ ] Template validation CLI tool
- [ ] Template library/marketplace
- [ ] Template versioning
- [ ] Hot-reload during development
## Contributing Templates
When contributing new templates:
1. Place in `pkg/writers/pgsql/templates/`
2. Use `.tmpl` extension
3. Document data structure in `templates.go`
4. Add executor method
5. Write tests
6. Update this documentation

View File

@@ -0,0 +1,74 @@
package pgsql
import (
"fmt"
)
// AuditConfig defines audit configuration for tables
type AuditConfig struct {
// EnabledTables maps table names (schema.table or just table) to audit settings
EnabledTables map[string]*TableAuditConfig
// AuditSchema is where audit tables are created (default: same as table schema)
AuditSchema string
// UserFunction is the function to get current user (default: current_user)
UserFunction string
}
// TableAuditConfig defines audit settings for a specific table
type TableAuditConfig struct {
// TableName is the name of the table to audit
TableName string
// SchemaName is the schema of the table
SchemaName string
// TablePrefix for compatibility with old audit system
TablePrefix string
// AuditInsert tracks INSERT operations
AuditInsert bool
// AuditUpdate tracks UPDATE operations
AuditUpdate bool
// AuditDelete tracks DELETE operations
AuditDelete bool
// ExcludedColumns are columns to skip from audit
ExcludedColumns []string
// EncryptedColumns are columns to hide in audit (show as ***)
EncryptedColumns []string
}
// NewAuditConfig creates a default audit configuration
func NewAuditConfig() *AuditConfig {
return &AuditConfig{
EnabledTables: make(map[string]*TableAuditConfig),
AuditSchema: "public",
UserFunction: "current_user",
}
}
// EnableTableAudit enables audit for a specific table
func (ac *AuditConfig) EnableTableAudit(schemaName, tableName string) *TableAuditConfig {
key := fmt.Sprintf("%s.%s", schemaName, tableName)
config := &TableAuditConfig{
TableName: tableName,
SchemaName: schemaName,
TablePrefix: "",
AuditInsert: true,
AuditUpdate: true,
AuditDelete: true,
ExcludedColumns: []string{"updatecnt", "prefix"},
EncryptedColumns: []string{},
}
ac.EnabledTables[key] = config
return config
}
// IsTableAudited checks if a table is configured for auditing
func (ac *AuditConfig) IsTableAudited(schemaName, tableName string) bool {
key := fmt.Sprintf("%s.%s", schemaName, tableName)
_, exists := ac.EnabledTables[key]
return exists
}
// GetTableConfig returns the audit config for a specific table
func (ac *AuditConfig) GetTableConfig(schemaName, tableName string) *TableAuditConfig {
key := fmt.Sprintf("%s.%s", schemaName, tableName)
return ac.EnabledTables[key]
}

View File

@@ -11,13 +11,7 @@ import (
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
// MigrationWriter generates differential migration SQL scripts
type MigrationWriter struct {
options *writers.WriterOptions
writer io.Writer
}
// MigrationScript represents a single migration script with priority and sequence
// MigrationScript represents a single migration script with priority
type MigrationScript struct {
ObjectName string
ObjectType string
@@ -27,14 +21,27 @@ type MigrationScript struct {
Body string
}
// NewMigrationWriter creates a new migration writer
func NewMigrationWriter(options *writers.WriterOptions) *MigrationWriter {
return &MigrationWriter{
options: options,
}
// MigrationWriter generates differential migration SQL scripts using templates
type MigrationWriter struct {
options *writers.WriterOptions
writer io.Writer
executor *TemplateExecutor
}
// WriteMigration generates migration scripts by comparing model (desired) vs current (actual) database
// NewMigrationWriter creates a new templated migration writer
func NewMigrationWriter(options *writers.WriterOptions) (*MigrationWriter, error) {
executor, err := NewTemplateExecutor()
if err != nil {
return nil, fmt.Errorf("failed to create template executor: %w", err)
}
return &MigrationWriter{
options: options,
executor: executor,
}, nil
}
// WriteMigration generates migration scripts using templates
func (w *MigrationWriter) WriteMigration(model *models.Database, current *models.Database) error {
var writer io.Writer
var file *os.File
@@ -56,9 +63,26 @@ func (w *MigrationWriter) WriteMigration(model *models.Database, current *models
w.writer = writer
// Check if audit is configured in metadata
var auditConfig *AuditConfig
if w.options.Metadata != nil {
if ac, ok := w.options.Metadata["audit_config"].(*AuditConfig); ok {
auditConfig = ac
}
}
// Generate all migration scripts
scripts := make([]MigrationScript, 0)
// Generate audit tables if needed (priority 90)
if auditConfig != nil && len(auditConfig.EnabledTables) > 0 {
auditTableScript, err := w.generateAuditTablesScript(auditConfig)
if err != nil {
return fmt.Errorf("failed to generate audit tables: %w", err)
}
scripts = append(scripts, auditTableScript...)
}
// Process each schema in the model
for _, modelSchema := range model.Schemas {
// Find corresponding schema in current database
@@ -71,8 +95,20 @@ func (w *MigrationWriter) WriteMigration(model *models.Database, current *models
}
// Generate schema-level scripts
schemaScripts := w.generateSchemaScripts(modelSchema, currentSchema)
schemaScripts, err := w.generateSchemaScripts(modelSchema, currentSchema)
if err != nil {
return fmt.Errorf("failed to generate schema scripts: %w", err)
}
scripts = append(scripts, schemaScripts...)
// Generate audit scripts for this schema (if configured)
if auditConfig != nil {
auditScripts, err := w.generateAuditScripts(modelSchema, auditConfig)
if err != nil {
return fmt.Errorf("failed to generate audit scripts: %w", err)
}
scripts = append(scripts, auditScripts...)
}
}
// Sort scripts by priority and sequence
@@ -98,37 +134,52 @@ func (w *MigrationWriter) WriteMigration(model *models.Database, current *models
return nil
}
// generateSchemaScripts generates migration scripts for a schema
func (w *MigrationWriter) generateSchemaScripts(model *models.Schema, current *models.Schema) []MigrationScript {
// generateSchemaScripts generates migration scripts for a schema using templates
func (w *MigrationWriter) generateSchemaScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0)
// Phase 1: Drop constraints and indexes that changed (Priority 11-50)
if current != nil {
scripts = append(scripts, w.generateDropScripts(model, current)...)
}
// Phase 2: Rename tables and columns (Priority 60-90)
if current != nil {
scripts = append(scripts, w.generateRenameScripts(model, current)...)
dropScripts, err := w.generateDropScripts(model, current)
if err != nil {
return nil, fmt.Errorf("failed to generate drop scripts: %w", err)
}
scripts = append(scripts, dropScripts...)
}
// Phase 3: Create/Alter tables and columns (Priority 100-145)
scripts = append(scripts, w.generateTableScripts(model, current)...)
tableScripts, err := w.generateTableScripts(model, current)
if err != nil {
return nil, fmt.Errorf("failed to generate table scripts: %w", err)
}
scripts = append(scripts, tableScripts...)
// Phase 4: Create indexes (Priority 160-180)
scripts = append(scripts, w.generateIndexScripts(model, current)...)
indexScripts, err := w.generateIndexScripts(model, current)
if err != nil {
return nil, fmt.Errorf("failed to generate index scripts: %w", err)
}
scripts = append(scripts, indexScripts...)
// Phase 5: Create foreign keys (Priority 195)
scripts = append(scripts, w.generateForeignKeyScripts(model, current)...)
fkScripts, err := w.generateForeignKeyScripts(model, current)
if err != nil {
return nil, fmt.Errorf("failed to generate foreign key scripts: %w", err)
}
scripts = append(scripts, fkScripts...)
// Phase 6: Add comments (Priority 200+)
scripts = append(scripts, w.generateCommentScripts(model, current)...)
commentScripts, err := w.generateCommentScripts(model, current)
if err != nil {
return nil, fmt.Errorf("failed to generate comment scripts: %w", err)
}
scripts = append(scripts, commentScripts...)
return scripts
return scripts, nil
}
// generateDropScripts generates DROP scripts for removed/changed objects
func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *models.Schema) []MigrationScript {
// generateDropScripts generates DROP scripts using templates
func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0)
// Build map of model tables for quick lookup
@@ -142,35 +193,37 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
modelTable, existsInModel := modelTables[strings.ToLower(currentTable.Name)]
if !existsInModel {
// Table will be dropped, skip individual constraint drops
continue
}
// Check each constraint in current database
for constraintName, currentConstraint := range currentTable.Constraints {
// Check if constraint exists in model
modelConstraint, existsInModel := modelTable.Constraints[constraintName]
shouldDrop := false
if !existsInModel {
shouldDrop = true
} else if !constraintsEqual(modelConstraint, currentConstraint) {
// Constraint changed, drop and recreate
shouldDrop = true
}
if shouldDrop {
sql, err := w.executor.ExecuteDropConstraint(DropConstraintData{
SchemaName: current.Name,
TableName: currentTable.Name,
ConstraintName: constraintName,
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", current.Name, currentTable.Name, constraintName),
ObjectType: "drop constraint",
Schema: current.Name,
Priority: 11,
Sequence: len(scripts),
Body: fmt.Sprintf(
"ALTER TABLE %s.%s DROP CONSTRAINT IF EXISTS %s;",
current.Name, currentTable.Name, constraintName,
),
Body: sql,
}
scripts = append(scripts, script)
}
@@ -181,7 +234,6 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
modelIndex, existsInModel := modelTable.Indexes[indexName]
shouldDrop := false
if !existsInModel {
shouldDrop = true
} else if !indexesEqual(modelIndex, currentIndex) {
@@ -189,42 +241,32 @@ func (w *MigrationWriter) generateDropScripts(model *models.Schema, current *mod
}
if shouldDrop {
sql, err := w.executor.ExecuteDropIndex(DropIndexData{
SchemaName: current.Name,
IndexName: indexName,
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", current.Name, currentTable.Name, indexName),
ObjectType: "drop index",
Schema: current.Name,
Priority: 20,
Sequence: len(scripts),
Body: fmt.Sprintf(
"DROP INDEX IF EXISTS %s.%s CASCADE;",
current.Name, indexName,
),
Body: sql,
}
scripts = append(scripts, script)
}
}
}
return scripts
return scripts, nil
}
// generateRenameScripts generates RENAME scripts for renamed objects
func (w *MigrationWriter) generateRenameScripts(model *models.Schema, current *models.Schema) []MigrationScript {
scripts := make([]MigrationScript, 0)
// For now, we don't attempt to detect renames automatically
// This would require GUID matching or other heuristics
// Users would need to handle renames manually or through metadata
// Suppress unused parameter warnings
_ = model
_ = current
return scripts
}
// generateTableScripts generates CREATE/ALTER TABLE scripts
func (w *MigrationWriter) generateTableScripts(model *models.Schema, current *models.Schema) []MigrationScript {
// generateTableScripts generates CREATE/ALTER TABLE scripts using templates
func (w *MigrationWriter) generateTableScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0)
// Build map of current tables
@@ -241,59 +283,35 @@ func (w *MigrationWriter) generateTableScripts(model *models.Schema, current *mo
if !exists {
// Table doesn't exist, create it
script := w.generateCreateTableScript(model, modelTable)
sql, err := w.executor.ExecuteCreateTable(BuildCreateTableData(model.Name, modelTable))
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s", model.Name, modelTable.Name),
ObjectType: "create table",
Schema: model.Name,
Priority: 100,
Sequence: len(scripts),
Body: sql,
}
scripts = append(scripts, script)
} else {
// Table exists, check for column changes
alterScripts := w.generateAlterTableScripts(model, modelTable, currentTable)
alterScripts, err := w.generateAlterTableScripts(model, modelTable, currentTable)
if err != nil {
return nil, err
}
scripts = append(scripts, alterScripts...)
}
}
return scripts
return scripts, nil
}
// generateCreateTableScript generates a CREATE TABLE script
func (w *MigrationWriter) generateCreateTableScript(schema *models.Schema, table *models.Table) MigrationScript {
var body strings.Builder
body.WriteString(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (\n", schema.Name, table.Name))
// Get sorted columns
columns := getSortedColumns(table.Columns)
columnDefs := make([]string, 0, len(columns))
for _, col := range columns {
colDef := fmt.Sprintf(" %s %s", col.Name, col.Type)
// Add default value if present
if col.Default != nil {
colDef += fmt.Sprintf(" DEFAULT %v", col.Default)
}
// Add NOT NULL if needed
if col.NotNull {
colDef += " NOT NULL"
}
columnDefs = append(columnDefs, colDef)
}
body.WriteString(strings.Join(columnDefs, ",\n"))
body.WriteString("\n);")
return MigrationScript{
ObjectName: fmt.Sprintf("%s.%s", schema.Name, table.Name),
ObjectType: "create table",
Schema: schema.Name,
Priority: 100,
Sequence: 0,
Body: body.String(),
}
}
// generateAlterTableScripts generates ALTER TABLE scripts for column changes
func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, modelTable *models.Table, currentTable *models.Table) []MigrationScript {
// generateAlterTableScripts generates ALTER TABLE scripts using templates
func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, modelTable *models.Table, currentTable *models.Table) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0)
// Build map of current columns
@@ -308,85 +326,93 @@ func (w *MigrationWriter) generateAlterTableScripts(schema *models.Schema, model
if !exists {
// Column doesn't exist, add it
defaultVal := ""
if modelCol.Default != nil {
defaultVal = fmt.Sprintf("%v", modelCol.Default)
}
sql, err := w.executor.ExecuteAddColumn(AddColumnData{
SchemaName: schema.Name,
TableName: modelTable.Name,
ColumnName: modelCol.Name,
ColumnType: modelCol.Type,
Default: defaultVal,
NotNull: modelCol.NotNull,
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
ObjectType: "create column",
Schema: schema.Name,
Priority: 120,
Sequence: len(scripts),
Body: fmt.Sprintf(
"ALTER TABLE %s.%s\n ADD COLUMN IF NOT EXISTS %s %s%s%s;",
schema.Name, modelTable.Name, modelCol.Name, modelCol.Type,
func() string {
if modelCol.Default != nil {
return fmt.Sprintf(" DEFAULT %v", modelCol.Default)
}
return ""
}(),
func() string {
if modelCol.NotNull {
return " NOT NULL"
}
return ""
}(),
),
Body: sql,
}
scripts = append(scripts, script)
} else if !columnsEqual(modelCol, currentCol) {
// Column exists but type or properties changed
// Column exists but properties changed
if modelCol.Type != currentCol.Type {
sql, err := w.executor.ExecuteAlterColumnType(AlterColumnTypeData{
SchemaName: schema.Name,
TableName: modelTable.Name,
ColumnName: modelCol.Name,
NewType: modelCol.Type,
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
ObjectType: "alter column type",
Schema: schema.Name,
Priority: 120,
Sequence: len(scripts),
Body: fmt.Sprintf(
"ALTER TABLE %s.%s\n ALTER COLUMN %s TYPE %s;",
schema.Name, modelTable.Name, modelCol.Name, modelCol.Type,
),
Body: sql,
}
scripts = append(scripts, script)
}
// Check default value changes
if fmt.Sprintf("%v", modelCol.Default) != fmt.Sprintf("%v", currentCol.Default) {
if modelCol.Default != nil {
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
ObjectType: "alter column default",
Schema: schema.Name,
Priority: 145,
Sequence: len(scripts),
Body: fmt.Sprintf(
"ALTER TABLE %s.%s\n ALTER COLUMN %s SET DEFAULT %v;",
schema.Name, modelTable.Name, modelCol.Name, modelCol.Default,
),
}
scripts = append(scripts, script)
} else {
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
ObjectType: "alter column default",
Schema: schema.Name,
Priority: 145,
Sequence: len(scripts),
Body: fmt.Sprintf(
"ALTER TABLE %s.%s\n ALTER COLUMN %s DROP DEFAULT;",
schema.Name, modelTable.Name, modelCol.Name,
),
}
scripts = append(scripts, script)
setDefault := modelCol.Default != nil
defaultVal := ""
if setDefault {
defaultVal = fmt.Sprintf("%v", modelCol.Default)
}
sql, err := w.executor.ExecuteAlterColumnDefault(AlterColumnDefaultData{
SchemaName: schema.Name,
TableName: modelTable.Name,
ColumnName: modelCol.Name,
SetDefault: setDefault,
DefaultValue: defaultVal,
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", schema.Name, modelTable.Name, modelCol.Name),
ObjectType: "alter column default",
Schema: schema.Name,
Priority: 145,
Sequence: len(scripts),
Body: sql,
}
scripts = append(scripts, script)
}
}
}
return scripts
return scripts, nil
}
// generateIndexScripts generates CREATE INDEX scripts
func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *models.Schema) []MigrationScript {
// generateIndexScripts generates CREATE INDEX scripts using templates
func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0)
// Build map of current tables
@@ -401,47 +427,7 @@ func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *mo
for _, modelTable := range model.Tables {
currentTable := currentTables[strings.ToLower(modelTable.Name)]
// Process each index in model
for indexName, modelIndex := range modelTable.Indexes {
shouldCreate := true
// Check if index exists in current
if currentTable != nil {
if currentIndex, exists := currentTable.Indexes[indexName]; exists {
if indexesEqual(modelIndex, currentIndex) {
shouldCreate = false
}
}
}
if shouldCreate {
unique := ""
if modelIndex.Unique {
unique = "UNIQUE "
}
indexType := "btree"
if modelIndex.Type != "" {
indexType = modelIndex.Type
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, indexName),
ObjectType: "create index",
Schema: model.Name,
Priority: 180,
Sequence: len(scripts),
Body: fmt.Sprintf(
"CREATE %sINDEX IF NOT EXISTS %s\n ON %s.%s USING %s (%s);",
unique, indexName, model.Name, modelTable.Name, indexType,
strings.Join(modelIndex.Columns, ", "),
),
}
scripts = append(scripts, script)
}
}
// Add primary key constraint if it exists
// Process primary keys first
for constraintName, constraint := range modelTable.Constraints {
if constraint.Type == models.PrimaryKeyConstraint {
shouldCreate := true
@@ -455,39 +441,82 @@ func (w *MigrationWriter) generateIndexScripts(model *models.Schema, current *mo
}
if shouldCreate {
sql, err := w.executor.ExecuteCreatePrimaryKey(CreatePrimaryKeyData{
SchemaName: model.Name,
TableName: modelTable.Name,
ConstraintName: constraintName,
Columns: strings.Join(constraint.Columns, ", "),
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, constraintName),
ObjectType: "create primary key",
Schema: model.Name,
Priority: 160,
Sequence: len(scripts),
Body: fmt.Sprintf(
"DO $$\nBEGIN\n IF NOT EXISTS (\n"+
" SELECT 1 FROM information_schema.table_constraints\n"+
" WHERE table_schema = '%s'\n"+
" AND table_name = '%s'\n"+
" AND constraint_name = '%s'\n"+
" ) THEN\n"+
" ALTER TABLE %s.%s\n"+
" ADD CONSTRAINT %s PRIMARY KEY (%s);\n"+
" END IF;\n"+
"END;\n$$;",
model.Name, modelTable.Name, constraintName,
model.Name, modelTable.Name, constraintName,
strings.Join(constraint.Columns, ", "),
),
Body: sql,
}
scripts = append(scripts, script)
}
}
}
// Process indexes
for indexName, modelIndex := range modelTable.Indexes {
// Skip primary key indexes
if strings.HasPrefix(strings.ToLower(indexName), "pk_") {
continue
}
shouldCreate := true
if currentTable != nil {
if currentIndex, exists := currentTable.Indexes[indexName]; exists {
if indexesEqual(modelIndex, currentIndex) {
shouldCreate = false
}
}
}
if shouldCreate {
indexType := "btree"
if modelIndex.Type != "" {
indexType = modelIndex.Type
}
sql, err := w.executor.ExecuteCreateIndex(CreateIndexData{
SchemaName: model.Name,
TableName: modelTable.Name,
IndexName: indexName,
IndexType: indexType,
Columns: strings.Join(modelIndex.Columns, ", "),
Unique: modelIndex.Unique,
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, indexName),
ObjectType: "create index",
Schema: model.Name,
Priority: 180,
Sequence: len(scripts),
Body: sql,
}
scripts = append(scripts, script)
}
}
}
return scripts
return scripts, nil
}
// generateForeignKeyScripts generates ADD CONSTRAINT FOREIGN KEY scripts
func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, current *models.Schema) []MigrationScript {
// generateForeignKeyScripts generates ADD CONSTRAINT FOREIGN KEY scripts using templates
func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0)
// Build map of current tables
@@ -510,7 +539,6 @@ func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, curren
shouldCreate := true
// Check if constraint exists in current
if currentTable != nil {
if currentConstraint, exists := currentTable.Constraints[constraintName]; exists {
if constraintsEqual(constraint, currentConstraint) {
@@ -530,59 +558,62 @@ func (w *MigrationWriter) generateForeignKeyScripts(model *models.Schema, curren
onUpdate = strings.ToUpper(constraint.OnUpdate)
}
sql, err := w.executor.ExecuteCreateForeignKey(CreateForeignKeyData{
SchemaName: model.Name,
TableName: modelTable.Name,
ConstraintName: constraintName,
SourceColumns: strings.Join(constraint.Columns, ", "),
TargetSchema: constraint.ReferencedSchema,
TargetTable: constraint.ReferencedTable,
TargetColumns: strings.Join(constraint.ReferencedColumns, ", "),
OnDelete: onDelete,
OnUpdate: onUpdate,
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, constraintName),
ObjectType: "create foreign key",
Schema: model.Name,
Priority: 195,
Sequence: len(scripts),
Body: fmt.Sprintf(
"ALTER TABLE %s.%s\n"+
" DROP CONSTRAINT IF EXISTS %s;\n\n"+
"ALTER TABLE %s.%s\n"+
" ADD CONSTRAINT %s\n"+
" FOREIGN KEY (%s)\n"+
" REFERENCES %s.%s (%s)\n"+
" ON DELETE %s\n"+
" ON UPDATE %s\n"+
" DEFERRABLE;",
model.Name, modelTable.Name, constraintName,
model.Name, modelTable.Name, constraintName,
strings.Join(constraint.Columns, ", "),
constraint.ReferencedSchema, constraint.ReferencedTable,
strings.Join(constraint.ReferencedColumns, ", "),
onDelete, onUpdate,
),
Body: sql,
}
scripts = append(scripts, script)
}
}
}
return scripts
return scripts, nil
}
// generateCommentScripts generates COMMENT ON scripts
func (w *MigrationWriter) generateCommentScripts(model *models.Schema, current *models.Schema) []MigrationScript {
// generateCommentScripts generates COMMENT ON scripts using templates
func (w *MigrationWriter) generateCommentScripts(model *models.Schema, current *models.Schema) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0)
// Suppress unused parameter warning (current not used yet, could be used for diffing)
_ = current
_ = current // TODO: Compare with current schema to only add new/changed comments
// Process each model table
for _, modelTable := range model.Tables {
// Table comment
if modelTable.Description != "" {
sql, err := w.executor.ExecuteCommentTable(CommentTableData{
SchemaName: model.Name,
TableName: modelTable.Name,
Comment: escapeQuote(modelTable.Description),
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s", model.Name, modelTable.Name),
ObjectType: "comment on table",
Schema: model.Name,
Priority: 200,
Sequence: len(scripts),
Body: fmt.Sprintf(
"COMMENT ON TABLE %s.%s IS '%s';",
model.Name, modelTable.Name, escapeQuote(modelTable.Description),
),
Body: sql,
}
scripts = append(scripts, script)
}
@@ -590,79 +621,218 @@ func (w *MigrationWriter) generateCommentScripts(model *models.Schema, current *
// Column comments
for _, col := range modelTable.Columns {
if col.Description != "" {
sql, err := w.executor.ExecuteCommentColumn(CommentColumnData{
SchemaName: model.Name,
TableName: modelTable.Name,
ColumnName: col.Name,
Comment: escapeQuote(col.Description),
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s.%s", model.Name, modelTable.Name, col.Name),
ObjectType: "comment on column",
Schema: model.Name,
Priority: 200,
Sequence: len(scripts),
Body: fmt.Sprintf(
"COMMENT ON COLUMN %s.%s.%s IS '%s';",
model.Name, modelTable.Name, col.Name, escapeQuote(col.Description),
),
Body: sql,
}
scripts = append(scripts, script)
}
}
}
return scripts
return scripts, nil
}
// Comparison helper functions
// generateAuditTablesScript generates audit table creation scripts using templates
func (w *MigrationWriter) generateAuditTablesScript(auditConfig *AuditConfig) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0)
func constraintsEqual(a, b *models.Constraint) bool {
if a.Type != b.Type {
auditSchema := auditConfig.AuditSchema
if auditSchema == "" {
auditSchema = "public"
}
sql, err := w.executor.ExecuteAuditTables(AuditTablesData{
AuditSchema: auditSchema,
})
if err != nil {
return nil, err
}
script := MigrationScript{
ObjectName: fmt.Sprintf("%s.atevent+atdetail", auditSchema),
ObjectType: "create audit tables",
Schema: auditSchema,
Priority: 90,
Sequence: 0,
Body: sql,
}
scripts = append(scripts, script)
return scripts, nil
}
// generateAuditScripts generates audit functions and triggers using templates
func (w *MigrationWriter) generateAuditScripts(schema *models.Schema, auditConfig *AuditConfig) ([]MigrationScript, error) {
scripts := make([]MigrationScript, 0)
// Process each table in the schema
for _, table := range schema.Tables {
if !auditConfig.IsTableAudited(schema.Name, table.Name) {
continue
}
config := auditConfig.GetTableConfig(schema.Name, table.Name)
if config == nil {
continue
}
// Find primary key
pk := table.GetPrimaryKey()
if pk == nil {
continue
}
auditSchema := auditConfig.AuditSchema
if auditSchema == "" {
auditSchema = schema.Name
}
// Generate audit function
funcName := fmt.Sprintf("ft_audit_%s", table.Name)
funcData := BuildAuditFunctionData(schema.Name, table, pk, config, auditSchema, auditConfig.UserFunction)
funcSQL, err := w.executor.ExecuteAuditFunction(funcData)
if err != nil {
return nil, err
}
functionScript := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s", schema.Name, funcName),
ObjectType: "create audit function",
Schema: schema.Name,
Priority: 345,
Sequence: len(scripts),
Body: funcSQL,
}
scripts = append(scripts, functionScript)
// Generate audit trigger
triggerName := fmt.Sprintf("t_audit_%s", table.Name)
events := make([]string, 0)
if config.AuditInsert {
events = append(events, "INSERT")
}
if config.AuditUpdate {
events = append(events, "UPDATE")
}
if config.AuditDelete {
events = append(events, "DELETE")
}
if len(events) == 0 {
continue
}
triggerSQL, err := w.executor.ExecuteAuditTrigger(AuditTriggerData{
SchemaName: schema.Name,
TableName: table.Name,
TriggerName: triggerName,
FunctionName: funcName,
Events: strings.Join(events, " OR "),
})
if err != nil {
return nil, err
}
triggerScript := MigrationScript{
ObjectName: fmt.Sprintf("%s.%s", schema.Name, triggerName),
ObjectType: "create audit trigger",
Schema: schema.Name,
Priority: 355,
Sequence: len(scripts),
Body: triggerSQL,
}
scripts = append(scripts, triggerScript)
}
return scripts, nil
}
// Helper functions for comparing database objects
// columnsEqual checks if two columns have the same definition
func columnsEqual(col1, col2 *models.Column) bool {
if col1 == nil || col2 == nil {
return false
}
if len(a.Columns) != len(b.Columns) {
return strings.EqualFold(col1.Type, col2.Type) &&
col1.NotNull == col2.NotNull &&
fmt.Sprintf("%v", col1.Default) == fmt.Sprintf("%v", col2.Default)
}
// constraintsEqual checks if two constraints are equal
func constraintsEqual(c1, c2 *models.Constraint) bool {
if c1 == nil || c2 == nil {
return false
}
for i := range a.Columns {
if !strings.EqualFold(a.Columns[i], b.Columns[i]) {
if c1.Type != c2.Type {
return false
}
// Compare columns
if len(c1.Columns) != len(c2.Columns) {
return false
}
for i, col := range c1.Columns {
if !strings.EqualFold(col, c2.Columns[i]) {
return false
}
}
if a.Type == models.ForeignKeyConstraint {
if a.ReferencedTable != b.ReferencedTable || a.ReferencedSchema != b.ReferencedSchema {
// For foreign keys, also compare referenced table and columns
if c1.Type == models.ForeignKeyConstraint {
if !strings.EqualFold(c1.ReferencedTable, c2.ReferencedTable) {
return false
}
if len(a.ReferencedColumns) != len(b.ReferencedColumns) {
if len(c1.ReferencedColumns) != len(c2.ReferencedColumns) {
return false
}
for i := range a.ReferencedColumns {
if !strings.EqualFold(a.ReferencedColumns[i], b.ReferencedColumns[i]) {
for i, col := range c1.ReferencedColumns {
if !strings.EqualFold(col, c2.ReferencedColumns[i]) {
return false
}
}
if c1.OnDelete != c2.OnDelete || c1.OnUpdate != c2.OnUpdate {
return false
}
}
return true
}
func indexesEqual(a, b *models.Index) bool {
if a.Unique != b.Unique {
// indexesEqual checks if two indexes are equal
func indexesEqual(idx1, idx2 *models.Index) bool {
if idx1 == nil || idx2 == nil {
return false
}
if len(a.Columns) != len(b.Columns) {
if idx1.Unique != idx2.Unique {
return false
}
for i := range a.Columns {
if !strings.EqualFold(a.Columns[i], b.Columns[i]) {
if !strings.EqualFold(idx1.Type, idx2.Type) {
return false
}
if len(idx1.Columns) != len(idx2.Columns) {
return false
}
for i, col := range idx1.Columns {
if !strings.EqualFold(col, idx2.Columns[i]) {
return false
}
}
return true
}
func columnsEqual(a, b *models.Column) bool {
if a.Type != b.Type {
return false
}
if a.NotNull != b.NotNull {
return false
}
if fmt.Sprintf("%v", a.Default) != fmt.Sprintf("%v", b.Default) {
return false
}
return true
}

View File

@@ -34,10 +34,13 @@ func TestWriteMigration_NewTable(t *testing.T) {
// Generate migration
var buf bytes.Buffer
writer := NewMigrationWriter(&writers.WriterOptions{})
writer, err := NewMigrationWriter(&writers.WriterOptions{})
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
err := writer.WriteMigration(model, current)
err = writer.WriteMigration(model, current)
if err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
@@ -54,234 +57,161 @@ func TestWriteMigration_NewTable(t *testing.T) {
}
}
func TestWriteMigration_AddColumn(t *testing.T) {
// Current database (with table but missing column)
func TestWriteMigration_WithAudit(t *testing.T) {
// Current database (empty)
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
currentTable := models.InitTable("users", "public")
current.Schemas = append(current.Schemas, currentSchema)
// Model database (with table to audit)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
table := models.InitTable("users", "public")
idCol := models.InitColumn("id", "users", "public")
idCol.Type = "integer"
currentTable.Columns["id"] = idCol
idCol.IsPrimaryKey = true
table.Columns["id"] = idCol
currentSchema.Tables = append(currentSchema.Tables, currentTable)
current.Schemas = append(current.Schemas, currentSchema)
nameCol := models.InitColumn("name", "users", "public")
nameCol.Type = "text"
table.Columns["name"] = nameCol
// Model database (with additional column)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
modelTable := models.InitTable("users", "public")
passwordCol := models.InitColumn("password", "users", "public")
passwordCol.Type = "text"
table.Columns["password"] = passwordCol
idCol2 := models.InitColumn("id", "users", "public")
idCol2.Type = "integer"
modelTable.Columns["id"] = idCol2
emailCol := models.InitColumn("email", "users", "public")
emailCol.Type = "text"
modelTable.Columns["email"] = emailCol
modelSchema.Tables = append(modelSchema.Tables, modelTable)
modelSchema.Tables = append(modelSchema.Tables, table)
model.Schemas = append(model.Schemas, modelSchema)
// Generate migration
// Configure audit
auditConfig := NewAuditConfig()
auditConfig.AuditSchema = "public"
tableConfig := auditConfig.EnableTableAudit("public", "users")
tableConfig.EncryptedColumns = []string{"password"}
// Generate migration with audit
var buf bytes.Buffer
writer := NewMigrationWriter(&writers.WriterOptions{})
options := &writers.WriterOptions{
Metadata: map[string]interface{}{
"audit_config": auditConfig,
},
}
writer, err := NewMigrationWriter(options)
if err != nil {
t.Fatalf("Failed to create writer: %v", err)
}
writer.writer = &buf
err := writer.WriteMigration(model, current)
err = writer.WriteMigration(model, current)
if err != nil {
t.Fatalf("WriteMigration failed: %v", err)
}
output := buf.String()
t.Logf("Generated migration:\n%s", output)
t.Logf("Generated migration with audit:\n%s", output)
// Verify ADD COLUMN is present
if !strings.Contains(output, "ADD COLUMN") {
t.Error("Migration missing ADD COLUMN statement")
// Verify audit tables
if !strings.Contains(output, "CREATE TABLE IF NOT EXISTS public.atevent") {
t.Error("Migration missing atevent table")
}
if !strings.Contains(output, "email") {
t.Error("Migration missing column name 'email'")
if !strings.Contains(output, "CREATE TABLE IF NOT EXISTS public.atdetail") {
t.Error("Migration missing atdetail table")
}
// Verify audit function
if !strings.Contains(output, "CREATE OR REPLACE FUNCTION public.ft_audit_users()") {
t.Error("Migration missing audit function")
}
// Verify audit trigger
if !strings.Contains(output, "CREATE TRIGGER t_audit_users") {
t.Error("Migration missing audit trigger")
}
// Verify encrypted column handling
if !strings.Contains(output, "'****************'") {
t.Error("Migration missing encrypted column handling")
}
}
func TestWriteMigration_ChangeColumnType(t *testing.T) {
// Current database (with integer column)
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
currentTable := models.InitTable("users", "public")
idCol := models.InitColumn("id", "users", "public")
idCol.Type = "integer"
currentTable.Columns["id"] = idCol
currentSchema.Tables = append(currentSchema.Tables, currentTable)
current.Schemas = append(current.Schemas, currentSchema)
// Model database (changed to bigint)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
modelTable := models.InitTable("users", "public")
idCol2 := models.InitColumn("id", "users", "public")
idCol2.Type = "bigint"
modelTable.Columns["id"] = idCol2
modelSchema.Tables = append(modelSchema.Tables, modelTable)
model.Schemas = append(model.Schemas, modelSchema)
// Generate migration
var buf bytes.Buffer
writer := NewMigrationWriter(&writers.WriterOptions{})
writer.writer = &buf
err := writer.WriteMigration(model, current)
func TestTemplateExecutor_CreateTable(t *testing.T) {
executor, err := NewTemplateExecutor()
if err != nil {
t.Fatalf("WriteMigration failed: %v", err)
t.Fatalf("Failed to create executor: %v", err)
}
output := buf.String()
t.Logf("Generated migration:\n%s", output)
// Verify ALTER COLUMN TYPE is present
if !strings.Contains(output, "ALTER COLUMN") {
t.Error("Migration missing ALTER COLUMN statement")
data := CreateTableData{
SchemaName: "public",
TableName: "test_table",
Columns: []ColumnData{
{Name: "id", Type: "integer", NotNull: true},
{Name: "name", Type: "text", Default: "'unknown'"},
},
}
if !strings.Contains(output, "TYPE bigint") {
t.Error("Migration missing TYPE bigint")
}
}
func TestWriteMigration_AddForeignKey(t *testing.T) {
// Current database (two tables, no relationship)
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
usersTable := models.InitTable("users", "public")
idCol := models.InitColumn("id", "users", "public")
idCol.Type = "integer"
usersTable.Columns["id"] = idCol
postsTable := models.InitTable("posts", "public")
postIdCol := models.InitColumn("id", "posts", "public")
postIdCol.Type = "integer"
postsTable.Columns["id"] = postIdCol
userIdCol := models.InitColumn("user_id", "posts", "public")
userIdCol.Type = "integer"
postsTable.Columns["user_id"] = userIdCol
currentSchema.Tables = append(currentSchema.Tables, usersTable, postsTable)
current.Schemas = append(current.Schemas, currentSchema)
// Model database (with foreign key)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
modelUsersTable := models.InitTable("users", "public")
modelIdCol := models.InitColumn("id", "users", "public")
modelIdCol.Type = "integer"
modelUsersTable.Columns["id"] = modelIdCol
modelPostsTable := models.InitTable("posts", "public")
modelPostIdCol := models.InitColumn("id", "posts", "public")
modelPostIdCol.Type = "integer"
modelPostsTable.Columns["id"] = modelPostIdCol
modelUserIdCol := models.InitColumn("user_id", "posts", "public")
modelUserIdCol.Type = "integer"
modelPostsTable.Columns["user_id"] = modelUserIdCol
// Add foreign key constraint
fkConstraint := &models.Constraint{
Name: "fk_posts_users",
Type: models.ForeignKeyConstraint,
Columns: []string{"user_id"},
ReferencedTable: "users",
ReferencedSchema: "public",
ReferencedColumns: []string{"id"},
OnDelete: "CASCADE",
OnUpdate: "CASCADE",
}
modelPostsTable.Constraints["fk_posts_users"] = fkConstraint
modelSchema.Tables = append(modelSchema.Tables, modelUsersTable, modelPostsTable)
model.Schemas = append(model.Schemas, modelSchema)
// Generate migration
var buf bytes.Buffer
writer := NewMigrationWriter(&writers.WriterOptions{})
writer.writer = &buf
err := writer.WriteMigration(model, current)
sql, err := executor.ExecuteCreateTable(data)
if err != nil {
t.Fatalf("WriteMigration failed: %v", err)
t.Fatalf("Failed to execute template: %v", err)
}
output := buf.String()
t.Logf("Generated migration:\n%s", output)
t.Logf("Generated SQL:\n%s", sql)
// Verify FOREIGN KEY is present
if !strings.Contains(output, "FOREIGN KEY") {
t.Error("Migration missing FOREIGN KEY statement")
if !strings.Contains(sql, "CREATE TABLE IF NOT EXISTS public.test_table") {
t.Error("SQL missing CREATE TABLE statement")
}
if !strings.Contains(output, "ON DELETE CASCADE") {
t.Error("Migration missing ON DELETE CASCADE")
if !strings.Contains(sql, "id integer NOT NULL") {
t.Error("SQL missing id column definition")
}
if !strings.Contains(sql, "name text DEFAULT 'unknown'") {
t.Error("SQL missing name column definition")
}
}
func TestWriteMigration_AddIndex(t *testing.T) {
// Current database (table without index)
current := models.InitDatabase("testdb")
currentSchema := models.InitSchema("public")
currentTable := models.InitTable("users", "public")
emailCol := models.InitColumn("email", "users", "public")
emailCol.Type = "text"
currentTable.Columns["email"] = emailCol
currentSchema.Tables = append(currentSchema.Tables, currentTable)
current.Schemas = append(current.Schemas, currentSchema)
// Model database (with unique index)
model := models.InitDatabase("testdb")
modelSchema := models.InitSchema("public")
modelTable := models.InitTable("users", "public")
modelEmailCol := models.InitColumn("email", "users", "public")
modelEmailCol.Type = "text"
modelTable.Columns["email"] = modelEmailCol
// Add unique index
index := &models.Index{
Name: "uk_users_email",
Unique: true,
Columns: []string{"email"},
Type: "btree",
}
modelTable.Indexes["uk_users_email"] = index
modelSchema.Tables = append(modelSchema.Tables, modelTable)
model.Schemas = append(model.Schemas, modelSchema)
// Generate migration
var buf bytes.Buffer
writer := NewMigrationWriter(&writers.WriterOptions{})
writer.writer = &buf
err := writer.WriteMigration(model, current)
func TestTemplateExecutor_AuditFunction(t *testing.T) {
executor, err := NewTemplateExecutor()
if err != nil {
t.Fatalf("WriteMigration failed: %v", err)
t.Fatalf("Failed to create executor: %v", err)
}
output := buf.String()
t.Logf("Generated migration:\n%s", output)
// Verify CREATE UNIQUE INDEX is present
if !strings.Contains(output, "CREATE UNIQUE INDEX") {
t.Error("Migration missing CREATE UNIQUE INDEX statement")
data := AuditFunctionData{
SchemaName: "public",
FunctionName: "ft_audit_users",
TableName: "users",
TablePrefix: "NULL",
PrimaryKey: "id",
AuditSchema: "public",
UserFunction: "current_user",
AuditInsert: true,
AuditUpdate: true,
AuditDelete: true,
UpdateCondition: "old.name IS DISTINCT FROM new.name",
UpdateColumns: []AuditColumnData{
{Name: "name", OldValue: "old.name::text", NewValue: "new.name::text"},
},
DeleteColumns: []AuditColumnData{
{Name: "name", OldValue: "old.name::text"},
},
}
if !strings.Contains(output, "uk_users_email") {
t.Error("Migration missing index name")
sql, err := executor.ExecuteAuditFunction(data)
if err != nil {
t.Fatalf("Failed to execute template: %v", err)
}
t.Logf("Generated SQL:\n%s", sql)
if !strings.Contains(sql, "CREATE OR REPLACE FUNCTION public.ft_audit_users()") {
t.Error("SQL missing function definition")
}
if !strings.Contains(sql, "IF TG_OP = 'INSERT'") {
t.Error("SQL missing INSERT handling")
}
if !strings.Contains(sql, "ELSIF TG_OP = 'UPDATE'") {
t.Error("SQL missing UPDATE handling")
}
if !strings.Contains(sql, "ELSIF TG_OP = 'DELETE'") {
t.Error("SQL missing DELETE handling")
}
}

View File

@@ -0,0 +1,285 @@
package pgsql
import (
"fmt"
"regexp"
"strings"
"unicode"
)
// TemplateFunctions returns a map of custom template functions
func TemplateFunctions() map[string]interface{} {
return map[string]interface{}{
// String manipulation
"upper": strings.ToUpper,
"lower": strings.ToLower,
"snake_case": toSnakeCase,
"camelCase": toCamelCase,
// SQL formatting
"indent": indent,
"quote": quote,
"escape": escape,
"safe_identifier": safeIdentifier,
// Type conversion
"goTypeToSQL": goTypeToSQL,
"sqlTypeToGo": sqlTypeToGo,
"isNumeric": isNumeric,
"isText": isText,
// Collection helpers
"first": first,
"last": last,
"filter": filter,
"mapFunc": mapFunc,
"join_with": joinWith,
// Built-in Go template function (for convenience)
"join": strings.Join,
}
}
// String manipulation functions
// toSnakeCase converts a string to snake_case
func toSnakeCase(s string) string {
// Insert underscore before uppercase letters
var result strings.Builder
for i, r := range s {
if unicode.IsUpper(r) {
if i > 0 {
result.WriteRune('_')
}
result.WriteRune(unicode.ToLower(r))
} else {
result.WriteRune(r)
}
}
return result.String()
}
// toCamelCase converts a string to camelCase
func toCamelCase(s string) string {
// Split by underscore
parts := strings.Split(s, "_")
if len(parts) == 0 {
return s
}
// First part stays lowercase
result := strings.ToLower(parts[0])
// Capitalize first letter of remaining parts
for _, part := range parts[1:] {
if len(part) > 0 {
result += strings.ToUpper(part[0:1]) + strings.ToLower(part[1:])
}
}
return result
}
// SQL formatting functions
// indent indents each line of text by the specified number of spaces
func indent(spaces int, text string) string {
prefix := strings.Repeat(" ", spaces)
lines := strings.Split(text, "\n")
for i, line := range lines {
if line != "" {
lines[i] = prefix + line
}
}
return strings.Join(lines, "\n")
}
// quote quotes a string value for SQL (escapes single quotes)
func quote(s string) string {
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
}
// escape escapes a string for SQL (escapes single quotes and backslashes)
func escape(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "'", "''")
return s
}
// safeIdentifier makes a string safe to use as a SQL identifier
func safeIdentifier(s string) string {
// Remove or replace dangerous characters
// Allow: letters, numbers, underscore
reg := regexp.MustCompile(`[^a-zA-Z0-9_]`)
safe := reg.ReplaceAllString(s, "_")
// Ensure it doesn't start with a number
if len(safe) > 0 && unicode.IsDigit(rune(safe[0])) {
safe = "_" + safe
}
// Convert to lowercase (PostgreSQL convention)
return strings.ToLower(safe)
}
// Type conversion functions
// goTypeToSQL converts Go type to PostgreSQL type
func goTypeToSQL(goType string) string {
typeMap := map[string]string{
"string": "text",
"int": "integer",
"int32": "integer",
"int64": "bigint",
"float32": "real",
"float64": "double precision",
"bool": "boolean",
"time.Time": "timestamp",
"[]byte": "bytea",
}
if sqlType, ok := typeMap[goType]; ok {
return sqlType
}
return "text" // Default
}
// sqlTypeToGo converts PostgreSQL type to Go type
func sqlTypeToGo(sqlType string) string {
sqlType = strings.ToLower(sqlType)
typeMap := map[string]string{
"text": "string",
"varchar": "string",
"char": "string",
"integer": "int",
"int": "int",
"bigint": "int64",
"smallint": "int16",
"serial": "int",
"bigserial": "int64",
"real": "float32",
"double precision": "float64",
"numeric": "float64",
"decimal": "float64",
"boolean": "bool",
"timestamp": "time.Time",
"timestamptz": "time.Time",
"date": "time.Time",
"time": "time.Time",
"bytea": "[]byte",
"json": "json.RawMessage",
"jsonb": "json.RawMessage",
"uuid": "string",
}
if goType, ok := typeMap[sqlType]; ok {
return goType
}
return "string" // Default
}
// isNumeric checks if a SQL type is numeric
func isNumeric(sqlType string) bool {
sqlType = strings.ToLower(sqlType)
numericTypes := []string{
"integer", "int", "bigint", "smallint", "serial", "bigserial",
"real", "double precision", "numeric", "decimal", "float",
}
for _, t := range numericTypes {
if strings.Contains(sqlType, t) {
return true
}
}
return false
}
// isText checks if a SQL type is text-based
func isText(sqlType string) bool {
sqlType = strings.ToLower(sqlType)
textTypes := []string{
"text", "varchar", "char", "character", "string",
}
for _, t := range textTypes {
if strings.Contains(sqlType, t) {
return true
}
}
return false
}
// Collection helper functions
// first returns the first element of a slice, or nil if empty
func first(slice interface{}) interface{} {
switch v := slice.(type) {
case []string:
if len(v) > 0 {
return v[0]
}
case []int:
if len(v) > 0 {
return v[0]
}
case []interface{}:
if len(v) > 0 {
return v[0]
}
}
return nil
}
// last returns the last element of a slice, or nil if empty
func last(slice interface{}) interface{} {
switch v := slice.(type) {
case []string:
if len(v) > 0 {
return v[len(v)-1]
}
case []int:
if len(v) > 0 {
return v[len(v)-1]
}
case []interface{}:
if len(v) > 0 {
return v[len(v)-1]
}
}
return nil
}
// filter filters a slice based on a predicate (simplified version)
// Usage in template: {{filter .Columns "NotNull"}}
func filter(slice interface{}, fieldName string) interface{} {
// This is a simplified implementation
// In templates, you'd use: {{range $col := .Columns}}{{if $col.NotNull}}...{{end}}{{end}}
// This function is mainly for documentation purposes
return slice
}
// mapFunc maps a function over a slice (simplified version)
// Usage in template: {{range .Columns}}{{mapFunc .Name "upper"}}{{end}}
func mapFunc(value interface{}, funcName string) interface{} {
// This is a simplified implementation
// In templates, you'd directly call: {{upper .Name}}
// This function is mainly for documentation purposes
return value
}
// joinWith joins a slice of strings with a separator
func joinWith(slice []string, separator string) string {
return strings.Join(slice, separator)
}
// Additional helper functions
// formatType formats a SQL type with length/precision
func formatType(baseType string, length, precision int) string {
if length > 0 && precision > 0 {
return fmt.Sprintf("%s(%d,%d)", baseType, length, precision)
}
if length > 0 {
return fmt.Sprintf("%s(%d)", baseType, length)
}
return baseType
}

View File

@@ -0,0 +1,332 @@
package pgsql
import (
"strings"
"testing"
)
func TestToSnakeCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"UserId", "user_id"},
{"UserID", "user_i_d"},
{"HTTPResponse", "h_t_t_p_response"},
{"already_snake", "already_snake"},
{"", ""},
}
for _, tt := range tests {
result := toSnakeCase(tt.input)
if result != tt.expected {
t.Errorf("toSnakeCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestToCamelCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"user_id", "userId"},
{"user_name", "userName"},
{"http_response", "httpResponse"},
{"", ""},
{"alreadycamel", "alreadycamel"},
}
for _, tt := range tests {
result := toCamelCase(tt.input)
if result != tt.expected {
t.Errorf("toCamelCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestQuote(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"hello", "'hello'"},
{"O'Brien", "'O''Brien'"},
{"", "''"},
}
for _, tt := range tests {
result := quote(tt.input)
if result != tt.expected {
t.Errorf("quote(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestEscape(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"hello", "hello"},
{"O'Brien", "O''Brien"},
{"path\\to\\file", "path\\\\to\\\\file"},
}
for _, tt := range tests {
result := escape(tt.input)
if result != tt.expected {
t.Errorf("escape(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestSafeIdentifier(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"User-Id", "user_id"},
{"123column", "_123column"},
{"valid_name", "valid_name"},
{"Column@Name!", "column_name_"},
{"UPPERCASE", "uppercase"},
}
for _, tt := range tests {
result := safeIdentifier(tt.input)
if result != tt.expected {
t.Errorf("safeIdentifier(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestGoTypeToSQL(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"string", "text"},
{"int", "integer"},
{"int64", "bigint"},
{"bool", "boolean"},
{"time.Time", "timestamp"},
{"unknown", "text"},
}
for _, tt := range tests {
result := goTypeToSQL(tt.input)
if result != tt.expected {
t.Errorf("goTypeToSQL(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestSQLTypeToGo(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"text", "string"},
{"integer", "int"},
{"bigint", "int64"},
{"boolean", "bool"},
{"timestamp", "time.Time"},
{"unknown", "string"},
}
for _, tt := range tests {
result := sqlTypeToGo(tt.input)
if result != tt.expected {
t.Errorf("sqlTypeToGo(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestIsNumeric(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"integer", true},
{"bigint", true},
{"numeric(10,2)", true},
{"text", false},
{"varchar", false},
}
for _, tt := range tests {
result := isNumeric(tt.input)
if result != tt.expected {
t.Errorf("isNumeric(%q) = %v, want %v", tt.input, result, tt.expected)
}
}
}
func TestIsText(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"text", true},
{"varchar(255)", true},
{"character varying", true},
{"integer", false},
{"bigint", false},
}
for _, tt := range tests {
result := isText(tt.input)
if result != tt.expected {
t.Errorf("isText(%q) = %v, want %v", tt.input, result, tt.expected)
}
}
}
func TestIndent(t *testing.T) {
input := "line1\nline2\nline3"
expected := " line1\n line2\n line3"
result := indent(2, input)
if result != expected {
t.Errorf("indent(2, %q) = %q, want %q", input, result, expected)
}
}
func TestFirst(t *testing.T) {
tests := []struct {
input interface{}
expected interface{}
}{
{[]string{"a", "b", "c"}, "a"},
{[]string{}, nil},
{[]int{1, 2, 3}, 1},
}
for _, tt := range tests {
result := first(tt.input)
if result != tt.expected {
t.Errorf("first(%v) = %v, want %v", tt.input, result, tt.expected)
}
}
}
func TestLast(t *testing.T) {
tests := []struct {
input interface{}
expected interface{}
}{
{[]string{"a", "b", "c"}, "c"},
{[]string{}, nil},
{[]int{1, 2, 3}, 3},
}
for _, tt := range tests {
result := last(tt.input)
if result != tt.expected {
t.Errorf("last(%v) = %v, want %v", tt.input, result, tt.expected)
}
}
}
func TestJoinWith(t *testing.T) {
input := []string{"a", "b", "c"}
expected := "a, b, c"
result := joinWith(input, ", ")
if result != expected {
t.Errorf("joinWith(%v, \", \") = %q, want %q", input, result, expected)
}
}
func TestTemplateFunctions(t *testing.T) {
funcs := TemplateFunctions()
// Check that all expected functions are registered
expectedFuncs := []string{
"upper", "lower", "snake_case", "camelCase",
"indent", "quote", "escape", "safe_identifier",
"goTypeToSQL", "sqlTypeToGo", "isNumeric", "isText",
"first", "last", "filter", "mapFunc", "join_with",
"join",
}
for _, name := range expectedFuncs {
if _, ok := funcs[name]; !ok {
t.Errorf("Expected function %q not found in TemplateFunctions()", name)
}
}
// Test that they're callable
if upperFunc, ok := funcs["upper"].(func(string) string); ok {
result := upperFunc("hello")
if result != "HELLO" {
t.Errorf("upper function not working correctly")
}
} else {
t.Error("upper function has wrong type")
}
}
func TestFormatType(t *testing.T) {
tests := []struct {
baseType string
length int
precision int
expected string
}{
{"varchar", 255, 0, "varchar(255)"},
{"numeric", 10, 2, "numeric(10,2)"},
{"integer", 0, 0, "integer"},
}
for _, tt := range tests {
result := formatType(tt.baseType, tt.length, tt.precision)
if result != tt.expected {
t.Errorf("formatType(%q, %d, %d) = %q, want %q",
tt.baseType, tt.length, tt.precision, result, tt.expected)
}
}
}
// Test that template functions work in actual templates
func TestTemplateFunctionsInTemplate(t *testing.T) {
executor, err := NewTemplateExecutor()
if err != nil {
t.Fatalf("Failed to create executor: %v", err)
}
// Create a simple test template
tmpl, err := executor.templates.New("test").Parse(`
{{- upper .Name -}}
{{- lower .Type -}}
{{- snake_case .CamelName -}}
{{- safe_identifier .UnsafeName -}}
`)
if err != nil {
t.Fatalf("Failed to parse test template: %v", err)
}
data := struct {
Name string
Type string
CamelName string
UnsafeName string
}{
Name: "hello",
Type: "TEXT",
CamelName: "UserId",
UnsafeName: "user-id!",
}
var buf strings.Builder
err = tmpl.Execute(&buf, data)
if err != nil {
t.Fatalf("Failed to execute template: %v", err)
}
result := buf.String()
expected := "HELLOtextuser_iduser_id_"
if result != expected {
t.Errorf("Template output = %q, want %q", result, expected)
}
}

View File

@@ -0,0 +1,457 @@
package pgsql
import (
"bytes"
"embed"
"fmt"
"strings"
"text/template"
"git.warky.dev/wdevs/relspecgo/pkg/models"
)
//go:embed templates/*.tmpl
var templateFS embed.FS
// TemplateExecutor manages and executes SQL templates
type TemplateExecutor struct {
templates *template.Template
}
// NewTemplateExecutor creates a new template executor
func NewTemplateExecutor() (*TemplateExecutor, error) {
// Create template with custom functions
funcMap := make(template.FuncMap)
for k, v := range TemplateFunctions() {
funcMap[k] = v
}
tmpl, err := template.New("").Funcs(funcMap).ParseFS(templateFS, "templates/*.tmpl")
if err != nil {
return nil, fmt.Errorf("failed to parse templates: %w", err)
}
return &TemplateExecutor{
templates: tmpl,
}, nil
}
// Template data structures
// CreateTableData contains data for create table template
type CreateTableData struct {
SchemaName string
TableName string
Columns []ColumnData
}
// ColumnData represents column information
type ColumnData struct {
Name string
Type string
Default string
NotNull bool
}
// AddColumnData contains data for add column template
type AddColumnData struct {
SchemaName string
TableName string
ColumnName string
ColumnType string
Default string
NotNull bool
}
// AlterColumnTypeData contains data for alter column type template
type AlterColumnTypeData struct {
SchemaName string
TableName string
ColumnName string
NewType string
}
// AlterColumnDefaultData contains data for alter column default template
type AlterColumnDefaultData struct {
SchemaName string
TableName string
ColumnName string
SetDefault bool
DefaultValue string
}
// CreatePrimaryKeyData contains data for create primary key template
type CreatePrimaryKeyData struct {
SchemaName string
TableName string
ConstraintName string
Columns string
}
// CreateIndexData contains data for create index template
type CreateIndexData struct {
SchemaName string
TableName string
IndexName string
IndexType string
Columns string
Unique bool
}
// CreateForeignKeyData contains data for create foreign key template
type CreateForeignKeyData struct {
SchemaName string
TableName string
ConstraintName string
SourceColumns string
TargetSchema string
TargetTable string
TargetColumns string
OnDelete string
OnUpdate string
}
// DropConstraintData contains data for drop constraint template
type DropConstraintData struct {
SchemaName string
TableName string
ConstraintName string
}
// DropIndexData contains data for drop index template
type DropIndexData struct {
SchemaName string
IndexName string
}
// CommentTableData contains data for table comment template
type CommentTableData struct {
SchemaName string
TableName string
Comment string
}
// CommentColumnData contains data for column comment template
type CommentColumnData struct {
SchemaName string
TableName string
ColumnName string
Comment string
}
// AuditTablesData contains data for audit tables template
type AuditTablesData struct {
AuditSchema string
}
// AuditColumnData represents a column in audit template
type AuditColumnData struct {
Name string
OldValue string
NewValue string
}
// AuditFunctionData contains data for audit function template
type AuditFunctionData struct {
SchemaName string
FunctionName string
TableName string
TablePrefix string
PrimaryKey string
AuditSchema string
UserFunction string
AuditInsert bool
AuditUpdate bool
AuditDelete bool
UpdateCondition string
UpdateColumns []AuditColumnData
DeleteColumns []AuditColumnData
}
// AuditTriggerData contains data for audit trigger template
type AuditTriggerData struct {
SchemaName string
TableName string
TriggerName string
FunctionName string
Events string
}
// Execute methods for each template
// ExecuteCreateTable executes the create table template
func (te *TemplateExecutor) ExecuteCreateTable(data CreateTableData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "create_table.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute create_table template: %w", err)
}
return buf.String(), nil
}
// ExecuteAddColumn executes the add column template
func (te *TemplateExecutor) ExecuteAddColumn(data AddColumnData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "add_column.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute add_column template: %w", err)
}
return buf.String(), nil
}
// ExecuteAlterColumnType executes the alter column type template
func (te *TemplateExecutor) ExecuteAlterColumnType(data AlterColumnTypeData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "alter_column_type.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute alter_column_type template: %w", err)
}
return buf.String(), nil
}
// ExecuteAlterColumnDefault executes the alter column default template
func (te *TemplateExecutor) ExecuteAlterColumnDefault(data AlterColumnDefaultData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "alter_column_default.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute alter_column_default template: %w", err)
}
return buf.String(), nil
}
// ExecuteCreatePrimaryKey executes the create primary key template
func (te *TemplateExecutor) ExecuteCreatePrimaryKey(data CreatePrimaryKeyData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "create_primary_key.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute create_primary_key template: %w", err)
}
return buf.String(), nil
}
// ExecuteCreateIndex executes the create index template
func (te *TemplateExecutor) ExecuteCreateIndex(data CreateIndexData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "create_index.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute create_index template: %w", err)
}
return buf.String(), nil
}
// ExecuteCreateForeignKey executes the create foreign key template
func (te *TemplateExecutor) ExecuteCreateForeignKey(data CreateForeignKeyData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "create_foreign_key.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute create_foreign_key template: %w", err)
}
return buf.String(), nil
}
// ExecuteDropConstraint executes the drop constraint template
func (te *TemplateExecutor) ExecuteDropConstraint(data DropConstraintData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "drop_constraint.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute drop_constraint template: %w", err)
}
return buf.String(), nil
}
// ExecuteDropIndex executes the drop index template
func (te *TemplateExecutor) ExecuteDropIndex(data DropIndexData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "drop_index.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute drop_index template: %w", err)
}
return buf.String(), nil
}
// ExecuteCommentTable executes the table comment template
func (te *TemplateExecutor) ExecuteCommentTable(data CommentTableData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "comment_table.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute comment_table template: %w", err)
}
return buf.String(), nil
}
// ExecuteCommentColumn executes the column comment template
func (te *TemplateExecutor) ExecuteCommentColumn(data CommentColumnData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "comment_column.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute comment_column template: %w", err)
}
return buf.String(), nil
}
// ExecuteAuditTables executes the audit tables template
func (te *TemplateExecutor) ExecuteAuditTables(data AuditTablesData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "audit_tables.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute audit_tables template: %w", err)
}
return buf.String(), nil
}
// ExecuteAuditFunction executes the audit function template
func (te *TemplateExecutor) ExecuteAuditFunction(data AuditFunctionData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "audit_function.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute audit_function template: %w", err)
}
return buf.String(), nil
}
// ExecuteAuditTrigger executes the audit trigger template
func (te *TemplateExecutor) ExecuteAuditTrigger(data AuditTriggerData) (string, error) {
var buf bytes.Buffer
err := te.templates.ExecuteTemplate(&buf, "audit_trigger.tmpl", data)
if err != nil {
return "", fmt.Errorf("failed to execute audit_trigger template: %w", err)
}
return buf.String(), nil
}
// Helper functions to build template data from models
// BuildCreateTableData builds CreateTableData from a models.Table
func BuildCreateTableData(schemaName string, table *models.Table) CreateTableData {
columns := make([]ColumnData, 0, len(table.Columns))
// Get sorted columns
sortedCols := getSortedColumns(table.Columns)
for _, col := range sortedCols {
colData := ColumnData{
Name: col.Name,
Type: col.Type,
NotNull: col.NotNull,
}
if col.Default != nil {
colData.Default = fmt.Sprintf("%v", col.Default)
}
columns = append(columns, colData)
}
return CreateTableData{
SchemaName: schemaName,
TableName: table.Name,
Columns: columns,
}
}
// BuildAuditFunctionData builds AuditFunctionData from table and config
func BuildAuditFunctionData(
schemaName string,
table *models.Table,
pk *models.Column,
config *TableAuditConfig,
auditSchema string,
userFunction string,
) AuditFunctionData {
funcName := fmt.Sprintf("ft_audit_%s", table.Name)
// Build list of audited columns
auditedColumns := make([]*models.Column, 0)
for _, col := range table.Columns {
if col.Name == pk.Name {
continue
}
excluded := false
for _, excl := range config.ExcludedColumns {
if strings.EqualFold(col.Name, excl) {
excluded = true
break
}
}
if excluded {
continue
}
auditedColumns = append(auditedColumns, col)
}
// Build update condition
updateComparisons := make([]string, 0)
for _, col := range auditedColumns {
updateComparisons = append(updateComparisons,
fmt.Sprintf("old.%s IS DISTINCT FROM new.%s", col.Name, col.Name))
}
updateCondition := strings.Join(updateComparisons, " OR ")
// Build update columns data
updateColumns := make([]AuditColumnData, 0)
for _, col := range auditedColumns {
isEncrypted := false
for _, enc := range config.EncryptedColumns {
if strings.EqualFold(col.Name, enc) {
isEncrypted = true
break
}
}
oldValue := fmt.Sprintf("old.%s::text", col.Name)
newValue := fmt.Sprintf("new.%s::text", col.Name)
if isEncrypted {
oldValue = "'****************'"
newValue = "'****************'"
}
updateColumns = append(updateColumns, AuditColumnData{
Name: col.Name,
OldValue: oldValue,
NewValue: newValue,
})
}
// Build delete columns data (same as update but only old values)
deleteColumns := make([]AuditColumnData, 0)
for _, col := range auditedColumns {
isEncrypted := false
for _, enc := range config.EncryptedColumns {
if strings.EqualFold(col.Name, enc) {
isEncrypted = true
break
}
}
oldValue := fmt.Sprintf("old.%s::text", col.Name)
if isEncrypted {
oldValue = "'****************'"
}
deleteColumns = append(deleteColumns, AuditColumnData{
Name: col.Name,
OldValue: oldValue,
})
}
tablePrefix := "NULL"
if config.TablePrefix != "" {
tablePrefix = fmt.Sprintf("'%s'", config.TablePrefix)
}
return AuditFunctionData{
SchemaName: schemaName,
FunctionName: funcName,
TableName: table.Name,
TablePrefix: tablePrefix,
PrimaryKey: pk.Name,
AuditSchema: auditSchema,
UserFunction: userFunction,
AuditInsert: config.AuditInsert,
AuditUpdate: config.AuditUpdate,
AuditDelete: config.AuditDelete,
UpdateCondition: updateCondition,
UpdateColumns: updateColumns,
DeleteColumns: deleteColumns,
}
}

View File

@@ -0,0 +1,4 @@
ALTER TABLE {{.SchemaName}}.{{.TableName}}
ADD COLUMN IF NOT EXISTS {{.ColumnName}} {{.ColumnType}}
{{- if .Default}} DEFAULT {{.Default}}{{end}}
{{- if .NotNull}} NOT NULL{{end}};

View File

@@ -0,0 +1,7 @@
{{- if .SetDefault -}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
ALTER COLUMN {{.ColumnName}} SET DEFAULT {{.DefaultValue}};
{{- else -}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
ALTER COLUMN {{.ColumnName}} DROP DEFAULT;
{{- end -}}

View File

@@ -0,0 +1,2 @@
ALTER TABLE {{.SchemaName}}.{{.TableName}}
ALTER COLUMN {{.ColumnName}} TYPE {{.NewType}};

View File

@@ -0,0 +1,84 @@
CREATE OR REPLACE FUNCTION {{.SchemaName}}.{{.FunctionName}}()
RETURNS trigger AS
$body$
DECLARE
m_funcname text = '{{.FunctionName}}';
m_user text;
m_atevent integer;
BEGIN
-- Get current user
m_user := {{.UserFunction}}::text;
-- Skip audit for specific users if needed
IF m_user IN ('noaudit', 'importuser') THEN
IF (TG_OP = 'DELETE') THEN
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
RETURN NEW;
END IF;
END IF;
{{- if .AuditInsert}}
IF TG_OP = 'INSERT' THEN
-- Record INSERT
INSERT INTO {{.AuditSchema}}.atevent (tablename, tableprefix, rid_parent, changeuser, changedate, changetime, actionx)
VALUES ('{{.TableName}}', {{.TablePrefix}}, new.{{.PrimaryKey}}, m_user, CURRENT_DATE, CURRENT_TIME, 1)
RETURNING rid_atevent INTO m_atevent;
{{- end}}
{{- if .AuditUpdate}}
ELSIF TG_OP = 'UPDATE' THEN
-- Check if any audited columns changed
IF ({{.UpdateCondition}}) THEN
INSERT INTO {{.AuditSchema}}.atevent (tablename, tableprefix, rid_parent, changeuser, changedate, changetime, actionx)
VALUES ('{{.TableName}}', {{.TablePrefix}}, new.{{.PrimaryKey}}, m_user, CURRENT_DATE, CURRENT_TIME, 2)
RETURNING rid_atevent INTO m_atevent;
-- Record column changes
{{- range .UpdateColumns}}
IF (old.{{.Name}} IS DISTINCT FROM new.{{.Name}}) THEN
INSERT INTO {{$.AuditSchema}}.atdetail(rid_atevent, datacolumn, changedfrom, changedto)
VALUES (m_atevent, '{{.Name}}', substr({{.OldValue}}, 1, 1000), substr({{.NewValue}}, 1, 1000));
END IF;
{{- end}}
END IF;
{{- end}}
{{- if .AuditDelete}}
ELSIF TG_OP = 'DELETE' THEN
-- Record DELETE
INSERT INTO {{.AuditSchema}}.atevent (tablename, tableprefix, rid_parent, rid_deletedparent, changeuser, changedate, changetime, actionx)
VALUES ('{{.TableName}}', {{.TablePrefix}}, old.{{.PrimaryKey}}, old.{{.PrimaryKey}}, m_user, CURRENT_DATE, CURRENT_TIME, 3)
RETURNING rid_atevent INTO m_atevent;
-- Record deleted column values
{{- range .DeleteColumns}}
INSERT INTO {{$.AuditSchema}}.atdetail(rid_atevent, datacolumn, changedfrom, changedto)
VALUES (m_atevent, '{{.Name}}', substr({{.OldValue}}, 1, 1000), NULL);
{{- end}}
{{- end}}
END IF;
IF (TG_OP = 'DELETE') THEN
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
RETURN NEW;
END IF;
RETURN NULL;
EXCEPTION
WHEN OTHERS THEN
RAISE WARNING 'Audit function % failed: %', m_funcname, SQLERRM;
RETURN NULL;
END;
$body$
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER;
COMMENT ON FUNCTION {{.SchemaName}}.{{.FunctionName}}() IS 'Audit trigger function for table {{.SchemaName}}.{{.TableName}}';

View File

@@ -0,0 +1,49 @@
-- Audit Event Header Table
CREATE TABLE IF NOT EXISTS {{.AuditSchema}}.atevent (
rid_atevent serial PRIMARY KEY,
tablename text NOT NULL,
tableprefix text,
rid_parent integer NOT NULL,
rid_deletedparent integer,
changeuser text NOT NULL,
changedate date NOT NULL,
changetime time NOT NULL,
actionx smallint NOT NULL,
CONSTRAINT ck_atevent_action CHECK (actionx IN (1, 2, 3))
);
CREATE INDEX IF NOT EXISTS idx_atevent_tablename ON {{.AuditSchema}}.atevent(tablename);
CREATE INDEX IF NOT EXISTS idx_atevent_rid_parent ON {{.AuditSchema}}.atevent(rid_parent);
CREATE INDEX IF NOT EXISTS idx_atevent_changedate ON {{.AuditSchema}}.atevent(changedate);
CREATE INDEX IF NOT EXISTS idx_atevent_changeuser ON {{.AuditSchema}}.atevent(changeuser);
COMMENT ON TABLE {{.AuditSchema}}.atevent IS 'Audit trail header table - tracks all data changes';
COMMENT ON COLUMN {{.AuditSchema}}.atevent.rid_atevent IS 'Audit event ID';
COMMENT ON COLUMN {{.AuditSchema}}.atevent.tablename IS 'Name of the table that was modified';
COMMENT ON COLUMN {{.AuditSchema}}.atevent.rid_parent IS 'Primary key value of the modified record';
COMMENT ON COLUMN {{.AuditSchema}}.atevent.rid_deletedparent IS 'Parent reference for deleted records';
COMMENT ON COLUMN {{.AuditSchema}}.atevent.changeuser IS 'User who made the change';
COMMENT ON COLUMN {{.AuditSchema}}.atevent.changedate IS 'Date of change';
COMMENT ON COLUMN {{.AuditSchema}}.atevent.changetime IS 'Time of change';
COMMENT ON COLUMN {{.AuditSchema}}.atevent.actionx IS 'Action type: 1=INSERT, 2=UPDATE, 3=DELETE';
-- Audit Event Detail Table
CREATE TABLE IF NOT EXISTS {{.AuditSchema}}.atdetail (
rid_atdetail serial PRIMARY KEY,
rid_atevent integer NOT NULL,
datacolumn text NOT NULL,
changedfrom text,
changedto text,
CONSTRAINT fk_atdetail_atevent FOREIGN KEY (rid_atevent)
REFERENCES {{.AuditSchema}}.atevent(rid_atevent) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_atdetail_rid_atevent ON {{.AuditSchema}}.atdetail(rid_atevent);
CREATE INDEX IF NOT EXISTS idx_atdetail_datacolumn ON {{.AuditSchema}}.atdetail(datacolumn);
COMMENT ON TABLE {{.AuditSchema}}.atdetail IS 'Audit trail detail table - stores individual column changes';
COMMENT ON COLUMN {{.AuditSchema}}.atdetail.rid_atdetail IS 'Audit detail ID';
COMMENT ON COLUMN {{.AuditSchema}}.atdetail.rid_atevent IS 'Reference to audit event';
COMMENT ON COLUMN {{.AuditSchema}}.atdetail.datacolumn IS 'Name of the column that changed';
COMMENT ON COLUMN {{.AuditSchema}}.atdetail.changedfrom IS 'Old value before change';
COMMENT ON COLUMN {{.AuditSchema}}.atdetail.changedto IS 'New value after change';

View File

@@ -0,0 +1,16 @@
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_trigger
WHERE tgname = '{{.TriggerName}}'
AND tgrelid = '{{.SchemaName}}.{{.TableName}}'::regclass
) THEN
CREATE TRIGGER {{.TriggerName}}
AFTER {{.Events}}
ON {{.SchemaName}}.{{.TableName}}
FOR EACH ROW
EXECUTE FUNCTION {{.SchemaName}}.{{.FunctionName}}();
END IF;
END;
$$;

View File

@@ -0,0 +1,39 @@
{{/* Base constraint template */}}
{{- define "constraint_base" -}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
ADD CONSTRAINT {{.ConstraintName}}
{{block "constraint_definition" .}}{{end}};
{{- end -}}
{{/* Drop constraint with check */}}
{{- define "drop_constraint_safe" -}}
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE table_schema = '{{.SchemaName}}'
AND table_name = '{{.TableName}}'
AND constraint_name = '{{.ConstraintName}}'
) THEN
ALTER TABLE {{.SchemaName}}.{{.TableName}}
DROP CONSTRAINT {{.ConstraintName}};
END IF;
END;
$$;
{{- end -}}
{{/* Add constraint with existence check */}}
{{- define "add_constraint_safe" -}}
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE table_schema = '{{.SchemaName}}'
AND table_name = '{{.TableName}}'
AND constraint_name = '{{.ConstraintName}}'
) THEN
{{template "constraint_base" .}}
END IF;
END;
$$;
{{- end -}}

View File

@@ -0,0 +1,34 @@
{{/* Base DDL template with common structure */}}
{{- define "ddl_header" -}}
-- DDL Operation: {{.Operation}}
-- Schema: {{.Schema}}
-- Object: {{.ObjectName}}
{{- end -}}
{{- define "ddl_footer" -}}
-- End of {{.Operation}}
{{- end -}}
{{/* Base ALTER TABLE structure */}}
{{- define "alter_table_base" -}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
{{block "alter_operation" .}}{{end}};
{{- end -}}
{{/* Common existence check pattern */}}
{{- define "exists_check" -}}
DO $$
BEGIN
IF NOT EXISTS (
{{block "exists_query" .}}{{end}}
) THEN
{{block "create_statement" .}}{{end}}
END IF;
END;
$$;
{{- end -}}
{{/* Common drop pattern */}}
{{- define "drop_if_exists" -}}
{{block "drop_type" .}}{{end}} IF EXISTS {{.SchemaName}}.{{.ObjectName}};
{{- end -}}

View File

@@ -0,0 +1 @@
COMMENT ON COLUMN {{.SchemaName}}.{{.TableName}}.{{.ColumnName}} IS '{{.Comment}}';

View File

@@ -0,0 +1 @@
COMMENT ON TABLE {{.SchemaName}}.{{.TableName}} IS '{{.Comment}}';

View File

@@ -0,0 +1,10 @@
ALTER TABLE {{.SchemaName}}.{{.TableName}}
DROP CONSTRAINT IF EXISTS {{.ConstraintName}};
ALTER TABLE {{.SchemaName}}.{{.TableName}}
ADD CONSTRAINT {{.ConstraintName}}
FOREIGN KEY ({{.SourceColumns}})
REFERENCES {{.TargetSchema}}.{{.TargetTable}} ({{.TargetColumns}})
ON DELETE {{.OnDelete}}
ON UPDATE {{.OnUpdate}}
DEFERRABLE;

View File

@@ -0,0 +1,2 @@
CREATE {{if .Unique}}UNIQUE {{end}}INDEX IF NOT EXISTS {{.IndexName}}
ON {{.SchemaName}}.{{.TableName}} USING {{.IndexType}} ({{.Columns}});

View File

@@ -0,0 +1,13 @@
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE table_schema = '{{.SchemaName}}'
AND table_name = '{{.TableName}}'
AND constraint_name = '{{.ConstraintName}}'
) THEN
ALTER TABLE {{.SchemaName}}.{{.TableName}}
ADD CONSTRAINT {{.ConstraintName}} PRIMARY KEY ({{.Columns}});
END IF;
END;
$$;

View File

@@ -0,0 +1,4 @@
{{/* Example of using template inheritance for primary key creation */}}
{{/* This demonstrates how to use the base exists_check pattern */}}
{{/* Note: This is an example and not used by the actual migration writer */}}
{{/* The actual create_primary_key.tmpl is used instead */}}

View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS {{.SchemaName}}.{{.TableName}} (
{{- range $i, $col := .Columns}}
{{- if $i}},{{end}}
{{$col.Name}} {{$col.Type}}
{{- if $col.Default}} DEFAULT {{$col.Default}}{{end}}
{{- if $col.NotNull}} NOT NULL{{end}}
{{- end}}
);

View File

@@ -0,0 +1,9 @@
{{/* Example of table creation using composition */}}
{{- define "create_table_composed" -}}
CREATE TABLE IF NOT EXISTS {{template "qualified_table" .}} (
{{- range $i, $col := .Columns}}
{{- if $i}},{{end}}
{{template "column_definition" $col}}
{{- end}}
);
{{- end -}}

View File

@@ -0,0 +1 @@
ALTER TABLE {{.SchemaName}}.{{.TableName}} DROP CONSTRAINT IF EXISTS {{.ConstraintName}};

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS {{.SchemaName}}.{{.IndexName}} CASCADE;

View File

@@ -0,0 +1,45 @@
{{/* Reusable template fragments */}}
{{/* Column definition fragment */}}
{{- define "column_definition" -}}
{{.Name}} {{.Type}}
{{- if .Default}} DEFAULT {{.Default}}{{end}}
{{- if .NotNull}} NOT NULL{{end}}
{{- end -}}
{{/* Comma-separated column list */}}
{{- define "column_list" -}}
{{- range $i, $col := . -}}
{{- if $i}}, {{end}}{{$col}}
{{- end -}}
{{- end -}}
{{/* Qualified table name */}}
{{- define "qualified_table" -}}
{{.SchemaName}}.{{.TableName}}
{{- end -}}
{{/* Index method clause */}}
{{- define "index_method" -}}
{{- if .IndexType}}USING {{.IndexType}}{{end -}}
{{- end -}}
{{/* Uniqueness keyword */}}
{{- define "unique_keyword" -}}
{{- if .Unique}}UNIQUE {{end -}}
{{- end -}}
{{/* Referential action clauses */}}
{{- define "referential_actions" -}}
{{- if .OnDelete}}
ON DELETE {{.OnDelete}}
{{- end}}
{{- if .OnUpdate}}
ON UPDATE {{.OnUpdate}}
{{- end}}
{{- end -}}
{{/* Comment statement */}}
{{- define "comment_on" -}}
COMMENT ON {{.ObjectType}} {{.ObjectName}} IS {{quote .Comment}};
{{- end -}}

View File

@@ -0,0 +1,274 @@
# RelSpec Template Editor for VS Code
Visual editor and tooling for RelSpec PostgreSQL migration templates.
## Features
### 1. Template Preview
- **Command**: `RelSpec: Preview Template`
- **Shortcut**: Click the preview icon in the editor title bar
- Preview your templates with sample data
- Side-by-side view of template, data, and rendered SQL
### 2. Syntax Validation
- **Command**: `RelSpec: Validate Template`
- Automatic validation on save (configurable)
- Highlights syntax errors inline
- Checks for unclosed template tags
### 3. IntelliSense
- Auto-completion for template functions
- Function signatures and documentation on hover
- Keyword completions (`if`, `range`, `template`, etc.)
### 4. Template Scaffolding
- **Command**: `RelSpec: New Template`
- Quick scaffolding for common template types:
- DDL operations
- Constraints
- Indexes
- Audit trails
- Reusable fragments
### 5. Function Library
- **Command**: `RelSpec: List Available Functions`
- Browse all available template functions
- See examples and documentation
- Quick reference for template development
## Installation
### From Source
1. Clone the RelSpec repository
2. Navigate to the extension directory:
```bash
cd vscode-extension
```
3. Install dependencies:
```bash
npm install
```
4. Compile the extension:
```bash
npm run compile
```
5. Open in VS Code:
```bash
code .
```
6. Press `F5` to launch the extension in a new window
### From VSIX (when published)
```bash
code --install-extension relspec-template-editor-0.1.0.vsix
```
## Usage
### Opening Templates
1. Open your RelSpec project in VS Code
2. Navigate to `pkg/writers/pgsql/templates/`
3. Open any `.tmpl` file
4. The extension will automatically activate
### Previewing Templates
1. Open a template file
2. Click the preview icon in the editor title bar OR
3. Run command: `RelSpec: Preview Template` (Ctrl+Shift+P)
4. The preview pane will show:
- Your template source
- Sample data (configurable)
- Rendered SQL output
### Configuring Sample Data
Set custom sample data for preview in settings:
```json
{
"relspec.previewSampleData": {
"SchemaName": "public",
"TableName": "users",
"ColumnName": "email",
"Columns": [
{"Name": "id", "Type": "integer", "NotNull": true},
{"Name": "email", "Type": "text"}
]
}
}
```
### Creating New Templates
1. Run command: `RelSpec: New Template`
2. Select template type
3. Enter template name
4. The extension creates a scaffolded template
5. Edit and customize
### Using IntelliSense
Type `{{` to trigger auto-completion:
```gotmpl
{{upper // Shows function signature and documentation
{{. // Shows available fields from data structure
```
Hover over function names to see:
- Function signature
- Description
- Usage examples
## Configuration
Available settings:
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `relspec.templatePath` | string | `pkg/writers/pgsql/templates` | Path to template directory |
| `relspec.autoValidate` | boolean | `true` | Validate templates on save |
| `relspec.showDataStructures` | boolean | `true` | Show data structure hints in preview |
| `relspec.previewSampleData` | object | `{}` | Sample JSON data for preview |
## Keyboard Shortcuts
| Command | Shortcut | Description |
|---------|----------|-------------|
| Preview Template | N/A | Click preview icon in title bar |
| Validate Template | N/A | Use command palette |
| New Template | N/A | Use command palette |
## Template Functions Reference
The extension provides IntelliSense for all RelSpec template functions:
### String Manipulation
- `upper` - Convert to uppercase
- `lower` - Convert to lowercase
- `title` - Title case
- `snake_case` - Convert to snake_case
- `camelCase` - Convert to camelCase
### SQL Formatting
- `indent` - Indent text
- `quote` - Quote for SQL
- `escape` - Escape special characters
- `safe_identifier` - Make safe SQL identifier
### Type Conversion
- `goTypeToSQL` - Go type → SQL type
- `sqlTypeToGo` - SQL type → Go type
- `isNumeric` - Check if numeric type
- `isText` - Check if text type
### Collection Helpers
- `first` - First element
- `last` - Last element
- `filter` - Filter elements
- `mapFunc` - Map function
- `join_with` - Join with separator
- `join` - Join strings
See full documentation: [Template Functions](../pkg/writers/pgsql/TEMPLATE_FUNCTIONS.md)
## Code Snippets
The extension includes snippets for common patterns:
| Prefix | Description |
|--------|-------------|
| `tmpl-define` | Define a new template |
| `tmpl-template` | Use a template |
| `tmpl-block` | Define a block |
| `tmpl-if` | If statement |
| `tmpl-range` | Range loop |
| `tmpl-with` | With statement |
## Development
### Building
```bash
npm run compile
```
### Watching
```bash
npm run watch
```
### Linting
```bash
npm run lint
```
### Testing
```bash
npm test
```
## Troubleshooting
### Extension Not Activating
**Problem**: Extension doesn't activate when opening .tmpl files
**Solution**:
1. Check that file has `.tmpl` extension
2. Reload VS Code window (Ctrl+Shift+P → "Reload Window")
3. Check extension is enabled in Extensions panel
### Preview Not Working
**Problem**: Preview shows error or doesn't update
**Solution**:
1. Ensure RelSpec binary is in PATH
2. Check template syntax is valid
3. Verify sample data is valid JSON
### IntelliSense Not Working
**Problem**: Auto-completion doesn't trigger
**Solution**:
1. Type `{{` to trigger
2. Check language mode is set to "Go Template"
3. Reload window
## Contributing
Contributions welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests
5. Submit a pull request
## License
Same as RelSpec project license.
## Links
- [RelSpec Documentation](../README.md)
- [Template Documentation](../pkg/writers/pgsql/TEMPLATES.md)
- [Template Inheritance](../pkg/writers/pgsql/TEMPLATE_INHERITANCE.md)
- [Issue Tracker](https://github.com/yourorg/relspec/issues)
## Changelog
### 0.1.0 (Initial Release)
- Template preview with sample data
- Syntax validation
- IntelliSense for functions
- Template scaffolding
- Function library browser

View File

@@ -0,0 +1,119 @@
{
"name": "relspec-template-editor",
"displayName": "RelSpec Template Editor",
"description": "Visual editor for RelSpec PostgreSQL migration templates",
"version": "0.1.0",
"engines": {
"vscode": "^1.80.0"
},
"categories": [
"Programming Languages",
"Formatters",
"Snippets"
],
"activationEvents": [
"onLanguage:gotmpl",
"onCommand:relspec.previewTemplate",
"onCommand:relspec.validateTemplate",
"onCommand:relspec.newTemplate"
],
"main": "./out/extension.js",
"contributes": {
"languages": [
{
"id": "gotmpl",
"aliases": ["Go Template", "gotmpl"],
"extensions": [".tmpl"],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "gotmpl",
"scopeName": "source.gotmpl",
"path": "./syntaxes/gotmpl.tmLanguage.json"
}
],
"commands": [
{
"command": "relspec.previewTemplate",
"title": "RelSpec: Preview Template",
"icon": "$(preview)"
},
{
"command": "relspec.validateTemplate",
"title": "RelSpec: Validate Template"
},
{
"command": "relspec.newTemplate",
"title": "RelSpec: New Template"
},
{
"command": "relspec.listFunctions",
"title": "RelSpec: List Available Functions"
}
],
"menus": {
"editor/title": [
{
"command": "relspec.previewTemplate",
"when": "resourceLangId == gotmpl",
"group": "navigation"
}
],
"editor/context": [
{
"command": "relspec.validateTemplate",
"when": "resourceLangId == gotmpl",
"group": "relspec"
}
]
},
"configuration": {
"title": "RelSpec Template Editor",
"properties": {
"relspec.templatePath": {
"type": "string",
"default": "pkg/writers/pgsql/templates",
"description": "Path to template directory"
},
"relspec.autoValidate": {
"type": "boolean",
"default": true,
"description": "Automatically validate templates on save"
},
"relspec.showDataStructures": {
"type": "boolean",
"default": true,
"description": "Show data structure hints in preview"
},
"relspec.previewSampleData": {
"type": "string",
"default": "{}",
"description": "Sample JSON data for template preview"
}
}
},
"snippets": [
{
"language": "gotmpl",
"path": "./snippets/relspec.json"
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"lint": "eslint src --ext ts"
},
"devDependencies": {
"@types/vscode": "^1.80.0",
"@types/node": "16.x",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"eslint": "^8.41.0",
"typescript": "^5.0.4"
},
"dependencies": {}
}

View File

@@ -0,0 +1,438 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
// Extension activation
export function activate(context: vscode.ExtensionContext) {
console.log('RelSpec Template Editor activated');
// Register commands
context.subscriptions.push(
vscode.commands.registerCommand('relspec.previewTemplate', previewTemplate)
);
context.subscriptions.push(
vscode.commands.registerCommand('relspec.validateTemplate', validateTemplate)
);
context.subscriptions.push(
vscode.commands.registerCommand('relspec.newTemplate', newTemplate)
);
context.subscriptions.push(
vscode.commands.registerCommand('relspec.listFunctions', listFunctions)
);
// Register completion provider
const completionProvider = vscode.languages.registerCompletionItemProvider(
'gotmpl',
new TemplateCompletionProvider(),
'{{', '.'
);
context.subscriptions.push(completionProvider);
// Register hover provider
const hoverProvider = vscode.languages.registerHoverProvider(
'gotmpl',
new TemplateHoverProvider()
);
context.subscriptions.push(hoverProvider);
// Auto-validate on save
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument((document) => {
const config = vscode.workspace.getConfiguration('relspec');
if (config.get('autoValidate') && document.languageId === 'gotmpl') {
validateTemplate();
}
})
);
}
// Preview template with sample data
async function previewTemplate() {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.languageId !== 'gotmpl') {
vscode.window.showErrorMessage('Please open a .tmpl file');
return;
}
const panel = vscode.window.createWebviewPanel(
'templatePreview',
'Template Preview',
vscode.ViewColumn.Beside,
{
enableScripts: true
}
);
const templateContent = editor.document.getText();
const config = vscode.workspace.getConfiguration('relspec');
const sampleDataString = config.get<string>('previewSampleData', '{}');
try {
const sampleData = JSON.parse(sampleDataString);
const preview = await renderTemplate(templateContent, sampleData);
panel.webview.html = getWebviewContent(templateContent, preview, sampleData);
} catch (error) {
panel.webview.html = getErrorWebviewContent(String(error));
}
}
// Validate template syntax
async function validateTemplate() {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.languageId !== 'gotmpl') {
return;
}
const templateContent = editor.document.getText();
const diagnostics: vscode.Diagnostic[] = [];
// Check for common template errors
const errors = checkTemplateSyntax(templateContent);
for (const error of errors) {
const range = new vscode.Range(
error.line,
error.column,
error.line,
error.column + error.length
);
const diagnostic = new vscode.Diagnostic(
range,
error.message,
vscode.DiagnosticSeverity.Error
);
diagnostics.push(diagnostic);
}
// Update diagnostics
const collection = vscode.languages.createDiagnosticCollection('relspec');
collection.set(editor.document.uri, diagnostics);
if (diagnostics.length === 0) {
vscode.window.showInformationMessage('Template is valid');
}
}
// Create new template from scaffold
async function newTemplate() {
const templateType = await vscode.window.showQuickPick(
[
{ label: 'DDL Template', value: 'ddl' },
{ label: 'Constraint Template', value: 'constraint' },
{ label: 'Index Template', value: 'index' },
{ label: 'Audit Template', value: 'audit' },
{ label: 'Custom Fragment', value: 'fragment' }
],
{ placeHolder: 'Select template type' }
);
if (!templateType) {
return;
}
const templateName = await vscode.window.showInputBox({
prompt: 'Enter template name',
placeHolder: 'my_template'
});
if (!templateName) {
return;
}
const scaffold = getTemplateScaffold(templateType.value, templateName);
const config = vscode.workspace.getConfiguration('relspec');
const templatePath = config.get<string>('templatePath', 'pkg/writers/pgsql/templates');
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
vscode.window.showErrorMessage('No workspace folder open');
return;
}
const filePath = path.join(workspaceFolders[0].uri.fsPath, templatePath, `${templateName}.tmpl`);
fs.writeFileSync(filePath, scaffold);
const document = await vscode.workspace.openTextDocument(filePath);
await vscode.window.showTextDocument(document);
}
// List available template functions
async function listFunctions() {
const functions = getTemplateFunctions();
const panel = vscode.window.createWebviewPanel(
'functionsList',
'RelSpec Template Functions',
vscode.ViewColumn.Beside,
{}
);
panel.webview.html = getFunctionsWebviewContent(functions);
}
// Completion provider for template functions and keywords
class TemplateCompletionProvider implements vscode.CompletionItemProvider {
provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
): vscode.CompletionItem[] {
const linePrefix = document.lineAt(position).text.substr(0, position.character);
if (!linePrefix.endsWith('{{')) {
return [];
}
const functions = getTemplateFunctions();
const completionItems: vscode.CompletionItem[] = [];
// Add function completions
for (const func of functions) {
const item = new vscode.CompletionItem(func.name, vscode.CompletionItemKind.Function);
item.detail = func.signature;
item.documentation = new vscode.MarkdownString(func.description);
item.insertText = new vscode.SnippetString(`${func.name} \${1:arg}}}`)
;
completionItems.push(item);
}
// Add keyword completions
const keywords = ['if', 'else', 'end', 'range', 'with', 'define', 'template', 'block'];
for (const keyword of keywords) {
const item = new vscode.CompletionItem(keyword, vscode.CompletionItemKind.Keyword);
completionItems.push(item);
}
return completionItems;
}
}
// Hover provider for template functions
class TemplateHoverProvider implements vscode.HoverProvider {
provideHover(
document: vscode.TextDocument,
position: vscode.Position
): vscode.Hover | undefined {
const range = document.getWordRangeAtPosition(position);
if (!range) {
return undefined;
}
const word = document.getText(range);
const functions = getTemplateFunctions();
const func = functions.find(f => f.name === word);
if (func) {
const markdown = new vscode.MarkdownString();
markdown.appendCodeblock(func.signature, 'go');
markdown.appendMarkdown('\n\n' + func.description);
markdown.appendMarkdown('\n\n**Example:**\n');
markdown.appendCodeblock(func.example, 'gotmpl');
return new vscode.Hover(markdown);
}
return undefined;
}
}
// Helper functions
function renderTemplate(template: string, data: any): Promise<string> {
// In a real implementation, this would call the Go binary
// For now, return a placeholder
return Promise.resolve(`-- Rendered SQL would appear here\n-- Template: ${template.substring(0, 50)}...`);
}
function checkTemplateSyntax(template: string): Array<{ line: number; column: number; length: number; message: string }> {
const errors: Array<{ line: number; column: number; length: number; message: string }> = [];
// Basic syntax checking
const lines = template.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for unclosed {{
const openCount = (line.match(/\{\{/g) || []).length;
const closeCount = (line.match(/\}\}/g) || []).length;
if (openCount > closeCount) {
errors.push({
line: i,
column: line.indexOf('{{'),
length: 2,
message: 'Unclosed template tag'
});
}
}
return errors;
}
function getTemplateScaffold(type: string, name: string): string {
const scaffolds: Record<string, string> = {
ddl: `{{/* ${name} - DDL operation template */}}
{{- define "${name}" -}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
-- Add your DDL operation here
{{- end -}}`,
constraint: `{{/* ${name} - Constraint template */}}
{{- define "${name}" -}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
ADD CONSTRAINT {{.ConstraintName}}
-- Add constraint definition here
{{- end -}}`,
index: `{{/* ${name} - Index template */}}
{{- define "${name}" -}}
CREATE {{if .Unique}}UNIQUE {{end}}INDEX IF NOT EXISTS {{.IndexName}}
ON {{.SchemaName}}.{{.TableName}}
USING {{.IndexType}} ({{join .Columns ", "}});
{{- end -}}`,
audit: `{{/* ${name} - Audit template */}}
{{- define "${name}" -}}
-- Audit configuration for {{.TableName}}
{{- end -}}`,
fragment: `{{/* ${name} - Reusable fragment */}}
{{- define "${name}" -}}
-- Add your reusable SQL fragment here
{{- end -}}`
};
return scaffolds[type] || scaffolds.fragment;
}
function getTemplateFunctions() {
return [
{
name: 'upper',
signature: 'upper(s string) string',
description: 'Convert string to uppercase',
example: '{{upper "hello"}} // HELLO'
},
{
name: 'lower',
signature: 'lower(s string) string',
description: 'Convert string to lowercase',
example: '{{lower "HELLO"}} // hello'
},
{
name: 'snake_case',
signature: 'snake_case(s string) string',
description: 'Convert string to snake_case',
example: '{{snake_case "UserId"}} // user_id'
},
{
name: 'camelCase',
signature: 'camelCase(s string) string',
description: 'Convert string to camelCase',
example: '{{camelCase "user_id"}} // userId'
},
{
name: 'quote',
signature: 'quote(s string) string',
description: 'Quote string for SQL (escapes single quotes)',
example: '{{quote "O\'Brien"}} // \'O\'\'Brien\''
},
{
name: 'safe_identifier',
signature: 'safe_identifier(s string) string',
description: 'Make string safe for SQL identifier',
example: '{{safe_identifier "User-Id"}} // user_id'
},
{
name: 'join',
signature: 'join(slice []string, sep string) string',
description: 'Join string slice with separator',
example: '{{join .Columns ", "}}'
}
];
}
function getWebviewContent(template: string, preview: string, data: any): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Template Preview</title>
<style>
body { font-family: var(--vscode-font-family); padding: 20px; }
.section { margin-bottom: 30px; }
h2 { color: var(--vscode-foreground); border-bottom: 1px solid var(--vscode-panel-border); }
pre { background: var(--vscode-editor-background); padding: 15px; border-radius: 4px; overflow-x: auto; }
.data { color: var(--vscode-editor-foreground); }
.sql { color: var(--vscode-textPreformat-foreground); }
</style>
</head>
<body>
<div class="section">
<h2>Template</h2>
<pre class="data">${escapeHtml(template)}</pre>
</div>
<div class="section">
<h2>Sample Data</h2>
<pre class="data">${escapeHtml(JSON.stringify(data, null, 2))}</pre>
</div>
<div class="section">
<h2>Rendered SQL</h2>
<pre class="sql">${escapeHtml(preview)}</pre>
</div>
</body>
</html>`;
}
function getErrorWebviewContent(error: string): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Error</title>
<style>
body { font-family: var(--vscode-font-family); padding: 20px; }
.error { color: var(--vscode-errorForeground); }
</style>
</head>
<body>
<h2 class="error">Template Error</h2>
<pre>${escapeHtml(error)}</pre>
</body>
</html>`;
}
function getFunctionsWebviewContent(functions: any[]): string {
const functionsHtml = functions.map(f => `
<div class="function">
<h3>${f.name}</h3>
<code>${f.signature}</code>
<p>${f.description}</p>
<pre>${f.example}</pre>
</div>
`).join('');
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Template Functions</title>
<style>
body { font-family: var(--vscode-font-family); padding: 20px; }
.function { margin-bottom: 30px; border-bottom: 1px solid var(--vscode-panel-border); padding-bottom: 20px; }
h3 { color: var(--vscode-foreground); margin-bottom: 10px; }
code { background: var(--vscode-textCodeBlock-background); padding: 4px 8px; border-radius: 3px; }
pre { background: var(--vscode-editor-background); padding: 15px; border-radius: 4px; }
</style>
</head>
<body>
<h1>Available Template Functions</h1>
${functionsHtml}
</body>
</html>`;
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
export function deactivate() {}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"lib": ["ES2020"],
"sourceMap": true,
"rootDir": "src",
"outDir": "out",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "out"]
}