├── .gitignore
├── .idea
├── .gitignore
├── modules.xml
├── sqlc.iml
└── vcs.xml
├── LICENSE
├── README.md
├── builder.go
├── context.go
├── db.go
├── example
├── db.go
├── models.go
├── query.sql
└── query.sql.go
├── go.mod
├── go.sum
├── interface.go
├── sqlc.json
├── wrap.go
└── wrap_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/go,macos,goland,visualstudiocode
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,macos,goland,visualstudiocode
4 |
5 | ### Go ###
6 | # If you prefer the allow list template instead of the deny list, see community template:
7 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
8 | #
9 | # Binaries for programs and plugins
10 | *.exe
11 | *.exe~
12 | *.dll
13 | *.so
14 | *.dylib
15 |
16 | # Test binary, built with `go test -c`
17 | *.test
18 |
19 | # Output of the go coverage tool, specifically when used with LiteIDE
20 | *.out
21 |
22 | # Dependency directories (remove the comment below to include it)
23 | # vendor/
24 |
25 | # Go workspace file
26 | go.work
27 |
28 | ### Go Patch ###
29 | /vendor/
30 | /Godeps/
31 |
32 | ### GoLand ###
33 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
34 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
35 |
36 | # User-specific stuff
37 | .idea/**/workspace.xml
38 | .idea/**/tasks.xml
39 | .idea/**/usage.statistics.xml
40 | .idea/**/dictionaries
41 | .idea/**/shelf
42 |
43 | # AWS User-specific
44 | .idea/**/aws.xml
45 |
46 | # Generated files
47 | .idea/**/contentModel.xml
48 |
49 | # Sensitive or high-churn files
50 | .idea/**/dataSources/
51 | .idea/**/dataSources.ids
52 | .idea/**/dataSources.local.xml
53 | .idea/**/sqlDataSources.xml
54 | .idea/**/dynamic.xml
55 | .idea/**/uiDesigner.xml
56 | .idea/**/dbnavigator.xml
57 |
58 | # Gradle
59 | .idea/**/gradle.xml
60 | .idea/**/libraries
61 |
62 | # Gradle and Maven with auto-import
63 | # When using Gradle or Maven with auto-import, you should exclude module files,
64 | # since they will be recreated, and may cause churn. Uncomment if using
65 | # auto-import.
66 | # .idea/artifacts
67 | # .idea/compiler.xml
68 | # .idea/jarRepositories.xml
69 | # .idea/modules.xml
70 | # .idea/*.iml
71 | # .idea/modules
72 | # *.iml
73 | # *.ipr
74 |
75 | # CMake
76 | cmake-build-*/
77 |
78 | # Mongo Explorer plugin
79 | .idea/**/mongoSettings.xml
80 |
81 | # File-based project format
82 | *.iws
83 |
84 | # IntelliJ
85 | out/
86 |
87 | # mpeltonen/sbt-idea plugin
88 | .idea_modules/
89 |
90 | # JIRA plugin
91 | atlassian-ide-plugin.xml
92 |
93 | # Cursive Clojure plugin
94 | .idea/replstate.xml
95 |
96 | # SonarLint plugin
97 | .idea/sonarlint/
98 |
99 | # Crashlytics plugin (for Android Studio and IntelliJ)
100 | com_crashlytics_export_strings.xml
101 | crashlytics.properties
102 | crashlytics-build.properties
103 | fabric.properties
104 |
105 | # Editor-based Rest Client
106 | .idea/httpRequests
107 |
108 | # Android studio 3.1+ serialized cache file
109 | .idea/caches/build_file_checksums.ser
110 |
111 | ### GoLand Patch ###
112 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
113 |
114 | # *.iml
115 | # modules.xml
116 | # .idea/misc.xml
117 | # *.ipr
118 |
119 | # Sonarlint plugin
120 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
121 | .idea/**/sonarlint/
122 |
123 | # SonarQube Plugin
124 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
125 | .idea/**/sonarIssues.xml
126 |
127 | # Markdown Navigator plugin
128 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
129 | .idea/**/markdown-navigator.xml
130 | .idea/**/markdown-navigator-enh.xml
131 | .idea/**/markdown-navigator/
132 |
133 | # Cache file creation bug
134 | # See https://youtrack.jetbrains.com/issue/JBR-2257
135 | .idea/$CACHE_FILE$
136 |
137 | # CodeStream plugin
138 | # https://plugins.jetbrains.com/plugin/12206-codestream
139 | .idea/codestream.xml
140 |
141 | ### macOS ###
142 | # General
143 | .DS_Store
144 | .AppleDouble
145 | .LSOverride
146 |
147 | # Icon must end with two \r
148 | Icon
149 |
150 | # Thumbnails
151 | ._*
152 |
153 | # Files that might appear in the root of a volume
154 | .DocumentRevisions-V100
155 | .fseventsd
156 | .Spotlight-V100
157 | .TemporaryItems
158 | .Trashes
159 | .VolumeIcon.icns
160 | .com.apple.timemachine.donotpresent
161 |
162 | # Directories potentially created on remote AFP share
163 | .AppleDB
164 | .AppleDesktop
165 | Network Trash Folder
166 | Temporary Items
167 | .apdisk
168 |
169 | ### macOS Patch ###
170 | # iCloud generated files
171 | *.icloud
172 |
173 | ### VisualStudioCode ###
174 | .vscode/*
175 | !.vscode/settings.json
176 | !.vscode/tasks.json
177 | !.vscode/launch.json
178 | !.vscode/extensions.json
179 | !.vscode/*.code-snippets
180 |
181 | # Local History for Visual Studio Code
182 | .history/
183 |
184 | # Built Visual Studio Code Extensions
185 | *.vsix
186 |
187 | ### VisualStudioCode Patch ###
188 | # Ignore all local history of files
189 | .history
190 | .ionide
191 |
192 | # Support for Project snippet scope
193 | .vscode/*.code-snippets
194 |
195 | # Ignore code-workspaces
196 | *.code-workspace
197 |
198 | # End of https://www.toptal.com/developers/gitignore/api/go,macos,goland,visualstudiocode
199 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/sqlc.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 yiplee
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sqlc: A simple dynamic query builder for [kyleconroy/sqlc](https://github.com/kyleconroy/sqlc)
2 |
3 | ## How to use
4 |
5 | ```go
6 | package sqlc_test
7 |
8 | import (
9 | "context"
10 | "database/sql"
11 | "log"
12 |
13 | "github.com/yiplee/sqlc"
14 | "github.com/yiplee/sqlc/example"
15 | )
16 |
17 | func Example_build() {
18 | ctx := context.Background()
19 |
20 | db, err := sql.Open("postgres", "dsn")
21 | if err != nil {
22 | panic(err)
23 | }
24 | defer db.Close()
25 |
26 | // Wrap the db in order to hook the query
27 | query := example.New(sqlc.Wrap(db))
28 |
29 | /*
30 | the original query must be simple and not contain any WHERE/ORDER/LIMIT/OFFSET clause
31 |
32 | -- name: ListAuthors :many
33 | SELECT * FROM authors;
34 | */
35 |
36 | // customize the builder
37 | authors, err := query.ListAuthors(sqlc.Build(ctx, func(b *sqlc.Builder) {
38 | b.Where("age > $1", 10)
39 | b.Where("name = $2", "foo")
40 | b.Order("age,name DESC")
41 | b.Limit(10)
42 | }))
43 |
44 | if err != nil {
45 | log.Fatalln("ListAuthors", err)
46 | }
47 |
48 | log.Printf("list %d authors", len(authors))
49 | }
50 | ```
51 |
52 |
53 | Not perfect, but enough for now.
54 |
--------------------------------------------------------------------------------
/builder.go:
--------------------------------------------------------------------------------
1 | package sqlc
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | type (
10 | Builder struct {
11 | filters []filter
12 | order string
13 | offset, limit int
14 | }
15 |
16 | filter struct {
17 | expression string
18 | args []interface{}
19 | }
20 | )
21 |
22 | func (b *Builder) clone() *Builder {
23 | var cb Builder
24 | cb = *b
25 | return &cb
26 | }
27 |
28 | // Where set conditions of where in SELECT
29 | // Where("user = ?","tom")
30 | // Where("a = ? OR b = ?",1,2)
31 | // Where("foo = $1","bar")
32 | func (b *Builder) Where(query string, args ...interface{}) *Builder {
33 | b.filters = append(b.filters, filter{
34 | expression: query,
35 | args: args,
36 | })
37 |
38 | return b
39 | }
40 |
41 | // In is an equivalent of Where("column IN (?,?,?)", args...).
42 | // In("id", 1, 2, 3)
43 | func (b *Builder) In(column string, args ...interface{}) *Builder {
44 | placeholders := make([]string, len(args))
45 | for i := range args {
46 | placeholders[i] = "?"
47 | }
48 |
49 | query := fmt.Sprintf("%s IN (%s)", column, strings.Join(placeholders, ","))
50 | return b.Where(query, args...)
51 | }
52 |
53 | // Order sets columns of ORDER BY in SELECT.
54 | // Order("name, age DESC")
55 | func (b *Builder) Order(cols string) *Builder {
56 | b.order = cols
57 | return b
58 | }
59 |
60 | // Offset sets the offset in SELECT.
61 | func (b *Builder) Offset(x int) *Builder {
62 | b.offset = x
63 | return b
64 | }
65 |
66 | // Limit sets the limit in SELECT.
67 | func (b *Builder) Limit(x int) *Builder {
68 | b.limit = x
69 | return b
70 | }
71 |
72 | // Build returns compiled SELECT string and args.
73 | func (b *Builder) Build(query string, args ...interface{}) (string, []interface{}) {
74 | var sb strings.Builder
75 |
76 | sb.WriteString(query)
77 | sb.WriteByte('\n')
78 |
79 | // append where conditions
80 | for idx, filter := range b.filters {
81 | if idx == 0 {
82 | sb.WriteString("WHERE ")
83 | } else {
84 | sb.WriteString("AND ")
85 | }
86 |
87 | sb.WriteByte('(')
88 | sb.WriteString(filter.expression)
89 | sb.WriteByte(')')
90 | sb.WriteByte('\n')
91 |
92 | args = append(args, filter.args...)
93 | }
94 |
95 | if b.order != "" {
96 | sb.WriteString("ORDER BY ")
97 | sb.WriteString(b.order)
98 | sb.WriteByte('\n')
99 | }
100 |
101 | if b.limit > 0 {
102 | sb.WriteString("LIMIT ")
103 | sb.WriteString(strconv.Itoa(b.limit))
104 | sb.WriteByte('\n')
105 | }
106 |
107 | if b.offset > 0 {
108 | sb.WriteString("OFFSET ")
109 | sb.WriteString(strconv.Itoa(b.offset))
110 | sb.WriteByte('\n')
111 | }
112 |
113 | return sb.String(), args
114 | }
115 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | package sqlc
2 |
3 | import "context"
4 |
5 | type builderContextKey struct{}
6 |
7 | func WithBuilder(ctx context.Context, b *Builder) context.Context {
8 | return context.WithValue(ctx, builderContextKey{}, b)
9 | }
10 |
11 | func BuilderFrom(ctx context.Context) (*Builder, bool) {
12 | b, ok := ctx.Value(builderContextKey{}).(*Builder)
13 | return b, ok
14 | }
15 |
16 | func Build(ctx context.Context, f func(builder *Builder)) context.Context {
17 | b, ok := BuilderFrom(ctx)
18 | if !ok {
19 | b = &Builder{}
20 | } else {
21 | b = b.clone()
22 | }
23 |
24 | f(b)
25 | return WithBuilder(ctx, b)
26 | }
27 |
--------------------------------------------------------------------------------
/db.go:
--------------------------------------------------------------------------------
1 | package sqlc
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 |
7 | "github.com/yiplee/nap"
8 | )
9 |
10 | var _ DBTX = (*DB)(nil)
11 |
12 | type DB struct {
13 | *nap.DB
14 | }
15 |
16 | func Connect(driverName, master string, slaves ...string) (*DB, error) {
17 | db, err := nap.Open(driverName, master, slaves...)
18 | if err != nil {
19 | return nil, err
20 | }
21 |
22 | if err := db.Ping(); err != nil {
23 | return nil, err
24 | }
25 |
26 | return &DB{db}, nil
27 | }
28 |
29 | // PrepareContext creates a prepared statement for later queries or executions.
30 | //
31 | // Read or update can not be determined by the query string currently, use master database.
32 | func (db *DB) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) {
33 | return db.Master().PrepareContext(ctx, query)
34 | }
35 |
--------------------------------------------------------------------------------
/example/db.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.13.0
4 |
5 | package example
6 |
7 | import (
8 | "context"
9 | "database/sql"
10 | )
11 |
12 | type DBTX interface {
13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
14 | PrepareContext(context.Context, string) (*sql.Stmt, error)
15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row
17 | }
18 |
19 | func New(db DBTX) *Queries {
20 | return &Queries{db: db}
21 | }
22 |
23 | type Queries struct {
24 | db DBTX
25 | }
26 |
27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries {
28 | return &Queries{
29 | db: tx,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example/models.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.13.0
4 |
5 | package example
6 |
7 | import (
8 | "database/sql"
9 | )
10 |
11 | type Author struct {
12 | ID int64
13 | Name string
14 | Age int32
15 | Bio sql.NullString
16 | }
17 |
--------------------------------------------------------------------------------
/example/query.sql:
--------------------------------------------------------------------------------
1 | -- Example queries for sqlc
2 | CREATE TABLE authors
3 | (
4 | id BIGSERIAL PRIMARY KEY,
5 | name text NOT NULL,
6 | age integer NOT NULL,
7 | bio text
8 | );
9 |
10 | -- name: GetAuthor :one
11 | SELECT *
12 | FROM authors
13 | WHERE id = $1
14 | LIMIT 1;
15 |
16 | -- name: ListAuthors :many
17 | SELECT *
18 | FROM authors;
19 |
--------------------------------------------------------------------------------
/example/query.sql.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.13.0
4 | // source: query.sql
5 |
6 | package example
7 |
8 | import (
9 | "context"
10 | )
11 |
12 | const getAuthor = `-- name: GetAuthor :one
13 | SELECT id, name, age, bio
14 | FROM authors
15 | WHERE id = $1
16 | LIMIT 1
17 | `
18 |
19 | func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) {
20 | row := q.db.QueryRowContext(ctx, getAuthor, id)
21 | var i Author
22 | err := row.Scan(
23 | &i.ID,
24 | &i.Name,
25 | &i.Age,
26 | &i.Bio,
27 | )
28 | return i, err
29 | }
30 |
31 | const listAuthors = `-- name: ListAuthors :many
32 | SELECT id, name, age, bio
33 | FROM authors
34 | `
35 |
36 | func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) {
37 | rows, err := q.db.QueryContext(ctx, listAuthors)
38 | if err != nil {
39 | return nil, err
40 | }
41 | defer rows.Close()
42 | var items []Author
43 | for rows.Next() {
44 | var i Author
45 | if err := rows.Scan(
46 | &i.ID,
47 | &i.Name,
48 | &i.Age,
49 | &i.Bio,
50 | ); err != nil {
51 | return nil, err
52 | }
53 | items = append(items, i)
54 | }
55 | if err := rows.Close(); err != nil {
56 | return nil, err
57 | }
58 | if err := rows.Err(); err != nil {
59 | return nil, err
60 | }
61 | return items, nil
62 | }
63 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/yiplee/sqlc
2 |
3 | go 1.13
4 |
5 | require github.com/yiplee/nap v1.0.1
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
2 | github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
3 | github.com/yiplee/nap v1.0.1 h1:5p8KAIkYy+PIMGSk+ScF13Hh/OFkIEBHPuD14OFvStg=
4 | github.com/yiplee/nap v1.0.1/go.mod h1:7Zvro/en8ARhkqgv3vpj037yJSBRvGeNyj5Np5XUFgc=
5 | golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
6 | golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
7 |
--------------------------------------------------------------------------------
/interface.go:
--------------------------------------------------------------------------------
1 | package sqlc
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | )
7 |
8 | type DBTX interface {
9 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
10 | PrepareContext(context.Context, string) (*sql.Stmt, error)
11 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
12 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row
13 | }
14 |
--------------------------------------------------------------------------------
/sqlc.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1",
3 | "packages": [
4 | {
5 | "path": "example",
6 | "engine": "postgresql",
7 | "schema": "example/query.sql",
8 | "queries": "example/query.sql"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/wrap.go:
--------------------------------------------------------------------------------
1 | package sqlc
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | )
7 |
8 | var _ DBTX = (*wrappedDB)(nil)
9 |
10 | func Wrap(db DBTX) DBTX {
11 | return &wrappedDB{db}
12 | }
13 |
14 | type wrappedDB struct {
15 | DBTX
16 | }
17 |
18 | func (w wrappedDB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
19 | if b, ok := BuilderFrom(ctx); ok {
20 | query, args = b.Build(query, args...)
21 | }
22 |
23 | return w.DBTX.ExecContext(ctx, query, args...)
24 | }
25 |
26 | func (w wrappedDB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
27 | if b, ok := BuilderFrom(ctx); ok {
28 | query, args = b.Build(query, args...)
29 | }
30 |
31 | return w.DBTX.QueryContext(ctx, query, args...)
32 | }
33 |
34 | func (w wrappedDB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
35 | if b, ok := BuilderFrom(ctx); ok {
36 | query, args = b.Build(query, args...)
37 | }
38 |
39 | return w.DBTX.QueryRowContext(ctx, query, args...)
40 | }
41 |
--------------------------------------------------------------------------------
/wrap_test.go:
--------------------------------------------------------------------------------
1 | package sqlc_test
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "log"
7 |
8 | "github.com/yiplee/sqlc"
9 | "github.com/yiplee/sqlc/example"
10 | )
11 |
12 | func Example_build() {
13 | ctx := context.Background()
14 |
15 | db, err := sql.Open("postgres", "dsn")
16 | if err != nil {
17 | panic(err)
18 | }
19 | defer db.Close()
20 |
21 | query := example.New(sqlc.Wrap(db))
22 | authors, err := query.ListAuthors(sqlc.Build(ctx, func(b *sqlc.Builder) {
23 | b.Where("age > $1", 10)
24 | b.Where("name = $2", "foo")
25 | b.In("id", 1, 2, 3)
26 | b.Order("age,name DESC")
27 | b.Limit(10)
28 | }))
29 |
30 | if err != nil {
31 | log.Fatalln("ListAuthors", err)
32 | }
33 |
34 | log.Printf("list %d authors", len(authors))
35 | }
36 |
--------------------------------------------------------------------------------