sql writer
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
1
.gitignore
vendored
@@ -47,3 +47,4 @@ test_output/
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
bin/
|
||||
5
TODO.md
5
TODO.md
@@ -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
165
go.mod
@@ -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
474
go.sum
@@ -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=
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
696
pkg/writers/pgsql/TEMPLATES.md
Normal file
696
pkg/writers/pgsql/TEMPLATES.md
Normal 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
|
||||
74
pkg/writers/pgsql/audit.go
Normal file
74
pkg/writers/pgsql/audit.go
Normal 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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
285
pkg/writers/pgsql/template_functions.go
Normal file
285
pkg/writers/pgsql/template_functions.go
Normal 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
|
||||
}
|
||||
332
pkg/writers/pgsql/template_functions_test.go
Normal file
332
pkg/writers/pgsql/template_functions_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
457
pkg/writers/pgsql/templates.go
Normal file
457
pkg/writers/pgsql/templates.go
Normal 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,
|
||||
}
|
||||
}
|
||||
4
pkg/writers/pgsql/templates/add_column.tmpl
Normal file
4
pkg/writers/pgsql/templates/add_column.tmpl
Normal 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}};
|
||||
7
pkg/writers/pgsql/templates/alter_column_default.tmpl
Normal file
7
pkg/writers/pgsql/templates/alter_column_default.tmpl
Normal 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 -}}
|
||||
2
pkg/writers/pgsql/templates/alter_column_type.tmpl
Normal file
2
pkg/writers/pgsql/templates/alter_column_type.tmpl
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE {{.SchemaName}}.{{.TableName}}
|
||||
ALTER COLUMN {{.ColumnName}} TYPE {{.NewType}};
|
||||
84
pkg/writers/pgsql/templates/audit_function.tmpl
Normal file
84
pkg/writers/pgsql/templates/audit_function.tmpl
Normal 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}}';
|
||||
49
pkg/writers/pgsql/templates/audit_tables.tmpl
Normal file
49
pkg/writers/pgsql/templates/audit_tables.tmpl
Normal 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';
|
||||
16
pkg/writers/pgsql/templates/audit_trigger.tmpl
Normal file
16
pkg/writers/pgsql/templates/audit_trigger.tmpl
Normal 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;
|
||||
$$;
|
||||
39
pkg/writers/pgsql/templates/base_constraint.tmpl
Normal file
39
pkg/writers/pgsql/templates/base_constraint.tmpl
Normal 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 -}}
|
||||
34
pkg/writers/pgsql/templates/base_ddl.tmpl
Normal file
34
pkg/writers/pgsql/templates/base_ddl.tmpl
Normal 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 -}}
|
||||
1
pkg/writers/pgsql/templates/comment_column.tmpl
Normal file
1
pkg/writers/pgsql/templates/comment_column.tmpl
Normal file
@@ -0,0 +1 @@
|
||||
COMMENT ON COLUMN {{.SchemaName}}.{{.TableName}}.{{.ColumnName}} IS '{{.Comment}}';
|
||||
1
pkg/writers/pgsql/templates/comment_table.tmpl
Normal file
1
pkg/writers/pgsql/templates/comment_table.tmpl
Normal file
@@ -0,0 +1 @@
|
||||
COMMENT ON TABLE {{.SchemaName}}.{{.TableName}} IS '{{.Comment}}';
|
||||
10
pkg/writers/pgsql/templates/create_foreign_key.tmpl
Normal file
10
pkg/writers/pgsql/templates/create_foreign_key.tmpl
Normal 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;
|
||||
2
pkg/writers/pgsql/templates/create_index.tmpl
Normal file
2
pkg/writers/pgsql/templates/create_index.tmpl
Normal file
@@ -0,0 +1,2 @@
|
||||
CREATE {{if .Unique}}UNIQUE {{end}}INDEX IF NOT EXISTS {{.IndexName}}
|
||||
ON {{.SchemaName}}.{{.TableName}} USING {{.IndexType}} ({{.Columns}});
|
||||
13
pkg/writers/pgsql/templates/create_primary_key.tmpl
Normal file
13
pkg/writers/pgsql/templates/create_primary_key.tmpl
Normal 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;
|
||||
$$;
|
||||
@@ -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 */}}
|
||||
8
pkg/writers/pgsql/templates/create_table.tmpl
Normal file
8
pkg/writers/pgsql/templates/create_table.tmpl
Normal 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}}
|
||||
);
|
||||
9
pkg/writers/pgsql/templates/create_table_composed.tmpl
Normal file
9
pkg/writers/pgsql/templates/create_table_composed.tmpl
Normal 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 -}}
|
||||
1
pkg/writers/pgsql/templates/drop_constraint.tmpl
Normal file
1
pkg/writers/pgsql/templates/drop_constraint.tmpl
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE {{.SchemaName}}.{{.TableName}} DROP CONSTRAINT IF EXISTS {{.ConstraintName}};
|
||||
1
pkg/writers/pgsql/templates/drop_index.tmpl
Normal file
1
pkg/writers/pgsql/templates/drop_index.tmpl
Normal file
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS {{.SchemaName}}.{{.IndexName}} CASCADE;
|
||||
45
pkg/writers/pgsql/templates/fragments.tmpl
Normal file
45
pkg/writers/pgsql/templates/fragments.tmpl
Normal 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 -}}
|
||||
274
vscode-extension/relspec-template-editor/README.md
Normal file
274
vscode-extension/relspec-template-editor/README.md
Normal 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
|
||||
119
vscode-extension/relspec-template-editor/package.json
Normal file
119
vscode-extension/relspec-template-editor/package.json
Normal 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": {}
|
||||
}
|
||||
438
vscode-extension/relspec-template-editor/src/extension.ts
Normal file
438
vscode-extension/relspec-template-editor/src/extension.ts
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
||||
19
vscode-extension/relspec-template-editor/tsconfig.json
Normal file
19
vscode-extension/relspec-template-editor/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user