├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── _benchmark └── vssquirrel_test.go ├── _example ├── go.mod ├── go.sum ├── group.gen.go ├── group.go ├── group.plugin.gen.go ├── group_plugin_test.go ├── group_priv_test.go ├── group_test.go ├── id │ └── id.go ├── mysql.sql ├── plugins │ └── myrelations.tmpl ├── postgresql │ ├── account.go │ ├── account_test.go │ ├── accounts.gen.go │ ├── db_test.go │ ├── generate.go │ ├── group.go │ ├── groups.gen.go │ ├── identities.gen.go │ ├── identities.plugin.gen.go │ ├── identity.go │ └── postgresql.sql ├── sqlite3.sql ├── tools.go ├── user.gen.go ├── user.go ├── user.plugin.gen.go ├── user_external.gen.go ├── user_external.go ├── user_hook.go ├── user_item.gen.go ├── user_item.go ├── user_plugin_test.go ├── user_priv_test.go ├── user_sns.gen.go ├── user_sns.go ├── user_sns_test.go ├── user_test.go └── user_withmysql_test.go ├── cmd ├── mysql2schema │ └── main.go └── sqlla │ └── main.go ├── column.go ├── column_test.go ├── dialect.go ├── expr.go ├── exprmysql.go ├── generator.go ├── go.mod ├── go.sum ├── interface.go ├── main.go ├── main_priv.go ├── operator.go ├── opts.go ├── plugin.go ├── plugin_priv_test.go ├── plugin_test.go ├── table.go ├── template ├── delete.tmpl ├── delete_column.tmpl ├── delete_column_nullt.tmpl ├── insert.tmpl ├── insert_column.tmpl ├── insert_mysql.tmpl ├── insert_on_conflict_do_update_column.tmpl ├── insert_on_duplicate_key_update_column.tmpl ├── insert_postgresql.tmpl ├── plugins │ ├── count.tmpl │ ├── relations.tmpl │ ├── slice.tmpl │ ├── table.tmpl │ └── timeHooks.tmpl ├── select.tmpl ├── select_column.tmpl ├── select_column_nullt.tmpl ├── table.tmpl ├── update.tmpl ├── update_column.tmpl └── update_column_nullt.tmpl └── testdata └── nullt └── repoa ├── go.mod └── schema.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.22 11 | uses: actions/setup-go@v2 12 | with: 13 | go-version: 1.22.x 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v2 18 | 19 | - name: Build 20 | run: make test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | _bin 27 | .envrc 28 | vendor/ 29 | _artifacts 30 | _gobin 31 | go.work 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 mackee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell git describe --tags) 2 | 3 | export PATH=$(shell echo $$PWD/_gobin):$(shell echo $$PATH) 4 | export GOBIN=${PWD}/_gobin 5 | 6 | _bin/sqlla: *.go 7 | go generate 8 | go build -o _bin/sqlla -ldflags="-X main.Version=$(VERSION)" cmd/sqlla/main.go 9 | 10 | .PHONY: clean install get-deps test build 11 | 12 | test: generate 13 | go test -v -race ./... 14 | cd _example && go test -v -race ./... 15 | go vet ./... 16 | cd _example && go vet ./... 17 | 18 | clean: 19 | rm -Rf _bin/* _artifacts/* 20 | 21 | install: _bin/sqlla 22 | install _bin/sqlla $(GOPATH)/bin 23 | 24 | get-deps: 25 | go mod download 26 | cd _example && go mod download 27 | mkdir -p _gobin 28 | go install github.com/Songmu/goxz/cmd/goxz@latest 29 | go install github.com/tcnksm/ghr@latest 30 | go install github.com/mackee/go-genddl/cmd/genddl@latest 31 | 32 | generate: get-deps 33 | go generate ./... 34 | 35 | build: clean test 36 | mkdir -p _artifacts 37 | goxz -pv=${VERSION} -d=_artifacts -build-ldflags="-w -s -X main.Version=$(VERSION)" ./cmd/sqlla 38 | 39 | release: get-deps 40 | ghr ${VERSION} _artifacts 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-sqlla 2 | Type safe, reflect free, generative SQL Builder 3 | 4 | ## INSTALL 5 | 6 | ``` 7 | $ go install github.com/mackee/go-sqlla/v2/cmd/sqlla@latest 8 | ``` 9 | 10 | ## SYNOPSIS 11 | 12 | **person.go**: 13 | ```go 14 | package table 15 | 16 | //go:generate sqlla 17 | 18 | //+table: person 19 | type Person struct { 20 | ID uint64 `db:"id"` 21 | FirstName string `db:"first_name"` 22 | LastName string `db:"last_name"` 23 | } 24 | ``` 25 | 26 | Run generate: 27 | ``` 28 | $ ls 29 | person.go 30 | $ go generate 31 | $ ls 32 | person.go person_auto.go 33 | ``` 34 | 35 | Same package as the person.go: 36 | ```go 37 | 38 | import ( 39 | "database/sql" 40 | "log" 41 | 42 | _ "github.com/mattn/go-sqlite3" 43 | ) 44 | 45 | func main() { 46 | db, err := sql.Open("sqlite3", "./foo.db") 47 | if err != nil { 48 | log.Fatalf("failed connect database: %s", err) 49 | } 50 | 51 | q := NewPersonSQL().Select().ID(uint64(1)) 52 | query, args, err := q.ToSql() 53 | if err != nil { 54 | log.Fatalf("query build error: %s", err) 55 | } 56 | 57 | row := db.QueryRow(query, args...) 58 | var id uint64 59 | var firstName, lastName string 60 | err = row.Scan(&id, &firstName, &lastName) 61 | if err != nil { 62 | log.Fatalf("query exec error: %s", err) 63 | } 64 | log.Printf("id=%d, first_name=%s, last_name=%s", id, firstName, lastName) 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /_benchmark/vssquirrel_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | sq "github.com/lann/squirrel" 8 | ) 9 | 10 | func BenchmarkSelect__Squirrel(b *testing.B) { 11 | for i := 0; i < b.N; i++ { 12 | q := sq.Select("id", "name").From("user").Where(sq.Eq{"name": "hogehoge"}) 13 | q.ToSql() 14 | } 15 | } 16 | 17 | func BenchmarkSelect__Sqlla(b *testing.B) { 18 | for i := 0; i < b.N; i++ { 19 | q := example.NewUserSQL().Select().Name("hogehoge") 20 | q.ToSql() 21 | } 22 | } 23 | 24 | func TestSelect__Squirrel(t *testing.T) { 25 | q := sq.Select("id", "name").From("user").Where(sq.Eq{"name": "hogehoge"}) 26 | query, args, err := q.ToSql() 27 | if err != nil { 28 | t.Fatal("unexpected error:", err) 29 | } 30 | if query != "SELECT id, name FROM `user` WHERE name = ?" { 31 | t.Fatal("unexpected query:", query) 32 | } 33 | if !reflect.DeepEqual(args, []interface{}{"hogehoge"}) { 34 | t.Fatal("unexpected args:", args) 35 | } 36 | } 37 | 38 | func TestSelect__Sqlla(t *testing.T) { 39 | q := example.NewUserSQL().Select().Name("hogehoge") 40 | query, args, err := q.ToSql() 41 | if err != nil { 42 | t.Fatal("unexpected error:", err) 43 | } 44 | if query != "SELECT id, name FROM `user` WHERE name = ?;" { 45 | t.Fatal("unexpected query:", query) 46 | } 47 | if !reflect.DeepEqual(args, []interface{}{"hogehoge"}) { 48 | t.Fatal("unexpected args:", args) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /_example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mackee/go-sqlla/_example 2 | 3 | go 1.24.2 4 | 5 | replace github.com/mackee/go-sqlla/v2 => ../ 6 | 7 | require ( 8 | github.com/go-sql-driver/mysql v1.8.1 9 | github.com/google/go-cmp v0.6.0 10 | github.com/jackc/pgx/v5 v5.7.2 11 | github.com/mackee/go-genddl v0.0.0-20240912022326-fade26b3e8ea 12 | github.com/mackee/go-sqlla/v2 v2.0.0-00010101000000-000000000000 13 | github.com/mattn/go-sqlite3 v1.14.9 14 | github.com/ory/dockertest/v3 v3.11.0 15 | github.com/pgvector/pgvector-go v0.2.2 16 | ) 17 | 18 | require ( 19 | dario.cat/mergo v1.0.0 // indirect 20 | filippo.io/edwards25519 v1.1.0 // indirect 21 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 22 | github.com/Masterminds/goutils v1.1.1 // indirect 23 | github.com/Microsoft/go-winio v0.6.2 // indirect 24 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 25 | github.com/alecthomas/kong v1.6.1 // indirect 26 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 27 | github.com/containerd/continuity v0.4.3 // indirect 28 | github.com/docker/cli v26.1.4+incompatible // indirect 29 | github.com/docker/docker v27.1.1+incompatible // indirect 30 | github.com/docker/go-connections v0.5.0 // indirect 31 | github.com/docker/go-units v0.5.0 // indirect 32 | github.com/gertd/go-pluralize v0.2.1 // indirect 33 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect 34 | github.com/gogo/protobuf v1.3.2 // indirect 35 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 36 | github.com/jackc/pgpassfile v1.0.0 // indirect 37 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 38 | github.com/jackc/puddle/v2 v2.2.2 // indirect 39 | github.com/kr/text v0.2.0 // indirect 40 | github.com/mitchellh/mapstructure v1.5.0 // indirect 41 | github.com/moby/docker-image-spec v1.3.1 // indirect 42 | github.com/moby/term v0.5.0 // indirect 43 | github.com/nxadm/tail v1.4.8 // indirect 44 | github.com/opencontainers/go-digest v1.0.0 // indirect 45 | github.com/opencontainers/image-spec v1.1.0 // indirect 46 | github.com/opencontainers/runc v1.1.14 // indirect 47 | github.com/pkg/errors v0.9.1 // indirect 48 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect 49 | github.com/sirupsen/logrus v1.9.3 // indirect 50 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 51 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 52 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 53 | golang.org/x/crypto v0.35.0 // indirect 54 | golang.org/x/mod v0.21.0 // indirect 55 | golang.org/x/sync v0.11.0 // indirect 56 | golang.org/x/sys v0.30.0 // indirect 57 | golang.org/x/text v0.22.0 // indirect 58 | golang.org/x/tools v0.25.0 // indirect 59 | gopkg.in/yaml.v2 v2.4.0 // indirect 60 | ) 61 | -------------------------------------------------------------------------------- /_example/go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | entgo.io/ent v0.13.1 h1:uD8QwN1h6SNphdCCzmkMN3feSUzNnVvV/WIkHKMbzOE= 4 | entgo.io/ent v0.13.1/go.mod h1:qCEmo+biw3ccBn9OyL4ZK5dfpwg++l1Gxwac5B1206A= 5 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 6 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 7 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 8 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 9 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 10 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 11 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 12 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 13 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= 14 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= 15 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 16 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 17 | github.com/alecthomas/kong v1.6.1 h1:/7bVimARU3uxPD0hbryPE8qWrS3Oz3kPQoxA/H2NKG8= 18 | github.com/alecthomas/kong v1.6.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 19 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 20 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 21 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 22 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 23 | github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= 24 | github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= 25 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 26 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 27 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 28 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 30 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= 32 | github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 33 | github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= 34 | github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 35 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 36 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 37 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 38 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 39 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 40 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 41 | github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= 42 | github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= 43 | github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0= 44 | github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA= 45 | github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= 46 | github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= 47 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 48 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 49 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= 50 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 51 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 52 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 53 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 54 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 55 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 56 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 57 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 58 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 59 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 60 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 61 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 62 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 63 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 64 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 65 | github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= 66 | github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= 67 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 68 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 69 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 70 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 71 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 72 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 73 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 74 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 75 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 76 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 77 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 78 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 79 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 80 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 81 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 82 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 83 | github.com/mackee/go-genddl v0.0.0-20240912022326-fade26b3e8ea h1:6GFogLix2UUvFzCkAdDUnL+aWoHcTPjUrfBu9vLTEas= 84 | github.com/mackee/go-genddl v0.0.0-20240912022326-fade26b3e8ea/go.mod h1:F3rm5G9VxQDLs8gBBK0JOG0ybZSzxvZ4Lr25IY6HQOw= 85 | github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= 86 | github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 87 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 88 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 89 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 90 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 91 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 92 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 93 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 94 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 95 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 96 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 97 | github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= 98 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 99 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 100 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 101 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 102 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 103 | github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= 104 | github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= 105 | github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= 106 | github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= 107 | github.com/pgvector/pgvector-go v0.2.2 h1:Q/oArmzgbEcio88q0tWQksv/u9Gnb1c3F1K2TnalxR0= 108 | github.com/pgvector/pgvector-go v0.2.2/go.mod h1:u5sg3z9bnqVEdpe1pkTij8/rFhTaMCMNyQagPDLK8gQ= 109 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 110 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 111 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 112 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 113 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 114 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 115 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE= 116 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= 117 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 118 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 119 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 120 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 121 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 122 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 123 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 124 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 125 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= 126 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= 127 | github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ= 128 | github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0= 129 | github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk= 130 | github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc= 131 | github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w= 132 | github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM= 133 | github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= 134 | github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= 135 | github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= 136 | github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 137 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 138 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 139 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 140 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 141 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 142 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 143 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 144 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 145 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 146 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 147 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 148 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 149 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 150 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 151 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 152 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 153 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 154 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 155 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 156 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 157 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 158 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 159 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 160 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 161 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 162 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 163 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 164 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 165 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 166 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 168 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 169 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 170 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 171 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 174 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 175 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 176 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 177 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 178 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 179 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 180 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 181 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 182 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 183 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 184 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 185 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 186 | golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= 187 | golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= 188 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 189 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 190 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 191 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 192 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 193 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 194 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 195 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 196 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 197 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 198 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 199 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 200 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 201 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 202 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 203 | gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= 204 | gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= 205 | gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= 206 | gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 207 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 208 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 209 | mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= 210 | mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= 211 | -------------------------------------------------------------------------------- /_example/group.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | ) 7 | 8 | //go:generate go run github.com/mackee/go-sqlla/v2/cmd/sqlla --plugins plugins/*.tmpl 9 | 10 | type GroupID uint64 11 | 12 | //sqlla:table group 13 | //genddl:table group 14 | //sqlla:plugin myrelations key=LeaderUserID:User.ID method=Leader 15 | //sqlla:plugin slice 16 | //sqlla:plugin table get=ID&LeaderUserID,ID list=LeaderUserID&SubLeaderUserID create=Name,LeaderUserID,SubLeaderUserID,ChildGroupID,CreatedAt 17 | type Group struct { 18 | ID GroupID `db:"id,primarykey,autoincrement"` 19 | Name string `db:"name"` 20 | LeaderUserID UserId `db:"leader_user_id"` 21 | SubLeaderUserID sql.Null[int64] `db:"sub_leader_user_id"` 22 | ChildGroupID sql.Null[int64] `db:"child_group_id"` 23 | 24 | CreatedAt time.Time `db:"created_at"` 25 | UpdatedAt sql.NullTime `db:"updated_at"` 26 | } 27 | -------------------------------------------------------------------------------- /_example/group.plugin.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/mackee/go-sqlla/v2/cmd/sqlla. DO NOT EDIT. 2 | package example 3 | 4 | import ( 5 | "context" 6 | "database/sql" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/mackee/go-sqlla/v2" 11 | ) 12 | 13 | func (g *Group) Leader(ctx context.Context, db sqlla.DB) (*User, error) { 14 | row, err := NewUserSQL().Select().ID(g.LeaderUserID).SingleContext(ctx, db) 15 | if err != nil { 16 | return nil, fmt.Errorf("failed to get User: %w", err) 17 | } 18 | return &row, nil 19 | } 20 | 21 | type Groups []*Group 22 | 23 | type GroupTable struct{} 24 | 25 | func NewGroupTable() *GroupTable { 26 | return &GroupTable{} 27 | } 28 | 29 | func (g *GroupTable) GetByIDAndLeaderUserID(ctx context.Context, db sqlla.DB, c0 GroupID, c1 UserId) (*Group, error) { 30 | row, err := NewGroupSQL().Select(). 31 | ID(c0). 32 | LeaderUserID(c1). 33 | SingleContext(ctx, db) 34 | if err != nil { 35 | return nil, fmt.Errorf("failed to get Group by ID and LeaderUserID: %w", err) 36 | } 37 | return &row, nil 38 | } 39 | 40 | func (g *GroupTable) GetByID(ctx context.Context, db sqlla.DB, c0 GroupID) (*Group, error) { 41 | row, err := NewGroupSQL().Select(). 42 | ID(c0). 43 | SingleContext(ctx, db) 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to get Group by ID: %w", err) 46 | } 47 | return &row, nil 48 | } 49 | 50 | func (g *GroupTable) ListByIDS(ctx context.Context, db sqlla.DB, cs []GroupID) (Groups, error) { 51 | _rows, err := NewGroupSQL().Select(). 52 | IDIn(cs...). 53 | AllContext(ctx, db) 54 | if err != nil { 55 | return nil, fmt.Errorf("failed to list Groups by IDS: %w", err) 56 | } 57 | rows := make(Groups, len(_rows)) 58 | for i := range _rows { 59 | rows[i] = &_rows[i] 60 | } 61 | return rows, nil 62 | } 63 | 64 | func (g *GroupTable) ListByLeaderUserIDAndSubLeaderUserID(ctx context.Context, db sqlla.DB, c0 UserId, c1 int64) (Groups, error) { 65 | _rows, err := NewGroupSQL().Select(). 66 | LeaderUserID(c0). 67 | SubLeaderUserID(c1). 68 | AllContext(ctx, db) 69 | if err != nil { 70 | return nil, fmt.Errorf("failed to list Group by LeaderUserID and SubLeaderUserID: %w", err) 71 | } 72 | rows := make(Groups, len(_rows)) 73 | for i := range _rows { 74 | rows[i] = &_rows[i] 75 | } 76 | return rows, nil 77 | } 78 | 79 | type GroupTableCreateInput struct { 80 | Name string 81 | LeaderUserID UserId 82 | SubLeaderUserID sql.Null[int64] 83 | ChildGroupID sql.Null[int64] 84 | CreatedAt time.Time 85 | } 86 | 87 | func (g *GroupTable) newCreateSQL(input GroupTableCreateInput) groupInsertSQL { 88 | query := NewGroupSQL().Insert(). 89 | ValueName(input.Name). 90 | ValueLeaderUserID(input.LeaderUserID). 91 | ValueCreatedAt(input.CreatedAt) 92 | if input.SubLeaderUserID.Valid { 93 | query = query.ValueSubLeaderUserID(input.SubLeaderUserID.V) 94 | } else { 95 | query = query.ValueSubLeaderUserIDIsNull() 96 | } 97 | if input.ChildGroupID.Valid { 98 | query = query.ValueChildGroupID(input.ChildGroupID.V) 99 | } else { 100 | query = query.ValueChildGroupIDIsNull() 101 | } 102 | return query 103 | } 104 | 105 | func (g *GroupTable) Create(ctx context.Context, db sqlla.DB, input GroupTableCreateInput) (*Group, error) { 106 | row, err := g.newCreateSQL(input).ExecContext(ctx, db) 107 | if err != nil { 108 | return nil, fmt.Errorf("failed to create Group: %w", err) 109 | } 110 | return &row, nil 111 | } 112 | 113 | func (g *GroupTable) CreateMulti(ctx context.Context, db sqlla.DB, inputs []GroupTableCreateInput) error { 114 | bi := NewGroupSQL().BulkInsert() 115 | for _, input := range inputs { 116 | bi.Append(g.newCreateSQL(input)) 117 | } 118 | if _, err := bi.ExecContext(ctx, db); err != nil { 119 | return fmt.Errorf("failed to create Groups: %w", err) 120 | } 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /_example/group_plugin_test.go: -------------------------------------------------------------------------------- 1 | package example_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "testing" 7 | "time" 8 | 9 | example "github.com/mackee/go-sqlla/_example" 10 | ) 11 | 12 | func TestPlugin__Group__Table(t *testing.T) { 13 | ctx := context.Background() 14 | db := setupDB(t) 15 | 16 | table := example.NewGroupTable() 17 | 18 | now := time.Now() 19 | inputs := make([]example.GroupTableCreateInput, 4) 20 | for i, name := range []string{"hoge", "fuga", "piyo", "barr"} { 21 | inputs[i] = example.GroupTableCreateInput{ 22 | Name: name, 23 | LeaderUserID: 1, 24 | CreatedAt: now, 25 | } 26 | if i%2 == 0 { 27 | inputs[i].SubLeaderUserID = sql.Null[int64]{ 28 | Valid: true, 29 | V: 2, 30 | } 31 | } 32 | } 33 | if err := table.CreateMulti(ctx, db, inputs); err != nil { 34 | t.Error("cannot create rows error:", err) 35 | } 36 | 37 | row1, err := table.GetByIDAndLeaderUserID(ctx, db, 1, 1) 38 | if err != nil { 39 | t.Error("cannot get row error:", err) 40 | } 41 | if row1.SubLeaderUserID.V != 2 { 42 | t.Error("unexpected row1.SubLeaderUserID.V:", row1.SubLeaderUserID.V) 43 | } 44 | 45 | rows, err := table.ListByLeaderUserIDAndSubLeaderUserID(ctx, db, 1, 2) 46 | if err != nil { 47 | t.Error("cannot list rows error:", err) 48 | } 49 | if len(rows) != 2 { 50 | t.Error("unexpected len(rows):", len(rows)) 51 | } 52 | 53 | allRows, closer := example.NewGroupSQL().Select().IterContext(ctx, db) 54 | var ids []example.GroupID 55 | func() { 56 | defer closer() 57 | for row, err := range allRows { 58 | if err != nil { 59 | t.Error("cannot iterate rows error:", err) 60 | } 61 | if row.ID == 0 { 62 | t.Error("unexpected row.ID:", row.ID) 63 | } 64 | ids = append(ids, row.ID) 65 | } 66 | }() 67 | expectedRows, err := table.ListByIDS(ctx, db, ids) 68 | if err != nil { 69 | t.Error("cannot list rows by ids error:", err) 70 | } 71 | if len(expectedRows) != len(ids) { 72 | t.Error("unexpected len(expectedRows):", len(expectedRows)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /_example/group_priv_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | var GroupAllColumns = groupAllColumns 4 | -------------------------------------------------------------------------------- /_example/group_test.go: -------------------------------------------------------------------------------- 1 | package example_test 2 | 3 | import ( 4 | "database/sql" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | example "github.com/mackee/go-sqlla/_example" 10 | "github.com/mackee/go-sqlla/v2" 11 | ) 12 | 13 | var groupAllColumns = strings.Join(example.GroupAllColumns, ", ") 14 | 15 | type queryTestCase struct { 16 | name string 17 | query interface { 18 | ToSql() (string, []any, error) 19 | } 20 | expect string 21 | args []any 22 | } 23 | 24 | func (q queryTestCase) assert(t *testing.T) { 25 | t.Helper() 26 | t.Run(q.name, func(t *testing.T) { 27 | query, args, err := q.query.ToSql() 28 | if err != nil { 29 | t.Error("unexpected error:", err) 30 | } 31 | if query != q.expect { 32 | t.Errorf("unexpected query: got=%s, expect=%s", query, q.expect) 33 | } 34 | if diff := cmp.Diff(args, q.args); diff != "" { 35 | t.Errorf("unexpected args: diff=%s", diff) 36 | } 37 | }) 38 | } 39 | 40 | type queryTestCases []queryTestCase 41 | 42 | func (q queryTestCases) assert(t *testing.T) { 43 | t.Helper() 44 | for _, tc := range q { 45 | tc.assert(t) 46 | } 47 | } 48 | 49 | func TestSelectNullable(t *testing.T) { 50 | testCases := queryTestCases{ 51 | { 52 | name: "IS NULL", 53 | query: example.NewGroupSQL().Select().SubLeaderUserIDIsNull(), 54 | expect: "SELECT " + groupAllColumns + " FROM `group` WHERE `sub_leader_user_id` IS NULL;", 55 | args: []any{}, 56 | }, 57 | { 58 | name: "IS NOT NULL", 59 | query: example.NewGroupSQL().Select().ChildGroupIDIsNotNull(), 60 | expect: "SELECT " + groupAllColumns + " FROM `group` WHERE `child_group_id` IS NOT NULL;", 61 | args: []any{}, 62 | }, 63 | { 64 | name: "query by type parameter", 65 | query: example.NewGroupSQL().Select().SubLeaderUserID(42), 66 | expect: "SELECT " + groupAllColumns + " FROM `group` WHERE `sub_leader_user_id` = ?;", 67 | args: []any{int64(42)}, 68 | }, 69 | { 70 | name: "query by type parameter with operator", 71 | query: example.NewGroupSQL().Select().SubLeaderUserID(42, sqlla.OpLess), 72 | expect: "SELECT " + groupAllColumns + " FROM `group` WHERE `sub_leader_user_id` < ?;", 73 | args: []any{int64(42)}, 74 | }, 75 | { 76 | name: "query by type parameters multiple", 77 | query: example.NewGroupSQL().Select().SubLeaderUserIDIn(42, 43, 44), 78 | expect: "SELECT " + groupAllColumns + " FROM `group` WHERE `sub_leader_user_id` IN(?,?,?);", 79 | args: []any{ 80 | int64(42), int64(43), int64(44), 81 | }, 82 | }, 83 | } 84 | testCases.assert(t) 85 | } 86 | 87 | func TestUpdateNullable(t *testing.T) { 88 | testCases := queryTestCases{ 89 | { 90 | name: "SET NOT NULL WHERE IS NULL", 91 | query: example.NewGroupSQL().Update().SetSubLeaderUserID(42).WhereSubLeaderUserIDIsNull(), 92 | expect: "UPDATE `group` SET `sub_leader_user_id` = ? WHERE `sub_leader_user_id` IS NULL;", 93 | args: []any{int64(42)}, 94 | }, 95 | { 96 | name: "SET NULL WHERE IS NULL", 97 | query: example.NewGroupSQL().Update().SetSubLeaderUserIDToNull().WhereSubLeaderUserIDIsNull(), 98 | expect: "UPDATE `group` SET `sub_leader_user_id` = ? WHERE `sub_leader_user_id` IS NULL;", 99 | args: []any{sql.Null[int64]{Valid: false}}, 100 | }, 101 | { 102 | name: "SET NULL WHERE IS NOT NULL", 103 | query: example.NewGroupSQL().Update().SetChildGroupIDToNull().WhereSubLeaderUserIDIsNotNull(), 104 | expect: "UPDATE `group` SET `child_group_id` = ? WHERE `sub_leader_user_id` IS NOT NULL;", 105 | args: []any{sql.Null[int64]{Valid: false}}, 106 | }, 107 | { 108 | name: "SET NOT NULL WHERE equal type parameters", 109 | query: example.NewGroupSQL().Update().SetChildGroupID(42).WhereChildGroupID(100), 110 | expect: "UPDATE `group` SET `child_group_id` = ? WHERE `child_group_id` = ?;", 111 | args: []any{ 112 | int64(42), 113 | int64(100), 114 | }, 115 | }, 116 | { 117 | name: "SET NOT NULL WHERE type parameters and operator", 118 | query: example.NewGroupSQL().Update().SetChildGroupID(42).WhereChildGroupID(100, sqlla.OpGreater), 119 | expect: "UPDATE `group` SET `child_group_id` = ? WHERE `child_group_id` > ?;", 120 | args: []any{ 121 | int64(42), 122 | int64(100), 123 | }, 124 | }, 125 | { 126 | name: "SET NOT NULL WHERE IN type parameters", 127 | query: example.NewGroupSQL().Update().SetChildGroupID(42).WhereChildGroupIDIn(100, 101, 102), 128 | expect: "UPDATE `group` SET `child_group_id` = ? WHERE `child_group_id` IN(?,?,?);", 129 | args: []any{ 130 | int64(42), 131 | int64(100), 132 | int64(101), 133 | int64(102), 134 | }, 135 | }, 136 | } 137 | testCases.assert(t) 138 | } 139 | 140 | func TestInsertNullable(t *testing.T) { 141 | testCases := queryTestCases{ 142 | { 143 | name: "INSERT NULL column", 144 | query: example.NewGroupSQL().Insert().ValueSubLeaderUserIDIsNull(), 145 | expect: "INSERT INTO `group` (`sub_leader_user_id`) VALUES (?);", 146 | args: []any{sql.Null[int64]{Valid: false}}, 147 | }, 148 | { 149 | name: "INSERT with type parameter", 150 | query: example.NewGroupSQL().Insert().ValueSubLeaderUserID(42), 151 | expect: "INSERT INTO `group` (`sub_leader_user_id`) VALUES (?);", 152 | args: []any{int64(42)}, 153 | }, 154 | { 155 | name: "INSERT ON DUPLICATE KEY UPDATE SET with type parameter", 156 | query: example.NewGroupSQL().Insert().ValueSubLeaderUserID(42). 157 | OnDuplicateKeyUpdate(). 158 | ValueOnUpdateSubLeaderUserID(43), 159 | expect: "INSERT INTO `group` (`sub_leader_user_id`) VALUES (?) ON DUPLICATE KEY UPDATE `sub_leader_user_id` = ?;", 160 | args: []any{ 161 | int64(42), 162 | int64(43), 163 | }, 164 | }, 165 | { 166 | name: "INSERT ON DUPLICATE KEY UPDATE SET TO NULL", 167 | query: example.NewGroupSQL().Insert().ValueSubLeaderUserID(42). 168 | OnDuplicateKeyUpdate(). 169 | ValueOnUpdateSubLeaderUserIDToNull(), 170 | expect: "INSERT INTO `group` (`sub_leader_user_id`) VALUES (?) ON DUPLICATE KEY UPDATE `sub_leader_user_id` = ?;", 171 | args: []any{ 172 | int64(42), 173 | sql.Null[int64]{Valid: false}, 174 | }, 175 | }, 176 | } 177 | testCases.assert(t) 178 | } 179 | 180 | func TestDeleteNullable(t *testing.T) { 181 | testCases := queryTestCases{ 182 | { 183 | name: "IS NULL", 184 | query: example.NewGroupSQL().Delete().SubLeaderUserIDIsNull(), 185 | expect: "DELETE FROM `group` WHERE `sub_leader_user_id` IS NULL;", 186 | args: []any{}, 187 | }, 188 | { 189 | name: "IS NOT NULL", 190 | query: example.NewGroupSQL().Delete().ChildGroupIDIsNotNull(), 191 | expect: "DELETE FROM `group` WHERE `child_group_id` IS NOT NULL;", 192 | args: []any{}, 193 | }, 194 | { 195 | name: "query by type parameter", 196 | query: example.NewGroupSQL().Delete().SubLeaderUserID(42), 197 | expect: "DELETE FROM `group` WHERE `sub_leader_user_id` = ?;", 198 | args: []any{int64(42)}, 199 | }, 200 | { 201 | name: "query by type parameter with operator", 202 | query: example.NewGroupSQL().Delete().SubLeaderUserID(42, sqlla.OpLess), 203 | expect: "DELETE FROM `group` WHERE `sub_leader_user_id` < ?;", 204 | args: []any{int64(42)}, 205 | }, 206 | { 207 | name: "query by type parameters multiple", 208 | query: example.NewGroupSQL().Delete().SubLeaderUserIDIn(42, 43, 44), 209 | expect: "DELETE FROM `group` WHERE `sub_leader_user_id` IN(?,?,?);", 210 | args: []any{ 211 | int64(42), 212 | int64(43), 213 | int64(44), 214 | }, 215 | }, 216 | } 217 | testCases.assert(t) 218 | } 219 | -------------------------------------------------------------------------------- /_example/id/id.go: -------------------------------------------------------------------------------- 1 | package id 2 | 3 | type GroupID uint64 4 | -------------------------------------------------------------------------------- /_example/mysql.sql: -------------------------------------------------------------------------------- 1 | -- generated by github.com/mackee/go-genddl. DO NOT EDIT!!! 2 | 3 | DROP TABLE IF EXISTS `group`; 4 | 5 | CREATE TABLE `group` ( 6 | `id` BIGINT unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, 7 | `name` VARCHAR(191) NOT NULL, 8 | `leader_user_id` BIGINT unsigned NOT NULL, 9 | `sub_leader_user_id` BIGINT NULL, 10 | `child_group_id` BIGINT NULL, 11 | `created_at` DATETIME NOT NULL, 12 | `updated_at` DATETIME NULL 13 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 14 | 15 | 16 | DROP TABLE IF EXISTS `user`; 17 | 18 | CREATE TABLE `user` ( 19 | `id` BIGINT unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, 20 | `name` VARCHAR(191) NOT NULL, 21 | `age` BIGINT NULL, 22 | `rate` DOUBLE NOT NULL DEFAULT 0, 23 | `icon_image` BLOB NOT NULL, 24 | `created_at` DATETIME NOT NULL, 25 | `updated_at` DATETIME NULL, 26 | UNIQUE (`name`) 27 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 28 | 29 | 30 | DROP TABLE IF EXISTS `user_external`; 31 | 32 | CREATE TABLE `user_external` ( 33 | `id` BIGINT unsigned NOT NULL PRIMARY KEY, 34 | `user_id` BIGINT unsigned NOT NULL, 35 | `icon_image` BLOB NULL, 36 | `created_at` DATETIME NOT NULL, 37 | `updated_at` DATETIME NOT NULL 38 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 39 | 40 | 41 | DROP TABLE IF EXISTS `user_item`; 42 | 43 | CREATE TABLE `user_item` ( 44 | `id` BIGINT unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, 45 | `user_id` BIGINT unsigned NOT NULL, 46 | `item_id` VARCHAR(191) NOT NULL, 47 | `is_used` BOOLEAN NOT NULL, 48 | `has_extension` BOOLEAN NULL, 49 | `used_at` DATETIME NULL 50 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 51 | 52 | 53 | DROP TABLE IF EXISTS `user_sns`; 54 | 55 | CREATE TABLE `user_sns` ( 56 | `id` BIGINT unsigned NOT NULL PRIMARY KEY, 57 | `sns_type` VARCHAR(191) NOT NULL, 58 | `created_at` DATETIME NOT NULL, 59 | `updated_at` DATETIME NOT NULL 60 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 61 | 62 | -------------------------------------------------------------------------------- /_example/plugins/myrelations.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "plugin.myrelations" -}} 2 | {{- $structName := .Table.StructName }} 3 | {{- $receiver := substr 0 1 $structName | lower }} 4 | {{- $splitted := splitn ":" 2 .Args.key }} 5 | {{- $srcColumn := $splitted._0 }} 6 | {{- $dstTableColumn := splitn "." 2 $splitted._1 }} 7 | {{- $dstTable := $dstTableColumn._0 }} 8 | {{- $methodName := default $dstTable .Args.method }} 9 | {{- $dstColumn := $dstTableColumn._1 }} 10 | {{- $srcColumnField := .Table.Lookup $srcColumn }} 11 | {{- $isNullable := contains ".Null" $srcColumnField.TypeName }} 12 | func ({{ $receiver }} *{{ $structName }}) {{ $methodName }}(ctx context.Context, db sqlla.DB) (*{{ $dstTable }}, error) { 13 | {{- if $isNullable }} 14 | {{- $nullValue := "V" }} 15 | {{- if not $srcColumnField.IsNullT }} 16 | {{- $nullValue = trimPrefix "sql.Null" $srcColumnField.TypeName }} 17 | {{- if eq $srcColumnField.TypeName "mysql.NullTime" }} 18 | {{- $nullValue = "Time" }} 19 | {{- end }} 20 | {{- end }} 21 | if !{{ $receiver }}.{{ $srcColumn }}.Valid { 22 | return nil, nil 23 | } 24 | row, err := New{{ $dstTable }}SQL().Select().{{ $dstColumn }}({{ $receiver }}.{{ $srcColumn }}.{{ $nullValue }}).SingleContext(ctx, db) 25 | {{- else }} 26 | row, err := New{{ $dstTable }}SQL().Select().{{ $dstColumn }}({{ $receiver }}.{{ $srcColumn }}).SingleContext(ctx, db) 27 | {{- end }} 28 | if err != nil { 29 | return nil, fmt.Errorf("failed to get {{ $dstTable }}: %w", err) 30 | } 31 | return &row, nil 32 | } 33 | {{ end }} 34 | -------------------------------------------------------------------------------- /_example/postgresql/account.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pgvector/pgvector-go" 7 | ) 8 | 9 | type AccountID int64 10 | 11 | //genddl:table accounts 12 | //sqlla:table accounts 13 | type Account struct { 14 | ID AccountID `db:"id,autoincrement,primarykey"` 15 | Name string `db:"name"` 16 | Embedding pgvector.Vector `db:"embedding,type=vector(3)"` 17 | CreatedAt time.Time `db:"created_at"` 18 | UpdatedAt time.Time `db:"updated_at"` 19 | } 20 | 21 | var FixedNow time.Time 22 | 23 | func (a Account) DefaultInsertHook(q accountInsertSQL) (accountInsertSQL, error) { 24 | now := FixedNow 25 | if FixedNow.IsZero() { 26 | now = time.Now() 27 | } 28 | return q. 29 | ValueCreatedAt(now). 30 | ValueUpdatedAt(now), nil 31 | } 32 | 33 | func (a Account) DefaultUpdateHook(q accountUpdateSQL) (accountUpdateSQL, error) { 34 | now := FixedNow 35 | if FixedNow.IsZero() { 36 | now = time.Now() 37 | } 38 | return q. 39 | SetUpdatedAt(now), nil 40 | } 41 | -------------------------------------------------------------------------------- /_example/postgresql/db_test.go: -------------------------------------------------------------------------------- 1 | //go:build withpostgresql 2 | 3 | package postgresql_test 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "strconv" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/google/go-cmp/cmp" 17 | "github.com/google/go-cmp/cmp/cmpopts" 18 | _ "github.com/jackc/pgx/v5/stdlib" 19 | "github.com/mackee/go-sqlla/_example/postgresql" 20 | "github.com/ory/dockertest/v3" 21 | "github.com/ory/dockertest/v3/docker" 22 | "github.com/pgvector/pgvector-go" 23 | ) 24 | 25 | func TestMain(m *testing.M) { 26 | // uses a sensible default on windows (tcp/http) and linux/osx (socket) 27 | pool, err := dockertest.NewPool("") 28 | if err != nil { 29 | log.Fatalf("Could not connect to docker: %s", err) 30 | } 31 | 32 | // pulls an image, creates a container based on it and runs it 33 | resource, err := pool.RunWithOptions(&dockertest.RunOptions{ 34 | Repository: "pgvector/pgvector", 35 | Tag: "pg17", 36 | Env: []string{ 37 | "POSTGRES_USER=sqlla_test", 38 | "POSTGRES_PASSWORD=secret", 39 | }, 40 | }, 41 | func(config *docker.HostConfig) { 42 | config.AutoRemove = true 43 | config.RestartPolicy = docker.RestartPolicy{Name: "no"} 44 | }, 45 | ) 46 | if err != nil { 47 | log.Fatalf("Could not start resource: %s", err) 48 | } 49 | 50 | // exponential backoff-retry, because the application in the container might not be ready to accept connections yet 51 | if err := pool.Retry(func() error { 52 | port, err := strconv.Atoi(resource.GetPort("5432/tcp")) 53 | if err != nil { 54 | return fmt.Errorf("cannot convert port to int: %w", err) 55 | } 56 | db, err = sql.Open("pgx", fmt.Sprintf("host=localhost port=%d user=sqlla_test password=secret dbname=sqlla_test sslmode=disable", port)) 57 | if err != nil { 58 | return fmt.Errorf("cannot open database: %w", err) 59 | } 60 | return db.Ping() 61 | }); err != nil { 62 | log.Fatalf("Could not connect to database: %s", err) 63 | } 64 | 65 | if _, err := db.Exec("CREATE EXTENSION IF NOT EXISTS vector;"); err != nil { 66 | log.Fatalf("Could not create extension: %s", err) 67 | } 68 | 69 | schemaFile, err := os.Open("./postgresql.sql") 70 | if err != nil { 71 | log.Fatal("cannot open schema file error:", err) 72 | } 73 | 74 | b, err := io.ReadAll(schemaFile) 75 | if err != nil { 76 | log.Fatal("cannot read schema file error:", err) 77 | } 78 | 79 | stmts := strings.Split(string(b), ";") 80 | for _, stmt := range stmts { 81 | stmt = strings.TrimSpace(stmt) 82 | if stmt == "" { 83 | continue 84 | } 85 | _, err := db.Exec(stmt) 86 | if err != nil { 87 | log.Fatal("cannot load schema error:", err) 88 | } 89 | } 90 | 91 | code := m.Run() 92 | 93 | // You can't defer this because os.Exit doesn't care for defer 94 | if err := pool.Purge(resource); err != nil { 95 | log.Fatalf("Could not purge resource: %s", err) 96 | } 97 | 98 | os.Exit(code) 99 | } 100 | 101 | func cleanupDB(t *testing.T) { 102 | t.Helper() 103 | 104 | ctx := context.Background() 105 | if _, err := db.ExecContext(ctx, "TRUNCATE accounts RESTART IDENTITY"); err != nil { 106 | t.Fatal(err) 107 | } 108 | if _, err := db.ExecContext(ctx, "TRUNCATE identities RESTART IDENTITY"); err != nil { 109 | t.Fatal(err) 110 | } 111 | if _, err := db.ExecContext(ctx, "TRUNCATE groups RESTART IDENTITY"); err != nil { 112 | t.Fatal(err) 113 | } 114 | } 115 | 116 | func TestDB(t *testing.T) { 117 | postgresql.FixedNow = sampleDate 118 | 119 | tcs := testCases() 120 | opts := cmp.Options{ 121 | cmpopts.EquateEmpty(), 122 | cmpopts.IgnoreUnexported(pgvector.Vector{}), 123 | } 124 | for _, tc := range tcs { 125 | t.Run(tc.Name(), func(t *testing.T) { 126 | defer cleanupDB(t) 127 | tc.assert(t, opts...) 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /_example/postgresql/generate.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | //go:generate go run github.com/mackee/go-sqlla/v2/cmd/sqlla --dialect=postgresql --dir-all 4 | //go:generate go run github.com/mackee/go-genddl/cmd/genddl -outpath=./postgresql.sql -driver=pg 5 | -------------------------------------------------------------------------------- /_example/postgresql/group.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | ) 7 | 8 | type GroupID int64 9 | 10 | //sqlla:table groups 11 | //genddl:table groups 12 | type Group struct { 13 | ID GroupID `db:"id,primarykey,autoincrement"` 14 | Name string `db:"name"` 15 | LeaderAccountID AccountID `db:"leader_account_id"` 16 | SubLeaderAccountID sql.Null[AccountID] `db:"sub_leader_account_id"` 17 | ChildGroupID sql.Null[GroupID] `db:"child_group_id"` 18 | 19 | CreatedAt time.Time `db:"created_at"` 20 | UpdatedAt time.Time `db:"updated_at"` 21 | } 22 | -------------------------------------------------------------------------------- /_example/postgresql/identities.plugin.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/mackee/go-sqlla/v2/cmd/sqlla. DO NOT EDIT. 2 | package postgresql 3 | 4 | import "time" 5 | 6 | func (i Identity) DefaultInsertHook(_q identityInsertSQL) (identityInsertSQL, error) { 7 | now := time.Now() 8 | return _q. 9 | ValueCreatedAt(now). 10 | ValueUpdatedAt(now), nil 11 | } 12 | 13 | func (i Identity) DefaultUpdateHook(_q identityUpdateSQL) (identityUpdateSQL, error) { 14 | now := time.Now() 15 | return _q. 16 | SetUpdatedAt(now), nil 17 | } 18 | -------------------------------------------------------------------------------- /_example/postgresql/identity.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import "time" 4 | 5 | type IdentityID int64 6 | 7 | //genddl:table identities 8 | //sqlla:table identities 9 | //sqlla:plugin timeHooks create=CreatedAt,UpdatedAt update=UpdatedAt 10 | type Identity struct { 11 | ID IdentityID `db:"id,autoincrement,primarykey"` 12 | AccountID AccountID `db:"account_id"` 13 | Email string `db:"email"` 14 | CreatedAt time.Time `db:"created_at"` 15 | UpdatedAt time.Time `db:"updated_at"` 16 | } 17 | -------------------------------------------------------------------------------- /_example/postgresql/postgresql.sql: -------------------------------------------------------------------------------- 1 | -- generated by github.com/mackee/go-genddl. DO NOT EDIT!!! 2 | 3 | DROP TABLE IF EXISTS "accounts"; 4 | 5 | CREATE TABLE "accounts" ( 6 | "id" BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, 7 | "name" VARCHAR NOT NULL, 8 | "embedding" vector(3) NOT NULL, 9 | "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, 10 | "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL 11 | ) ; 12 | 13 | 14 | DROP TABLE IF EXISTS "groups"; 15 | 16 | CREATE TABLE "groups" ( 17 | "id" BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, 18 | "name" VARCHAR NOT NULL, 19 | "leader_account_id" BIGINT NOT NULL, 20 | "sub_leader_account_id" BIGINT NULL, 21 | "child_group_id" BIGINT NULL, 22 | "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, 23 | "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL 24 | ) ; 25 | 26 | 27 | DROP TABLE IF EXISTS "identities"; 28 | 29 | CREATE TABLE "identities" ( 30 | "id" BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, 31 | "account_id" BIGINT NOT NULL, 32 | "email" VARCHAR NOT NULL, 33 | "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, 34 | "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL 35 | ) ; 36 | 37 | -------------------------------------------------------------------------------- /_example/sqlite3.sql: -------------------------------------------------------------------------------- 1 | -- generated by github.com/mackee/go-genddl. DO NOT EDIT!!! 2 | 3 | DROP TABLE IF EXISTS "group"; 4 | 5 | CREATE TABLE "group" ( 6 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 7 | "name" TEXT NOT NULL, 8 | "leader_user_id" INTEGER NOT NULL, 9 | "sub_leader_user_id" INTEGER NULL, 10 | "child_group_id" INTEGER NULL, 11 | "created_at" DATETIME NOT NULL, 12 | "updated_at" DATETIME NULL 13 | ) ; 14 | 15 | 16 | DROP TABLE IF EXISTS "user"; 17 | 18 | CREATE TABLE "user" ( 19 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 20 | "name" TEXT NOT NULL, 21 | "age" INTEGER NULL, 22 | "rate" REAL NOT NULL DEFAULT 0, 23 | "icon_image" BLOB NOT NULL, 24 | "created_at" DATETIME NOT NULL, 25 | "updated_at" DATETIME NULL, 26 | UNIQUE ("name") 27 | ) ; 28 | 29 | 30 | DROP TABLE IF EXISTS "user_external"; 31 | 32 | CREATE TABLE "user_external" ( 33 | "id" INTEGER NOT NULL PRIMARY KEY, 34 | "user_id" INTEGER NOT NULL, 35 | "icon_image" BLOB NULL, 36 | "created_at" DATETIME NOT NULL, 37 | "updated_at" DATETIME NOT NULL 38 | ) ; 39 | 40 | 41 | DROP TABLE IF EXISTS "user_item"; 42 | 43 | CREATE TABLE "user_item" ( 44 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 45 | "user_id" INTEGER NOT NULL, 46 | "item_id" TEXT NOT NULL, 47 | "is_used" INTEGER NOT NULL, 48 | "has_extension" INTEGER NULL, 49 | "used_at" DATETIME NULL 50 | ) ; 51 | 52 | 53 | DROP TABLE IF EXISTS "user_sns"; 54 | 55 | CREATE TABLE "user_sns" ( 56 | "id" INTEGER NOT NULL PRIMARY KEY, 57 | "sns_type" TEXT NOT NULL, 58 | "created_at" DATETIME NOT NULL, 59 | "updated_at" DATETIME NOT NULL 60 | ) ; 61 | 62 | -------------------------------------------------------------------------------- /_example/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package example 4 | 5 | import ( 6 | _ "github.com/mackee/go-genddl" 7 | _ "github.com/mackee/go-sqlla/v2/cmd/sqlla" 8 | ) 9 | -------------------------------------------------------------------------------- /_example/user.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | 7 | "github.com/go-sql-driver/mysql" 8 | "github.com/mackee/go-genddl/index" 9 | ) 10 | 11 | //go:generate go run github.com/mackee/go-sqlla/v2/cmd/sqlla 12 | //go:generate go run github.com/mackee/go-genddl/cmd/genddl -outpath=./sqlite3.sql -driver=sqlite3 13 | 14 | type UserId uint64 15 | 16 | // +table: user 17 | // 18 | //sqlla:plugin count 19 | //sqlla:plugin timeHooks create=CreatedAt sameOnUpdate=UpdatedAt 20 | //sqlla:plugin slice columns=Id,Name keyBy=Id groupBy=Name 21 | type User struct { 22 | Id UserId `db:"id,primarykey,autoincrement"` 23 | Name string `db:"name"` 24 | Age sql.NullInt64 `db:"age"` 25 | Rate float64 `db:"rate,default=0"` 26 | IconImage []byte `db:"icon_image"` 27 | CreatedAt time.Time `db:"created_at"` 28 | UpdatedAt mysql.NullTime `db:"updated_at"` 29 | } 30 | 31 | func (s User) _schemaIndex(methods index.Methods) []index.Definition { 32 | return []index.Definition{ 33 | methods.Unique(s.Name), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /_example/user.plugin.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/mackee/go-sqlla/v2/cmd/sqlla. DO NOT EDIT. 2 | package example 3 | 4 | import ( 5 | "context" 6 | "time" 7 | 8 | "github.com/mackee/go-sqlla/v2" 9 | ) 10 | 11 | func (q userSelectSQL) CountContext(ctx context.Context, db sqlla.DB, column string) (int64, error) { 12 | query, args, err := q.SetColumns("COUNT(" + column + ")").ToSql() 13 | if err != nil { 14 | return 0, err 15 | } 16 | row := db.QueryRowContext(ctx, query, args...) 17 | var count int64 18 | if err := row.Scan(&count); err != nil { 19 | return 0, err 20 | } 21 | return count, nil 22 | } 23 | 24 | func (u User) DefaultInsertHook(_q userInsertSQL) (userInsertSQL, error) { 25 | now := time.Now() 26 | return _q. 27 | ValueCreatedAt(now), nil 28 | } 29 | 30 | func (u User) DefaultInsertOnDuplicateKeyUpdateHook(_q userInsertOnDuplicateKeyUpdateSQL) (userInsertOnDuplicateKeyUpdateSQL, error) { 31 | return _q. 32 | SameOnUpdateUpdatedAt(), nil 33 | } 34 | 35 | type Users []*User 36 | 37 | func (u Users) Ids() []UserId { 38 | vs := make([]UserId, len(u)) 39 | for _i := range u { 40 | vs[_i] = u[_i].Id 41 | } 42 | return vs 43 | } 44 | 45 | func (u Users) Names() []string { 46 | vs := make([]string, len(u)) 47 | for _i := range u { 48 | vs[_i] = u[_i].Name 49 | } 50 | return vs 51 | } 52 | 53 | func (u Users) AssociateByIds() map[UserId]*User { 54 | _m := make(map[UserId]*User, len(u)) 55 | for _, _v := range u { 56 | _m[_v.Id] = _v 57 | } 58 | return _m 59 | } 60 | 61 | func (u Users) GroupByNames() map[string]Users { 62 | _m := make(map[string]Users, len(u)) 63 | for _, _v := range u { 64 | _m[_v.Name] = append(_m[_v.Name], _v) 65 | } 66 | return _m 67 | } 68 | -------------------------------------------------------------------------------- /_example/user_external.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import "time" 4 | 5 | //go:generate go run github.com/mackee/go-sqlla/v2/cmd/sqlla 6 | 7 | // +table: user_external 8 | type UserExternal struct { 9 | Id uint64 `db:"id,primarykey"` 10 | UserId uint64 `db:"user_id"` 11 | IconImage []byte `db:"icon_image,null"` 12 | CreatedAt time.Time `db:"created_at"` 13 | UpdatedAt time.Time `db:"updated_at"` 14 | } 15 | -------------------------------------------------------------------------------- /_example/user_hook.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-sql-driver/mysql" 7 | ) 8 | 9 | func (u User) DefaultUpdateHook(q userUpdateSQL) (userUpdateSQL, error) { 10 | now := time.Now() 11 | return q.SetUpdatedAt(mysql.NullTime{Time: now, Valid: true}), nil 12 | } 13 | -------------------------------------------------------------------------------- /_example/user_item.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "database/sql" 5 | ) 6 | 7 | //go:generate go run github.com/mackee/go-sqlla/v2/cmd/sqlla 8 | 9 | // +table: user_item 10 | type UserItem struct { 11 | Id uint64 `db:"id,primarykey,autoincrement"` 12 | UserId uint64 `db:"user_id"` 13 | ItemId string `db:"item_id"` 14 | IsUsed bool `db:"is_used"` 15 | HasExtension sql.NullBool `db:"has_extension"` 16 | UsedAt sql.NullTime `db:"used_at"` 17 | } 18 | -------------------------------------------------------------------------------- /_example/user_plugin_test.go: -------------------------------------------------------------------------------- 1 | package example_test 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | "testing" 7 | 8 | example "github.com/mackee/go-sqlla/_example" 9 | ) 10 | 11 | func TestPlugin__User__Count(t *testing.T) { 12 | ctx := context.Background() 13 | db := setupDB(t) 14 | 15 | for _, name := range []string{"hoge", "fuga", "piyo"} { 16 | if _, err := example.NewUserSQL().Insert(). 17 | ValueName(name). 18 | ValueIconImage([]byte{}). 19 | ExecContextWithoutSelect(ctx, db); err != nil { 20 | t.Error("cannot insert row error:", err) 21 | } 22 | } 23 | 24 | allCount, err := example.NewUserSQL().Select().CountContext(ctx, db, "id") 25 | if err != nil { 26 | t.Error("cannot select row error:", err) 27 | } 28 | if allCount != 3 { 29 | t.Error("unexpected allCount:", allCount) 30 | } 31 | hogeCount, err := example.NewUserSQL().Select().Name("hoge").CountContext(ctx, db, "id") 32 | if err != nil { 33 | t.Error("cannot select row error:", err) 34 | } 35 | if hogeCount != 1 { 36 | t.Error("unexpected hogeCount:", hogeCount) 37 | } 38 | notFoundCount, err := example.NewUserSQL().Select().Name("notfound").CountContext(ctx, db, "id") 39 | if err != nil { 40 | t.Error("cannot select row error:", err) 41 | } 42 | if notFoundCount != 0 { 43 | t.Error("unexpected notFoundCount:", notFoundCount) 44 | } 45 | } 46 | 47 | func TestPlugin__User__List(t *testing.T) { 48 | ctx := context.Background() 49 | db := setupDB(t) 50 | 51 | for _, name := range []string{"hoge", "fuga", "piyo"} { 52 | if _, err := example.NewUserSQL().Insert(). 53 | ValueName(name). 54 | ValueIconImage([]byte{}). 55 | ExecContextWithoutSelect(ctx, db); err != nil { 56 | t.Error("cannot insert row error:", err) 57 | } 58 | } 59 | 60 | _users, err := example.NewUserSQL().Select().AllContext(ctx, db) 61 | if err != nil { 62 | t.Error("cannot select row error:", err) 63 | } 64 | users := make(example.Users, len(_users)) 65 | for i, u := range _users { 66 | users[i] = &u 67 | } 68 | 69 | ids := users.Ids() 70 | slices.Sort(ids) 71 | if !slices.Equal(ids, []example.UserId{1, 2, 3}) { 72 | t.Error("unexpected ids:", ids) 73 | } 74 | 75 | names := users.Names() 76 | slices.Sort(names) 77 | if !slices.Equal(names, []string{"fuga", "hoge", "piyo"}) { 78 | t.Error("unexpected names:", names) 79 | } 80 | 81 | userIdMap := users.AssociateByIds() 82 | expectedUserNameMap := map[example.UserId]string{ 83 | 1: "hoge", 84 | 2: "fuga", 85 | 3: "piyo", 86 | } 87 | for id, u := range userIdMap { 88 | if id != u.Id { 89 | t.Error("unexpected id:", id) 90 | } 91 | if expectedUserNameMap[id] != u.Name { 92 | t.Error("unexpected name:", u.Name) 93 | } 94 | } 95 | 96 | userNameMap := users.GroupByNames() 97 | expectedUserIdMap := map[string]example.UserId{ 98 | "hoge": 1, 99 | "fuga": 2, 100 | "piyo": 3, 101 | } 102 | for name, us := range userNameMap { 103 | if !slices.Equal(us.Names(), []string{name}) { 104 | t.Error("unexpected names:", us.Names()) 105 | } 106 | if !slices.Equal(us.Ids(), []example.UserId{expectedUserIdMap[name]}) { 107 | t.Error("unexpected names:", us.Ids()) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /_example/user_priv_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | var UserAllColumns = userAllColumns 4 | -------------------------------------------------------------------------------- /_example/user_sns.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import "time" 4 | 5 | //go:generate go run github.com/mackee/go-sqlla/v2/cmd/sqlla 6 | 7 | // +table: user_sns 8 | // 9 | //sqlla:table user_sns 10 | type UserSNS struct { 11 | ID uint64 `db:"id,primarykey"` 12 | SNSType string `db:"sns_type"` 13 | CreatedAt time.Time `db:"created_at"` 14 | UpdatedAt time.Time `db:"updated_at"` 15 | } 16 | -------------------------------------------------------------------------------- /_example/user_sns_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSelectUserSNS(t *testing.T) { 9 | query, args, err := NewUserSNSSQL().Select().SNSType("GITHUB").ToSql() 10 | if err != nil { 11 | t.Error("unexpected error:", err) 12 | } 13 | if query != "SELECT `id`, `sns_type`, `created_at`, `updated_at` FROM `user_sns` WHERE `sns_type` = ?;" { 14 | t.Error("unexpected query:", query) 15 | } 16 | if !reflect.DeepEqual(args, []interface{}{"GITHUB"}) { 17 | t.Error("unexpected args:", args) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /_example/user_test.go: -------------------------------------------------------------------------------- 1 | package example_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "io" 7 | "os" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "github.com/go-sql-driver/mysql" 15 | "github.com/google/go-cmp/cmp" 16 | example "github.com/mackee/go-sqlla/_example" 17 | "github.com/mackee/go-sqlla/v2" 18 | _ "github.com/mattn/go-sqlite3" 19 | ) 20 | 21 | var userAllColumns = strings.Join(example.UserAllColumns, ", ") 22 | 23 | func TestQuery(t *testing.T) { 24 | sampleDate := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) 25 | ignoreDate := time.Date(2019, 12, 31, 23, 59, 59, 0, time.UTC) 26 | type toSqler interface { 27 | ToSql() (string, []any, error) 28 | } 29 | tcs := []struct { 30 | name string 31 | query toSqler 32 | expected string 33 | vs []any 34 | }{ 35 | { 36 | name: "select", 37 | query: example.NewUserSQL().Select().Name("hoge"), 38 | expected: "SELECT " + userAllColumns + " FROM `user` WHERE `name` = ?;", 39 | vs: []any{"hoge"}, 40 | }, 41 | { 42 | name: "select with order by and limit", 43 | query: example.NewUserSQL().Select().Name("hoge").OrderByID(sqlla.Asc).Limit(100), 44 | expected: "SELECT " + userAllColumns + " FROM `user` WHERE `name` = ? ORDER BY `id` ASC LIMIT 100;", 45 | vs: []any{"hoge"}, 46 | }, 47 | { 48 | name: "select with in operator", 49 | query: example.NewUserSQL().Select().IDIn(1, 2, 3, 4, 5), 50 | expected: "SELECT " + userAllColumns + " FROM `user` WHERE `id` IN(?,?,?,?,?);", 51 | vs: []any{uint64(1), uint64(2), uint64(3), uint64(4), uint64(5)}, 52 | }, 53 | { 54 | name: "select with null int64", 55 | query: example.NewUserSQL().Select().Age(sql.NullInt64{}), 56 | expected: "SELECT " + userAllColumns + " FROM `user` WHERE `age` IS NULL;", 57 | vs: []any{}, 58 | }, 59 | { 60 | name: "select with not null int64", 61 | query: example.NewUserSQL().Select().Age(sql.NullInt64{}, sqlla.OpNot), 62 | expected: "SELECT " + userAllColumns + " FROM `user` WHERE `age` IS NOT NULL;", 63 | vs: []any{}, 64 | }, 65 | { 66 | name: "select with for update", 67 | query: example.NewUserSQL().Select().ID(1).ForUpdate(), 68 | expected: "SELECT " + userAllColumns + " FROM `user` WHERE `id` = ? FOR UPDATE;", 69 | vs: []any{uint64(1)}, 70 | }, 71 | { 72 | name: "select or", 73 | query: example.NewUserSQL().Select().Or( 74 | example.NewUserSQL().Select().ID(1), 75 | example.NewUserSQL().Select().ID(2), 76 | ), 77 | expected: "SELECT " + userAllColumns + " FROM `user` WHERE (( `id` = ? ) OR ( `id` = ? ));", 78 | vs: []any{uint64(1), uint64(2)}, 79 | }, 80 | { 81 | name: "select or null", 82 | query: example.NewUserItemSQL().Select(). 83 | IDIn(1, 2). 84 | Or( 85 | example.NewUserItemSQL().Select().UsedAt(sql.NullTime{}, sqlla.OpIs), 86 | example.NewUserItemSQL().Select().UsedAt(sql.NullTime{Time: sampleDate, Valid: true}, sqlla.OpLess), 87 | ), 88 | expected: "SELECT `id`, `user_id`, `item_id`, `is_used`, `has_extension`, `used_at` FROM `user_item` WHERE `id` IN(?,?) AND (( `used_at` IS NULL ) OR ( `used_at` < ? ));", 89 | vs: []any{uint64(1), uint64(2), sampleDate}, 90 | }, 91 | { 92 | name: "select join clause and table alias", 93 | query: example.NewUserSQL().Select(). 94 | SetColumns(append(example.UserAllColumns, "ui.item_id", "ui.is_used")...). 95 | TableAlias("u"). 96 | JoinClause("INNER JOIN user_item AS ui ON u.id = ui.user_id"). 97 | Name("hogehoge"). 98 | AdditionalWhereClause("AND ui.item_id IN (?,?,?)", 1, 2, 3). 99 | OrderByID(sqlla.Desc), 100 | expected: "SELECT `u`.`id`, `u`.`name`, `u`.`age`, `u`.`rate`, `u`.`icon_image`, `u`.`created_at`, `u`.`updated_at`, ui.item_id, ui.is_used FROM `user` AS `u` INNER JOIN user_item AS ui ON u.id = ui.user_id WHERE `u`.`name` = ? AND ui.item_id IN (?,?,?) ORDER BY `u`.`id` DESC;", 101 | vs: []any{"hogehoge", int(1), int(2), int(3)}, 102 | }, 103 | { 104 | name: "select set column", 105 | query: example.NewUserSQL().Select().SetColumns("rate", "COUNT(u.id)").TableAlias("u").OrderByRate(sqlla.Desc).GroupBy("rate"), 106 | expected: "SELECT `u`.`rate`, COUNT(u.id) FROM `user` AS `u` GROUP BY `u`.`rate` ORDER BY `u`.`rate` DESC;", 107 | vs: nil, 108 | }, 109 | { 110 | name: "select group by dotted column", 111 | query: example.NewUserSQL().Select().SetColumns("rate", "COUNT(u.id)").TableAlias("u").OrderByRate(sqlla.Desc).GroupBy("u.rate"), 112 | expected: "SELECT `u`.`rate`, COUNT(u.id) FROM `user` AS `u` GROUP BY u.rate ORDER BY `u`.`rate` DESC;", 113 | vs: nil, 114 | }, 115 | { 116 | name: "select like operator", 117 | query: example.NewUserSQL().Select().Name("%foobar%", sqlla.OpLike), 118 | expected: "SELECT " + userAllColumns + " FROM `user` WHERE `name` LIKE ?;", 119 | vs: []any{string("%foobar%")}, 120 | }, 121 | { 122 | name: "update", 123 | query: example.NewUserSQL().Update().SetName("barbar").WhereID(example.UserId(1)), 124 | expected: "UPDATE `user` SET `name` = ?, `updated_at` = ? WHERE `id` = ?;", 125 | vs: []any{"barbar", sql.Null[time.Time]{V: ignoreDate, Valid: true}, uint64(1)}, 126 | }, 127 | { 128 | name: "update in operator", 129 | query: example.NewUserSQL().Update().SetRate(42).WhereIDIn(example.UserId(1), example.UserId(2), example.UserId(3)), 130 | expected: "UPDATE `user` SET `rate` = ?, `updated_at` = ? WHERE `id` IN(?,?,?);", 131 | vs: []any{float64(42), sql.Null[time.Time]{V: ignoreDate, Valid: true}, uint64(1), uint64(2), uint64(3)}, 132 | }, 133 | { 134 | name: "insert", 135 | query: example.NewUserSQL().Insert().ValueName("hogehoge"), 136 | expected: "INSERT INTO `user` (`created_at`,`name`) VALUES (?,?);", 137 | vs: []any{ignoreDate, "hogehoge"}, 138 | }, 139 | { 140 | name: "insert on duplicate key update", 141 | query: example.NewUserSQL().Insert(). 142 | ValueID(1). 143 | ValueName("hogehoge"). 144 | ValueUpdatedAt(mysql.NullTime{ 145 | Valid: true, 146 | Time: sampleDate, 147 | }). 148 | OnDuplicateKeyUpdate(). 149 | ValueOnUpdateAge(sql.NullInt64{ 150 | Valid: true, 151 | Int64: 17, 152 | }), 153 | expected: "INSERT INTO `user` (`created_at`,`id`,`name`,`updated_at`) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE `age` = ?, `updated_at` = VALUES(`updated_at`);", 154 | vs: []any{ignoreDate, uint64(1), "hogehoge", sql.Null[time.Time]{V: sampleDate, Valid: true}, sql.Null[int64]{V: 17, Valid: true}}, 155 | }, 156 | { 157 | name: "bulk insert", 158 | query: func() toSqler { 159 | items := example.NewUserItemSQL().BulkInsert() 160 | for i := 1; i <= 10; i++ { 161 | q := example.NewUserItemSQL().Insert(). 162 | ValueUserID(42). 163 | ValueItemID(strconv.Itoa(i)) 164 | items.Append(q) 165 | } 166 | return items 167 | }(), 168 | expected: "INSERT INTO `user_item` (`item_id`,`user_id`) VALUES (?,?),(?,?),(?,?),(?,?),(?,?),(?,?),(?,?),(?,?),(?,?),(?,?);", 169 | vs: []any{"1", uint64(42), "2", uint64(42), "3", uint64(42), "4", uint64(42), "5", uint64(42), "6", uint64(42), "7", uint64(42), "8", uint64(42), "9", uint64(42), "10", uint64(42)}, 170 | }, 171 | { 172 | name: "bulk insert with on duplicate key update", 173 | query: func() toSqler { 174 | items := example.NewUserItemSQL().BulkInsert() 175 | items.Append( 176 | example.NewUserItemSQL().Insert().ValueUserID(42).ValueItemID("1").ValueIsUsed(true), 177 | example.NewUserItemSQL().Insert().ValueUserID(42).ValueItemID("2").ValueIsUsed(true), 178 | ) 179 | 180 | return items.OnDuplicateKeyUpdate(). 181 | SameOnUpdateIsUsed(). 182 | ValueOnUpdateUsedAt(sql.NullTime{ 183 | Time: sampleDate, 184 | Valid: true, 185 | }) 186 | }(), 187 | expected: "INSERT INTO `user_item` (`is_used`,`item_id`,`user_id`) VALUES (?,?,?),(?,?,?) ON DUPLICATE KEY UPDATE `is_used` = VALUES(`is_used`), `used_at` = ?;", 188 | vs: []any{true, "1", uint64(42), true, "2", uint64(42), sql.Null[time.Time]{V: sampleDate, Valid: true}}, 189 | }, 190 | { 191 | name: "delete", 192 | query: example.NewUserSQL().Delete().Name("hogehoge"), 193 | expected: "DELETE FROM `user` WHERE `name` = ?;", 194 | vs: []any{"hogehoge"}, 195 | }, 196 | { 197 | name: "delete in operator", 198 | query: example.NewUserSQL().Delete().NameIn("hogehoge", "fugafuga"), 199 | expected: "DELETE FROM `user` WHERE `name` IN(?,?);", 200 | vs: []any{"hogehoge", "fugafuga"}, 201 | }, 202 | } 203 | opts := cmp.Options{ 204 | cmp.FilterValues( 205 | func(x, y time.Time) bool { 206 | return x == ignoreDate || y == ignoreDate 207 | }, 208 | cmp.Ignore(), 209 | ), 210 | } 211 | for _, tc := range tcs { 212 | t.Run(tc.name, func(t *testing.T) { 213 | sql, vs, err := tc.query.ToSql() 214 | if err != nil { 215 | t.Fatalf("unexpected error: %v", err) 216 | } 217 | if sql != tc.expected { 218 | t.Errorf("expected: \n%s\n, but got: \n%s", tc.expected, sql) 219 | } 220 | if len(vs) != len(tc.vs) { 221 | t.Errorf("expected: %v, but got: %v", tc.vs, vs) 222 | } 223 | if diff := cmp.Diff(vs, tc.vs, opts...); diff != "" { 224 | t.Errorf("has diff: %s", diff) 225 | } 226 | }) 227 | } 228 | } 229 | 230 | func setupDB(t *testing.T) *sql.DB { 231 | dbFile, err := os.CreateTemp("", "sqlla_test") 232 | if err != nil { 233 | t.Fatal("cannot create tempfile error:", err) 234 | } 235 | db, err := sql.Open("sqlite3", dbFile.Name()) 236 | if err != nil { 237 | t.Fatal("cannot open database error:", err) 238 | } 239 | 240 | schemaFile, err := os.Open("./sqlite3.sql") 241 | if err != nil { 242 | t.Fatal("cannot open schema file error:", err) 243 | } 244 | 245 | b, err := io.ReadAll(schemaFile) 246 | if err != nil { 247 | t.Fatal("cannot read schema file error:", err) 248 | } 249 | 250 | stmts := strings.Split(string(b), ";") 251 | for _, stmt := range stmts { 252 | _, err := db.Exec(stmt) 253 | if err != nil { 254 | t.Fatal("cannot load schema error:", err) 255 | } 256 | } 257 | return db 258 | } 259 | 260 | func TestCRUD__WithSqlite3(t *testing.T) { 261 | db := setupDB(t) 262 | 263 | query, args, err := example.NewUserSQL().Insert().ValueName("hogehoge").ValueIconImage([]byte{}).ToSql() 264 | if err != nil { 265 | t.Error("unexpected error:", err) 266 | } 267 | _, err = db.Exec(query, args...) 268 | if err != nil { 269 | t.Error("cannot insert row error:", err) 270 | } 271 | 272 | query, args, err = example.NewUserSQL().Select().Name("hogehoge").ToSql() 273 | if err != nil { 274 | t.Error("unexpected error:", err) 275 | } 276 | row := db.QueryRow(query, args...) 277 | var id uint64 278 | var name string 279 | var age sql.NullInt64 280 | var rate float64 281 | var iconImage []byte 282 | var createdAt time.Time 283 | var updatedAt mysql.NullTime 284 | err = row.Scan(&id, &name, &age, &rate, &iconImage, &createdAt, &updatedAt) 285 | if err != nil { 286 | t.Error("unexpected error:", err) 287 | } 288 | if name != "hogehoge" { 289 | t.Error("unexpected name:", name) 290 | } 291 | if id == uint64(0) { 292 | t.Error("empty id:", id) 293 | } 294 | 295 | query, args, err = example.NewUserSQL().Select().IDIn(example.UserId(1)).ToSql() 296 | if err != nil { 297 | t.Error("unexpected error:", err) 298 | } 299 | row = db.QueryRow(query, args...) 300 | var rescanID uint64 301 | var rescanName string 302 | var rescanAge sql.NullInt64 303 | var rescanRate float64 304 | var rescanIconImage []byte 305 | var rescanCreatedAt time.Time 306 | var rescanUpdatedAt mysql.NullTime 307 | err = row.Scan(&rescanID, &rescanName, &rescanAge, &rescanRate, &rescanIconImage, &rescanCreatedAt, &rescanUpdatedAt) 308 | if err != nil { 309 | t.Error("unexpected error:", err) 310 | } 311 | if name != "hogehoge" { 312 | t.Error("unexpected name:", name) 313 | } 314 | if rescanID != id { 315 | t.Error("unmatched id:", rescanID) 316 | } 317 | 318 | query, args, err = example.NewUserSQL().Update().WhereID(example.UserId(id)).SetName("barbar").ToSql() 319 | if err != nil { 320 | t.Error("unexpected error:", err) 321 | } 322 | result, err := db.Exec(query, args...) 323 | if err != nil { 324 | t.Error("cannot update row error:", err) 325 | } 326 | if rows, _ := result.RowsAffected(); rows != 1 { 327 | t.Error("unexpected row affected:", rows) 328 | } 329 | 330 | query, args, err = example.NewUserSQL().Delete().Name("barbar").ToSql() 331 | if err != nil { 332 | t.Error("unexpected error:", err) 333 | } 334 | result, err = db.Exec(query, args...) 335 | if err != nil { 336 | t.Error("cannot delete row error:", err) 337 | } 338 | if rows, _ := result.RowsAffected(); rows != 1 { 339 | t.Error("unexpected row affected:", rows) 340 | } 341 | } 342 | 343 | func TestORM__WithSqlite3(t *testing.T) { 344 | db := setupDB(t) 345 | 346 | insertedRow, err := example.NewUserSQL().Insert().ValueName("hogehoge").ValueIconImage([]byte{}).Exec(db) 347 | if err != nil { 348 | t.Error("cannot insert row error:", err) 349 | } 350 | if insertedRow.Id == example.UserId(0) { 351 | t.Error("empty id:", insertedRow.Id) 352 | } 353 | if insertedRow.Name != "hogehoge" { 354 | t.Error("unexpected name:", insertedRow.Name) 355 | } 356 | 357 | singleRow, err := example.NewUserSQL().Select().ID(insertedRow.Id).Single(db) 358 | if err != nil { 359 | t.Error("cannot select row error:", err) 360 | } 361 | if singleRow.Id == example.UserId(0) { 362 | t.Error("empty id:", singleRow.Id) 363 | } 364 | if singleRow.Name != "hogehoge" { 365 | t.Error("unexpected name:", singleRow.Name) 366 | } 367 | 368 | _, err = example.NewUserSQL().Insert().ValueName("fugafuga").ValueIconImage([]byte{}).Exec(db) 369 | if err != nil { 370 | t.Error("cannot insert row error:", err) 371 | } 372 | 373 | rows, err := example.NewUserSQL().Select().All(db) 374 | if err != nil { 375 | t.Error("cannot select row error:", err) 376 | } 377 | if len(rows) != 2 { 378 | t.Error("missing rows error:", len(rows)) 379 | } 380 | 381 | for _, row := range rows { 382 | if row.Id == example.UserId(0) { 383 | t.Error("empty id:", row.Id) 384 | } 385 | if row.Name != "hogehoge" && row.Name != "fugafuga" { 386 | t.Error("unexpected name:", row.Name) 387 | } 388 | } 389 | 390 | targetRow := rows[0] 391 | results, err := targetRow.Update().SetName("barbar").Exec(db) 392 | if err != nil { 393 | t.Error("cannnot update row error", err) 394 | } 395 | if len(results) != 1 { 396 | t.Error("unexpected rows results:", len(results)) 397 | } 398 | result := results[0] 399 | if result.Id != targetRow.Id { 400 | t.Errorf("result.Id is not targetRow.Id: %d vs %d", result.Id, targetRow.Id) 401 | } 402 | if result.Name != "barbar" { 403 | t.Errorf("result.Name is not replaced to \"barbar\": %s", result.Name) 404 | } 405 | 406 | deletedResult, err := targetRow.Delete(db) 407 | if err != nil { 408 | t.Error("cannnot delete row error", err) 409 | } 410 | if affected, _ := deletedResult.RowsAffected(); affected != int64(1) { 411 | t.Error("unexpected rows affected:", affected) 412 | } 413 | 414 | _, err = targetRow.Select().Single(db) 415 | if err != sql.ErrNoRows { 416 | t.Error("not deleted rows") 417 | } 418 | } 419 | 420 | func TestORM__WithSqlite3__Binary(t *testing.T) { 421 | db := setupDB(t) 422 | binary := []byte("binary") 423 | 424 | insertedRow, err := example.NewUserSQL().Insert(). 425 | ValueName("hogehoge"). 426 | ValueIconImage(binary). 427 | Exec(db) 428 | if err != nil { 429 | t.Error("cannot insert row error:", err) 430 | } 431 | if !reflect.DeepEqual(insertedRow.IconImage, binary) { 432 | t.Error("unexpected IconImage:", insertedRow.IconImage) 433 | } 434 | 435 | singleRow, err := example.NewUserSQL().Select().ID(insertedRow.Id).Single(db) 436 | if err != nil { 437 | t.Error("cannot select row error:", err) 438 | } 439 | if !reflect.DeepEqual(singleRow.IconImage, binary) { 440 | t.Error("unexpected IconImage:", singleRow.IconImage) 441 | } 442 | 443 | updatedBinary := []byte("updated") 444 | results, err := singleRow.Update().SetIconImage(updatedBinary).Exec(db) 445 | if err != nil { 446 | t.Error("cannnot update row error", err) 447 | } 448 | if len(results) != 1 { 449 | t.Error("unexpected rows results:", len(results)) 450 | } 451 | result := results[0] 452 | if !reflect.DeepEqual(result.IconImage, updatedBinary) { 453 | t.Errorf("result.IconImage is not replaced to \"updated\": %s", result.IconImage) 454 | } 455 | } 456 | 457 | func TestORM__WithSqlite3__NullBinary(t *testing.T) { 458 | ctx := context.Background() 459 | now := time.Now() 460 | db := setupDB(t) 461 | 462 | _, err := example.NewUserExternalSQL().Insert(). 463 | ValueID(42). 464 | ValueUserID(4242). 465 | ValueIconImage(nil). 466 | ValueCreatedAt(now). 467 | ValueUpdatedAt(now). 468 | ExecContextWithoutSelect(ctx, db) 469 | if err != nil { 470 | t.Error("cannot insert row error:", err) 471 | } 472 | 473 | singleRow, err := example.NewUserExternalSQL().Select().ID(42).SingleContext(ctx, db) 474 | if err != nil { 475 | t.Error("cannot select row error:", err) 476 | } 477 | if singleRow.IconImage != nil { 478 | t.Error("unexpected IconImage:", singleRow.IconImage) 479 | } 480 | 481 | updatedBinary := []byte("updated") 482 | results, err := singleRow.Update().SetIconImage(updatedBinary).ExecContext(ctx, db) 483 | if err != nil { 484 | t.Error("cannnot update row error", err) 485 | } 486 | if len(results) != 1 { 487 | t.Error("unexpected rows results:", len(results)) 488 | } 489 | result := results[0] 490 | if !reflect.DeepEqual(result.IconImage, updatedBinary) { 491 | t.Errorf("result.IconImage is not replaced to \"updated\": %s", result.IconImage) 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /_example/user_withmysql_test.go: -------------------------------------------------------------------------------- 1 | //go:build withmysql 2 | 3 | package example 4 | 5 | import ( 6 | "context" 7 | "database/sql" 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "strconv" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/go-sql-driver/mysql" 18 | "github.com/mackee/go-sqlla/v2" 19 | "github.com/ory/dockertest/v3" 20 | ) 21 | 22 | var db *sql.DB 23 | 24 | //go:generate go run github.com/mackee/go-genddl/cmd/genddl -outpath=./mysql.sql -driver=mysql 25 | 26 | func TestMain(m *testing.M) { 27 | // uses a sensible default on windows (tcp/http) and linux/osx (socket) 28 | pool, err := dockertest.NewPool("") 29 | if err != nil { 30 | log.Fatalf("Could not connect to docker: %s", err) 31 | } 32 | 33 | // pulls an image, creates a container based on it and runs it 34 | resource, err := pool.Run("mysql", "8.0", []string{ 35 | "MYSQL_ROOT_PASSWORD=secret", 36 | "MYSQL_DATABASE=test", 37 | }) 38 | if err != nil { 39 | log.Fatalf("Could not start resource: %s", err) 40 | } 41 | 42 | // exponential backoff-retry, because the application in the container might not be ready to accept connections yet 43 | if err := pool.Retry(func() error { 44 | var err error 45 | db, err = sql.Open("mysql", fmt.Sprintf("root:secret@(localhost:%s)/test?parseTime=true", resource.GetPort("3306/tcp"))) 46 | if err != nil { 47 | return err 48 | } 49 | return db.Ping() 50 | }); err != nil { 51 | log.Fatalf("Could not connect to database: %s", err) 52 | } 53 | 54 | schemaFile, err := os.Open("./mysql.sql") 55 | if err != nil { 56 | log.Fatal("cannot open schema file error:", err) 57 | } 58 | 59 | b, err := io.ReadAll(schemaFile) 60 | if err != nil { 61 | log.Fatal("cannot read schema file error:", err) 62 | } 63 | 64 | stmts := strings.Split(string(b), ";") 65 | for _, stmt := range stmts { 66 | stmt = strings.TrimSpace(stmt) 67 | if stmt == "" { 68 | continue 69 | } 70 | _, err := db.Exec(stmt) 71 | if err != nil { 72 | log.Fatal("cannot load schema error:", err) 73 | } 74 | } 75 | 76 | code := m.Run() 77 | 78 | // You can't defer this because os.Exit doesn't care for defer 79 | if err := pool.Purge(resource); err != nil { 80 | log.Fatalf("Could not purge resource: %s", err) 81 | } 82 | 83 | os.Exit(code) 84 | } 85 | 86 | func TestInsertWithoutSelect__WithMySQL(t *testing.T) { 87 | ctx := context.Background() 88 | now := time.Now() 89 | 90 | q1 := NewUserExternalSQL().Insert(). 91 | ValueID(42). 92 | ValueUserID(42). 93 | ValueCreatedAt(now). 94 | ValueUpdatedAt(now) 95 | _, err := q1.ExecContextWithoutSelect(ctx, db) 96 | if err != nil { 97 | t.Fatal("unexpected error:", err) 98 | } 99 | } 100 | 101 | func TestInsertOnDuplicateKeyUpdate__WithMySQL(t *testing.T) { 102 | ctx := context.Background() 103 | now1 := time.Now() 104 | 105 | q1 := NewUserSQL().Insert(). 106 | ValueName("hogehoge"). 107 | ValueRate(3.14). 108 | ValueIconImage([]byte{}). 109 | ValueAge(sql.NullInt64{Valid: true, Int64: 17}). 110 | ValueUpdatedAt(mysql.NullTime{Valid: true, Time: now1}) 111 | query, args, _ := q1.ToSql() 112 | t.Logf("query=%s, args=%+v", query, args) 113 | r1, err := q1.ExecContext(ctx, db) 114 | if err != nil { 115 | t.Fatal("unexpected error:", err) 116 | } 117 | 118 | now2 := now1.Add(1 * time.Second) 119 | 120 | q2 := NewUserSQL().Insert(). 121 | ValueName("hogehoge"). 122 | ValueAge(sql.NullInt64{Valid: true, Int64: 17}). 123 | ValueIconImage([]byte{}). 124 | ValueUpdatedAt(mysql.NullTime{Valid: true, Time: now2}). 125 | OnDuplicateKeyUpdate(). 126 | RawValueOnUpdateAge(sqlla.SetMapRawValue("`age` + 1")) 127 | r2, err := q2.ExecContext(ctx, db) 128 | if err != nil { 129 | t.Fatal("unexpected error:", err) 130 | } 131 | 132 | if r2.Rate != 3.14 { 133 | t.Fatal("rate is not match:", r2.Rate) 134 | } 135 | if r2.Age.Int64 != 18 { 136 | t.Fatal("age does not incremented:", r2.Age.Int64) 137 | } 138 | if r2.UpdatedAt.Time.Unix() <= r1.UpdatedAt.Time.Unix() { 139 | t.Fatal("updated_at does not updated:", r1.UpdatedAt.Time.Unix(), r2.UpdatedAt.Time.Unix()) 140 | } 141 | } 142 | 143 | func TestBulkInsert__WithMySQL(t *testing.T) { 144 | ctx := context.Background() 145 | 146 | if _, err := NewUserItemSQL().Delete().ExecContext(ctx, db); err != nil { 147 | t.Fatal("unexpected error:", err) 148 | } 149 | 150 | items := NewUserItemSQL().BulkInsert() 151 | items.Append( 152 | NewUserItemSQL().Insert().ValueUserID(42).ValueItemID("1").ValueIsUsed(true), 153 | NewUserItemSQL().Insert().ValueUserID(42).ValueItemID("2").ValueIsUsed(true), 154 | ) 155 | 156 | if _, err := items.ExecContext(ctx, db); err != nil { 157 | t.Fatal("unexpected error:", err) 158 | } 159 | 160 | uis, err := NewUserItemSQL().Select().AllContext(ctx, db) 161 | if err != nil { 162 | t.Fatal("unexpected error:", err) 163 | } 164 | for i, ui := range uis { 165 | if ui.UserId != 42 { 166 | t.Error("UserId is not match:", ui.UserId) 167 | } 168 | if ui.ItemId != strconv.Itoa(i+1) { 169 | t.Errorf("ItemId is not match: index=%d, got=%s", i, ui.ItemId) 170 | } 171 | if !ui.IsUsed { 172 | t.Error("IsUsed is false") 173 | } 174 | } 175 | } 176 | 177 | func TestBulkInsertOnDuplicateKeyUpdate__WithMySQL(t *testing.T) { 178 | ctx := context.Background() 179 | 180 | if _, err := NewUserItemSQL().Delete().ExecContext(ctx, db); err != nil { 181 | t.Fatal("unexpected error:", err) 182 | } 183 | 184 | items := NewUserItemSQL().BulkInsert() 185 | items.Append( 186 | NewUserItemSQL().Insert().ValueUserID(42).ValueItemID("1").ValueIsUsed(false), 187 | NewUserItemSQL().Insert().ValueUserID(42).ValueItemID("2").ValueIsUsed(false), 188 | ) 189 | 190 | if _, err := items.ExecContext(ctx, db); err != nil { 191 | t.Fatal("unexpected error:", err) 192 | } 193 | 194 | uis, err := NewUserItemSQL().Select().AllContext(ctx, db) 195 | if err != nil { 196 | t.Fatal("unexpected error:", err) 197 | } 198 | uitems := NewUserItemSQL().BulkInsert() 199 | for _, ui := range uis { 200 | uitems.Append( 201 | NewUserItemSQL().Insert(). 202 | ValueID(ui.Id). 203 | ValueUserID(42). 204 | ValueItemID(ui.ItemId). 205 | ValueIsUsed(true), 206 | ) 207 | } 208 | uitems.Append( 209 | NewUserItemSQL().Insert(). 210 | ValueID(uis[len(uis)-1].Id + 1). 211 | ValueUserID(42). 212 | ValueItemID("3"). 213 | ValueIsUsed(true), 214 | ) 215 | now := time.Now() 216 | dup := uitems.OnDuplicateKeyUpdate(). 217 | SameOnUpdateIsUsed(). 218 | ValueOnUpdateUsedAt(sql.NullTime{ 219 | Valid: true, 220 | Time: now, 221 | }) 222 | 223 | if _, err := dup.ExecContext(ctx, db); err != nil { 224 | t.Fatal("unexpected error:", err) 225 | } 226 | 227 | uuis, err := NewUserItemSQL().Select().OrderByID(sqlla.Asc).AllContext(ctx, db) 228 | if err != nil { 229 | t.Fatal("unexpected error:", err) 230 | } 231 | for i, ui := range uuis { 232 | if !ui.IsUsed { 233 | t.Errorf("IsUsed is false: index=%d", i) 234 | } 235 | switch i { 236 | case 0, 1: 237 | if !ui.UsedAt.Valid { 238 | t.Errorf("UsedAt is not valid: index=%d", i) 239 | } 240 | case 2: 241 | if ui.UsedAt.Valid { 242 | t.Errorf("UsedAt is valid: index=%d", i) 243 | } 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /cmd/mysql2schema/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "database/sql" 7 | "flag" 8 | "fmt" 9 | "go/format" 10 | "io" 11 | "log" 12 | "os" 13 | "path/filepath" 14 | "strconv" 15 | "strings" 16 | 17 | "golang.org/x/tools/imports" 18 | 19 | "github.com/serenize/snaker" 20 | 21 | _ "github.com/go-sql-driver/mysql" 22 | ) 23 | 24 | const ( 25 | ScanStateBeforeColmuns = iota 26 | ScanStateInColumns 27 | ScanStateDefineIndex 28 | ScanStateAfterColmuns 29 | ) 30 | 31 | var ( 32 | dsn string 33 | tables []string 34 | outdir string 35 | packageName string 36 | ) 37 | 38 | func main() { 39 | var err error 40 | flag.StringVar(&dsn, "dsn", "user:password@tcp(localhost:3306)/dbname", "target data source name (see github.com/go-sql-driver/mysql)") 41 | flag.StringVar(&outdir, "outdir", "", "output directory") 42 | flag.StringVar(&packageName, "package", "", "package name when output to file") 43 | 44 | flag.Parse() 45 | tables = flag.Args() 46 | 47 | db, err := sql.Open("mysql", dsn) 48 | if err != nil { 49 | log.Fatalf("failed connect to data source: %s", err) 50 | } 51 | defer db.Close() 52 | 53 | for _, table := range tables { 54 | out := new(bytes.Buffer) 55 | if packageName != "" { 56 | io.WriteString(out, "package "+packageName+"\n\n") 57 | io.WriteString(out, "//go:generate sqlla\n\n") 58 | } 59 | err = outputSchema(db, table, out) 60 | if err != nil { 61 | log.Fatalf("failed output source in %s: %s", table, err) 62 | } 63 | if outdir != "" { 64 | filename := filepath.Join(outdir, fmt.Sprintf("%s.schema.go", table)) 65 | f, err := os.Create(filename) 66 | if err != nil { 67 | log.Fatalf("fail create output file: %s", err) 68 | } 69 | bs, err := imports.Process(filename, out.Bytes(), nil) 70 | if err != nil { 71 | log.Fatalf("fail run goimports to output file: %s", err) 72 | } 73 | _, err = f.Write(bs) 74 | if err != nil { 75 | log.Fatalf("fail write to output file: %s", err) 76 | } 77 | f.Close() 78 | } else { 79 | out.WriteTo(os.Stdout) 80 | } 81 | } 82 | } 83 | 84 | type Column struct { 85 | Name string 86 | Type string 87 | IsUnsigned bool 88 | IsNull bool 89 | IsPk bool 90 | } 91 | 92 | func outputSchema(db *sql.DB, table string, out io.Writer) error { 93 | query := fmt.Sprintf("SHOW CREATE TABLE %s", table) 94 | row := db.QueryRow(query) 95 | var tableName string 96 | var ddl string 97 | err := row.Scan(&tableName, &ddl) 98 | if err != nil { 99 | return err 100 | } 101 | buf := bytes.NewBufferString(ddl) 102 | s := bufio.NewScanner(buf) 103 | 104 | columns := make([]Column, 0, 16) 105 | state := ScanStateBeforeColmuns 106 | for s.Scan() { 107 | l := s.Text() 108 | l = strings.TrimSpace(l) 109 | switch state { 110 | case ScanStateBeforeColmuns: 111 | if strings.HasSuffix(l, "(") { 112 | state = ScanStateInColumns 113 | } 114 | continue 115 | case ScanStateInColumns, ScanStateDefineIndex: 116 | if strings.HasPrefix(l, "`") { 117 | state = ScanStateInColumns 118 | } else { 119 | state = ScanStateDefineIndex 120 | } 121 | if strings.HasPrefix(l, ")") { 122 | state = ScanStateAfterColmuns 123 | } 124 | default: 125 | break 126 | } 127 | if state == ScanStateDefineIndex { 128 | if strings.HasPrefix(l, "PRIMARY KEY") { 129 | trimed := strings.Trim(l, "PRIMARY KEY (") 130 | trimed = strings.TrimSuffix(trimed, ",") 131 | trimed = strings.TrimSuffix(trimed, ")") 132 | unquoted, err := strconv.Unquote(trimed) 133 | if err != nil { 134 | println(trimed) 135 | return err 136 | } 137 | if strings.Contains(unquoted, ",") { 138 | // ignore complex primary key 139 | continue 140 | } 141 | for i, c := range columns { 142 | if c.Name == unquoted { 143 | c.IsPk = true 144 | columns[i] = c 145 | break 146 | } 147 | } 148 | } 149 | } 150 | if state != ScanStateInColumns { 151 | continue 152 | } 153 | var c Column 154 | var beforeToken string 155 | defs := strings.Split(l, " ") 156 | for _, d := range defs { 157 | switch { 158 | case c.Name == "" && strings.HasPrefix(d, "`"): 159 | c.Name = strings.Trim(d, "`") 160 | case c.Type == "" && c.Name != "": 161 | c.Type = d 162 | case d == "unsigned": 163 | c.IsUnsigned = true 164 | case d == "NULL": 165 | switch beforeToken { 166 | case "NOT": 167 | c.IsNull = false 168 | case "DEFAULT": 169 | c.IsNull = true 170 | } 171 | } 172 | beforeToken = d 173 | } 174 | columns = append(columns, c) 175 | } 176 | 177 | outBuf := new(bytes.Buffer) 178 | outBuf.WriteString("// +table: " + table + "\n") 179 | outBuf.WriteString("type ") 180 | outBuf.WriteString(snaker.SnakeToCamel(table)) 181 | outBuf.WriteString(" struct {\n") 182 | for _, c := range columns { 183 | outBuf.WriteString(snaker.SnakeToCamel(c.Name)) 184 | outBuf.WriteString(" ") 185 | schemaType, err := sqlTypeToSchemaType(c) 186 | if err != nil { 187 | return err 188 | } 189 | outBuf.WriteString(schemaType) 190 | if c.IsPk { 191 | if _, err := fmt.Fprintf(outBuf, " `db:\"%s,primarykey\"`\n", c.Name); err != nil { 192 | return err 193 | } 194 | } else { 195 | if _, err := fmt.Fprintf(outBuf, " `db:\"%s\"`\n", c.Name); err != nil { 196 | return err 197 | } 198 | } 199 | } 200 | outBuf.WriteString("}") 201 | 202 | formated, err := format.Source(outBuf.Bytes()) 203 | if err != nil { 204 | return fmt.Errorf("go format error: %s", err) 205 | } 206 | out.Write(formated) 207 | 208 | return nil 209 | } 210 | 211 | func sqlTypeToSchemaType(c Column) (string, error) { 212 | switch { 213 | case strings.HasPrefix(c.Type, "bigint"): 214 | if c.IsNull { 215 | return "sql.NullInt64", nil 216 | } 217 | if c.IsUnsigned { 218 | return "uint64", nil 219 | } 220 | return "int64", nil 221 | case strings.HasPrefix(c.Name, "is_") || strings.HasPrefix(c.Name, "has_"): 222 | if c.IsNull { 223 | return "sql.NullBool", nil 224 | } 225 | return "bool", nil 226 | case strings.HasPrefix(c.Type, "int"): 227 | if c.IsNull { 228 | return "sql.NullInt64", nil 229 | } 230 | if c.IsUnsigned { 231 | return "uint32", nil 232 | } 233 | return "int32", nil 234 | case strings.HasPrefix(c.Type, "tinyint"): 235 | if c.IsNull { 236 | return "sql.NullInt64", nil 237 | } 238 | if c.IsUnsigned { 239 | return "uint8", nil 240 | } 241 | return "int8", nil 242 | case strings.HasPrefix(c.Type, "char"), strings.HasPrefix(c.Type, "varchar"), strings.HasPrefix(c.Type, "text"), strings.HasPrefix(c.Type, "json"): 243 | if c.IsNull { 244 | return "sql.NullString", nil 245 | } 246 | return "string", nil 247 | case strings.HasPrefix(c.Type, "datetime"): 248 | if c.IsNull { 249 | return "sql.NullTime", nil 250 | } 251 | return "time.Time", nil 252 | default: 253 | return "", fmt.Errorf("unexpected column type: %+v", c) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /cmd/sqlla/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | 8 | "github.com/alecthomas/kong" 9 | "github.com/mackee/go-sqlla/v2" 10 | ) 11 | 12 | var Version string 13 | 14 | func main() { 15 | var opts sqlla.Options 16 | kong.Parse(&opts) 17 | 18 | if opts.Version { 19 | fmt.Println("sqlla - Type safe, reflect free, generative SQL Builder + ORM-like methods") 20 | fmt.Printf("version %s\n", Version) 21 | os.Exit(0) 22 | } 23 | 24 | if err := sqlla.Run(opts); err != nil { 25 | slog.Error("occurred error", slog.Any("error", err)) 26 | os.Exit(1) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /column.go: -------------------------------------------------------------------------------- 1 | package sqlla 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type Columns []Column 12 | 13 | type Column struct { 14 | Field *ast.Field 15 | Name string 16 | MethodName string 17 | typeName string 18 | PkgName string 19 | baseTypeName string 20 | altTypeName string 21 | TableName string 22 | IsPk bool 23 | isNullT bool 24 | } 25 | 26 | func (c Column) HasUnderlyingType() bool { 27 | return c.baseTypeName != c.typeName 28 | } 29 | 30 | func (c Column) TypeName() string { 31 | tn := c.typeName 32 | return tn 33 | } 34 | 35 | func (c Column) BaseTypeName() string { 36 | return c.baseTypeName 37 | } 38 | 39 | func (c Column) AltTypeName() string { 40 | if c.altTypeName == "" { 41 | return "" 42 | } 43 | return c.altTypeName 44 | } 45 | 46 | func (c Column) IsNullT() bool { 47 | return c.isNullT 48 | } 49 | 50 | func (c Column) nullTypeSuffix() string { 51 | nv := strings.TrimPrefix(c.baseTypeName, "sql.Null") 52 | nv = strings.TrimPrefix(nv, "mysql.Null") 53 | if nv == c.baseTypeName { 54 | return "" 55 | } 56 | return nv 57 | } 58 | 59 | func (c Column) nullBaseType(t string) string { 60 | if t == "" { 61 | return "" 62 | } 63 | if t == "Time" { 64 | return "time.Time" 65 | } 66 | 67 | return strings.ToLower(t) 68 | } 69 | 70 | func (c Column) ExprValue() string { 71 | if nv := c.nullBaseType(c.nullTypeSuffix()); nv != "" { 72 | return "sqlla.ExprNull[" + nv + "]" 73 | } 74 | if c.isNullT { 75 | return "sqlla.ExprNull[" + c.baseTypeName + "]" 76 | } 77 | return "sqlla.ExprValue[" + c.baseTypeName + "]" 78 | } 79 | 80 | func (c Column) ExprMultiValue() string { 81 | return "sqlla.ExprMultiValue[" + c.baseTypeName + "]" 82 | } 83 | 84 | func (c Column) ExprValueIdentifier() string { 85 | if nt := c.nullTypeSuffix(); nt != "" { 86 | return "sql.Null[" + c.nullBaseType(nt) + "]{Valid: v.Valid, V: v." + nt + "}" 87 | } 88 | if c.typeName != c.baseTypeName { 89 | return c.baseTypeName + "(v)" 90 | } 91 | return "v" 92 | } 93 | 94 | func (c Column) String() string { 95 | return c.Name 96 | } 97 | 98 | func (c Column) FieldName() string { 99 | if len(c.Field.Names) > 0 { 100 | return c.Field.Names[0].Name 101 | } 102 | return "" 103 | } 104 | 105 | type Where []Expr 106 | 107 | func (wh Where) ToSql() (string, []any, error) { 108 | if len(wh) == 0 { 109 | return "", nil, nil 110 | } 111 | wheres := " " 112 | vs := []any{} 113 | for i, w := range wh { 114 | s, v, err := w.ToSql() 115 | if err != nil { 116 | return "", nil, err 117 | } 118 | vs = append(vs, v...) 119 | 120 | if i == 0 { 121 | wheres += s 122 | continue 123 | } 124 | wheres += " AND " + s 125 | } 126 | 127 | return wheres, vs, nil 128 | } 129 | 130 | func (wh Where) ToSqlPg(offset int) (string, int, []any, error) { 131 | if len(wh) == 0 { 132 | return "", offset, nil, nil 133 | } 134 | wheres := " " 135 | vs := []any{} 136 | for i, w := range wh { 137 | s, n, v, err := w.ToSqlPg(offset) 138 | if err != nil { 139 | return "", offset, nil, err 140 | } 141 | offset = n 142 | vs = append(vs, v...) 143 | if i == 0 { 144 | wheres += s 145 | continue 146 | } 147 | wheres += " AND " + s 148 | } 149 | 150 | return wheres, offset, vs, nil 151 | } 152 | 153 | type SetMapRawValue string 154 | 155 | type SetMap map[string]any 156 | 157 | func (sm SetMap) NewIterator() *SetMapIterator { 158 | keys := make(sort.StringSlice, 0, len(sm)) 159 | for k := range sm { 160 | keys = append(keys, k) 161 | } 162 | sort.Sort(keys) 163 | return &SetMapIterator{ 164 | sm: sm, 165 | keys: keys, 166 | cursor: -1, 167 | } 168 | } 169 | 170 | type SetMapIterator struct { 171 | sm SetMap 172 | cursor int 173 | keys []string 174 | } 175 | 176 | func (s *SetMapIterator) Iterate() bool { 177 | s.cursor++ 178 | return len(s.keys)-1 >= s.cursor 179 | } 180 | 181 | func (s *SetMapIterator) Key() string { 182 | return s.keys[s.cursor] 183 | } 184 | 185 | func (s *SetMapIterator) Value() any { 186 | return s.sm[s.keys[s.cursor]] 187 | } 188 | 189 | // ToUpdateSqlPg generates to set values SQL expressions with placeholders for MySQL/SQLite. 190 | func (sm SetMap) ToUpdateSql() (string, []any, error) { 191 | var setColumns string 192 | vs := []any{} 193 | columnCount := 0 194 | iter := sm.NewIterator() 195 | for iter.Iterate() { 196 | k, v := iter.Key(), iter.Value() 197 | if columnCount != 0 { 198 | setColumns += "," 199 | } 200 | if rv, ok := v.(SetMapRawValue); ok { 201 | setColumns += " " + k + " = " + string(rv) 202 | } else { 203 | setColumns += " " + k + " = ?" 204 | vs = append(vs, v) 205 | } 206 | columnCount++ 207 | } 208 | 209 | return setColumns, vs, nil 210 | } 211 | 212 | // ToUpdateSqlPg generates to set values SQL expressions with numbered placeholders for PostgreSQL. 213 | func (sm SetMap) ToUpdateSqlPg(offset int) (string, int, []any, error) { 214 | var setColumns string 215 | vs := []any{} 216 | columnCount := 0 217 | placeholderNum := offset 218 | iter := sm.NewIterator() 219 | for iter.Iterate() { 220 | k, v := iter.Key(), iter.Value() 221 | if columnCount != 0 { 222 | setColumns += "," 223 | } 224 | if rv, ok := v.(SetMapRawValue); ok { 225 | setColumns += " " + k + " = " + string(rv) 226 | } else { 227 | placeholderNum++ 228 | setColumns += " " + k + " = $" + strconv.Itoa(placeholderNum) 229 | vs = append(vs, v) 230 | } 231 | columnCount++ 232 | } 233 | 234 | return setColumns, placeholderNum, vs, nil 235 | } 236 | 237 | // ToInsertColumnsAndValues generates to insert columns and values SQL expressions with placeholders. 238 | func (sm SetMap) ToInsertColumnsAndValues() (string, string, []any) { 239 | qs, ps := "(", "(" 240 | vs := []any{} 241 | columnCount := 0 242 | iter := sm.NewIterator() 243 | for iter.Iterate() { 244 | k, v := iter.Key(), iter.Value() 245 | if columnCount != 0 { 246 | qs += "," 247 | ps += "," 248 | } 249 | qs += k 250 | ps += "?" 251 | vs = append(vs, v) 252 | columnCount++ 253 | } 254 | qs += ")" 255 | ps += ")" 256 | return qs, ps, vs 257 | } 258 | 259 | // ToInsertColumnAndValuesPg generates to insert columns and values SQL expressions with numbered placeholders for PostgreSQL. 260 | func (sm SetMap) ToInsertColumnsAndValuesPg(offset int) (string, string, int, []any) { 261 | qs, ps := "(", "(" 262 | vs := []any{} 263 | columnCount := 0 264 | placeholderNum := offset 265 | iter := sm.NewIterator() 266 | for iter.Iterate() { 267 | k, v := iter.Key(), iter.Value() 268 | if columnCount != 0 { 269 | qs += "," 270 | ps += "," 271 | } 272 | qs += k 273 | placeholderNum++ 274 | columnCount++ 275 | ps += "$" + strconv.Itoa(placeholderNum) 276 | vs = append(vs, v) 277 | } 278 | qs += ")" 279 | ps += ")" 280 | return qs, ps, placeholderNum, vs 281 | } 282 | 283 | // ToInsertSql generates to insert SQL expressions with placeholders. 284 | func (sm SetMap) ToInsertSql() (string, []any, error) { 285 | qs, ps, vs := sm.ToInsertColumnsAndValues() 286 | return qs + " VALUES " + ps, vs, nil 287 | } 288 | 289 | // ToInsertSqlPg generates to insert SQL expressions with numbered placeholders for PostgreSQL. 290 | func (sm SetMap) ToInsertSqlPg(offset int) (string, int, []any, error) { 291 | qs, ps, placeholderNum, vs := sm.ToInsertColumnsAndValuesPg(offset) 292 | return qs + " VALUES " + ps, placeholderNum, vs, nil 293 | } 294 | 295 | type SetMaps []SetMap 296 | 297 | // ToInsertSql generates to insert SQL expressions with placeholders. 298 | func (s SetMaps) ToInsertSql() (string, []any, error) { 299 | if len(s) == 0 { 300 | return "", nil, fmt.Errorf("sqlla: SetMaps is empty") 301 | } 302 | 303 | first := s[0] 304 | columns, values, vs := first.ToInsertColumnsAndValues() 305 | var b strings.Builder 306 | if _, err := b.WriteString(values); err != nil { 307 | return "", nil, err 308 | } 309 | for i, _s := range s[1:] { 310 | _columns, _values, _vs := _s.ToInsertColumnsAndValues() 311 | if columns != _columns { 312 | return "", nil, fmt.Errorf("sqlla: two SetMap are not match keys: [0]=%s, [%d]=%s", columns, i, _columns) 313 | } 314 | vs = append(vs, _vs...) 315 | if _, err := b.WriteString(","); err != nil { 316 | return "", nil, err 317 | } 318 | if _, err := b.WriteString(_values); err != nil { 319 | return "", nil, err 320 | } 321 | } 322 | return columns + " VALUES " + b.String(), vs, nil 323 | } 324 | 325 | // ToInsertSqlPg generates to insert SQL expressions with numbered placeholders for PostgreSQL. 326 | func (s SetMaps) ToInsertSqlPg(offset int) (string, int, []any, error) { 327 | if len(s) == 0 { 328 | return "", 0, nil, fmt.Errorf("sqlla: SetMaps is empty") 329 | } 330 | 331 | first := s[0] 332 | columns, values, placeholderNum, vs := first.ToInsertColumnsAndValuesPg(offset) 333 | var b strings.Builder 334 | if _, err := b.WriteString(values); err != nil { 335 | return "", 0, nil, err 336 | } 337 | for i, _s := range s[1:] { 338 | _columns, _values, _placeholderNum, _vs := _s.ToInsertColumnsAndValuesPg(placeholderNum) 339 | if columns != _columns { 340 | return "", 0, nil, fmt.Errorf("sqlla: two SetMap are not match keys: [0]=%s, [%d]=%s", columns, i, _columns) 341 | } 342 | vs = append(vs, _vs...) 343 | if _, err := b.WriteString(","); err != nil { 344 | return "", 0, nil, err 345 | } 346 | if _, err := b.WriteString(_values); err != nil { 347 | return "", 0, nil, err 348 | } 349 | placeholderNum = _placeholderNum 350 | } 351 | return columns + " VALUES " + b.String(), placeholderNum, vs, nil 352 | } 353 | -------------------------------------------------------------------------------- /column_test.go: -------------------------------------------------------------------------------- 1 | package sqlla_test 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/mackee/go-sqlla/v2" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestColumnNullT(t *testing.T) { 15 | cwd, err := os.Getwd() 16 | t.Cleanup(func() { 17 | require.NoError(t, os.Chdir(cwd)) 18 | }) 19 | 20 | require.NoError(t, err) 21 | fullpath, err := filepath.Abs(filepath.Join("./testdata", "nullt", "repoa", "schema.go")) 22 | require.NoError(t, err) 23 | dir := filepath.Dir(fullpath) 24 | require.NoError(t, os.Chdir(dir)) 25 | 26 | pkgs, err := sqlla.ToPackages(dir) 27 | require.NoError(t, err) 28 | var table *sqlla.Table 29 | for _, pkg := range pkgs { 30 | files := pkg.Syntax 31 | for _, f := range files { 32 | for _, decl := range f.Decls { 33 | _table, err := sqlla.DeclToTable(pkg, decl, fullpath, true) 34 | if errors.Is(err, sqlla.ErrNotTargetDecl) { 35 | continue 36 | } 37 | require.NoError(t, err) 38 | table = _table 39 | } 40 | } 41 | } 42 | require.NotNil(t, table) 43 | assert.False(t, table.Columns[0].IsNullT()) // ID 44 | assert.Equal(t, table.Columns[0].BaseTypeName(), "uint64") 45 | assert.Equal(t, table.Columns[0].ExprValue(), "sqlla.ExprValue[uint64]") 46 | assert.Equal(t, table.Columns[0].ExprMultiValue(), "sqlla.ExprMultiValue[uint64]") 47 | assert.True(t, table.Columns[1].IsNullT()) // ModifiedAt 48 | assert.Equal(t, table.Columns[1].BaseTypeName(), "time.Time") 49 | assert.Equal(t, table.Columns[1].ExprValue(), "sqlla.ExprNull[time.Time]") 50 | assert.Equal(t, table.Columns[1].ExprMultiValue(), "sqlla.ExprMultiValue[time.Time]") 51 | assert.False(t, table.Columns[2].IsNullT()) 52 | assert.Equal(t, table.Columns[2].BaseTypeName(), "NullableTime") 53 | assert.Equal(t, table.Columns[2].ExprValue(), "sqlla.ExprValue[NullableTime]") 54 | assert.Equal(t, table.Columns[2].ExprMultiValue(), "sqlla.ExprMultiValue[NullableTime]") 55 | } 56 | -------------------------------------------------------------------------------- /dialect.go: -------------------------------------------------------------------------------- 1 | package sqlla 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | func NewDialect(dialect string) (Dialect, error) { 9 | switch dialect { 10 | case "mysql", "sqlite": 11 | return MySQLDialect{}, nil 12 | case "postgresql": 13 | return PostgreSQLDialect{}, nil 14 | default: 15 | return nil, fmt.Errorf("unsupported dialect: %s", dialect) 16 | } 17 | } 18 | 19 | type Dialect interface { 20 | Name() string 21 | CQuote() string 22 | CQuoteBy(column string) string 23 | } 24 | 25 | type MySQLDialect struct{} 26 | 27 | func (MySQLDialect) Name() string { 28 | return "mysql" 29 | } 30 | 31 | func (MySQLDialect) CQuote() string { 32 | return strconv.Quote("`") 33 | } 34 | 35 | func (MySQLDialect) CQuoteBy(column string) string { 36 | return strconv.Quote("`" + column + "`") 37 | } 38 | 39 | type PostgreSQLDialect struct{} 40 | 41 | func (PostgreSQLDialect) Name() string { 42 | return "postgresql" 43 | } 44 | 45 | func (PostgreSQLDialect) CQuote() string { 46 | return strconv.Quote(`"`) 47 | } 48 | 49 | func (PostgreSQLDialect) CQuoteBy(column string) string { 50 | return strconv.Quote(`"` + column + `"`) 51 | } 52 | -------------------------------------------------------------------------------- /expr.go: -------------------------------------------------------------------------------- 1 | package sqlla 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "errors" 7 | "strconv" 8 | ) 9 | 10 | type Expr interface { 11 | ToSql() (string, []any, error) 12 | ToSqlPg(offset int) (string, int, []any, error) 13 | } 14 | 15 | type ExprOr []Where 16 | 17 | func (e ExprOr) ToSql() (string, []any, error) { 18 | if len(e) == 0 { 19 | return "", []any{}, nil 20 | } 21 | 22 | b := new(bytes.Buffer) 23 | vs := make([]any, 0, 10) 24 | b.WriteString("(") 25 | for i, w := range e { 26 | if i > 0 { 27 | b.WriteString(" OR ") 28 | } 29 | b.WriteString("(") 30 | q, wvs, err := w.ToSql() 31 | if err != nil { 32 | return "", nil, err 33 | } 34 | b.WriteString(q) 35 | b.WriteString(" )") 36 | vs = append(vs, wvs...) 37 | } 38 | 39 | b.WriteString(")") 40 | 41 | return b.String(), vs, nil 42 | } 43 | 44 | func (e ExprOr) ToSqlPg(offset int) (string, int, []any, error) { 45 | if len(e) == 0 { 46 | return "", offset, []any{}, nil 47 | } 48 | 49 | b := new(bytes.Buffer) 50 | vs := make([]any, 0, 10) 51 | b.WriteString("(") 52 | for i, w := range e { 53 | if i > 0 { 54 | b.WriteString(" OR ") 55 | } 56 | b.WriteString("(") 57 | q, n, wvs, err := w.ToSqlPg(offset) 58 | if err != nil { 59 | return "", 0, nil, err 60 | } 61 | b.WriteString(q) 62 | b.WriteString(" )") 63 | vs = append(vs, wvs...) 64 | offset = n 65 | } 66 | 67 | b.WriteString(")") 68 | 69 | return b.String(), offset, vs, nil 70 | } 71 | 72 | type ExprValue[T any] struct { 73 | Column string 74 | Value T 75 | Op Operators 76 | } 77 | 78 | func (e ExprValue[T]) ToSql() (string, []any, error) { 79 | o := e.Op 80 | var o1 Operator = OpEqual 81 | if len(o) > 0 { 82 | o1, o = o[0], o[1:] 83 | } 84 | o1op, err := o1.ToSql() 85 | if err != nil { 86 | return "", nil, err 87 | } 88 | expr := e.Column + " " + o1op + " ?" 89 | for _, op := range o { 90 | on, err := op.ToSql() 91 | if err != nil { 92 | return "", nil, err 93 | } 94 | expr = "( " + expr + " ) " + on + " ?" 95 | } 96 | return expr, append([]any{e.Value}, o.Values()...), nil 97 | } 98 | 99 | func (e ExprValue[T]) ToSqlPg(offset int) (string, int, []any, error) { 100 | o := e.Op 101 | var o1 Operator = OpEqual 102 | if len(o) > 0 { 103 | o1, o = o[0], o[1:] 104 | } 105 | o1op, err := o1.ToSql() 106 | if err != nil { 107 | return "", 0, nil, err 108 | } 109 | offset++ 110 | expr := e.Column + " " + o1op + " $" + strconv.Itoa(offset) 111 | for _, op := range o { 112 | on, err := op.ToSql() 113 | if err != nil { 114 | return "", 0, nil, err 115 | } 116 | offset++ 117 | expr = "( " + expr + " ) " + on + " $" + strconv.Itoa(offset) 118 | } 119 | return expr, offset, append([]any{e.Value}, o.Values()...), nil 120 | } 121 | 122 | type ExprMultiValue[T any] struct { 123 | Column string 124 | Values []T 125 | Op OperatorMulti 126 | } 127 | 128 | func (e ExprMultiValue[T]) ToSql() (string, []any, error) { 129 | ops, err := e.Op.ToSql() 130 | if err != nil { 131 | return "", nil, err 132 | } 133 | vs := make([]any, 0, len(e.Values)) 134 | for _, v := range e.Values { 135 | vs = append(vs, any(v)) 136 | } 137 | return e.Column + " " + ops, vs, nil 138 | } 139 | 140 | var ErrNotSupportPg = errors.New("not support pg operator") 141 | 142 | func (e ExprMultiValue[T]) ToSqlPg(offset int) (string, int, []any, error) { 143 | ops, num, err := e.Op.ToSqlPg(offset) 144 | if err != nil { 145 | return "", 0, nil, err 146 | } 147 | vs := make([]any, 0, len(e.Values)) 148 | for _, v := range e.Values { 149 | vs = append(vs, any(v)) 150 | } 151 | return e.Column + " " + ops, num, vs, nil 152 | } 153 | 154 | type ExprNull[T any] struct { 155 | Column string 156 | Value sql.Null[T] 157 | Op Operators 158 | } 159 | 160 | func (e ExprNull[T]) ToSql() (string, []any, error) { 161 | var expr string 162 | o := e.Op 163 | vs := make([]any, 0, len(e.Op)) 164 | if !e.Value.Valid { 165 | var o1 Operator = opIsNull 166 | if len(o) > 0 { 167 | if o[0] == OpNot { 168 | o1 = opIsNotNull 169 | } 170 | o = o[1:] 171 | } 172 | ops, err := o1.ToSql() 173 | if err != nil { 174 | return "", nil, err 175 | } 176 | expr = e.Column + " " + ops 177 | } else { 178 | var o1 Operator = OpEqual 179 | if len(e.Op) > 0 { 180 | o1, o = o[0], o[1:] 181 | } 182 | ops, err := o1.ToSql() 183 | if err != nil { 184 | return "", nil, err 185 | } 186 | expr = e.Column + " " + ops + " ?" 187 | vs = append(vs, e.Value.V) 188 | } 189 | for _, op := range o { 190 | on, err := op.ToSql() 191 | if err != nil { 192 | return "", nil, err 193 | } 194 | expr = "( " + expr + " ) " + on + " ?" 195 | } 196 | return expr, append(vs, o.Values()...), nil 197 | } 198 | 199 | func (e ExprNull[T]) ToSqlPg(offset int) (string, int, []any, error) { 200 | var expr string 201 | o := e.Op 202 | vs := make([]any, 0, len(e.Op)) 203 | if !e.Value.Valid { 204 | var o1 Operator = opIsNull 205 | if len(o) > 0 { 206 | if o[0] == OpNot { 207 | o1 = opIsNotNull 208 | } 209 | o = o[1:] 210 | } 211 | ops, err := o1.ToSql() 212 | if err != nil { 213 | return "", 0, nil, err 214 | } 215 | expr = e.Column + " " + ops 216 | } else { 217 | var o1 Operator = OpEqual 218 | if len(e.Op) > 0 { 219 | o1, o = e.Op[0], e.Op[1:] 220 | } 221 | ops, err := o1.ToSql() 222 | if err != nil { 223 | return "", 0, nil, err 224 | } 225 | offset++ 226 | expr = e.Column + " " + ops + " $" + strconv.Itoa(offset) 227 | vs = append(vs, e.Value.V) 228 | } 229 | for _, op := range o { 230 | on, err := op.ToSql() 231 | if err != nil { 232 | return "", 0, nil, err 233 | } 234 | offset++ 235 | expr = "( " + expr + " ) " + on + " $" + strconv.Itoa(offset) 236 | } 237 | return expr, offset, append(vs, o.Values()...), nil 238 | } 239 | -------------------------------------------------------------------------------- /exprmysql.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo.wasm 2 | 3 | package sqlla 4 | 5 | import ( 6 | "github.com/go-sql-driver/mysql" 7 | ) 8 | 9 | type ExprMysqlNullTime struct { 10 | Column string 11 | Value mysql.NullTime 12 | Op Operator 13 | } 14 | 15 | func (e ExprMysqlNullTime) ToSql() (string, []interface{}, error) { 16 | var ops, placeholder string 17 | var err error 18 | vs := []interface{}{} 19 | if !e.Value.Valid { 20 | if e.Op == OpNot { 21 | ops, err = opIsNotNull.ToSql() 22 | } else { 23 | ops, err = opIsNull.ToSql() 24 | } 25 | } else { 26 | ops, err = e.Op.ToSql() 27 | placeholder = " ?" 28 | vs = append(vs, e.Value) 29 | } 30 | if err != nil { 31 | return "", nil, err 32 | } 33 | 34 | return e.Column + " " + ops + placeholder, vs, nil 35 | } 36 | 37 | type ExprMultiMysqlNullTime struct { 38 | Column string 39 | Values []mysql.NullTime 40 | Op Operator 41 | } 42 | 43 | func (e ExprMultiMysqlNullTime) ToSql() (string, []interface{}, error) { 44 | ops, err := e.Op.ToSql() 45 | if err != nil { 46 | return "", nil, err 47 | } 48 | vs := make([]interface{}, 0, len(e.Values)) 49 | for _, v := range e.Values { 50 | vs = append(vs, interface{}(v)) 51 | } 52 | return e.Column + " " + ops, vs, nil 53 | } 54 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo.wasm 2 | 3 | package sqlla 4 | 5 | import ( 6 | "bytes" 7 | "embed" 8 | "fmt" 9 | "go/format" 10 | "io" 11 | "text/template" 12 | 13 | "github.com/Masterminds/goutils" 14 | "github.com/gertd/go-pluralize" 15 | sprig "github.com/go-task/slim-sprig" 16 | "github.com/pkg/errors" 17 | "github.com/serenize/snaker" 18 | "golang.org/x/tools/imports" 19 | ) 20 | 21 | //go:embed template/* template/plugins/* 22 | var templates embed.FS 23 | 24 | //go:embed template/table.tmpl 25 | var tableTmpl []byte 26 | 27 | type Generator struct { 28 | tmpl *template.Template 29 | } 30 | 31 | func NewGenerator(dialect Dialect, additionals ...string) (*Generator, error) { 32 | tmpl := template.New("table") 33 | 34 | fm := sprig.FuncMap() 35 | fm["untitle"] = func(s string) string { 36 | return goutils.Uncapitalize(s) 37 | } 38 | fm["toSnake"] = snaker.CamelToSnake 39 | fm["toCamel"] = snaker.SnakeToCamel 40 | pc := pluralize.NewClient() 41 | fm["pluralize"] = pc.Plural 42 | fm["singular"] = pc.Singular 43 | fm["cquote"] = dialect.CQuote 44 | fm["cquoteby"] = dialect.CQuoteBy 45 | fm["dialect"] = dialect.Name 46 | tmpl = tmpl.Funcs(fm) 47 | 48 | tmpl, err := tmpl.ParseFS(templates, "template/*.tmpl", "template/plugins/*.tmpl") 49 | if err != nil { 50 | return nil, fmt.Errorf("failed to parse template: %w", err) 51 | } 52 | if len(additionals) > 0 { 53 | for _, add := range additionals { 54 | tmpl, err = tmpl.ParseGlob(add) 55 | if err != nil { 56 | return nil, fmt.Errorf("failed to parse additional plugins: path=%s: %w", add, err) 57 | } 58 | } 59 | } 60 | tmpl, err = tmpl.Parse(string(tableTmpl)) 61 | if err != nil { 62 | return nil, fmt.Errorf("failed to parse table template: %w", err) 63 | } 64 | return &Generator{tmpl: tmpl}, nil 65 | } 66 | 67 | func (g *Generator) WriteCode(w io.Writer, table *Table) error { 68 | buf := &bytes.Buffer{} 69 | if err := g.tmpl.Execute(buf, table); err != nil { 70 | return errors.Wrapf(err, "fail to render") 71 | } 72 | bs, err := format.Source(buf.Bytes()) 73 | if err != nil { 74 | if _, err := w.Write(buf.Bytes()); err != nil { 75 | return errors.Wrapf(err, "fail to write: table=%s", table.Name) 76 | } 77 | return errors.Wrapf(err, "fail to format: table=%s", table.Name) 78 | } 79 | if _, err := w.Write(bs); err != nil { 80 | return errors.Wrapf(err, "fail to write: table=%s", table.Name) 81 | } 82 | return nil 83 | } 84 | 85 | func (g *Generator) WriteCodeByPlugin(w io.Writer, tmplName string, p *Plugin) error { 86 | ptmpl := g.tmpl.Lookup(fmt.Sprintf("plugin.%s", tmplName)) 87 | if ptmpl == nil { 88 | return fmt.Errorf("template not found: template=%s", tmplName) 89 | } 90 | if err := ptmpl.Execute(w, p); err != nil { 91 | return fmt.Errorf("fail to render: %w", err) 92 | } 93 | return nil 94 | } 95 | 96 | func (g *Generator) Format(input []byte, filename string) ([]byte, error) { 97 | out, err := imports.Process(filename, input, nil) 98 | if err != nil { 99 | return nil, fmt.Errorf("fail to format: %w", err) 100 | } 101 | 102 | return out, nil 103 | } 104 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mackee/go-sqlla/v2 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.4 6 | 7 | require ( 8 | github.com/Masterminds/goutils v1.1.1 9 | github.com/alecthomas/kong v1.6.1 10 | github.com/gertd/go-pluralize v0.2.1 11 | github.com/go-sql-driver/mysql v1.6.0 12 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 13 | github.com/pkg/errors v0.9.1 14 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e 15 | github.com/stretchr/testify v1.9.0 16 | golang.org/x/tools v0.25.0 17 | ) 18 | 19 | require ( 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/onsi/ginkgo v1.16.5 // indirect 22 | github.com/onsi/gomega v1.17.0 // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | golang.org/x/mod v0.21.0 // indirect 25 | golang.org/x/sync v0.8.0 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 2 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 3 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 4 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 5 | github.com/alecthomas/kong v1.6.1 h1:/7bVimARU3uxPD0hbryPE8qWrS3Oz3kPQoxA/H2NKG8= 6 | github.com/alecthomas/kong v1.6.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 7 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 8 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 13 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 14 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 15 | github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= 16 | github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= 17 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 18 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 19 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= 20 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 21 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 22 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 23 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 24 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 25 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 26 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 27 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 28 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 29 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 30 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 31 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 32 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 33 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 34 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 35 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 36 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 37 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 38 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 39 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 40 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 41 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 42 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 43 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 44 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 45 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 46 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 47 | github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= 48 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 49 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 50 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE= 54 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= 55 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 56 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 57 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 58 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 59 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 60 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 61 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 62 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 63 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 64 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 65 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 66 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 67 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 68 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 69 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 70 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 71 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 72 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 73 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 74 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 75 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 76 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 77 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 78 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 79 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 80 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 81 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 84 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 86 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 87 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 91 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 92 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 93 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 94 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 95 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 96 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 97 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 98 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 99 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 100 | golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= 101 | golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= 102 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 103 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 104 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 105 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 106 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 107 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 108 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 109 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 110 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 111 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 112 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 113 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 114 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 115 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 116 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 117 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 118 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 119 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 120 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 121 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 122 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 123 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 124 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 125 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 126 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package sqlla 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | // DB is interface like *database/sql.DB 9 | type DB interface { 10 | QueryRow(string, ...interface{}) *sql.Row 11 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 12 | 13 | Query(string, ...interface{}) (*sql.Rows, error) 14 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 15 | 16 | Exec(string, ...interface{}) (sql.Result, error) 17 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 18 | } 19 | 20 | // Scanner is interface like *database/sql.Row 21 | type Scanner interface { 22 | Scan(...interface{}) error 23 | } 24 | 25 | // RowAffected is result of upsert 26 | type RowAffected int64 27 | 28 | // RowAffected results 29 | const ( 30 | RowAffectedNoupdated RowAffected = iota 31 | RowAffectedInserted 32 | RowAffectedUpdated 33 | ) 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo.wasm 2 | 3 | package sqlla 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "fmt" 9 | "go/ast" 10 | "go/types" 11 | "os" 12 | "path/filepath" 13 | "reflect" 14 | "strings" 15 | 16 | "golang.org/x/tools/go/packages" 17 | ) 18 | 19 | type Options struct { 20 | Version bool `help:"show this version"` 21 | From []string `help:"source file path" env:"GOFILE" arg:"" required:""` 22 | DirAll bool `help:"generate all files in the directory" env:"SQLLA_GENERATE_DIR_ALL" default:"false"` 23 | Ext string `help:"file extension" env:"SQLLA_GENERATE_FILE_EXT" default:".gen.go"` 24 | Plugins []string `help:"additional plugin files. allow asterisk(*) and multiple values" name:"plugins" env:"SQLLA_TEMPLATE_FILES"` 25 | Dialect string `help:"SQL Dialect" env:"SQLLA_DIALECT" enum:"mysql,sqlite,postgresql" default:"mysql"` 26 | } 27 | 28 | func Run(opts Options) error { 29 | dialect, err := NewDialect(opts.Dialect) 30 | if err != nil { 31 | return fmt.Errorf("failed to NewDialect: %w", err) 32 | } 33 | g, err := NewGenerator(dialect, opts.Plugins...) 34 | if err != nil { 35 | return fmt.Errorf("failed to NewGenerator: %w", err) 36 | } 37 | 38 | for _, from := range opts.From { 39 | if err := run(from, opts.Ext, opts.DirAll, g); err != nil { 40 | return fmt.Errorf("failed to run: %w", err) 41 | } 42 | } 43 | return nil 44 | } 45 | 46 | func run(from string, ext string, dirAll bool, g *Generator) error { 47 | fullpath, err := filepath.Abs(from) 48 | if err != nil { 49 | return fmt.Errorf("failed to filepath.Abs: %w", err) 50 | } 51 | dir := filepath.Dir(fullpath) 52 | 53 | pkgs, err := toPackages(dir) 54 | if err != nil { 55 | return fmt.Errorf("failed to toPackages: %w", err) 56 | } 57 | for _, pkg := range pkgs { 58 | files := pkg.Syntax 59 | for _, f := range files { 60 | for _, decl := range f.Decls { 61 | table, err := declToTable(pkg, decl, fullpath, dirAll) 62 | if err != nil { 63 | if errors.Is(err, errNotTargetDecl) { 64 | continue 65 | } 66 | return fmt.Errorf("error declToTable: %w", err) 67 | } 68 | filename := filepath.Join(dir, table.TableName+ext) 69 | bs := &bytes.Buffer{} 70 | if err := g.WriteCode(bs, table); err != nil { 71 | return fmt.Errorf("error WriteCode: filename=%s: %w", filename, err) 72 | } 73 | formatted, err := g.Format(bs.Bytes(), filename) 74 | if err != nil { 75 | return fmt.Errorf("error Format: filename=%s: %w", filename, err) 76 | } 77 | f, err := os.Create(filename) 78 | if err != nil { 79 | return fmt.Errorf("error create: filename=%s: %w", filename, err) 80 | } 81 | if _, err := f.Write(formatted); err != nil { 82 | return fmt.Errorf("error WriteTo: filename=%s: %w", filename, err) 83 | } 84 | if err := f.Close(); err != nil { 85 | return fmt.Errorf("error close: filename=%s: %w", filename, err) 86 | } 87 | if err := table.Plugins.WriteCode(g, table.PackageName); err != nil { 88 | return fmt.Errorf("error Plugins.WriteCode: %w", err) 89 | } 90 | } 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | func toPackages(dir string) ([]*packages.Package, error) { 97 | conf := &packages.Config{ 98 | Mode: packages.NeedCompiledGoFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo, 99 | } 100 | pkgs, err := packages.Load(conf, dir) 101 | if err != nil { 102 | return nil, fmt.Errorf("error toPackages: %w", err) 103 | } 104 | return pkgs, nil 105 | } 106 | 107 | var errNotTargetDecl = fmt.Errorf("not target decl") 108 | 109 | func declToTable(pkg *packages.Package, decl ast.Decl, fullpath string, dirAll bool) (*Table, error) { 110 | pos := pkg.Fset.Position(decl.Pos()) 111 | if !dirAll && pos.Filename != fullpath { 112 | return nil, errNotTargetDecl 113 | } 114 | genDecl, ok := decl.(*ast.GenDecl) 115 | if !ok { 116 | return nil, errNotTargetDecl 117 | } 118 | if genDecl.Doc == nil { 119 | return nil, errNotTargetDecl 120 | } 121 | var hasAnnotation bool 122 | var annotationComment string 123 | for _, comment := range genDecl.Doc.List { 124 | if trimmed := trimAnnotation(comment.Text); trimmed != comment.Text { 125 | hasAnnotation = true 126 | annotationComment = comment.Text 127 | } 128 | } 129 | if !hasAnnotation { 130 | return nil, errNotTargetDecl 131 | } 132 | table, err := toTable(pkg.Types, annotationComment, genDecl, pkg.TypesInfo) 133 | if err != nil { 134 | return nil, fmt.Errorf("error toTable: %w", err) 135 | } 136 | return table, nil 137 | } 138 | 139 | func toTable(tablePkg *types.Package, annotationComment string, gd *ast.GenDecl, ti *types.Info) (*Table, error) { 140 | table := new(Table) 141 | table.Package = tablePkg 142 | table.PackageName = tablePkg.Name() 143 | table.additionalPackagesMap = make(map[string]struct{}) 144 | 145 | table.TableName = trimAnnotation(annotationComment) 146 | qualifier := types.RelativeTo(tablePkg) 147 | 148 | spec := gd.Specs[0] 149 | ts, ok := spec.(*ast.TypeSpec) 150 | if !ok { 151 | return nil, fmt.Errorf("toTable: not type spec: table=%s", table.TableName) 152 | } 153 | structType, ok := ts.Type.(*ast.StructType) 154 | if !ok { 155 | return nil, fmt.Errorf("toTable: not struct type: table=%s", table.TableName) 156 | } 157 | table.StructName = ts.Name.Name 158 | 159 | if isV2Annotation(annotationComment) { 160 | table.Name = table.StructName 161 | } else { 162 | table.Name = table.TableName 163 | } 164 | comments := make([]string, 0, len(gd.Doc.List)) 165 | for _, comment := range gd.Doc.List { 166 | comments = append(comments, comment.Text) 167 | } 168 | plugins, err := parsePluginsByComments(comments) 169 | if err != nil { 170 | return nil, fmt.Errorf("toTable: error parsePluginsByComments: table=%s, err=%w", table.TableName, err) 171 | } 172 | table.SetPlugins(plugins) 173 | 174 | for _, field := range structType.Fields.List { 175 | tagText := field.Tag.Value[1 : len(field.Tag.Value)-1] 176 | tag := reflect.StructTag(tagText) 177 | columnInfo := tag.Get("db") 178 | columnMaps := strings.Split(columnInfo, ",") 179 | columnName := columnMaps[0] 180 | isPk := false 181 | for _, cm := range columnMaps { 182 | if cm == "primarykey" { 183 | isPk = true 184 | break 185 | } 186 | } 187 | t := ti.TypeOf(field.Type) 188 | var typeName, pkgName string 189 | var isNull bool 190 | baseTypeName := t.String() 191 | nt, ok := t.(*types.Named) 192 | if ok { 193 | pkgName = nt.Obj().Pkg().Path() 194 | if tablePkg.Path() != pkgName { 195 | typeName = strings.Join([]string{nt.Obj().Pkg().Name(), nt.Obj().Name()}, ".") 196 | } else { 197 | typeName = nt.Obj().Name() 198 | } 199 | baseTypeName = typeName 200 | var hasBasicTypeParam bool 201 | if typeName == "sql.Null" { 202 | isNull = true 203 | tas := nt.TypeArgs() 204 | if tas == nil { 205 | return nil, fmt.Errorf("toTable: has not type params: table=%s, field=%s", table.TableName, columnName) 206 | } 207 | tpsStr := make([]string, tas.Len()) 208 | var tnt *types.Named 209 | for i := 0; i < tas.Len(); i++ { 210 | ta := tas.At(i) 211 | switch ata := ta.(type) { 212 | case *types.Named: 213 | tn := ata.Obj() 214 | tpsStr[i] = tn.Id() 215 | if qualifier(tn.Pkg()) != "" { 216 | tpsStr[i] = tn.Pkg().Name() + "." + tn.Id() 217 | table.additionalPackagesMap[tn.Pkg().Path()] = struct{}{} 218 | } 219 | if tnt == nil { 220 | tnt = ata 221 | typeName = tpsStr[i] 222 | } 223 | case *types.Basic: 224 | tpsStr[i] = ata.Name() 225 | typeName = tpsStr[i] 226 | baseTypeName = typeName 227 | hasBasicTypeParam = true 228 | default: 229 | return nil, fmt.Errorf("toTable: unsupported type param: table=%s, field=%s, type=%s", table.TableName, columnName, ta.String()) 230 | } 231 | } 232 | if tnt != nil { 233 | nt = tnt 234 | baseTypeName = typeName 235 | } 236 | } 237 | if !hasBasicTypeParam { 238 | var rt types.Type = nt 239 | bt := nt.Underlying() 240 | for { 241 | switch btt := bt.(type) { 242 | case *types.Named: 243 | rt = btt 244 | bt = btt.Underlying() 245 | continue 246 | case *types.Basic: 247 | rt = btt 248 | } 249 | break 250 | } 251 | if rt != nt { 252 | baseTypeName = rt.String() 253 | } 254 | } 255 | } else { 256 | typeName = t.String() 257 | } 258 | column := Column{ 259 | Field: field, 260 | Name: columnName, 261 | IsPk: isPk, 262 | typeName: typeName, 263 | baseTypeName: baseTypeName, 264 | PkgName: pkgName, 265 | isNullT: isNull, 266 | } 267 | table.AddColumn(column) 268 | } 269 | 270 | return table, nil 271 | } 272 | 273 | func trimAnnotation(comment string) string { 274 | prefixes := []string{"//+table: ", "// +table: ", "//sqlla:table "} 275 | for _, prefix := range prefixes { 276 | if trimmed := strings.TrimPrefix(comment, prefix); trimmed != comment { 277 | return trimmed 278 | } 279 | } 280 | return comment 281 | } 282 | 283 | func isV2Annotation(comment string) bool { 284 | return strings.HasPrefix(comment, "//sqlla:table ") 285 | } 286 | -------------------------------------------------------------------------------- /main_priv.go: -------------------------------------------------------------------------------- 1 | package sqlla 2 | 3 | var ( 4 | ToPackages = toPackages 5 | DeclToTable = declToTable 6 | ErrNotTargetDecl = errNotTargetDecl 7 | ) 8 | -------------------------------------------------------------------------------- /operator.go: -------------------------------------------------------------------------------- 1 | package sqlla 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | OpEqual OperatorBinary = "=" // Operator for equal. Column(value, sqlla.OpEqual) same as Column = value 10 | OpGreater OperatorBinary = ">" // Operator for greater. Column(value, sqlla.OpGreater) same as Column > value 11 | OpGreaterEqual OperatorBinary = ">=" // Operator for greater equal. Column(value, sqlla.OpGreaterEqual) same as Column >= value 12 | OpLess OperatorBinary = "<" // Operator for less. Column(value, sqlla.OpLess) same as Column < value 13 | OpLessEqual OperatorBinary = "<=" // Operator for less equal. Column(value, sqlla.OpLessEqual) same as Column <= value 14 | OpNot OperatorBinary = "<>" // Operator for not equal. Column(value, sqlla.OpNot) same as Column <> value 15 | OpIs OperatorBinary = "IS" // Operator for is. Column(value, sqlla.OpIs) same as Column IS value 16 | opIsNull OperatorBinary = "IS NULL" 17 | opIsNotNull OperatorBinary = "IS NOT NULL" 18 | OpLike OperatorBinary = "LIKE" // Operator for like. Column(value, sqlla.OpLike) same as Column LIKE value 19 | OpPgvectorL2 OperatorBinary = "<->" // Operator for pgvector l2 distance. Column(value, sqlla.OpPgvectorL2) same as Column <-> value 20 | OpPgvectorNegativeInnerProduct OperatorBinary = "<#>" // Operator for pgvector negative inner product. Column(value, sqlla.OpPgvectorNegativeInnerProduct) same as Column <#> value 21 | OpPgvectorCosine OperatorBinary = "<=>" // Operator for pgvector cosine distance. Column(value, sqlla.OpPgvectorCosineDistance) same as Column <=> value 22 | OpPgvectorL1 OperatorBinary = "<+>" // Operator for pgvector l1 distance. Column(value, sqlla.OpPgvectorL1) same as Column <+> value 23 | OpPgvectorHamming OperatorBinary = "<~>" // Operator for pgvector hamming distance. Column(value, sqlla.OpPgvectorHamming) same as Column <~> value 24 | OpPgvectorJaccard OperatorBinary = "<%>" // Operator for pgvector jaccard distance. Column(value, sqlla.OpPgvectorJaccard) same as Column <%> value 25 | 26 | ) 27 | 28 | type OperatorBinary string 29 | 30 | func (op OperatorBinary) ToSql() (string, error) { 31 | return string(op), nil 32 | } 33 | 34 | type Operator interface { 35 | ToSql() (string, error) 36 | } 37 | 38 | type OperatorMulti interface { 39 | Operator 40 | ToSqlPg(offset int) (string, int, error) 41 | } 42 | 43 | type OperatorIn struct { 44 | num int 45 | } 46 | 47 | func (o *OperatorIn) String() string { 48 | return "IN(?" + strings.Repeat(",?", o.num-1) + ")" 49 | } 50 | 51 | func (o *OperatorIn) ToSql() (string, error) { 52 | return o.String(), nil 53 | } 54 | 55 | func (o *OperatorIn) ToSqlPg(offset int) (string, int, error) { 56 | b := &strings.Builder{} 57 | b.WriteString("IN(") 58 | for i := range o.num { 59 | if i > 0 { 60 | b.WriteString(",") 61 | } 62 | b.WriteString("$" + strconv.Itoa(offset+i+1)) 63 | } 64 | b.WriteString(")") 65 | return b.String(), offset + o.num, nil 66 | } 67 | 68 | func MakeInOperator(n int) *OperatorIn { 69 | return &OperatorIn{num: n} 70 | } 71 | 72 | type operatorAndValue struct { 73 | Op OperatorBinary 74 | Value any 75 | } 76 | 77 | func NewOperatorAndValue(op OperatorBinary, value any) OperatorAndValues { 78 | return &operatorAndValue{Op: op, Value: value} 79 | } 80 | 81 | type OperatorAndValues interface { 82 | ToSql() (string, error) 83 | Values() []any 84 | } 85 | 86 | func (o *operatorAndValue) ToSql() (string, error) { 87 | return o.Op.ToSql() 88 | } 89 | 90 | func (o *operatorAndValue) Values() []any { 91 | return []any{o.Value} 92 | } 93 | 94 | type Operators []Operator 95 | 96 | func (o Operators) ToSql() (string, error) { 97 | if len(o) == 0 { 98 | return OpEqual.ToSql() 99 | } 100 | return o[0].ToSql() 101 | } 102 | 103 | func (o Operators) Values() []any { 104 | vs := make([]any, 0, len(o)) 105 | for _, op := range o { 106 | oav, ok := op.(OperatorAndValues) 107 | if !ok { 108 | continue 109 | } 110 | vs = append(vs, oav.Values()...) 111 | } 112 | return vs 113 | } 114 | -------------------------------------------------------------------------------- /opts.go: -------------------------------------------------------------------------------- 1 | package sqlla 2 | 3 | import "strconv" 4 | 5 | const ( 6 | Asc OrderSimple = "ASC" 7 | Desc OrderSimple = "DESC" 8 | ) 9 | 10 | type Order interface { 11 | WithColumn(columnName string) OrderWithColumn 12 | } 13 | 14 | type OrderWithColumn interface { 15 | OrderExpr() string 16 | OrderExprPg(offset int) (string, int) 17 | Values() []any 18 | } 19 | 20 | type OrderSimple string 21 | 22 | type orderSimpleWithColumn struct { 23 | order OrderSimple 24 | columnName string 25 | } 26 | 27 | func (o OrderSimple) WithColumn(columnName string) OrderWithColumn { 28 | return &orderSimpleWithColumn{order: o, columnName: columnName} 29 | } 30 | 31 | func (o *orderSimpleWithColumn) OrderExpr() string { 32 | return o.columnName + " " + string(o.order) 33 | } 34 | 35 | func (o *orderSimpleWithColumn) OrderExprPg(offset int) (string, int) { 36 | return o.columnName + " " + string(o.order), offset 37 | } 38 | 39 | func (o *orderSimpleWithColumn) Values() []any { 40 | return nil 41 | } 42 | 43 | type OrderWithOperator struct { 44 | op OperatorBinary 45 | value any 46 | order OrderSimple 47 | } 48 | 49 | func NewOrderWithOperator(op OperatorBinary, value any, order OrderSimple) *OrderWithOperator { 50 | return &OrderWithOperator{op: op, value: value, order: order} 51 | } 52 | 53 | func (o *OrderWithOperator) WithColumn(columnName string) OrderWithColumn { 54 | return &orderWithOperatorAndColumn{owo: o, columnName: columnName} 55 | } 56 | 57 | type orderWithOperatorAndColumn struct { 58 | owo *OrderWithOperator 59 | columnName string 60 | } 61 | 62 | func (o *orderWithOperatorAndColumn) OrderExpr() string { 63 | return o.columnName + " " + string(o.owo.op) + " ? " + string(o.owo.order) 64 | } 65 | 66 | func (o *orderWithOperatorAndColumn) OrderExprPg(offset int) (string, int) { 67 | return o.columnName + " " + string(o.owo.op) + " $" + strconv.Itoa(offset+1) + " " + string(o.owo.order), offset + 1 68 | } 69 | 70 | func (o *orderWithOperatorAndColumn) Values() []any { 71 | return []any{o.owo.value} 72 | } 73 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package sqlla 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | type Plugin struct { 13 | Name string 14 | Args map[string]string 15 | Table *Table 16 | } 17 | 18 | func (p *Plugin) outpath() string { 19 | if outpath, ok := p.Args["outpath"]; ok { 20 | return outpath 21 | } 22 | return fmt.Sprintf("%s.plugin.gen.go", p.Table.TableName) 23 | } 24 | 25 | func (p *Plugin) WriteCode(g *Generator) error { 26 | return nil 27 | } 28 | 29 | type Plugins []*Plugin 30 | 31 | func (p Plugins) WriteCode(g *Generator, packageName string) error { 32 | files := make(map[string]*bytes.Buffer, len(p)) 33 | lookupOrCreateFile := func(outpath string) *bytes.Buffer { 34 | if buf, ok := files[outpath]; ok { 35 | return buf 36 | } 37 | buf := &bytes.Buffer{} 38 | buf.WriteString("// Code generated by github.com/mackee/go-sqlla/v2/cmd/sqlla. DO NOT EDIT.\n") 39 | fmt.Fprintf(buf, "package %s\n\n", packageName) 40 | files[outpath] = buf 41 | return buf 42 | } 43 | 44 | for _, plugin := range p { 45 | w := lookupOrCreateFile(plugin.outpath()) 46 | if err := g.WriteCodeByPlugin(w, plugin.Name, plugin); err != nil { 47 | return fmt.Errorf("fail to write: plugin=%s: %w", plugin.Name, err) 48 | } 49 | } 50 | for outpath, buf := range files { 51 | out, err := g.Format(buf.Bytes(), outpath) 52 | if err != nil { 53 | slog.Error("fail to format", slog.String("outpath", outpath), slog.Any("error", err)) 54 | out = buf.Bytes() 55 | } 56 | f, err := os.Create(outpath) 57 | if err != nil { 58 | return fmt.Errorf("fail to create file: outpath=%s: %w", outpath, err) 59 | } 60 | defer f.Close() 61 | if _, err := f.Write(out); err != nil { 62 | return fmt.Errorf("fail to write file: outpath=%s: %w", outpath, err) 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | var errThisCommentIsNotPlugin = errors.New("this comment is not plugin") 69 | 70 | func parsePluginsByComments(comments []string) (Plugins, error) { 71 | plugins := make(Plugins, 0, len(comments)) 72 | for _, comment := range comments { 73 | plugin, err := parsePluginByComment(comment) 74 | if errors.Is(err, errThisCommentIsNotPlugin) { 75 | continue 76 | } 77 | if err != nil { 78 | return nil, fmt.Errorf("fail to parse plugin: args=%s %w", comment, err) 79 | } 80 | plugins = append(plugins, plugin) 81 | } 82 | return plugins, nil 83 | } 84 | 85 | func parsePluginByComment(comment string) (*Plugin, error) { 86 | pluginStr := strings.TrimPrefix(comment, "//sqlla:plugin ") 87 | if pluginStr == comment { 88 | return nil, errThisCommentIsNotPlugin 89 | } 90 | nameArgs := strings.Split(pluginStr, " ") 91 | if len(nameArgs) == 0 { 92 | return nil, errors.New("plugin name is not specified") 93 | } 94 | name := nameArgs[0] 95 | args := make(map[string]string, len(nameArgs)-1) 96 | for _, arg := range nameArgs[1:] { 97 | kv := strings.Split(arg, "=") 98 | if len(kv) != 2 { 99 | return nil, errors.New("invalid argument") 100 | } 101 | args[kv[0]] = kv[1] 102 | } 103 | 104 | return &Plugin{ 105 | Name: name, 106 | Args: args, 107 | }, nil 108 | } 109 | -------------------------------------------------------------------------------- /plugin_priv_test.go: -------------------------------------------------------------------------------- 1 | package sqlla 2 | 3 | var ParsePluginsByComments = parsePluginsByComments 4 | -------------------------------------------------------------------------------- /plugin_test.go: -------------------------------------------------------------------------------- 1 | package sqlla_test 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/mackee/go-sqlla/v2" 10 | ) 11 | 12 | func Test_parsePluginsByComments(t *testing.T) { 13 | tc := []struct { 14 | name string 15 | comments []string 16 | want sqlla.Plugins 17 | }{ 18 | { 19 | name: "basic", 20 | comments: []string{ 21 | "//sqlla:plugin count outpath=user_count.gen.go", 22 | }, 23 | want: sqlla.Plugins{ 24 | { 25 | Name: "count", 26 | Args: map[string]string{ 27 | "outpath": "user_count.gen.go", 28 | }, 29 | }, 30 | }, 31 | }, 32 | { 33 | name: "multiargs", 34 | comments: []string{ 35 | "//sqlla:plugin table outpath=user_table.gen.go getcolumns=id,name listincolumns=id,name listcolumns=age", 36 | }, 37 | want: sqlla.Plugins{ 38 | { 39 | Name: "table", 40 | Args: map[string]string{ 41 | "outpath": "user_table.gen.go", 42 | "getcolumns": "id,name", 43 | "listincolumns": "id,name", 44 | "listcolumns": "age", 45 | }, 46 | }, 47 | }, 48 | }, 49 | } 50 | prettyPrint := func(v any) string { 51 | bs := &strings.Builder{} 52 | enc := json.NewEncoder(bs) 53 | enc.SetIndent("", " ") 54 | enc.Encode(v) 55 | return bs.String() 56 | } 57 | for _, tt := range tc { 58 | t.Run(tt.name, func(t *testing.T) { 59 | got, err := sqlla.ParsePluginsByComments(tt.comments) 60 | if err != nil { 61 | t.Errorf("parsePlguinByComments() error = %v", err) 62 | return 63 | } 64 | if !reflect.DeepEqual(got, tt.want) { 65 | t.Errorf("parsePlguinByComments() = %s, want %s", prettyPrint(got), prettyPrint(tt.want)) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /table.go: -------------------------------------------------------------------------------- 1 | //go:build !tinygo.wasm 2 | 3 | package sqlla 4 | 5 | import ( 6 | "go/types" 7 | 8 | "github.com/Masterminds/goutils" 9 | "github.com/serenize/snaker" 10 | ) 11 | 12 | type Table struct { 13 | Package *types.Package 14 | PackageName string 15 | Name string 16 | StructName string 17 | TableName string 18 | Columns Columns 19 | PkColumn *Column 20 | additionalPackagesMap map[string]struct{} 21 | Plugins Plugins 22 | } 23 | 24 | func (t *Table) NamingIsStructName() bool { 25 | return t.Name == t.StructName 26 | } 27 | 28 | func (t *Table) AddColumn(c Column) { 29 | if t.additionalPackagesMap == nil { 30 | t.additionalPackagesMap = make(map[string]struct{}) 31 | } 32 | c.TableName = t.Name 33 | if t.NamingIsStructName() { 34 | c.MethodName = c.FieldName() 35 | } else { 36 | c.MethodName = goutils.Capitalize(snaker.SnakeToCamel(c.Name)) 37 | } 38 | if c.IsPk { 39 | t.PkColumn = &c 40 | } 41 | if c.PkgName != "" { 42 | if t.Package.Path() != c.PkgName { 43 | t.additionalPackagesMap[c.PkgName] = struct{}{} 44 | } 45 | } 46 | t.Columns = append(t.Columns, c) 47 | } 48 | 49 | func (t *Table) AdditionalPackages() []string { 50 | packages := make([]string, 0, len(t.additionalPackagesMap)) 51 | for pkg := range t.additionalPackagesMap { 52 | packages = append(packages, pkg) 53 | } 54 | return packages 55 | } 56 | 57 | func (t *Table) HasPk() bool { 58 | return t.PkColumn != nil 59 | } 60 | 61 | func (t *Table) SetPlugins(plugins Plugins) { 62 | for i := range plugins { 63 | plugins[i].Table = t 64 | } 65 | t.Plugins = plugins 66 | } 67 | 68 | func (t *Table) Lookup(columnFieldName string) *Column { 69 | for _, column := range t.Columns { 70 | if column.FieldName() == columnFieldName { 71 | return &column 72 | } 73 | } 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /template/delete.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "Delete" }} 2 | {{- $camelName := .Name | toCamel | untitle -}} 3 | {{- $constructor := printf "New%sSQL" (.Name | toCamel | title) -}} 4 | type {{ $camelName }}DeleteSQL struct { 5 | {{ $camelName }}SQL 6 | } 7 | 8 | func (q {{ $camelName }}SQL) Delete() {{ $camelName }}DeleteSQL { 9 | return {{ $camelName }}DeleteSQL{ 10 | q, 11 | } 12 | } 13 | 14 | {{ range .Columns }}{{ template "DeleteColumn" . }}{{ end }} 15 | func (q {{ $camelName }}DeleteSQL) ToSql() (string, []interface{}, error) { 16 | {{- if eq (dialect) "mysql" }} 17 | wheres, vs, err := q.where.ToSql() 18 | {{- end }} 19 | {{- if eq (dialect) "postgresql" }} 20 | wheres, _, vs, err := q.where.ToSqlPg(0) 21 | {{- end }} 22 | if err != nil { 23 | return "", nil, err 24 | } 25 | 26 | query := "DELETE FROM " + {{ cquoteby .TableName }} 27 | if wheres != "" { 28 | query += " WHERE" + wheres 29 | } 30 | 31 | return query + ";", vs, nil 32 | } 33 | 34 | func ( q {{ $camelName }}DeleteSQL) Exec(db sqlla.DB) (sql.Result, error) { 35 | query, args, err := q.ToSql() 36 | if err != nil { 37 | return nil, err 38 | } 39 | return db.Exec(query, args...) 40 | } 41 | 42 | func ( q {{ $camelName }}DeleteSQL) ExecContext(ctx context.Context, db sqlla.DB) (sql.Result, error) { 43 | query, args, err := q.ToSql() 44 | if err != nil { 45 | return nil, err 46 | } 47 | return db.ExecContext(ctx, query, args...) 48 | } 49 | 50 | {{- if .HasPk }} 51 | func (s {{ .StructName }}) Delete(db sqlla.DB) (sql.Result, error) { 52 | query, args, err := {{ $constructor }}().Delete().{{ .PkColumn.Name | toCamel | title }}(s.{{ .PkColumn.FieldName }}).ToSql() 53 | if err != nil { 54 | return nil, err 55 | } 56 | return db.Exec(query, args...) 57 | } 58 | 59 | func (s {{ .StructName }}) DeleteContext(ctx context.Context, db sqlla.DB) (sql.Result, error) { 60 | query, args, err := {{ $constructor }}().Delete().{{ .PkColumn.Name | toCamel | title }}(s.{{ .PkColumn.FieldName }}).ToSql() 61 | if err != nil { 62 | return nil, err 63 | } 64 | return db.ExecContext(ctx, query, args...) 65 | } 66 | {{- end }} 67 | {{ end }} 68 | -------------------------------------------------------------------------------- /template/delete_column.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "DeleteColumn" }}{{ $smallTableName := .TableName | toCamel | untitle }} 2 | {{- if .IsNullT }} 3 | {{- template "DeleteColumnNullT" . }} 4 | {{- else }} 5 | func (q {{ $smallTableName }}DeleteSQL) {{ .MethodName }}(v {{ .TypeName }}, exprs ...sqlla.Operator) {{ $smallTableName }}DeleteSQL { 6 | where := {{ .ExprValue }}{Value: {{ .ExprValueIdentifier }}, Op: sqlla.Operators(exprs), Column: {{ cquoteby .Name }}} 7 | q.where = append(q.where, where) 8 | return q 9 | } 10 | 11 | 12 | func (q {{ $smallTableName }}DeleteSQL) {{ .MethodName }}In(vs ...{{ .TypeName }}) {{ $smallTableName }}DeleteSQL { 13 | {{- if .HasUnderlyingType }} 14 | _vs := make([]{{ .BaseTypeName }}, 0, len(vs)) 15 | for _, v := range vs { 16 | _vs = append(_vs, {{ .ExprValueIdentifier }}) 17 | } 18 | where := {{ .ExprMultiValue }}{Values: _vs, Op: sqlla.MakeInOperator(len(vs)), Column: {{ cquoteby .Name }}} 19 | {{- else }} 20 | where := {{ .ExprMultiValue }}{Values: vs, Op: sqlla.MakeInOperator(len(vs)), Column: {{ cquoteby .Name }}} 21 | {{- end }} 22 | q.where = append(q.where, where) 23 | return q 24 | } 25 | {{- end }} 26 | {{ end }} 27 | -------------------------------------------------------------------------------- /template/delete_column_nullt.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "DeleteColumnNullT" }} 2 | {{ $smallTableName := .TableName | toCamel | untitle }} 3 | func (q {{ $smallTableName }}DeleteSQL) {{ .MethodName }}(v {{ .TypeName }}, exprs ...sqlla.Operator) {{ $smallTableName }}DeleteSQL { 4 | where := {{ .ExprValue }}{Value: sql.Null[{{ .BaseTypeName }}]{ V: {{ .ExprValueIdentifier }}, Valid: true }, Op: sqlla.Operators(exprs), Column: {{ cquoteby .Name }}} 5 | q.where = append(q.where, where) 6 | return q 7 | } 8 | 9 | func (q {{ $smallTableName }}DeleteSQL) {{ .MethodName }}IsNull() {{ $smallTableName }}DeleteSQL { 10 | where := {{ .ExprValue }}{Value: sql.Null[{{ .BaseTypeName }}]{ Valid: false }, Op: sqlla.Operators{sqlla.OpEqual}, Column: {{ cquoteby .Name }}} 11 | q.where = append(q.where, where) 12 | return q 13 | } 14 | 15 | func (q {{ $smallTableName }}DeleteSQL) {{ .MethodName }}IsNotNull() {{ $smallTableName }}DeleteSQL { 16 | where := {{ .ExprValue }}{Value: sql.Null[{{ .BaseTypeName }}]{ Valid: false }, Op: sqlla.Operators{sqlla.OpNot}, Column: {{ cquoteby .Name }}} 17 | q.where = append(q.where, where) 18 | return q 19 | } 20 | 21 | func (q {{ $smallTableName }}DeleteSQL) {{ .MethodName }}In(vs ...{{ .TypeName }}) {{ $smallTableName }}DeleteSQL { 22 | _vs := make([]{{ .BaseTypeName }}, 0, len(vs)) 23 | for _, v := range vs { 24 | _vs = append(_vs, {{ .ExprValueIdentifier }}) 25 | } 26 | where := {{ .ExprMultiValue }}{Values: _vs, Op: sqlla.MakeInOperator(len(vs)), Column: {{ cquoteby .Name }}} 27 | q.where = append(q.where, where) 28 | return q 29 | } 30 | {{- end }} 31 | -------------------------------------------------------------------------------- /template/insert.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "Insert" }} 2 | {{- $camelName := .Name | toCamel | untitle -}} 3 | {{- $constructor := printf "New%sSQL" (.Name | toCamel) -}} 4 | type {{ $camelName }}InsertSQL struct { 5 | {{ $camelName }}SQL 6 | setMap sqlla.SetMap 7 | Columns []string 8 | } 9 | 10 | func (q {{ $camelName }}SQL) Insert() {{ $camelName }}InsertSQL { 11 | return {{ $camelName }}InsertSQL{ 12 | {{ $camelName }}SQL: q, 13 | setMap: sqlla.SetMap{}, 14 | } 15 | } 16 | 17 | {{ range .Columns }}{{ template "InsertColumn" . }}{{ end }} 18 | func (q {{ $camelName }}InsertSQL) ToSql() (string, []any, error) { 19 | {{- if eq (dialect) "mysql" }} 20 | query, vs, err := q.{{ $camelName }}InsertSQLToSql() 21 | {{- end }} 22 | {{- if eq (dialect) "postgresql" }} 23 | query, _, vs, err := q.{{ $camelName }}InsertSQLToSqlPg(0) 24 | {{- end }} 25 | if err != nil { 26 | return "", []any{}, err 27 | } 28 | {{- if and .HasPk (eq (dialect) "postgresql") }} 29 | return query + " RETURNING " + {{ cquoteby .PkColumn.Name }} + ";", vs, nil 30 | {{- else }} 31 | return query + ";", vs, nil 32 | {{- end }} 33 | } 34 | 35 | {{ if eq (dialect) "mysql" }} 36 | func (q {{ $camelName }}InsertSQL) {{ $camelName }}InsertSQLToSql() (string, []any, error) { 37 | {{- end }} 38 | {{- if eq (dialect) "postgresql" }} 39 | func (q {{ $camelName }}InsertSQL) {{ $camelName }}InsertSQLToSqlPg(offset int) (string, int, []any, error) { 40 | {{- end }} 41 | var err error 42 | var s interface{} = {{ .StructName }}{} 43 | if t, ok := s.({{ $camelName }}DefaultInsertHooker); ok { 44 | q, err = t.DefaultInsertHook(q) 45 | if err != nil { 46 | {{- if eq (dialect) "mysql" }} 47 | return "", []any{}, err 48 | {{- end }} 49 | {{- if eq (dialect) "postgresql" }} 50 | return "", 0, []any{}, err 51 | {{- end }} 52 | } 53 | } 54 | {{- if eq (dialect) "mysql" }} 55 | qs, vs, err := q.setMap.ToInsertSql() 56 | {{- end }} 57 | {{- if eq (dialect) "postgresql" }} 58 | qs, offset, vs, err := q.setMap.ToInsertSqlPg(offset) 59 | {{- end }} 60 | if err != nil { 61 | {{- if eq (dialect) "mysql" }} 62 | return "", []any{}, err 63 | {{- end }} 64 | {{- if eq (dialect) "postgresql" }} 65 | return "", 0, []any{}, err 66 | {{- end }} 67 | } 68 | 69 | query := "INSERT INTO " + {{ cquoteby .TableName }} + " " + qs 70 | 71 | {{- if eq (dialect) "mysql" }} 72 | return query, vs, nil 73 | {{- end }} 74 | {{- if eq (dialect) "postgresql" }} 75 | return query, offset, vs, nil 76 | {{- end }} 77 | } 78 | 79 | {{ if .HasPk -}} 80 | func (q {{ $camelName }}InsertSQL) Exec(db sqlla.DB) ({{ .StructName }}, error) { 81 | {{- else -}} 82 | func (q {{ $camelName }}InsertSQL) Exec(db sqlla.DB) (sql.Result, error) { 83 | {{- end }} 84 | query, args, err := q.ToSql() 85 | if err != nil { 86 | {{ if .HasPk -}} 87 | return {{ .StructName }}{}, err 88 | {{- else }} 89 | return nil, err 90 | {{- end }} 91 | } 92 | {{- if not .HasPk }} 93 | result, err := db.Exec(query, args...) 94 | return result, err 95 | {{- else }} 96 | {{- if eq (dialect) "mysql" }} 97 | result, err := db.Exec(query, args...) 98 | if err != nil { 99 | return {{ .StructName }}{}, err 100 | } 101 | id, err := result.LastInsertId() 102 | if err != nil { 103 | return {{ .StructName }}{}, err 104 | } 105 | return {{ $constructor }}().Select().PkColumn(id).Single(db) 106 | {{- end }} 107 | {{- if eq (dialect) "postgresql" }} 108 | row := db.QueryRow(query, args...) 109 | var pk {{ .PkColumn.TypeName }} 110 | if err := row.Scan(&pk); err != nil { 111 | return {{ .StructName }}{}, err 112 | } 113 | return {{ $constructor }}().Select().{{ .PkColumn.MethodName }}(pk).Single(db) 114 | {{- end }} 115 | {{- end }} 116 | } 117 | 118 | {{ if .HasPk -}} 119 | func (q {{ $camelName }}InsertSQL) ExecContext(ctx context.Context, db sqlla.DB) ({{ .StructName }}, error) { 120 | {{- else -}} 121 | func (q {{ $camelName }}InsertSQL) ExecContext(ctx context.Context, db sqlla.DB) (sql.Result, error) { 122 | {{- end }} 123 | query, args, err := q.ToSql() 124 | if err != nil { 125 | {{ if .HasPk -}} 126 | return {{ .StructName }}{}, err 127 | {{- else }} 128 | return nil, err 129 | {{- end }} 130 | } 131 | {{- if not .HasPk }} 132 | result, err := db.ExecContext(ctx, query, args...) 133 | return result, err 134 | {{- else }} 135 | {{- if eq (dialect) "mysql" }} 136 | result, err := db.ExecContext(ctx, query, args...) 137 | if err != nil { 138 | return {{ .StructName }}{}, err 139 | } 140 | id, err := result.LastInsertId() 141 | if err != nil { 142 | return {{ .StructName }}{}, err 143 | } 144 | return {{ $constructor }}().Select().PkColumn(id).SingleContext(ctx, db) 145 | {{- end }} 146 | {{- if eq (dialect) "postgresql" }} 147 | row := db.QueryRowContext(ctx, query, args...) 148 | var pk {{ .PkColumn.TypeName }} 149 | if err := row.Scan(&pk); err != nil { 150 | return {{ .StructName }}{}, err 151 | } 152 | return {{ $constructor }}().Select().{{ .PkColumn.MethodName }}(pk).SingleContext(ctx, db) 153 | {{- end }} 154 | {{- end }} 155 | } 156 | 157 | {{ if .HasPk -}} 158 | func (q {{ $camelName }}InsertSQL) ExecContextWithoutSelect(ctx context.Context, db sqlla.DB) (sql.Result, error) { 159 | query, args, err := q.ToSql() 160 | if err != nil { 161 | return nil, err 162 | } 163 | result, err := db.ExecContext(ctx, query, args...) 164 | return result, err 165 | } 166 | {{- end }} 167 | 168 | type {{ $camelName }}DefaultInsertHooker interface { 169 | DefaultInsertHook({{ $camelName }}InsertSQL) ({{ $camelName }}InsertSQL, error) 170 | } 171 | 172 | type {{ $camelName }}InsertSQLToSqler interface { 173 | {{- if eq (dialect) "mysql" }} 174 | {{ $camelName }}InsertSQLToSql() (string, []any, error) 175 | {{- end }} 176 | {{- if eq (dialect) "postgresql" }} 177 | {{ $camelName }}InsertSQLToSqlPg(offset int) (string, int, []any, error) 178 | {{- end }} 179 | } 180 | 181 | type {{ $camelName }}BulkInsertSQL struct { 182 | insertSQLs []{{ $camelName }}InsertSQL 183 | } 184 | 185 | func (q {{ $camelName }}SQL) BulkInsert() *{{ $camelName }}BulkInsertSQL { 186 | return &{{ $camelName }}BulkInsertSQL{ 187 | insertSQLs: []{{ $camelName }}InsertSQL{}, 188 | } 189 | } 190 | 191 | func (q *{{ $camelName }}BulkInsertSQL) Append(iqs ...{{ $camelName }}InsertSQL) { 192 | q.insertSQLs = append(q.insertSQLs, iqs...) 193 | } 194 | 195 | {{ if eq (dialect) "mysql" }} 196 | func (q *{{ $camelName }}BulkInsertSQL) {{ $camelName }}InsertSQLToSql() (string, []any, error) { 197 | {{- end }} 198 | {{- if eq (dialect) "postgresql" }} 199 | func (q *{{ $camelName }}BulkInsertSQL) {{ $camelName }}InsertSQLToSqlPg(offset int) (string, int, []any, error) { 200 | {{- end }} 201 | if len(q.insertSQLs) == 0 { 202 | {{- if eq (dialect) "mysql" }} 203 | return "", []any{}, fmt.Errorf("sqlla: This {{ $camelName }}BulkInsertSQL{{ "'s" }} InsertSQL was empty") 204 | {{- end }} 205 | {{- if eq (dialect) "postgresql" }} 206 | return "", 0, []any{}, fmt.Errorf("sqlla: This {{ $camelName }}BulkInsertSQL{{ "'s" }} InsertSQL was empty") 207 | {{- end }} 208 | } 209 | iqs := make([]{{ $camelName }}InsertSQL, len(q.insertSQLs)) 210 | copy(iqs, q.insertSQLs) 211 | 212 | var s interface{} = {{ .StructName }}{} 213 | if t, ok := s.({{ $camelName }}DefaultInsertHooker); ok { 214 | for i, iq := range iqs { 215 | var err error 216 | iq, err = t.DefaultInsertHook(iq) 217 | if err != nil { 218 | {{- if eq (dialect) "mysql" }} 219 | return "", []any{}, err 220 | {{- end }} 221 | {{- if eq (dialect) "postgresql" }} 222 | return "", 0, []any{}, err 223 | {{- end }} 224 | } 225 | iqs[i] = iq 226 | } 227 | } 228 | 229 | sms := make(sqlla.SetMaps, 0, len(q.insertSQLs)) 230 | for _, iq := range q.insertSQLs { 231 | sms = append(sms, iq.setMap) 232 | } 233 | 234 | {{ if eq (dialect) "mysql" }} 235 | query, vs, err := sms.ToInsertSql() 236 | {{ end -}} 237 | {{ if eq (dialect) "postgresql" }} 238 | query, offset, vs, err := sms.ToInsertSqlPg(offset) 239 | {{ end -}} 240 | if err != nil { 241 | {{- if eq (dialect) "mysql" }} 242 | return "", []any{}, err 243 | {{- end }} 244 | {{- if eq (dialect) "postgresql" }} 245 | return "", 0, []any{}, err 246 | {{- end }} 247 | } 248 | 249 | {{- if eq (dialect) "mysql" }} 250 | return "INSERT INTO " + {{ cquoteby .TableName }} + " " + query, vs, nil 251 | {{- end }} 252 | {{- if eq (dialect) "postgresql" }} 253 | return "INSERT INTO " + {{ cquoteby .TableName }} + " " + query, offset, vs, nil 254 | {{- end }} 255 | } 256 | 257 | func (q *{{ $camelName }}BulkInsertSQL) ToSql() (string, []any, error) { 258 | {{- if eq (dialect) "mysql" }} 259 | query, vs, err := q.{{ $camelName }}InsertSQLToSql() 260 | {{- end }} 261 | {{- if eq (dialect) "postgresql" }} 262 | query, _, vs, err := q.{{ $camelName }}InsertSQLToSqlPg(0) 263 | {{- end }} 264 | if err != nil { 265 | return "", []any{}, err 266 | } 267 | {{- if and .HasPk (eq (dialect) "postgresql") }} 268 | return query + " RETURNING " + {{ cquoteby .PkColumn.Name }} + ";", vs, nil 269 | {{- else }} 270 | return query + ";", vs, nil 271 | {{- end }} 272 | } 273 | 274 | {{- if and .HasPk (eq (dialect) "postgresql") }} 275 | func (q *{{ $camelName }}BulkInsertSQL) ExecContext(ctx context.Context, db sqlla.DB) ([]{{ .StructName }}, error) { 276 | {{- else }} 277 | func (q *{{ $camelName }}BulkInsertSQL) ExecContext(ctx context.Context, db sqlla.DB) (sql.Result, error) { 278 | {{- end }} 279 | query, args, err := q.ToSql() 280 | if err != nil { 281 | return nil, err 282 | } 283 | {{- if and .HasPk (eq (dialect) "postgresql") }} 284 | rows, err := db.QueryContext(ctx, query, args...) 285 | if err != nil { 286 | return nil, err 287 | } 288 | defer rows.Close() 289 | pks := make([]{{ .PkColumn.TypeName }}, 0, len(q.insertSQLs)) 290 | for rows.Next() { 291 | var pk {{ .PkColumn.TypeName }} 292 | if err := rows.Scan(&pk); err != nil { 293 | return nil, err 294 | } 295 | pks = append(pks, pk) 296 | } 297 | return {{ $constructor }}().Select().{{ .PkColumn.MethodName }}In(pks...).AllContext(ctx, db) 298 | {{- else }} 299 | result, err := db.ExecContext(ctx, query, args...) 300 | return result, err 301 | {{- end }} 302 | } 303 | 304 | {{- if and .HasPk (eq (dialect) "postgresql") }} 305 | func (q *{{ $camelName }}BulkInsertSQL) ExecContextWithoutSelect(ctx context.Context, db sqlla.DB) (sql.Result, error) { 306 | query, args, err := q.ToSql() 307 | if err != nil { 308 | return nil, err 309 | } 310 | result, err := db.ExecContext(ctx, query, args...) 311 | return result, err 312 | } 313 | {{- end }} 314 | 315 | {{- if eq (dialect) "mysql" }} 316 | {{ template "InsertMySQL" . }} 317 | {{- end }} 318 | {{- if eq (dialect) "postgresql" }} 319 | {{ template "InsertPostgreSQL" . }} 320 | {{- end }} 321 | {{ end }} 322 | -------------------------------------------------------------------------------- /template/insert_column.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "InsertColumn" }}{{ $smallTableName := .TableName | toCamel | untitle }} 2 | {{- if .IsNullT }} 3 | func (q {{ $smallTableName }}InsertSQL) Value{{ .MethodName }}(v {{ .TypeName }}) {{ $smallTableName }}InsertSQL { 4 | q.setMap[{{ cquoteby .Name }}] = {{ .ExprValueIdentifier }} 5 | return q 6 | } 7 | 8 | func (q {{ $smallTableName }}InsertSQL) Value{{ .MethodName }}IsNull() {{ $smallTableName }}InsertSQL { 9 | q.setMap[{{ cquoteby .Name }}] = sql.Null[{{ .BaseTypeName }}]{ Valid: false } 10 | return q 11 | } 12 | 13 | {{- else }} 14 | func (q {{ $smallTableName }}InsertSQL) Value{{ .MethodName }}(v {{ .TypeName }}) {{ $smallTableName }}InsertSQL { 15 | q.setMap[{{ cquoteby .Name }}] = {{ .ExprValueIdentifier }} 16 | return q 17 | } 18 | {{- end }} 19 | 20 | {{ end }} 21 | -------------------------------------------------------------------------------- /template/insert_mysql.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "InsertMySQL" }} 2 | {{- $camelName := .Name | toCamel | untitle -}} 3 | {{- $constructor := printf "New%sSQL" (.Name | toCamel) -}} 4 | type {{ $camelName }}InsertOnDuplicateKeyUpdateSQL struct { 5 | insertSQL {{ $camelName }}InsertSQLToSqler 6 | onDuplicateKeyUpdateMap sqlla.SetMap 7 | } 8 | 9 | func (q {{ $camelName }}InsertSQL) OnDuplicateKeyUpdate() {{ $camelName }}InsertOnDuplicateKeyUpdateSQL { 10 | return {{ $camelName }}InsertOnDuplicateKeyUpdateSQL{ 11 | insertSQL: q, 12 | onDuplicateKeyUpdateMap: sqlla.SetMap{}, 13 | } 14 | } 15 | 16 | {{ range .Columns }}{{ template "InsertOnDuplicateKeyUpdateColumn" . }}{{ end }} 17 | 18 | func (q {{ $camelName }}InsertOnDuplicateKeyUpdateSQL) ToSql() (string, []interface{}, error) { 19 | var err error 20 | var s interface{} = {{ .StructName }}{} 21 | if t, ok := s.({{ $camelName }}DefaultInsertOnDuplicateKeyUpdateHooker); ok { 22 | q, err = t.DefaultInsertOnDuplicateKeyUpdateHook(q) 23 | if err != nil { 24 | return "", []interface{}{}, err 25 | } 26 | } 27 | 28 | query, vs, err := q.insertSQL.{{ $camelName }}InsertSQLToSql() 29 | if err != nil { 30 | return "", []interface{}{}, err 31 | } 32 | 33 | os, ovs, err := q.onDuplicateKeyUpdateMap.ToUpdateSql() 34 | if err != nil { 35 | return "", []interface{}{}, err 36 | } 37 | query += " ON DUPLICATE KEY UPDATE" + os 38 | vs = append(vs, ovs...) 39 | 40 | return query + ";", vs, nil 41 | } 42 | 43 | {{ if .HasPk -}} 44 | func (q {{ $camelName }}InsertOnDuplicateKeyUpdateSQL) ExecContext(ctx context.Context, db sqlla.DB) ({{ .StructName }}, error) { 45 | {{- else -}} 46 | func (q {{ $camelName }}InsertOnDuplicateKeyUpdateSQL) ExecContext(ctx context.Context, db sqlla.DB) (sql.Result, error) { 47 | {{- end }} 48 | query, args, err := q.ToSql() 49 | if err != nil { 50 | {{ if .HasPk -}} 51 | return {{ .StructName }}{}, err 52 | {{- else }} 53 | return nil, err 54 | {{- end }} 55 | } 56 | result, err := db.ExecContext(ctx, query, args...) 57 | {{ if .HasPk -}} 58 | if err != nil { 59 | return {{ .StructName }}{}, err 60 | } 61 | id, err := result.LastInsertId() 62 | if err != nil { 63 | return {{ .StructName }}{}, err 64 | } 65 | return {{ $constructor }}().Select().PkColumn(id).SingleContext(ctx, db) 66 | {{- else -}} 67 | return result, err 68 | {{- end }} 69 | } 70 | 71 | {{ if .HasPk -}} 72 | func (q {{ $camelName }}InsertOnDuplicateKeyUpdateSQL) ExecContextWithoutSelect(ctx context.Context, db sqlla.DB) (sql.Result, error) { 73 | query, args, err := q.ToSql() 74 | if err != nil { 75 | return nil, err 76 | } 77 | result, err := db.ExecContext(ctx, query, args...) 78 | return result, err 79 | } 80 | {{- end }} 81 | 82 | type {{ $camelName }}DefaultInsertOnDuplicateKeyUpdateHooker interface { 83 | DefaultInsertOnDuplicateKeyUpdateHook({{ $camelName }}InsertOnDuplicateKeyUpdateSQL) ({{ $camelName }}InsertOnDuplicateKeyUpdateSQL, error) 84 | } 85 | 86 | func (q *{{ $camelName }}BulkInsertSQL) OnDuplicateKeyUpdate() {{ $camelName }}InsertOnDuplicateKeyUpdateSQL { 87 | return {{ $camelName }}InsertOnDuplicateKeyUpdateSQL{ 88 | insertSQL: q, 89 | onDuplicateKeyUpdateMap: sqlla.SetMap{}, 90 | } 91 | } 92 | {{ end }} 93 | -------------------------------------------------------------------------------- /template/insert_on_conflict_do_update_column.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "InsertOnConflictDoUpdateColumn" }}{{ $smallTableName := .TableName | toCamel | untitle }} 2 | {{- if .IsNullT }} 3 | func (q {{ $smallTableName }}InsertOnConflictDoUpdateSQL) ValueOnUpdate{{ .MethodName }} (v {{ .TypeName }}) {{ $smallTableName }}InsertOnConflictDoUpdateSQL { 4 | q.onConflictDoUpdateMap[{{ cquoteby .Name }}] = {{ .ExprValueIdentifier }} 5 | return q 6 | } 7 | 8 | func (q {{ $smallTableName }}InsertOnConflictDoUpdateSQL) ValueOnUpdate{{ .MethodName }}ToNull () {{ $smallTableName }}InsertOnConflictDoUpdateSQL { 9 | q.onConflictDoUpdateMap[{{ cquoteby .Name }}] = sql.Null[{{ .BaseTypeName }}]{ Valid: false } 10 | return q 11 | } 12 | {{- else }} 13 | func (q {{ $smallTableName }}InsertOnConflictDoUpdateSQL) ValueOnUpdate{{ .MethodName }} (v {{ .TypeName }}) {{ $smallTableName }}InsertOnConflictDoUpdateSQL { 14 | q.onConflictDoUpdateMap[{{ cquoteby .Name }}] = {{ .ExprValueIdentifier }} 15 | return q 16 | } 17 | {{- end }} 18 | 19 | func (q {{ $smallTableName }}InsertOnConflictDoUpdateSQL) RawValueOnUpdate{{ .MethodName }} (v sqlla.SetMapRawValue) {{ $smallTableName }}InsertOnConflictDoUpdateSQL { 20 | q.onConflictDoUpdateMap[{{ cquoteby .Name }}] = v 21 | return q 22 | } 23 | 24 | func (q {{ $smallTableName }}InsertOnConflictDoUpdateSQL) SameOnUpdate{{ .MethodName }} () {{ $smallTableName }}InsertOnConflictDoUpdateSQL { 25 | q.onConflictDoUpdateMap[{{ cquoteby .Name }}] = sqlla.SetMapRawValue(`"excluded".` + {{ cquoteby .Name }}) 26 | return q 27 | } 28 | {{ end }} 29 | {{ define "BulkInsertOnConflictDoUpdateColumn" }}{{ $smallTableName := .TableName | toCamel | untitle }} 30 | {{- if .IsNullT }} 31 | func (q {{ $smallTableName }}BulkInsertOnConflictDoUpdateSQL) ValueOnUpdate{{ .MethodName }} (v {{ .TypeName }}) {{ $smallTableName }}BulkInsertOnConflictDoUpdateSQL { 32 | q.onConflictDoUpdateMap[{{ cquoteby .Name }}] = {{ .ExprValueIdentifier }} 33 | return q 34 | } 35 | 36 | func (q {{ $smallTableName }}BulkInsertOnConflictDoUpdateSQL) ValueOnUpdate{{ .MethodName }}ToNull () {{ $smallTableName }}BulkInsertOnConflictDoUpdateSQL { 37 | q.onConflictDoUpdateMap[{{ cquoteby .Name }}] = sql.Null[{{ .BaseTypeName }}]{ Valid: false } 38 | return q 39 | } 40 | {{- else }} 41 | func (q {{ $smallTableName }}BulkInsertOnConflictDoUpdateSQL) ValueOnUpdate{{ .MethodName }} (v {{ .TypeName }}) {{ $smallTableName }}BulkInsertOnConflictDoUpdateSQL { 42 | q.onConflictDoUpdateMap[{{ cquoteby .Name }}] = {{ .ExprValueIdentifier }} 43 | return q 44 | } 45 | {{- end }} 46 | 47 | func (q {{ $smallTableName }}BulkInsertOnConflictDoUpdateSQL) RawValueOnUpdate{{ .MethodName }} (v sqlla.SetMapRawValue) {{ $smallTableName }}BulkInsertOnConflictDoUpdateSQL { 48 | q.onConflictDoUpdateMap[{{ cquoteby .Name }}] = v 49 | return q 50 | } 51 | 52 | func (q {{ $smallTableName }}BulkInsertOnConflictDoUpdateSQL) SameOnUpdate{{ .MethodName }} () {{ $smallTableName }}BulkInsertOnConflictDoUpdateSQL { 53 | q.onConflictDoUpdateMap[{{ cquoteby .Name }}] = sqlla.SetMapRawValue(`"excluded".` + {{ cquoteby .Name }}) 54 | return q 55 | } 56 | {{ end }} 57 | -------------------------------------------------------------------------------- /template/insert_on_duplicate_key_update_column.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "InsertOnDuplicateKeyUpdateColumn" }}{{ $smallTableName := .TableName | toCamel | untitle }} 2 | {{- if .IsNullT }} 3 | func (q {{ $smallTableName }}InsertOnDuplicateKeyUpdateSQL) ValueOnUpdate{{ .MethodName }} (v {{ .TypeName }}) {{ $smallTableName }}InsertOnDuplicateKeyUpdateSQL { 4 | q.onDuplicateKeyUpdateMap[{{ cquoteby .Name }}] = {{ .ExprValueIdentifier }} 5 | return q 6 | } 7 | 8 | func (q {{ $smallTableName }}InsertOnDuplicateKeyUpdateSQL) ValueOnUpdate{{ .MethodName }}ToNull () {{ $smallTableName }}InsertOnDuplicateKeyUpdateSQL { 9 | q.onDuplicateKeyUpdateMap[{{ cquoteby .Name }}] = sql.Null[{{ .BaseTypeName }}]{ Valid: false } 10 | return q 11 | } 12 | {{- else }} 13 | func (q {{ $smallTableName }}InsertOnDuplicateKeyUpdateSQL) ValueOnUpdate{{ .MethodName }} (v {{ .TypeName }}) {{ $smallTableName }}InsertOnDuplicateKeyUpdateSQL { 14 | q.onDuplicateKeyUpdateMap[{{ cquoteby .Name }}] = {{ .ExprValueIdentifier }} 15 | return q 16 | } 17 | {{- end }} 18 | 19 | func (q {{ $smallTableName }}InsertOnDuplicateKeyUpdateSQL) RawValueOnUpdate{{ .MethodName }} (v sqlla.SetMapRawValue) {{ $smallTableName }}InsertOnDuplicateKeyUpdateSQL { 20 | q.onDuplicateKeyUpdateMap[{{ cquoteby .Name }}] = v 21 | return q 22 | } 23 | 24 | func (q {{ $smallTableName }}InsertOnDuplicateKeyUpdateSQL) SameOnUpdate{{ .MethodName }} () {{ $smallTableName }}InsertOnDuplicateKeyUpdateSQL { 25 | q.onDuplicateKeyUpdateMap[{{ cquoteby .Name }}] = sqlla.SetMapRawValue("VALUES(" + {{ cquoteby .Name }} + ")") 26 | return q 27 | } 28 | {{ end }} 29 | -------------------------------------------------------------------------------- /template/insert_postgresql.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "InsertPostgreSQL.ExecContextHasPkSingle" }} 2 | {{- $constructor := printf "New%sSQL" (.Name | toCamel) -}} 3 | query, args, err := q.ToSql() 4 | if err != nil { 5 | return {{ .StructName }}{}, err 6 | } 7 | row := db.QueryRowContext(ctx, query, args...) 8 | var pk {{ .PkColumn.TypeName }} 9 | if err := row.Scan(&pk); err != nil { 10 | return {{ .StructName }}{}, err 11 | } 12 | return {{ $constructor }}().Select().{{ .PkColumn.MethodName }}(pk).SingleContext(ctx, db) 13 | {{ end }} 14 | {{ define "InsertPostgreSQL.ExecContextHasPkAll" }} 15 | {{- $constructor := printf "New%sSQL" (.Name | toCamel) -}} 16 | query, args, err := q.ToSql() 17 | if err != nil { 18 | return nil, err 19 | } 20 | rows, err := db.QueryContext(ctx, query, args...) 21 | if err != nil { 22 | return nil, err 23 | } 24 | defer rows.Close() 25 | pks := make([]{{ .PkColumn.TypeName }}, 0) 26 | for rows.Next() { 27 | var pk {{ .PkColumn.TypeName }} 28 | if err := rows.Scan(&pk); err != nil { 29 | return nil, err 30 | } 31 | pks = append(pks, pk) 32 | } 33 | 34 | return {{ $constructor }}().Select().{{ .PkColumn.MethodName }}In(pks...).AllContext(ctx, db) 35 | {{ end }} 36 | {{ define "InsertPostgreSQL.ExecContextWithoutSelect" }} 37 | query, args, err := q.ToSql() 38 | if err != nil { 39 | return nil, err 40 | } 41 | result, err := db.ExecContext(ctx, query, args...) 42 | return result, err 43 | {{ end }} 44 | {{ define "InsertPostgreSQL.DoNothingToSql" }} 45 | {{- $camelName := .Name | toCamel | untitle -}} 46 | query, _, vs, err := q.insertSQL.{{ $camelName }}InsertSQLToSqlPg(0) 47 | if err != nil { 48 | return "", nil, err 49 | } 50 | query += " ON CONFLICT DO NOTHING" 51 | {{- if .HasPk }} 52 | query += " RETURNING " + {{ cquoteby .PkColumn.Name }} 53 | {{- end }} 54 | return query + ";", vs, nil 55 | {{ end }} 56 | {{ define "InsertPostgreSQL.DoUpdateToSql" }} 57 | {{- $camelName := .Name | toCamel | untitle -}} 58 | var s any = {{ .StructName }}{} 59 | if t, ok := s.({{ $camelName }}DefaultInsertOnConflictDoUpdateHooker); ok { 60 | _q, err := t.DefaultInsertOnConflictDoUpdateHook(q) 61 | if err != nil { 62 | return "", nil, err 63 | } 64 | q = _q 65 | } 66 | 67 | query, offset, vs, err := q.insertSQL.{{ $camelName }}InsertSQLToSqlPg(0) 68 | if err != nil { 69 | return "", nil, err 70 | } 71 | 72 | os, _, ovs, err := q.onConflictDoUpdateMap.ToUpdateSqlPg(offset) 73 | if err != nil { 74 | return "", nil, err 75 | } 76 | query += " ON CONFLICT (" + q.target + ") DO UPDATE SET" + os 77 | vs = append(vs, ovs...) 78 | {{- if .HasPk }} 79 | query += " RETURNING " + {{ cquoteby .PkColumn.Name }} 80 | {{- end }} 81 | 82 | return query + ";", vs, nil 83 | {{ end }} 84 | 85 | {{ define "InsertPostgreSQL" }} 86 | {{- $camelName := .Name | toCamel | untitle -}} 87 | {{- $constructor := printf "New%sSQL" (.Name | toCamel) -}} 88 | 89 | type {{ $camelName }}InsertOnConflictDoNothingSQL struct { 90 | insertSQL {{ $camelName }}InsertSQLToSqler 91 | } 92 | 93 | func (q {{ $camelName }}InsertSQL) OnConflictDoNothing() {{ $camelName }}InsertOnConflictDoNothingSQL { 94 | return {{ $camelName }}InsertOnConflictDoNothingSQL{ 95 | insertSQL: q, 96 | } 97 | } 98 | 99 | func (q {{ $camelName }}InsertOnConflictDoNothingSQL) ToSql() (string, []any, error) { 100 | {{ template "InsertPostgreSQL.DoNothingToSql" . }} 101 | } 102 | 103 | {{ if .HasPk -}} 104 | func (q {{ $camelName }}InsertOnConflictDoNothingSQL) ExecContext(ctx context.Context, db sqlla.DB) ({{ .StructName }}, error) { 105 | {{ template "InsertPostgreSQL.ExecContextHasPkSingle" . }} 106 | } 107 | 108 | func (q {{ $camelName }}InsertOnConflictDoNothingSQL) ExecContextWithoutSelect(ctx context.Context, db sqlla.DB) (sql.Result, error) { 109 | {{ template "InsertPostgreSQL.ExecContextWithoutSelect" . }} 110 | } 111 | {{- else -}} 112 | func (q {{ $camelName }}InsertOnConflictDoNothingSQL) ExecContext(ctx context.Context, db sqlla.DB) (sql.Result, error) { 113 | {{ template "InsertPostgreSQL.ExecContextWithoutSelect" . }} 114 | } 115 | {{- end }} 116 | 117 | type {{ $camelName }}InsertOnConflictDoUpdateSQL struct { 118 | insertSQL {{ $camelName }}InsertSQLToSqler 119 | onConflictDoUpdateMap sqlla.SetMap 120 | target string 121 | } 122 | 123 | func (q {{ $camelName }}InsertSQL) OnConflictDoUpdate(target string) {{ $camelName }}InsertOnConflictDoUpdateSQL { 124 | return {{ $camelName }}InsertOnConflictDoUpdateSQL{ 125 | insertSQL: q, 126 | onConflictDoUpdateMap: sqlla.SetMap{}, 127 | target: target, 128 | } 129 | } 130 | 131 | {{ range .Columns }}{{ template "InsertOnConflictDoUpdateColumn" . }}{{ end }} 132 | 133 | func (q {{ $camelName }}InsertOnConflictDoUpdateSQL) ToSql() (string, []any, error) { 134 | var err error 135 | var s any = {{ .StructName }}{} 136 | if t, ok := s.({{ $camelName }}DefaultInsertOnConflictDoUpdateHooker); ok { 137 | q, err = t.DefaultInsertOnConflictDoUpdateHook(q) 138 | if err != nil { 139 | return "", nil, err 140 | } 141 | } 142 | 143 | query, offset, vs, err := q.insertSQL.{{ $camelName }}InsertSQLToSqlPg(0) 144 | if err != nil { 145 | return "", nil, err 146 | } 147 | 148 | os, _, ovs, err := q.onConflictDoUpdateMap.ToUpdateSqlPg(offset) 149 | if err != nil { 150 | return "", nil, err 151 | } 152 | query += " ON CONFLICT (" + q.target + ") DO UPDATE SET" + os 153 | vs = append(vs, ovs...) 154 | {{- if .HasPk }} 155 | query += " RETURNING " + {{ cquoteby .PkColumn.Name }} 156 | {{- end }} 157 | 158 | return query + ";", vs, nil 159 | } 160 | 161 | {{ if .HasPk -}} 162 | func (q {{ $camelName }}InsertOnConflictDoUpdateSQL) ExecContext(ctx context.Context, db sqlla.DB) ({{ .StructName }}, error) { 163 | {{ template "InsertPostgreSQL.ExecContextHasPkSingle" . }} 164 | } 165 | 166 | func (q {{ $camelName }}InsertOnConflictDoUpdateSQL) ExecContextWithoutSelect(ctx context.Context, db sqlla.DB) (sql.Result, error) { 167 | {{ template "InsertPostgreSQL.ExecContextWithoutSelect" . }} 168 | } 169 | {{- else -}} 170 | func (q {{ $camelName }}InsertOnConflictDoUpdateSQL) ExecContext(ctx context.Context, db sqlla.DB) (sql.Result, error) { 171 | {{ template "InsertPostgreSQL.ExecContextWithoutSelect" . }} 172 | } 173 | {{- end }} 174 | 175 | type {{ $camelName }}DefaultInsertOnConflictDoUpdateHooker interface { 176 | DefaultInsertOnConflictDoUpdateHook({{ $camelName }}InsertOnConflictDoUpdateSQL) ({{ $camelName }}InsertOnConflictDoUpdateSQL, error) 177 | } 178 | 179 | type {{ $camelName }}BulkInsertOnConflictDoNothingSQL struct { 180 | insertSQL {{ $camelName }}InsertSQLToSqler 181 | } 182 | 183 | func (q *{{ $camelName }}BulkInsertSQL) OnConflictDoNothing() {{ $camelName }}BulkInsertOnConflictDoNothingSQL { 184 | return {{ $camelName }}BulkInsertOnConflictDoNothingSQL{ 185 | insertSQL: q, 186 | } 187 | } 188 | 189 | func (q {{ $camelName }}BulkInsertOnConflictDoNothingSQL) ToSql() (string, []any, error) { 190 | {{ template "InsertPostgreSQL.DoNothingToSql" . }} 191 | } 192 | 193 | {{ if .HasPk -}} 194 | func (q {{ $camelName }}BulkInsertOnConflictDoNothingSQL) ExecContext(ctx context.Context, db sqlla.DB) ([]{{ .StructName }}, error) { 195 | {{ template "InsertPostgreSQL.ExecContextHasPkAll" . }} 196 | } 197 | 198 | func (q {{ $camelName }}BulkInsertOnConflictDoNothingSQL) ExecContextWithoutSelect(ctx context.Context, db sqlla.DB) (sql.Result, error) { 199 | {{ template "InsertPostgreSQL.ExecContextWithoutSelect" . }} 200 | } 201 | {{- else -}} 202 | func (q {{ $camelName }}BulkInsertOnConflictDoNothingSQL) ExecContext(ctx context.Context, db sqlla.DB) (sql.Result, error) { 203 | {{ template "InsertPostgreSQL.ExecContextWithoutSelect" . }} 204 | } 205 | {{- end }} 206 | 207 | type {{ $camelName }}BulkInsertOnConflictDoUpdateSQL struct { 208 | insertSQL {{ $camelName }}InsertSQLToSqler 209 | onConflictDoUpdateMap sqlla.SetMap 210 | target string 211 | } 212 | 213 | func (q *{{ $camelName }}BulkInsertSQL) OnConflictDoUpdate(target string) {{ $camelName }}BulkInsertOnConflictDoUpdateSQL { 214 | return {{ $camelName }}BulkInsertOnConflictDoUpdateSQL{ 215 | insertSQL: q, 216 | onConflictDoUpdateMap: sqlla.SetMap{}, 217 | target: target, 218 | } 219 | } 220 | 221 | {{ range .Columns }}{{ template "BulkInsertOnConflictDoUpdateColumn" . }}{{ end }} 222 | 223 | func (q {{ $camelName }}BulkInsertOnConflictDoUpdateSQL) ToSql() (string, []any, error) { 224 | var s any = {{ .StructName }}{} 225 | if t, ok := s.({{ $camelName }}DefaultInsertOnConflictDoUpdateHooker); ok { 226 | sq := {{ $camelName }}InsertOnConflictDoUpdateSQL{ 227 | insertSQL: q.insertSQL, 228 | onConflictDoUpdateMap: q.onConflictDoUpdateMap, 229 | target: q.target, 230 | } 231 | sq, err := t.DefaultInsertOnConflictDoUpdateHook(sq) 232 | if err != nil { 233 | return "", nil, err 234 | } 235 | q.insertSQL = sq.insertSQL 236 | q.onConflictDoUpdateMap = sq.onConflictDoUpdateMap 237 | q.target = sq.target 238 | } 239 | 240 | query, offset, vs, err := q.insertSQL.{{ $camelName }}InsertSQLToSqlPg(0) 241 | if err != nil { 242 | return "", nil, err 243 | } 244 | 245 | os, _, ovs, err := q.onConflictDoUpdateMap.ToUpdateSqlPg(offset) 246 | if err != nil { 247 | return "", nil, err 248 | } 249 | query += " ON CONFLICT (" + q.target + ") DO UPDATE SET" + os 250 | vs = append(vs, ovs...) 251 | {{- if .HasPk }} 252 | query += " RETURNING " + {{ cquoteby .PkColumn.Name }} 253 | {{- end }} 254 | 255 | return query + ";", vs, nil 256 | } 257 | 258 | {{ if .HasPk -}} 259 | func (q {{ $camelName }}BulkInsertOnConflictDoUpdateSQL) ExecContext(ctx context.Context, db sqlla.DB) ([]{{ .StructName }}, error) { 260 | {{ template "InsertPostgreSQL.ExecContextHasPkAll" . }} 261 | } 262 | 263 | func (q {{ $camelName }}BulkInsertOnConflictDoUpdateSQL) ExecContextWithoutSelect(ctx context.Context, db sqlla.DB) (sql.Result, error) { 264 | {{ template "InsertPostgreSQL.ExecContextWithoutSelect" . }} 265 | } 266 | {{- else -}} 267 | func (q {{ $camelName }}BulkInsertOnConflictDoUpdateSQL) ExecContext(ctx context.Context, db sqlla.DB) (sql.Result, error) { 268 | {{ template "InsertPostgreSQL.ExecContextWithoutSelect" . }} 269 | } 270 | {{- end }} 271 | 272 | {{ end }} 273 | -------------------------------------------------------------------------------- /template/plugins/count.tmpl: -------------------------------------------------------------------------------- 1 | {{- define "plugin.count" }} 2 | {{- $camelName := .Table.Name | toCamel | untitle }} 3 | func (q {{ $camelName }}SelectSQL) CountContext(ctx context.Context, db sqlla.DB, column string) (int64, error) { 4 | query, args, err := q.SetColumns("COUNT(" + column + ")").ToSql() 5 | if err != nil { 6 | return 0, err 7 | } 8 | row := db.QueryRowContext(ctx, query, args...) 9 | var count int64 10 | if err := row.Scan(&count); err != nil { 11 | return 0, err 12 | } 13 | return count, nil 14 | } 15 | {{ end }} 16 | -------------------------------------------------------------------------------- /template/plugins/relations.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "plugin.relations" -}} 2 | {{- $structName := .Table.StructName }} 3 | {{- $receiver := substr 0 1 $structName | lower }} 4 | {{- $splitted := splitn ":" 2 .Args.key }} 5 | {{- $srcColumn := $splitted._0 }} 6 | {{- $dstTableColumn := splitn "." 2 $splitted._1 }} 7 | {{- $dstTable := $dstTableColumn._0 }} 8 | {{- $methodName := default $dstTable .Args.method }} 9 | {{- $dstColumn := $dstTableColumn._1 }} 10 | {{- $srcColumnField := .Table.Lookup $srcColumn }} 11 | {{- $isNullable := contains ".Null" $srcColumnField.TypeName }} 12 | func ({{ $receiver }} *{{ $structName }}) {{ $methodName }}(ctx context.Context, db sqlla.DB) (*{{ $dstTable }}, error) { 13 | {{- if $isNullable }} 14 | {{- $nullValue := "V" }} 15 | {{- if not $srcColumnField.IsNullT }} 16 | {{- $nullValue = trimPrefix "sql.Null" $srcColumnField.TypeName }} 17 | {{- if eq $srcColumnField.TypeName "mysql.NullTime" }} 18 | {{- $nullValue = "Time" }} 19 | {{- end }} 20 | {{- end }} 21 | if !{{ $receiver }}.{{ $srcColumn }}.Valid { 22 | return nil, nil 23 | } 24 | row, err := New{{ $dstTable }}SQL().Select().{{ $dstColumn }}({{ $receiver }}.{{ $srcColumn }}.{{ $nullValue }}).SingleContext(ctx, db) 25 | {{- else }} 26 | row, err := New{{ $dstTable }}SQL().Select().{{ $dstColumn }}({{ $receiver }}.{{ $srcColumn }}).SingleContext(ctx, db) 27 | {{- end }} 28 | if err != nil { 29 | return nil, fmt.Errorf("failed to get {{ $dstTable }}: %w", err) 30 | } 31 | return &row, nil 32 | } 33 | {{ end }} 34 | -------------------------------------------------------------------------------- /template/plugins/slice.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "plugin.slice" }} 2 | {{- $structName := .Table.StructName }} 3 | {{- $receiver := substr 0 1 $structName | lower }} 4 | {{- $sliceTypeName := pluralize $structName }} 5 | type {{ $sliceTypeName }} []*{{ $structName }} 6 | 7 | {{- if not (empty .Args.columns) }} 8 | {{- $table := .Table }} 9 | {{ range $index, $columnName := splitList "," .Args.columns }} 10 | {{- $column := $table.Lookup $columnName }} 11 | {{- $methodName := pluralize $column.FieldName }} 12 | func ({{ $receiver }} {{ $sliceTypeName }}) {{ $methodName }}() []{{ $column.TypeName }} { 13 | vs := make([]{{ $column.TypeName }}, len({{ $receiver }})) 14 | for _i := range {{ $receiver }} { 15 | vs[_i] = {{ $receiver }}[_i].{{ $column.FieldName }} 16 | } 17 | return vs 18 | } 19 | {{ end }} 20 | {{- end }} 21 | 22 | {{- if not (empty .Args.keyBy) }} 23 | {{- $table := .Table }} 24 | {{ range $index, $columnName := splitList "," .Args.keyBy }} 25 | {{- $column := $table.Lookup $columnName }} 26 | {{- $fieldName := pluralize $column.FieldName }} 27 | func ( {{ $receiver }} {{ $sliceTypeName }}) AssociateBy{{ $fieldName }}() map[{{ $column.TypeName }}]*{{ $structName }} { 28 | _m := make(map[{{ $column.TypeName }}]*{{ $structName }}, len({{ $receiver }})) 29 | for _, _v := range {{ $receiver }} { 30 | _m[_v.{{ $column.FieldName }}] = _v 31 | } 32 | return _m 33 | } 34 | {{ end }} 35 | {{- end }} 36 | 37 | {{- if not (empty .Args.groupBy) }} 38 | {{- $table := .Table }} 39 | {{ range $index, $columnName := splitList "," .Args.groupBy }} 40 | {{- $column := $table.Lookup $columnName }} 41 | {{- $fieldName := pluralize $column.FieldName }} 42 | func ( {{ $receiver }} {{ $sliceTypeName }}) GroupBy{{ $fieldName }}() map[{{ $column.TypeName }}]{{ $sliceTypeName }} { 43 | _m := make(map[{{ $column.TypeName }}]{{ $sliceTypeName }}, len({{ $receiver }})) 44 | for _, _v := range {{ $receiver }} { 45 | _m[_v.{{ $column.FieldName }}] = append(_m[_v.{{ $column.FieldName }}], _v) 46 | } 47 | return _m 48 | } 49 | {{ end }} 50 | {{- end }} 51 | 52 | 53 | {{ end }} 54 | -------------------------------------------------------------------------------- /template/plugins/table.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "plugin.table" }} 2 | {{- $structName := .Table.StructName }} 3 | {{- $receiver := substr 0 1 $structName | lower }} 4 | {{- $tableTypeName := printf "%sTable" $structName }} 5 | {{- $table := .Table }} 6 | {{- $sliceTypeName := pluralize $structName }} 7 | type {{ $tableTypeName }} struct {} 8 | 9 | func New{{ $tableTypeName }}() *{{ $tableTypeName }} { 10 | return &{{ $tableTypeName }}{} 11 | } 12 | 13 | {{- if not (empty .Args.get) }} 14 | {{ $getColumns := splitList "," .Args.get }} 15 | {{ range $index, $joinedColumnNames := $getColumns }} 16 | {{ $columnNames := splitList "&" $joinedColumnNames }} 17 | {{ $methodName := join "And" $columnNames }} 18 | func ({{ $receiver }} *{{ $tableTypeName }}) GetBy{{ $methodName }}(ctx context.Context, db sqlla.DB, 19 | {{- range $index, $columnName := $columnNames -}} 20 | c{{ $index }}{{ " " }} 21 | {{- $column := $table.Lookup $columnName -}} 22 | {{ $column.TypeName }}, 23 | {{- end }}) (*{{ $structName }}, error) { 24 | row, err := New{{ $structName }}SQL().Select(). 25 | {{- range $index, $columnName := $columnNames }} 26 | {{ $column := $table.Lookup $columnName }} 27 | {{ $column.MethodName }}(c{{ $index }}). 28 | {{- end }} 29 | SingleContext(ctx, db) 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to get {{ $structName }} by {{ join " and " $columnNames }}: %w", err) 32 | } 33 | return &row, nil 34 | } 35 | {{- if eq (len $columnNames) 1 }} 36 | {{ $columnName := index $columnNames 0 }} 37 | {{ $listColumnName := $columnName | pluralize }} 38 | func ({{ $receiver }} *{{ $tableTypeName }}) ListBy{{ $listColumnName }}(ctx context.Context, db sqlla.DB, cs []{{ ($table.Lookup $columnName).TypeName }}) ({{ $sliceTypeName }}, error) { 39 | _rows, err := New{{ $structName }}SQL().Select(). 40 | {{ $column := $table.Lookup $columnName }} 41 | {{ $column.MethodName }}In(cs...). 42 | AllContext(ctx, db) 43 | if err != nil { 44 | return nil, fmt.Errorf("failed to list {{ $structName | pluralize }} by {{ $listColumnName }}: %w", err) 45 | } 46 | rows := make({{ $sliceTypeName }}, len(_rows)) 47 | for i := range _rows { 48 | rows[i] = &_rows[i] 49 | } 50 | return rows, nil 51 | } 52 | {{- end }} 53 | {{- end }} 54 | {{- end }} 55 | 56 | {{- if not (empty .Args.list) }} 57 | {{ $listColumns := splitList "," .Args.list }} 58 | {{ range $index, $joinedColumnNames := $listColumns }} 59 | {{ $columnNames := splitList "&" $joinedColumnNames }} 60 | {{ $methodName := join "And" $columnNames }} 61 | func ({{ $receiver }} *{{ $tableTypeName }}) ListBy{{ $methodName }}(ctx context.Context, db sqlla.DB, 62 | {{- range $index, $columnName := $columnNames -}} 63 | c{{ $index }}{{ " " }} 64 | {{- $column := $table.Lookup $columnName -}} 65 | {{ $column.TypeName }}, 66 | {{- end }}) ({{ $sliceTypeName }}, error) { 67 | _rows, err := New{{ $structName }}SQL().Select(). 68 | {{- range $index, $columnName := $columnNames }} 69 | {{ $column := $table.Lookup $columnName }} 70 | {{ $column.MethodName }}(c{{ $index }}). 71 | {{- end }} 72 | AllContext(ctx, db) 73 | if err != nil { 74 | return nil, fmt.Errorf("failed to list {{ $structName }} by {{ join " and " $columnNames }}: %w", err) 75 | } 76 | rows := make({{ $sliceTypeName }}, len(_rows)) 77 | for i := range _rows { 78 | rows[i] = &_rows[i] 79 | } 80 | return rows, nil 81 | } 82 | {{- end }} 83 | {{- end }} 84 | 85 | {{- if not (empty .Args.create) }} 86 | {{ $createColumns := splitList "," .Args.create }} 87 | {{ $createInputTypeName := printf "%sCreateInput" $tableTypeName }} 88 | 89 | {{ $hasPk := false }} 90 | {{ $noNullTColumns := list }} 91 | {{ $nullTColumns := list }} 92 | type {{ $createInputTypeName }} struct { 93 | {{- range $index, $columnName := $createColumns }} 94 | {{- $column := $table.Lookup $columnName -}} 95 | {{- if $column.IsPk }}{{ $hasPk = true }}{{ end }} 96 | {{- if $column.IsNullT }} 97 | {{- $nullTColumns = append $nullTColumns $column.FieldName }} 98 | {{ $column.FieldName }} sql.Null[{{ $column.TypeName }}] 99 | {{- else }} 100 | {{- $noNullTColumns = append $noNullTColumns $column.FieldName }} 101 | {{ $column.FieldName }} {{ $column.TypeName }} 102 | {{- end }} 103 | {{- end }} 104 | } 105 | 106 | func ({{ $receiver }} *{{ $tableTypeName }}) newCreateSQL(input {{ $createInputTypeName }}) {{ $structName | untitle }}InsertSQL { 107 | query := New{{ $structName }}SQL().Insert(). 108 | {{ $lastIndex := sub (len $noNullTColumns) 1 }} 109 | {{- range $index, $columnName := $noNullTColumns }} 110 | {{- $column := $table.Lookup $columnName }} 111 | Value{{ $column.MethodName }}(input.{{ $column.FieldName }}) 112 | {{- if ne $lastIndex $index }}.{{ end }} 113 | {{- end }} 114 | {{- range $index, $columnName := $nullTColumns }} 115 | {{- $column := $table.Lookup $columnName }} 116 | if input.{{ $columnName }}.Valid { 117 | query = query.Value{{ $column.MethodName }}(input.{{ $columnName }}.V) 118 | } else { 119 | query = query.Value{{ $column.MethodName }}IsNull() 120 | } 121 | {{- end }} 122 | return query 123 | } 124 | 125 | func ({{ $receiver }} *{{ $tableTypeName }}) Create(ctx context.Context, db sqlla.DB, input {{ $createInputTypeName }}) (*{{ $structName }}, error) { 126 | {{- if $hasPk }} 127 | _, err := {{ $receiver }}.newCreateSQL(input).ExecContextWithoutSelect(ctx, db) 128 | {{- else }} 129 | row, err := {{ $receiver }}.newCreateSQL(input).ExecContext(ctx, db) 130 | {{- end }} 131 | if err != nil { 132 | return nil, fmt.Errorf("failed to create {{ $structName }}: %w", err) 133 | } 134 | {{- if $hasPk }} 135 | row, err := New{{ $structName }}SQL().Select(). 136 | {{ $table.PkColumn.MethodName }}(input.{{ $table.PkColumn.FieldName }}). 137 | SingleContext(ctx, db) 138 | if err != nil { 139 | return nil, fmt.Errorf("failed to get created {{ $structName }}: %w", err) 140 | } 141 | {{- end }} 142 | return &row, nil 143 | } 144 | 145 | func ({{ $receiver }} *{{ $tableTypeName }}) CreateMulti(ctx context.Context, db sqlla.DB, inputs []{{ $createInputTypeName }}) ( 146 | {{- if $hasPk }}{{ $sliceTypeName }},{{ end }} error) { 147 | bi := New{{ $structName }}SQL().BulkInsert() 148 | {{- if $hasPk }} 149 | ids := make([]{{ $table.PkColumn.TypeName }}, len(inputs)) 150 | {{- end }} 151 | for _, input := range inputs { 152 | bi.Append({{ $receiver }}.newCreateSQL(input)) 153 | {{- if $hasPk }} 154 | ids = append(ids, input.{{ $table.PkColumn.FieldName }}) 155 | {{- end }} 156 | } 157 | if _, err := bi.ExecContext(ctx, db); err != nil { 158 | {{- if $hasPk }} 159 | return nil, fmt.Errorf("failed to create {{ $structName }}s: %w", err) 160 | {{- else }} 161 | return fmt.Errorf("failed to create {{ $structName }}s: %w", err) 162 | {{- end }} 163 | } 164 | {{- if $hasPk }} 165 | _rows, err := New{{ $structName }}SQL().Select(). 166 | {{ $table.PkColumn.MethodName }}In(ids...). 167 | AllContext(ctx, db) 168 | if err != nil { 169 | return nil, fmt.Errorf("failed to get created {{ $structName }}s: %w", err) 170 | } 171 | rows := make({{ $sliceTypeName }}, len(_rows)) 172 | for i := range _rows { 173 | rows[i] = &_rows[i] 174 | } 175 | return rows, nil 176 | {{- else }} 177 | return nil 178 | {{- end }} 179 | } 180 | {{- end }} 181 | {{ end }} 182 | -------------------------------------------------------------------------------- /template/plugins/timeHooks.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "plugin.timeHooks" }} 2 | {{- $structName := .Table.StructName }} 3 | {{- $receiver := substr 0 1 $structName | lower }} 4 | {{ if not (empty .Args.create) }} 5 | {{- $createHookColumns := splitList "," .Args.create }} 6 | {{- $insertSQLTypeName := printf "%sInsertSQL" ($structName | untitle) }} 7 | func ({{$receiver}} {{$structName}}) DefaultInsertHook(_q {{ $insertSQLTypeName }}) ({{ $insertSQLTypeName }}, error) { 8 | now := time.Now() 9 | return _q. 10 | {{- $lastIndex := sub (len $createHookColumns) 1 }} 11 | {{- range $index, $column := $createHookColumns }} 12 | Value{{ $column }}(now){{ if ne $lastIndex $index }}.{{ end }} 13 | {{- end }}, nil 14 | } 15 | {{- end }} 16 | 17 | {{ if not (empty .Args.update) }} 18 | {{- $updateHookColumns := splitList "," .Args.update }} 19 | {{- $updateSQLTypeName := printf "%sUpdateSQL" ($structName | untitle) }} 20 | func ({{$receiver}} {{$structName}}) DefaultUpdateHook(_q {{ $updateSQLTypeName }}) ({{ $updateSQLTypeName }}, error) { 21 | now := time.Now() 22 | return _q. 23 | {{- $lastIndex := sub (len $updateHookColumns) 1 }} 24 | {{- range $index, $column := $updateHookColumns }} 25 | Set{{ $column }}(now){{ if ne $lastIndex $index }}.{{ end }} 26 | {{- end }}, nil 27 | } 28 | {{- end }} 29 | 30 | {{ if not (empty .Args.sameOnUpdate) }} 31 | {{- $sameOnUpdateColumns := splitList "," .Args.sameOnUpdate }} 32 | {{- $insertOnDuplicateKeyUpdateSQLTypeName := printf "%sInsertOnDuplicateKeyUpdateSQL" (.Table.StructName | untitle) }} 33 | func ({{$receiver}} {{$structName}}) DefaultInsertOnDuplicateKeyUpdateHook(_q {{ $insertOnDuplicateKeyUpdateSQLTypeName }}) ({{ $insertOnDuplicateKeyUpdateSQLTypeName }}, error) { 34 | return _q. 35 | {{- $lastIndex := sub (len $sameOnUpdateColumns) 1 }} 36 | {{- range $index, $column := $sameOnUpdateColumns }} 37 | SameOnUpdate{{ $column }}(){{ if ne $lastIndex $index }}.{{ end }} 38 | {{- end }}, nil 39 | } 40 | {{- end }} 41 | {{ end }} 42 | -------------------------------------------------------------------------------- /template/select.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "Select" }} 2 | {{- $camelName := .Name | toCamel | untitle -}} 3 | {{- $constructor := printf "New%sSQL" (.Name | toCamel | title) -}} 4 | var {{ $camelName }}AllColumns = []string{ 5 | {{ range .Columns }}{{ cquoteby .Name }},{{ end }} 6 | } 7 | 8 | type {{ $camelName }}SelectSQL struct { 9 | {{ $camelName }}SQL 10 | Columns []string 11 | order sqlla.OrderWithColumn 12 | limit *uint64 13 | offset *uint64 14 | tableAlias string 15 | joinClauses []string 16 | 17 | {{ if eq (dialect) "mysql" }} 18 | additionalWhereClause string 19 | additionalWhereClauseArgs []interface{} 20 | {{ end -}} 21 | {{ if eq (dialect) "postgresql" }} 22 | additionalWhereClause func (int) (string, int, []any) 23 | {{ end -}} 24 | 25 | groupByColumns []string 26 | 27 | isForUpdate bool 28 | } 29 | 30 | func (q {{ $camelName }}SQL) Select() {{ $camelName }}SelectSQL { 31 | return {{ $camelName }}SelectSQL{ 32 | q, 33 | {{ $camelName }}AllColumns, 34 | nil, 35 | nil, 36 | nil, 37 | "", 38 | nil, 39 | {{- if eq (dialect) "mysql" }} 40 | "", 41 | nil, 42 | {{ end -}} 43 | {{- if eq (dialect) "postgresql" }}nil,{{ end }} 44 | nil, 45 | false, 46 | } 47 | } 48 | 49 | func (q {{ $camelName }}SelectSQL) Or(qs ...{{ $camelName }}SelectSQL) {{ $camelName }}SelectSQL { 50 | ws := make([]sqlla.Where, 0, len(qs)) 51 | for _, q := range qs { 52 | ws = append(ws, q.where) 53 | } 54 | q.where = append(q.where, sqlla.ExprOr(ws)) 55 | return q 56 | } 57 | 58 | func (q {{ $camelName }}SelectSQL) Limit(l uint64) {{ $camelName }}SelectSQL { 59 | q.limit = &l 60 | return q 61 | } 62 | 63 | func (q {{ $camelName }}SelectSQL) Offset(o uint64) {{ $camelName }}SelectSQL { 64 | q.offset = &o 65 | return q 66 | } 67 | 68 | func (q {{ $camelName }}SelectSQL) ForUpdate() {{ $camelName }}SelectSQL { 69 | q.isForUpdate = true 70 | return q 71 | } 72 | 73 | func (q {{ $camelName }}SelectSQL) TableAlias(alias string) {{ $camelName }}SelectSQL { 74 | q.tableAlias = {{ cquote }} + alias + {{ cquote }} 75 | return q 76 | } 77 | 78 | func (q {{ $camelName }}SelectSQL) SetColumns(columns ...string) {{ $camelName }}SelectSQL { 79 | q.Columns = make([]string, 0, len(columns)) 80 | for _, column := range columns { 81 | if strings.ContainsAny(column, "(." + {{ cquote }}) { 82 | q.Columns = append(q.Columns, column) 83 | } else { 84 | q.Columns = append(q.Columns, {{ cquote }} + column + {{ cquote }}) 85 | } 86 | } 87 | return q 88 | } 89 | 90 | func (q {{ $camelName }}SelectSQL) JoinClause(clause string) {{ $camelName }}SelectSQL { 91 | q.joinClauses = append(q.joinClauses, clause) 92 | return q 93 | } 94 | 95 | {{ if eq (dialect) "mysql" }} 96 | func (q {{ $camelName }}SelectSQL) AdditionalWhereClause(clause string, args ...interface{}) {{ $camelName }}SelectSQL { 97 | q.additionalWhereClause = clause 98 | q.additionalWhereClauseArgs = args 99 | return q 100 | } 101 | {{- end }} 102 | 103 | {{ if eq (dialect) "postgresql" }} 104 | func (q {{ $camelName }}SelectSQL) AdditionalWhereClause(clause func (int) (string, int, []any)) {{ $camelName }}SelectSQL { 105 | q.additionalWhereClause = clause 106 | return q 107 | } 108 | {{- end }} 109 | 110 | func (q {{ $camelName }}SelectSQL) appendColumnPrefix(column string) string { 111 | if q.tableAlias == "" || strings.ContainsAny(column, "(.") { 112 | return column 113 | } 114 | return q.tableAlias + "." + column 115 | } 116 | 117 | func (q {{ $camelName }}SelectSQL) GroupBy(columns ...string) {{ $camelName }}SelectSQL { 118 | q.groupByColumns = make([]string, 0, len(columns)) 119 | for _, column := range columns { 120 | if strings.ContainsAny(column, "(." + {{ cquote }}) { 121 | q.groupByColumns = append(q.groupByColumns, column) 122 | } else { 123 | q.groupByColumns = append(q.groupByColumns, {{ cquote }} + column + {{ cquote }}) 124 | } 125 | } 126 | return q 127 | } 128 | 129 | {{ range .Columns }}{{ template "SelectColumn" . }}{{ end }} 130 | func (q {{ $camelName }}SelectSQL) ToSql() (string, []interface{}, error) { 131 | columns := strings.Join(q.Columns, ", ") 132 | {{- if eq (dialect) "mysql" }} 133 | wheres, vs, err := q.where.ToSql() 134 | {{- end }} 135 | {{- if eq (dialect) "postgresql" }} 136 | wheres, offset, vs, err := q.where.ToSqlPg(0) 137 | {{- end }} 138 | if err != nil { 139 | return "", nil, err 140 | } 141 | 142 | tableName := {{ cquoteby .TableName }} 143 | if q.tableAlias != "" { 144 | tableName = tableName + " AS " + q.tableAlias 145 | pcs := make([]string, 0, len(q.Columns)) 146 | for _, column := range q.Columns { 147 | pcs = append(pcs, q.appendColumnPrefix(column)) 148 | } 149 | columns = strings.Join(pcs, ", ") 150 | } 151 | query := "SELECT " + columns + " FROM " + tableName 152 | if len(q.joinClauses) > 0 { 153 | jc := strings.Join(q.joinClauses, " ") 154 | query += " " + jc 155 | } 156 | if wheres != "" { 157 | query += " WHERE" + wheres 158 | } 159 | {{- if eq (dialect) "mysql" }} 160 | if q.additionalWhereClause != "" { 161 | query += " " + q.additionalWhereClause 162 | if len(q.additionalWhereClauseArgs) > 0 { 163 | vs = append(vs, q.additionalWhereClauseArgs...) 164 | } 165 | {{- end }} 166 | {{- if eq (dialect) "postgresql" }} 167 | if q.additionalWhereClause != nil { 168 | _query, _offset, _args := q.additionalWhereClause(offset) 169 | query += " " + _query 170 | if len(_args) > 0 { 171 | vs = append(vs, _args...) 172 | } 173 | offset = _offset 174 | {{- end }} 175 | } 176 | if len(q.groupByColumns) > 0 { 177 | query += " GROUP BY " 178 | gbcs := make([]string, 0, len(q.groupByColumns)) 179 | for _, column := range q.groupByColumns { 180 | gbcs = append(gbcs, q.appendColumnPrefix(column)) 181 | } 182 | query += strings.Join(gbcs, ", ") 183 | } 184 | if q.order != nil { 185 | {{- if eq (dialect) "mysql" }} 186 | query += " ORDER BY " + q.order.OrderExpr() 187 | {{- end }} 188 | {{- if eq (dialect) "postgresql" }} 189 | _query, _ := q.order.OrderExprPg(offset) 190 | query += " ORDER BY " + _query 191 | {{- end }} 192 | vs = append(vs, q.order.Values()...) 193 | } 194 | if q.limit != nil { 195 | query += " LIMIT " + strconv.FormatUint(*q.limit, 10) 196 | } 197 | if q.offset != nil { 198 | query += " OFFSET " + strconv.FormatUint(*q.offset, 10) 199 | } 200 | 201 | if q.isForUpdate { 202 | query += " FOR UPDATE" 203 | } 204 | 205 | return query + ";", vs, nil 206 | } 207 | 208 | {{ if .HasPk -}} 209 | func (s {{ .StructName }}) Select() ({{ $camelName }}SelectSQL) { 210 | return {{ $constructor }}().Select().{{ .PkColumn.Name | toCamel | title }}(s.{{ .PkColumn.FieldName }}) 211 | } 212 | {{ end -}} 213 | 214 | func (q {{ $camelName }}SelectSQL) Single(db sqlla.DB) ({{ .StructName }}, error) { 215 | q.Columns = {{ $camelName }}AllColumns 216 | query, args, err := q.ToSql() 217 | if err != nil { 218 | return {{ .StructName }}{}, err 219 | } 220 | 221 | row := db.QueryRow(query, args...) 222 | return q.Scan(row) 223 | } 224 | 225 | func (q {{ $camelName }}SelectSQL) SingleContext(ctx context.Context, db sqlla.DB) ({{ .StructName }}, error) { 226 | q.Columns = {{ $camelName }}AllColumns 227 | query, args, err := q.ToSql() 228 | if err != nil { 229 | return {{ .StructName }}{}, err 230 | } 231 | 232 | row := db.QueryRowContext(ctx, query, args...) 233 | return q.Scan(row) 234 | } 235 | 236 | func (q {{ $camelName }}SelectSQL) All(db sqlla.DB) ([]{{ .StructName }}, error) { 237 | rs := make([]{{ .StructName }}, 0, 10) 238 | q.Columns = {{ $camelName }}AllColumns 239 | query, args, err := q.ToSql() 240 | if err != nil { 241 | return nil, err 242 | } 243 | 244 | rows, err := db.Query(query, args...) 245 | if err != nil { 246 | return nil, err 247 | } 248 | defer rows.Close() 249 | for rows.Next() { 250 | r, err := q.Scan(rows) 251 | if err != nil { 252 | return nil, err 253 | } 254 | rs = append(rs, r) 255 | } 256 | return rs, nil 257 | } 258 | 259 | func (q {{ $camelName }}SelectSQL) AllContext(ctx context.Context, db sqlla.DB) ([]{{ .StructName }}, error) { 260 | rs := make([]{{ .StructName }}, 0, 10) 261 | q.Columns = {{ $camelName }}AllColumns 262 | query, args, err := q.ToSql() 263 | if err != nil { 264 | return nil, err 265 | } 266 | 267 | rows, err := db.QueryContext(ctx, query, args...) 268 | if err != nil { 269 | return nil, err 270 | } 271 | defer rows.Close() 272 | for rows.Next() { 273 | r, err := q.Scan(rows) 274 | if err != nil { 275 | return nil, err 276 | } 277 | rs = append(rs, r) 278 | } 279 | return rs, nil 280 | } 281 | 282 | func (q {{ $camelName }}SelectSQL) Scan(s sqlla.Scanner) ({{ .StructName }}, error) { 283 | var row {{ .StructName }} 284 | err := s.Scan( 285 | {{ range .Columns }}&row.{{ .FieldName }}, 286 | {{ end }} 287 | ) 288 | return row, err 289 | } 290 | 291 | // IterContext returns iter.Seq2[{{ .StructName }}, error] and closer. 292 | // 293 | // The returned Iter.Seq2 assembles and executes a query in the first iteration. 294 | // Therefore, the first iteration may return an error in assembling or executing the query. 295 | // Subsequent iterations read rows. Again, the read may return an error. 296 | // 297 | // closer is a function that closes the row reader object. Execution of this function is idempotent. 298 | // Be sure to call it when you are done using iter.Seq2. 299 | func (q {{ $camelName }}SelectSQL) IterContext(ctx context.Context, db sqlla.DB) (func (func ({{ .StructName }}, error) bool), func() error) { 300 | var rowClose func() error 301 | closer := func() error { 302 | if rowClose != nil { 303 | err := rowClose() 304 | rowClose = nil 305 | return err 306 | } 307 | return nil 308 | } 309 | 310 | q.Columns = {{ $camelName }}AllColumns 311 | query, args, err := q.ToSql() 312 | return func (yield func({{ .StructName}}, error) bool) { 313 | if err != nil { 314 | var r {{ .StructName }} 315 | yield(r, err) 316 | return 317 | } 318 | rows, err := db.QueryContext(ctx, query, args...) 319 | if err != nil { 320 | var r {{ .StructName }} 321 | yield(r, err) 322 | return 323 | } 324 | rowClose = rows.Close 325 | for rows.Next() { 326 | r, err := q.Scan(rows) 327 | if !yield(r, err) { 328 | break 329 | } 330 | } 331 | }, closer 332 | } 333 | {{ end }} 334 | -------------------------------------------------------------------------------- /template/select_column.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "SelectColumn" }} 2 | {{- $smallTableName := .TableName | toCamel | untitle }} 3 | {{- if .IsNullT }} 4 | {{- template "SelectColumnNullT" . }} 5 | {{- else }} 6 | func (q {{ $smallTableName }}SelectSQL) {{ .MethodName }}(v {{ .TypeName }}, exprs ...sqlla.Operator) {{ $smallTableName }}SelectSQL { 7 | where := {{ .ExprValue }}{Value: {{ .ExprValueIdentifier }}, Op: sqlla.Operators(exprs), Column: q.appendColumnPrefix({{ cquoteby .Name }})} 8 | q.where = append(q.where, where) 9 | return q 10 | } 11 | 12 | func (q {{ $smallTableName }}SelectSQL) {{ .MethodName }}In(vs ...{{ .TypeName }}) {{ $smallTableName }}SelectSQL { 13 | {{- if .HasUnderlyingType }} 14 | _vs := make([]{{ .BaseTypeName }}, 0, len(vs)) 15 | for _, v := range vs { 16 | _vs = append(_vs, {{ .ExprValueIdentifier }}) 17 | } 18 | where := {{ .ExprMultiValue }}{Values: _vs, Op: sqlla.MakeInOperator(len(vs)), Column: q.appendColumnPrefix({{ cquoteby .Name }})} 19 | {{- else }} 20 | where := {{ .ExprMultiValue }}{Values: vs, Op: sqlla.MakeInOperator(len(vs)), Column: q.appendColumnPrefix({{ cquoteby .Name }})} 21 | {{- end }} 22 | q.where = append(q.where, where) 23 | return q 24 | } 25 | {{- end }} 26 | 27 | {{ if and .IsPk (eq (dialect) "mysql") -}} 28 | func (q {{ $smallTableName }}SelectSQL) PkColumn(pk int64, exprs ...sqlla.Operator) {{ $smallTableName }}SelectSQL { 29 | v := {{ .TypeName }}(pk) 30 | return q.{{ .MethodName }}(v, exprs...) 31 | } 32 | {{- end }} 33 | 34 | func (q {{ $smallTableName }}SelectSQL) OrderBy{{ .MethodName }}(order sqlla.Order) {{ $smallTableName }}SelectSQL { 35 | q.order = order.WithColumn(q.appendColumnPrefix({{ cquoteby .Name }})) 36 | return q 37 | } 38 | {{ end }} 39 | -------------------------------------------------------------------------------- /template/select_column_nullt.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "SelectColumnNullT" }} 2 | {{ $smallTableName := .TableName | toCamel | untitle }} 3 | func (q {{ $smallTableName }}SelectSQL) {{ .MethodName }}(v {{ .TypeName }}, exprs ...sqlla.Operator) {{ $smallTableName }}SelectSQL { 4 | where := {{ .ExprValue }}{Value: sql.Null[{{ .BaseTypeName }}]{ V: {{ .ExprValueIdentifier }}, Valid: true }, Op: sqlla.Operators(exprs), Column: q.appendColumnPrefix({{ cquoteby .Name }})} 5 | q.where = append(q.where, where) 6 | return q 7 | } 8 | 9 | func (q {{ $smallTableName }}SelectSQL) {{ .MethodName }}IsNull() {{ $smallTableName }}SelectSQL { 10 | where := {{ .ExprValue }}{Value: sql.Null[{{ .BaseTypeName }}]{ Valid: false }, Op: sqlla.Operators{sqlla.OpEqual}, Column: q.appendColumnPrefix({{ cquoteby .Name }})} 11 | q.where = append(q.where, where) 12 | return q 13 | } 14 | 15 | func (q {{ $smallTableName }}SelectSQL) {{ .MethodName }}IsNotNull() {{ $smallTableName }}SelectSQL { 16 | where := {{ .ExprValue }}{Value: sql.Null[{{ .BaseTypeName }}]{ Valid: false }, Op: sqlla.Operators{sqlla.OpNot}, Column: q.appendColumnPrefix({{ cquoteby .Name }})} 17 | q.where = append(q.where, where) 18 | return q 19 | } 20 | 21 | func (q {{ $smallTableName }}SelectSQL) {{ .MethodName }}In(vs ...{{ .TypeName }}) {{ $smallTableName }}SelectSQL { 22 | _vs := make([]{{ .BaseTypeName }}, 0, len(vs)) 23 | for _, v := range vs { 24 | _vs = append(_vs, {{ .ExprValueIdentifier }}) 25 | } 26 | where := {{ .ExprMultiValue }}{Values: _vs, Op: sqlla.MakeInOperator(len(vs)), Column: q.appendColumnPrefix({{ cquoteby .Name }})} 27 | q.where = append(q.where, where) 28 | return q 29 | } 30 | {{- end }} 31 | -------------------------------------------------------------------------------- /template/table.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/mackee/go-sqlla/v2/cmd/sqlla - DO NOT EDIT. 2 | package {{ .PackageName }} 3 | 4 | import ( 5 | "strings" 6 | "strconv" 7 | "context" 8 | "fmt" 9 | 10 | "database/sql" 11 | {{ range .AdditionalPackages -}} 12 | "{{ . }}" 13 | {{ end }} 14 | "github.com/mackee/go-sqlla/v2" 15 | ) 16 | {{ $camelName := .Name | toCamel | untitle }} 17 | type {{ $camelName }}SQL struct { 18 | where sqlla.Where 19 | } 20 | 21 | func New{{ .Name | toCamel | title }}SQL() {{ $camelName }}SQL { 22 | q := {{ $camelName }}SQL{} 23 | return q 24 | } 25 | 26 | {{ template "Select" . }} 27 | {{ template "Update" . }} 28 | {{ template "Insert" . }} 29 | {{ template "Delete" . }} 30 | -------------------------------------------------------------------------------- /template/update.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "Update" }}{{ $camelName := .Name | toCamel | untitle }} 2 | {{- $constructor := printf "New%sSQL" (.Name | toCamel | title) -}} 3 | type {{ $camelName }}UpdateSQL struct { 4 | {{ $camelName }}SQL 5 | setMap sqlla.SetMap 6 | Columns []string 7 | } 8 | 9 | func (q {{ $camelName }}SQL) Update() {{ $camelName }}UpdateSQL { 10 | return {{ $camelName }}UpdateSQL{ 11 | {{ $camelName }}SQL: q, 12 | setMap: sqlla.SetMap{}, 13 | } 14 | } 15 | 16 | {{ range .Columns }}{{ template "UpdateColumn" . }}{{ end }} 17 | func (q {{ $camelName }}UpdateSQL) ToSql() (string, []interface{}, error) { 18 | var err error 19 | var s interface{} = {{ .StructName }}{} 20 | if t, ok := s.({{ $camelName }}DefaultUpdateHooker); ok { 21 | q, err = t.DefaultUpdateHook(q) 22 | if err != nil { 23 | return "", []interface{}{}, err 24 | } 25 | } 26 | {{- if eq (dialect) "mysql" }} 27 | setColumns, svs, err := q.setMap.ToUpdateSql() 28 | {{- end }} 29 | {{- if eq (dialect) "postgresql" }} 30 | setColumns, offset, svs, err := q.setMap.ToUpdateSqlPg(0) 31 | {{- end }} 32 | if err != nil { 33 | return "", []interface{}{}, err 34 | } 35 | {{- if eq (dialect) "mysql" }} 36 | wheres, wvs, err := q.where.ToSql() 37 | {{- end }} 38 | {{- if eq (dialect) "postgresql" }} 39 | wheres, _, wvs, err := q.where.ToSqlPg(offset) 40 | {{- end }} 41 | if err != nil { 42 | return "", []interface{}{}, err 43 | } 44 | 45 | query := "UPDATE " + {{ cquoteby .TableName }} + " SET" + setColumns 46 | if wheres != "" { 47 | query += " WHERE" + wheres 48 | } 49 | 50 | return query + ";", append(svs, wvs...), nil 51 | } 52 | 53 | {{- if .HasPk }} 54 | func (s {{ .StructName }}) Update() {{ $camelName }}UpdateSQL { 55 | return {{ $constructor }}().Update().Where{{ .PkColumn.Name | toCamel | title }}(s.{{ .PkColumn.FieldName }}) 56 | } 57 | 58 | func (q {{ $camelName }}UpdateSQL) Exec(db sqlla.DB) ([]{{ .StructName }}, error) { 59 | query, args, err := q.ToSql() 60 | if err != nil { 61 | return nil, err 62 | } 63 | _, err = db.Exec(query, args...) 64 | if err != nil { 65 | return nil, err 66 | } 67 | qq := q.{{ $camelName }}SQL 68 | 69 | return qq.Select().All(db) 70 | } 71 | 72 | func (q {{ $camelName }}UpdateSQL) ExecContext(ctx context.Context, db sqlla.DB) ([]{{ .StructName }}, error) { 73 | query, args, err := q.ToSql() 74 | if err != nil { 75 | return nil, err 76 | } 77 | _, err = db.ExecContext(ctx, query, args...) 78 | if err != nil { 79 | return nil, err 80 | } 81 | qq := q.{{ $camelName }}SQL 82 | 83 | return qq.Select().AllContext(ctx, db) 84 | } 85 | {{- else }} 86 | func (q {{ $camelName }}UpdateSQL) Exec(db sqlla.DB) (sql.Result, error) { 87 | query, args, err := q.ToSql() 88 | if err != nil { 89 | return nil, err 90 | } 91 | return db.Exec(query, args...) 92 | } 93 | 94 | func (q {{ $camelName }}UpdateSQL) ExecContext(ctx context.Context, db sqlla.DB) (sql.Result, error) { 95 | query, args, err := q.ToSql() 96 | if err != nil { 97 | return nil, err 98 | } 99 | return db.ExecContext(ctx, query, args...) 100 | } 101 | {{- end }} 102 | 103 | type {{ $camelName }}DefaultUpdateHooker interface { 104 | DefaultUpdateHook({{ $camelName }}UpdateSQL) ({{ $camelName }}UpdateSQL, error) 105 | } 106 | {{ end }} 107 | -------------------------------------------------------------------------------- /template/update_column.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "UpdateColumn" }}{{ $smallTableName := .TableName | toCamel | untitle }} 2 | {{- if .IsNullT }} 3 | {{- template "UpdateColumnNullT" . }} 4 | {{- else }} 5 | func (q {{ $smallTableName }}UpdateSQL) Set{{ .MethodName }}(v {{ .TypeName }}) {{ $smallTableName }}UpdateSQL { 6 | q.setMap[{{ cquoteby .Name }}] = {{ .ExprValueIdentifier }} 7 | return q 8 | } 9 | 10 | func (q {{ $smallTableName }}UpdateSQL) Where{{ .MethodName }}(v {{ .TypeName }}, exprs ...sqlla.Operator) {{ $smallTableName }}UpdateSQL { 11 | where := {{ .ExprValue }}{Value: {{ .ExprValueIdentifier }}, Op: sqlla.Operators(exprs), Column: {{ cquoteby .Name }}} 12 | q.where = append(q.where, where) 13 | return q 14 | } 15 | 16 | func (q {{ $smallTableName }}UpdateSQL) Where{{ .MethodName }}In(vs ...{{ .TypeName }}) {{ $smallTableName }}UpdateSQL { 17 | {{- if .HasUnderlyingType }} 18 | _vs := make([]{{ .BaseTypeName }}, 0, len(vs)) 19 | for _, v := range vs { 20 | _vs = append(_vs, {{ .BaseTypeName }}(v)) 21 | } 22 | where := {{ .ExprMultiValue }}{Values: _vs, Op: sqlla.MakeInOperator(len(vs)), Column: {{ cquoteby .Name }}} 23 | {{- else }} 24 | where := {{ .ExprMultiValue }}{Values: vs, Op: sqlla.MakeInOperator(len(vs)), Column: {{ cquoteby .Name }}} 25 | {{- end }} 26 | q.where = append(q.where, where) 27 | return q 28 | } 29 | {{- end }} 30 | {{ end }} 31 | -------------------------------------------------------------------------------- /template/update_column_nullt.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "UpdateColumnNullT" }}{{ $smallTableName := .TableName | toCamel | untitle }} 2 | func (q {{ $smallTableName }}UpdateSQL) Set{{ .MethodName }}(v {{ .TypeName }}) {{ $smallTableName }}UpdateSQL { 3 | q.setMap[{{ cquoteby .Name }}] = {{ .ExprValueIdentifier }} 4 | return q 5 | } 6 | 7 | func (q {{ $smallTableName }}UpdateSQL) Set{{ .MethodName }}ToNull() {{ $smallTableName }}UpdateSQL { 8 | q.setMap[{{ cquoteby .Name }}] = sql.Null[{{ .BaseTypeName }}]{ Valid: false } 9 | return q 10 | } 11 | 12 | func (q {{ $smallTableName }}UpdateSQL) Where{{ .MethodName }}(v {{ .TypeName }}, exprs ...sqlla.Operator) {{ $smallTableName }}UpdateSQL { 13 | where := {{ .ExprValue }}{Value: sql.Null[{{ .BaseTypeName }}]{ V: {{ .ExprValueIdentifier }}, Valid: true }, Op: sqlla.Operators(exprs), Column: {{ cquoteby .Name }}} 14 | q.where = append(q.where, where) 15 | return q 16 | } 17 | 18 | func (q {{ $smallTableName }}UpdateSQL) Where{{ .MethodName }}IsNull() {{ $smallTableName }}UpdateSQL { 19 | where := {{ .ExprValue }}{Value: sql.Null[{{ .BaseTypeName }}]{ Valid: false }, Op: sqlla.Operators([]sqlla.Operator{sqlla.OpEqual}), Column: {{ cquoteby .Name }}} 20 | q.where = append(q.where, where) 21 | return q 22 | } 23 | 24 | func (q {{ $smallTableName }}UpdateSQL) Where{{ .MethodName }}IsNotNull() {{ $smallTableName }}UpdateSQL { 25 | where := {{ .ExprValue }}{Value: sql.Null[{{ .BaseTypeName }}]{ Valid: false }, Op: sqlla.Operators([]sqlla.Operator{sqlla.OpNot}), Column: {{ cquoteby .Name }}} 26 | q.where = append(q.where, where) 27 | return q 28 | } 29 | 30 | func (q {{ $smallTableName }}UpdateSQL) Where{{ .MethodName }}In(vs ...{{ .TypeName }}) {{ $smallTableName }}UpdateSQL { 31 | _vs := make([]{{ .BaseTypeName }}, 0, len(vs)) 32 | for _, v := range vs { 33 | _vs = append(_vs, {{ .ExprValueIdentifier }}) 34 | } 35 | where := {{ .ExprMultiValue }}{Values: _vs, Op: sqlla.MakeInOperator(len(vs)), Column: {{ cquoteby .Name }}} 36 | q.where = append(q.where, where) 37 | return q 38 | } 39 | {{ end }} 40 | -------------------------------------------------------------------------------- /testdata/nullt/repoa/go.mod: -------------------------------------------------------------------------------- 1 | module gopher.example/repoa 2 | 3 | go 1.22.4 4 | -------------------------------------------------------------------------------- /testdata/nullt/repoa/schema.go: -------------------------------------------------------------------------------- 1 | package repoa 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | ) 7 | 8 | type NullableTime sql.Null[time.Time] 9 | 10 | //sqlla:table product 11 | type Product struct { 12 | ID uint64 `db:"id,primarykey,autoincrement"` 13 | ModifiedAt sql.Null[time.Time] `db:"modified_at"` 14 | RemovedAt NullableTime `db:"removed_at"` 15 | } 16 | --------------------------------------------------------------------------------