├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── alias.go ├── alias_test.go ├── build_helpers.go ├── build_helpers_test.go ├── datatype.go ├── delete.go ├── driver ├── autoqb │ └── main.go ├── msqb │ ├── main.go │ └── msqf │ │ └── functions.go ├── myqb │ ├── main.go │ └── myqf │ │ └── functions.go └── pgqb │ ├── main.go │ ├── pgqc │ └── conditions.go │ └── pgqf │ └── functions.go ├── go.mod ├── go.sum ├── insert.go ├── internal ├── filter │ └── filter.go ├── tests │ ├── db_test.go │ ├── dbstring_test.go │ ├── internal │ │ └── model │ │ │ ├── db.json │ │ │ ├── model.go │ │ │ └── tables.go │ ├── scripts │ │ └── sqlserver │ │ │ ├── create_database.sql │ │ │ └── create_login.sql │ └── tests_test.go └── testutil │ └── test_helpers.go ├── join.go ├── override.go ├── override_test.go ├── qb-architect ├── README.md ├── internal │ ├── db │ │ ├── db.go │ │ ├── msarchitect │ │ │ ├── msarchitect.go │ │ │ └── msmodel │ │ │ │ ├── db.json │ │ │ │ ├── model.go │ │ │ │ └── tables.go │ │ ├── myarchitect │ │ │ ├── myarchitect.go │ │ │ └── mymodel │ │ │ │ ├── db.json │ │ │ │ ├── model.go │ │ │ │ └── tables.go │ │ └── pgarchitect │ │ │ ├── pgarchitect.go │ │ │ └── pgmodel │ │ │ ├── db.json │ │ │ ├── model.go │ │ │ └── tables.go │ └── util │ │ └── panic.go ├── main.go └── types.go ├── qb-generator ├── main.go ├── main_test.go └── template.go ├── qb.go ├── qbdb ├── driver.go ├── driver_test.go ├── main.go ├── stmt.go ├── types.go └── types_test.go ├── qc ├── condition.go ├── condition_test.go └── helper.go ├── qf ├── case.go ├── case_test.go ├── field.go ├── functions.go └── functions_test.go ├── select.go ├── sql.go ├── sql_test.go ├── types.go └── update.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | golangci-lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: stable 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v6 19 | with: 20 | version: v1.62.2 21 | 22 | tests: 23 | needs: golangci-lint 24 | runs-on: ubuntu-latest 25 | strategy: 26 | matrix: 27 | go-version: [ 'oldstable', 'stable' ] 28 | 29 | services: 30 | pgsql: 31 | image: postgres:15-alpine 32 | env: 33 | POSTGRES_HOST_AUTH_METHOD: trust 34 | POSTGRES_USER: qb_test 35 | POSTGRES_DB: qb_test 36 | options: >- 37 | --health-cmd="pg_isready -U qb_test" 38 | --health-interval=10s 39 | --health-timeout=5s 40 | --health-retries=5 41 | ports: 42 | - 5432:5432 43 | 44 | mariadb: 45 | image: mariadb:11 46 | env: 47 | MYSQL_DATABASE: qb_test 48 | MYSQL_USER: qb_test 49 | MYSQL_PASSWORD: qb_test 50 | MYSQL_ALLOW_EMPTY_PASSWORD: true 51 | options: >- 52 | --health-cmd="healthcheck.sh --connect --innodb_initialized" 53 | --health-interval=10s 54 | --health-timeout=5s 55 | --health-retries=5 56 | ports: 57 | - 3306:3306 58 | 59 | sqlserver: 60 | image: mcr.microsoft.com/mssql/server:2022-latest 61 | env: 62 | ACCEPT_EULA: Y 63 | MSSQL_SA_PASSWORD: P@ssw0rd 64 | options: >- 65 | --health-cmd "/opt/mssql-tools18/bin/sqlcmd -U sa -P 'P@ssw0rd' -Q 'SELECT 1' -C" 66 | --health-interval=10s 67 | --health-timeout=5s 68 | --health-retries=10 69 | --health-start-period=20s 70 | ports: 71 | - 1433:1433 72 | 73 | steps: 74 | - uses: actions/checkout@v4 75 | - uses: actions/setup-go@v5 76 | with: 77 | go-version: ${{ matrix.go-version }} 78 | 79 | - name: Prepare MSSQL database 80 | run: | 81 | cat internal/tests/scripts/sqlserver/create_database.sql | docker exec -i $(docker ps -alq) /opt/mssql-tools18/bin/sqlcmd -U sa -P 'P@ssw0rd' -C 82 | cat internal/tests/scripts/sqlserver/create_login.sql | docker exec -i $(docker ps -alq) /opt/mssql-tools18/bin/sqlcmd -U sa -P 'P@ssw0rd' -C -d qb_test 83 | 84 | - name: Run unit tests 85 | run: make test 86 | 87 | - name: Run PostgresSQL tests 88 | run: make test_postgres 89 | 90 | - name: Run MSSQL tests 91 | run: make test_mssql 92 | 93 | - name: Run MySQL tests 94 | run: make test_mysql 95 | 96 | - name: Total coverage 97 | run: | 98 | cat unit.out > total.out 99 | tail -n +2 postgres.out >> total.out 100 | tail -n +2 mysql.out >> total.out 101 | tail -n +2 mssql.out >> total.out 102 | go tool cover -func total.out | tail -1 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/internal/model/tables.go 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | build-tags: [] 3 | 4 | output: 5 | print-issued-lines: true 6 | print-linter-name: true 7 | 8 | linters: 9 | disable-all: true 10 | enable: 11 | - bodyclose 12 | - errcheck 13 | - errorlint 14 | - exhaustive 15 | - funlen 16 | - goconst 17 | - gocritic 18 | - gocyclo 19 | - err113 20 | - gofumpt 21 | - goimports 22 | - gosimple 23 | - govet 24 | - ineffassign 25 | - lll 26 | - nakedret 27 | - nolintlint 28 | - prealloc 29 | - revive 30 | - staticcheck 31 | - stylecheck 32 | - typecheck 33 | - unconvert 34 | - unparam 35 | - unused 36 | - whitespace 37 | 38 | linters-settings: 39 | govet: 40 | enable: 41 | - shadow 42 | 43 | gocyclo: 44 | min-complexity: 15 45 | 46 | lll: 47 | line-length: 150 48 | tab-width: 4 49 | 50 | nakedret: 51 | max-func-lines: 10 52 | 53 | funlen: 54 | lines: 120 55 | statements: 60 56 | 57 | issues: 58 | exclude: [] 59 | exclude-rules: 60 | - linters: 61 | - stylecheck 62 | text: "ST1000:" 63 | - path: _test.go 64 | linters: 65 | - lll 66 | - funlen 67 | - dupl 68 | - linters: 69 | - revive 70 | text: "package-comments|exported" 71 | - linters: 72 | - errcheck 73 | source: "defer .*Rollback()" 74 | - linters: 75 | - errcheck 76 | source: "defer .*Close()" 77 | exclude-use-default: false 78 | max-same-issues: 0 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Ultraware Consultancy and Development B.V. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: setup test lint 3 | 4 | .PHONY: setup 5 | setup: 6 | @go get -t ./... 7 | @printf "\n\n" 8 | 9 | .PHONY: test 10 | test: 11 | @printf "Testing ...\n" 12 | @go test -short -cover -coverprofile unit.out ./... 13 | @printf "\n\n" 14 | 15 | .PHONY: test_all 16 | test_all: test_postgres test_mysql test_mssql 17 | 18 | .PHONY: test_postgres 19 | test_postgres: 20 | @TYPE=postgres go test -cover -coverprofile postgres.out -coverpkg ./... ./internal/tests -v 21 | 22 | .PHONY: test_mysql 23 | test_mysql: 24 | @TYPE=mysql go test -cover -coverprofile mysql.out -coverpkg ./... ./internal/tests -v 25 | 26 | .PHONY: test_mssql 27 | test_mssql: 28 | @TYPE=mssql go test -cover -coverprofile mssql.out -coverpkg ./... ./internal/tests -v 29 | 30 | .PHONY: lint 31 | lint: 32 | @printf "Running linters ...\n" 33 | @golangci-lint run 34 | @printf "\n\n" 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qb 2 | 3 | qb is a library that allows you to build queries without using strings. This offers some unique advantages: 4 | 5 | - When changing your database queries that refer to old fields or tables won't compile until you update them 6 | - You can't misspell keywords or fieldnames, this saves a lot of time and many bugs 7 | - You can use tab completion 8 | - You can easily port a query to a different database 9 | - The order of commands in your query does not matter, this makes building queries in parts or adding optional statements easier 10 | 11 | ## Installation 12 | 13 | ```bash 14 | go get git.ultraware.nl/Ultraware/qb/v2/... 15 | ``` 16 | 17 | ## Quick start guide 18 | 19 | ### 1. Create a db.json 20 | 21 | You can create a db.json manually or use qb-architect to generate it from your database 22 | 23 | `qb-architect` example: 24 | 25 | ```bash 26 | qb-architect -dbms psql host=127.0.0.1 username=qb_test dbname=qb_test search_path=schema1,schema2 > db.json 27 | ``` 28 | 29 | `db.json` example: 30 | 31 | ```json 32 | [ 33 | { 34 | "name": "TableOne", 35 | "alias": "one", // optional 36 | "fields": [ 37 | { 38 | "name": "Field1", 39 | "data_type": "int", // optional 40 | "read_only": true // optional 41 | }, 42 | { 43 | "name": "Field2", 44 | "data_type": "varchar", // optional 45 | "size": 50, // optional 46 | }, 47 | { ... } 48 | ] 49 | }, 50 | { 51 | "name": "TableTwo", 52 | "fields": [ 53 | {"name": "Field1"}, 54 | {"name": "Field2"}, 55 | {"name": "Field3"} 56 | ] 57 | } 58 | ] 59 | ``` 60 | 61 | ### 2. Run qb-generator 62 | 63 | ```bash 64 | qb-generator db.json tables.go 65 | ``` 66 | 67 | #### Recommendations 68 | 69 | - Don't commit qb-generator's generated code to your repo 70 | - Use a go generate command to run qb-generator 71 | 72 | ### 3. Make a qbdb.DB 73 | 74 | ```golang 75 | package main 76 | 77 | var db *qbdb.DB 78 | 79 | func main() { 80 | database, err := sql.Open(driver, connectionString) 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | db = autoqb.New(database) 86 | } 87 | ``` 88 | 89 | ### 4. Write queries! 90 | 91 | You can now write queries, you can find examples below 92 | 93 | ## Examples 94 | 95 | ### Select 96 | 97 | ```golang 98 | one := model.One() 99 | 100 | q := one.Select(one.Field1, one.Field2). 101 | Where(qc.In(Field1, 1, 2, 3)) 102 | 103 | rows, err := db.Query(q) 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | for rows.Next() { 109 | f1, f2 := 0, "" 110 | err := rows.Scan(&f1, &f2) 111 | if err != nil { 112 | panic(err) 113 | } 114 | 115 | fmt.Println(f1, f2) 116 | } 117 | ``` 118 | 119 | ### Insert 120 | 121 | ```golang 122 | one := model.One() 123 | 124 | q := one.Insert(one.Field1, one.Field2). 125 | Values(1, "Record 1"). 126 | Values(2, "Record 2"). 127 | Values(4, "Record 4") 128 | 129 | _, err := db.Exec(q) 130 | if err != nil { 131 | panic(err) 132 | } 133 | ``` 134 | 135 | ### Update 136 | 137 | ```golang 138 | one := model.One() 139 | 140 | q := one.Update(). 141 | Set(one.Field2, "Record 3"). 142 | Where(qc.Eq(one.Field1, 4)) 143 | 144 | _, err := db.Exec(q) 145 | if err != nil { 146 | panic(err) 147 | } 148 | ``` 149 | 150 | ### Delete 151 | 152 | ```golang 153 | one := model.One() 154 | 155 | q := one.Delete(qc.Eq(one.Field1, 4)) 156 | 157 | _, err := db.Exec(q) 158 | if err != nil { 159 | panic(err) 160 | } 161 | ``` 162 | 163 | ### Prepare 164 | 165 | ```golang 166 | one := model.One() 167 | 168 | id := 0 169 | q := one.Select(one.Field1, one.Field2). 170 | Where(qc.Eq(one.Field1, &id)) 171 | 172 | stmt, err := db.Prepare() 173 | if err != nil { 174 | panic(err) 175 | } 176 | 177 | for _, v := range []int{1,2,3,4,5} { 178 | id = v 179 | 180 | row := stmt.QueryRow() 181 | 182 | f1, f2 := 0, "" 183 | err := row.Scan(&field1, &field2) 184 | if err != nil { 185 | panic(err) 186 | } 187 | 188 | fmt.Println(f1, f2) 189 | } 190 | ``` 191 | 192 | ### Subqueries 193 | 194 | ```golang 195 | one := model.One() 196 | 197 | sq := one.Select(one.Field1).SubQuery() 198 | 199 | q := sq.Select(sq.F[0]) 200 | 201 | rows, err := db.Query(q) 202 | if err != nil { 203 | panic(err) 204 | } 205 | 206 | for rows.Next() { 207 | f1 := 0 208 | err := rows.Scan(&f1) 209 | if err != nil { 210 | panic(err) 211 | } 212 | 213 | fmt.Println(f1) 214 | } 215 | ``` 216 | 217 | Alternatively, `.CTE()` can be used instead of `.SubQuery()` to use a CTE instead of a subquery 218 | 219 | ### Custom functions 220 | 221 | ```golang 222 | func dbfunc(f qb.Field) qb.Field { 223 | return qf.NewCalculatedField("dbfunc(", f, ")") 224 | } 225 | ``` 226 | 227 | ```golang 228 | q := one.Select(dbfunc(one.Field1)) 229 | ``` 230 | 231 | ### Custom conditions 232 | 233 | ```golang 234 | func dbcond(f qb.Field) qb.Condition { 235 | return qc.NewCondition("dbcond(", f, ")") 236 | } 237 | ``` 238 | 239 | ```golang 240 | q := one.Select(one.Field1). 241 | Where(dbcond(one.Field1)) 242 | ``` -------------------------------------------------------------------------------- /alias.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import "strconv" 4 | 5 | type noAlias struct{} 6 | 7 | func (n *noAlias) Get(_ Source) string { 8 | return `` 9 | } 10 | 11 | // NoAlias returns no alias. 12 | // This function is not intended to be called directly 13 | func NoAlias() Alias { 14 | return &noAlias{} 15 | } 16 | 17 | type aliasGenerator struct { 18 | cache map[Source]string 19 | counters map[string]int 20 | } 21 | 22 | // AliasGenerator returns an incrementing alias for each new Source. 23 | // This function is not intended to be called directly 24 | func AliasGenerator() Alias { 25 | return &aliasGenerator{make(map[Source]string), make(map[string]int)} 26 | } 27 | 28 | func (g *aliasGenerator) Get(src Source) string { 29 | if src == nil { 30 | return `` 31 | } 32 | 33 | if v, ok := g.cache[src]; ok { 34 | return v 35 | } 36 | 37 | nv := g.new(src) 38 | g.cache[src] = nv 39 | return nv 40 | } 41 | 42 | func (g *aliasGenerator) new(src Source) string { 43 | a := src.aliasString() 44 | 45 | g.counters[a]++ 46 | 47 | if g.counters[a] == 1 { 48 | return a 49 | } 50 | 51 | return a + strconv.Itoa(g.counters[a]) 52 | } 53 | -------------------------------------------------------------------------------- /alias_test.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import ( 4 | "testing" 5 | 6 | "git.fuyu.moe/Fuyu/assert" 7 | ) 8 | 9 | func TestAliasGeneratorNil(t *testing.T) { 10 | assert := assert.New(t) 11 | ag := AliasGenerator() 12 | 13 | assert.Eq(``, ag.Get(nil)) 14 | } 15 | 16 | func TestAliasGeneratorCache(t *testing.T) { 17 | assert := assert.New(t) 18 | ag := AliasGenerator() 19 | tbl1, tbl2 := &Table{Name: `abc`}, &Table{Name: `abcd`} 20 | 21 | assert.Eq(`a`, ag.Get(tbl1)) 22 | assert.Eq(`a2`, ag.Get(tbl2)) 23 | assert.Eq(`a`, ag.Get(tbl1)) 24 | } 25 | 26 | func TestAliasGeneratorDuplicateTables(t *testing.T) { 27 | assert := assert.New(t) 28 | ag := AliasGenerator() 29 | tbl1, tbl2 := &Table{Name: `abc`}, &Table{Name: `abc`} 30 | 31 | assert.Eq(`a`, ag.Get(tbl1)) 32 | assert.Eq(`a2`, ag.Get(tbl2)) 33 | } 34 | 35 | func TestAliasGeneratorDefinedAlias(t *testing.T) { 36 | assert := assert.New(t) 37 | ag := AliasGenerator() 38 | tbl1, tbl2 := &Table{Name: `abc`, Alias: `q`}, &Table{Name: `abc`, Alias: `q`} 39 | 40 | assert.Eq(`q`, ag.Get(tbl1)) 41 | assert.Eq(`q2`, ag.Get(tbl2)) 42 | assert.Eq(`q`, ag.Get(tbl1)) 43 | } 44 | 45 | func TestNoAlias(t *testing.T) { 46 | assert := assert.New(t) 47 | ag := NoAlias() 48 | tbl1 := &Table{Name: `abc`} 49 | 50 | assert.Eq(``, ag.Get(tbl1)) 51 | } 52 | -------------------------------------------------------------------------------- /build_helpers.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Values used when building queries 8 | var ( 9 | COMMA = `, ` 10 | NEWLINE = "\n" 11 | INDENT = "\t" 12 | VALUE = `?` 13 | ) 14 | 15 | ///// Field ///// 16 | 17 | // MakeField returns the value as a Field, no operation performed when the value is already a field. 18 | // This function is not intended to be called directly 19 | func MakeField(i interface{}) Field { 20 | switch f := i.(type) { 21 | case Field: 22 | return f 23 | case SelectQuery: 24 | return subqueryField{f} 25 | } 26 | 27 | return Value(i) 28 | } 29 | 30 | // ConcatQuery combines strings and Fields into string. 31 | // This function is not intended to be called directly 32 | func ConcatQuery(c *Context, values ...interface{}) string { 33 | s := strings.Builder{} 34 | 35 | for _, val := range values { 36 | switch v := val.(type) { 37 | case (Field): 38 | s.WriteString(v.QueryString(c)) 39 | case (Condition): 40 | s.WriteString(v(c)) 41 | case (SelectQuery): 42 | sql, _ := v.SQL(SQLBuilder{Context: c}) 43 | s.WriteString(getSubQuerySQL(sql)) 44 | case (string): 45 | s.WriteString(v) 46 | default: 47 | panic(`Can only use strings, Fields, Conditions and SelectQueries to build SQL`) 48 | } 49 | } 50 | return s.String() 51 | } 52 | 53 | // JoinQuery joins fields or values into a string separated by sep. 54 | // This function is not intended to be called directly 55 | func JoinQuery(c *Context, sep string, values []interface{}) string { 56 | s := make([]interface{}, len(values)*2-1) 57 | for k, v := range values { 58 | if k > 0 { 59 | s[k*2-1] = sep 60 | } 61 | s[k*2] = MakeField(v) 62 | } 63 | 64 | return ConcatQuery(c, s...) 65 | } 66 | -------------------------------------------------------------------------------- /build_helpers_test.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import ( 4 | "testing" 5 | 6 | "git.fuyu.moe/Fuyu/assert" 7 | ) 8 | 9 | func TestMakeFieldWithField(t *testing.T) { 10 | assert := assert.New(t) 11 | f := TableField{Name: `f1`} 12 | 13 | assert.Eq(f, MakeField(f)) 14 | } 15 | 16 | func TestMakeFieldWithValue(t *testing.T) { 17 | assert := assert.New(t) 18 | 19 | assert.Eq(Value(`a`), MakeField(`a`)) 20 | } 21 | 22 | func TestConcatQueryString(t *testing.T) { 23 | assert := assert.New(t) 24 | 25 | output := ConcatQuery(nil, `a`, `, `, `b`) 26 | 27 | assert.Eq(`a, b`, output) 28 | } 29 | 30 | func TestConcatQueryField(t *testing.T) { 31 | assert := assert.New(t) 32 | f := TableField{Name: `f`} 33 | 34 | output := ConcatQuery(NewContext(nil, NoAlias()), f, `, `, f) 35 | 36 | assert.Eq(`f, f`, output) 37 | } 38 | 39 | func TestConcatQuerySubquery(t *testing.T) { 40 | assert := assert.New(t) 41 | sq := &SelectBuilder{source: &Table{Name: `tbl`}, fields: []Field{TableField{Name: `f1`}}} 42 | 43 | NEWLINE, INDENT = ``, `` 44 | output := ConcatQuery(NewContext(nil, NoAlias()), sq, `, `, sq) 45 | NEWLINE, INDENT = "\n", "\t" 46 | 47 | assert.Eq("(SELECT f1FROM tbl AS t), (SELECT f1FROM tbl AS t)", output) 48 | } 49 | 50 | func TestJoinQuery(t *testing.T) { 51 | assert := assert.New(t) 52 | f := TableField{Name: `f1`} 53 | 54 | output := JoinQuery(NewContext(nil, NoAlias()), `, `, []interface{}{f, f, f}) 55 | 56 | assert.Eq(`f1, f1, f1`, output) 57 | } 58 | -------------------------------------------------------------------------------- /datatype.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | // DataType represents a type in a database 4 | type DataType uint16 5 | 6 | // All defined DataTypes 7 | const ( 8 | Int = iota + 1 9 | String 10 | Bool 11 | Float 12 | Date 13 | Time 14 | ) 15 | -------------------------------------------------------------------------------- /delete.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | // DeleteBuilder builds a DELETE query 9 | type DeleteBuilder struct { 10 | table *Table 11 | c []Condition 12 | } 13 | 14 | // SQL returns a query string and a list of values 15 | // 16 | 17 | func (q DeleteBuilder) SQL(b SQLBuilder) (string, []interface{}) { 18 | drvPath := reflect.TypeOf(b.Context.Driver).PkgPath() 19 | if strings.HasSuffix(drvPath, `myqb`) || strings.HasSuffix(drvPath, `msqb`) { 20 | b.w.WriteLine(`DELETE ` + q.table.aliasString()) 21 | b.w.WriteLine(`FROM ` + b.SourceToSQL(q.table)) 22 | } else { 23 | b.Delete(q.table) 24 | } 25 | b.Where(q.c...) 26 | 27 | return b.w.String(), *b.Context.Values 28 | } 29 | -------------------------------------------------------------------------------- /driver/autoqb/main.go: -------------------------------------------------------------------------------- 1 | package autoqb // import "git.ultraware.nl/Ultraware/qb/v2/driver/autoqb" 2 | 3 | import ( 4 | "database/sql" 5 | "reflect" 6 | "strings" 7 | 8 | "git.ultraware.nl/Ultraware/qb/v2/driver/msqb" 9 | "git.ultraware.nl/Ultraware/qb/v2/driver/myqb" 10 | "git.ultraware.nl/Ultraware/qb/v2/driver/pgqb" 11 | "git.ultraware.nl/Ultraware/qb/v2/qbdb" 12 | ) 13 | 14 | // New automatically selects a qb driver 15 | func New(db *sql.DB) qbdb.DB { 16 | switch getPkgName(db) { 17 | case `github.com/lib/pq`, `github.com/jackc/pgx/stdlib`: 18 | return pgqb.New(db) 19 | case `github.com/go-sql-driver/mysql`, `github.com/ziutek/mymysql/godrv`: 20 | return myqb.New(db) 21 | case `github.com/denisenkom/go-mssqldb`, `github.com/microsoft/go-mssqldb`: 22 | return msqb.New(db) 23 | } 24 | panic(`Unknown database driver`) 25 | } 26 | 27 | func getPkgName(db *sql.DB) string { 28 | t := reflect.TypeOf(db.Driver()) 29 | if t.Kind() == reflect.Ptr { 30 | t = t.Elem() 31 | } 32 | path := t.PkgPath() 33 | parts := strings.Split(path, `/vendor/`) 34 | return parts[len(parts)-1] 35 | } 36 | -------------------------------------------------------------------------------- /driver/msqb/main.go: -------------------------------------------------------------------------------- 1 | package msqb // import "git.ultraware.nl/Ultraware/qb/v2/driver/msqb" 2 | 3 | import ( 4 | "database/sql" 5 | "strconv" 6 | "strings" 7 | 8 | "git.ultraware.nl/Ultraware/qb/v2" 9 | "git.ultraware.nl/Ultraware/qb/v2/driver/msqb/msqf" 10 | "git.ultraware.nl/Ultraware/qb/v2/qbdb" 11 | "git.ultraware.nl/Ultraware/qb/v2/qf" 12 | ) 13 | 14 | // Driver implements MSSQL-specific features 15 | type Driver struct{} 16 | 17 | // New returns the driver 18 | func New(db *sql.DB) qbdb.DB { 19 | return qbdb.New(Driver{}, db) 20 | } 21 | 22 | // ValueString returns a the SQL for a parameter value 23 | func (d Driver) ValueString(i int) string { 24 | return `@p` + strconv.Itoa(i) 25 | } 26 | 27 | // BoolString formats a boolean in a format supported by MSSQL 28 | func (d Driver) BoolString(v bool) string { 29 | if v { 30 | return `1` 31 | } 32 | return `0` 33 | } 34 | 35 | // EscapeCharacter returns the correct escape character for MSSQL 36 | func (d Driver) EscapeCharacter() string { 37 | return `"` 38 | } 39 | 40 | // UpsertSQL implements qb.Driver 41 | func (d Driver) UpsertSQL(_ *qb.Table, _ []qb.Field, _ qb.Query) (string, []interface{}) { 42 | panic(`mssql does not support upsert`) 43 | } 44 | 45 | // IgnoreConflictSQL implements qb.Driver 46 | func (d Driver) IgnoreConflictSQL(_ *qb.Table, _ []qb.Field) (string, []interface{}) { 47 | panic(`mssql does not support ignore conflicts`) 48 | } 49 | 50 | // LimitOffset implements qb.Driver 51 | func (d Driver) LimitOffset(sql qb.SQL, limit, offset int) { 52 | if offset > 0 { 53 | sql.WriteLine(`OFFSET ` + strconv.Itoa(offset) + ` ROWS`) 54 | if limit > 0 { 55 | sql.WriteLine(`FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY`) 56 | } 57 | return 58 | } 59 | 60 | if limit > 0 { 61 | s := sql.String() 62 | 63 | sql.Rewrite(`SELECT TOP ` + strconv.Itoa(limit) + s[6:]) 64 | } 65 | } 66 | 67 | // Returning implements qb.Driver 68 | func (d Driver) Returning(b qb.SQLBuilder, q qb.Query, f []qb.Field) (string, []interface{}) { 69 | sql, v := q.SQL(b) 70 | 71 | t, insertBefore := `INSERTED`, `FROM` 72 | 73 | switch strings.SplitN(sql, ` `, 2)[0] { 74 | case `DELETE`: 75 | t = `DELETED` 76 | case `INSERT`: 77 | insertBefore = `VALUES` 78 | } 79 | 80 | line := `` 81 | for k, field := range f { 82 | if k > 0 { 83 | line += `, ` 84 | } 85 | line += t + `.` + field.(*qb.TableField).Name 86 | } 87 | 88 | index := strings.Index(sql, insertBefore) 89 | if index < 0 { 90 | sql = sql + `OUTPUT ` + line + qb.NEWLINE 91 | } else { 92 | sql = sql[:index] + `OUTPUT ` + line + qb.NEWLINE + sql[index:] 93 | } 94 | 95 | return sql, v 96 | } 97 | 98 | // LateralJoin implements qb.Driver 99 | func (d Driver) LateralJoin(_ *qb.Context, _ *qb.SubQuery) string { 100 | panic(`mssql does not support lateral joins`) 101 | } 102 | 103 | var types = map[qb.DataType]string{ 104 | qb.Int: `int`, 105 | qb.String: `text`, 106 | qb.Bool: `bit`, 107 | qb.Float: `float`, 108 | qb.Date: `date`, 109 | qb.Time: `datetime`, 110 | } 111 | 112 | // TypeName implements qb.Driver 113 | func (d Driver) TypeName(t qb.DataType) string { 114 | if s, ok := types[t]; ok { 115 | return s 116 | } 117 | panic(`Unknown type`) 118 | } 119 | 120 | var override = qb.OverrideMap{} 121 | 122 | func init() { 123 | override.Add(qf.Concat, msqf.Concat) 124 | override.Add(qf.Extract, msqf.DatePart) 125 | override.Add(qf.Now, msqf.GetDate) 126 | } 127 | 128 | // Override implements qb.Driver 129 | func (d Driver) Override() qb.OverrideMap { 130 | return override 131 | } 132 | -------------------------------------------------------------------------------- /driver/msqb/msqf/functions.go: -------------------------------------------------------------------------------- 1 | package msqf // import "git.ultraware.nl/Ultraware/qb/v2/driver/msqb/msqf" 2 | 3 | import ( 4 | "git.ultraware.nl/Ultraware/qb/v2" 5 | "git.ultraware.nl/Ultraware/qb/v2/qf" 6 | ) 7 | 8 | // GetDate is a mssql-specific version of qf.Now 9 | func GetDate() qb.Field { 10 | return qf.NewCalculatedField(`getdate()`) 11 | } 12 | 13 | // Concat is a mssql-specific version of qf.Concat 14 | func Concat(i ...interface{}) qb.Field { 15 | return qf.CalculatedField(func(c *qb.Context) string { 16 | return qb.JoinQuery(c, ` + `, i) 17 | }) 18 | } 19 | 20 | // DatePart is a mssql-specific version of qf.Extract 21 | func DatePart(f qb.Field, part string) qb.Field { 22 | return qf.NewCalculatedField(`DATEPART(`, part, `, `, f, `)`) 23 | } 24 | -------------------------------------------------------------------------------- /driver/myqb/main.go: -------------------------------------------------------------------------------- 1 | package myqb // import "git.ultraware.nl/Ultraware/qb/v2/driver/myqb" 2 | 3 | import ( 4 | "database/sql" 5 | "strconv" 6 | "strings" 7 | 8 | "git.ultraware.nl/Ultraware/qb/v2" 9 | "git.ultraware.nl/Ultraware/qb/v2/driver/myqb/myqf" 10 | "git.ultraware.nl/Ultraware/qb/v2/qbdb" 11 | "git.ultraware.nl/Ultraware/qb/v2/qf" 12 | ) 13 | 14 | // Driver implements PostgreSQL-specific features 15 | type Driver struct{} 16 | 17 | // New returns the driver 18 | func New(db *sql.DB) qbdb.DB { 19 | _, _ = db.Exec(`SET SESSION sql_mode = 'ANSI'`) 20 | return qbdb.New(Driver{}, db) 21 | } 22 | 23 | // ValueString returns a the SQL for a parameter value 24 | func (d Driver) ValueString(_ int) string { 25 | return `?` 26 | } 27 | 28 | // BoolString formats a boolean in a format supported by MySQL 29 | func (d Driver) BoolString(v bool) string { 30 | if v { 31 | return `1` 32 | } 33 | return `0` 34 | } 35 | 36 | // EscapeCharacter returns the correct escape character for MySQL 37 | func (d Driver) EscapeCharacter() string { 38 | return "`" 39 | } 40 | 41 | // UpsertSQL implements qb.Driver 42 | func (d Driver) UpsertSQL(t *qb.Table, _ []qb.Field, q qb.Query) (string, []interface{}) { 43 | usql, values := q.SQL(qb.NewSQLBuilder(d)) 44 | if !strings.HasPrefix(usql, `UPDATE `+t.Name) { 45 | panic(`Update does not update the correct table`) 46 | } 47 | usql = strings.ReplaceAll(usql, `UPDATE `+t.Name+qb.NEWLINE+`SET`, `UPDATE`) 48 | 49 | return `ON DUPLICATE KEY ` + usql, values 50 | } 51 | 52 | // IgnoreConflictSQL implements qb.Driver 53 | func (d Driver) IgnoreConflictSQL(_ *qb.Table, _ []qb.Field) (string, []interface{}) { 54 | panic(`mysql does not support ignore conflicts`) 55 | } 56 | 57 | // LimitOffset implements qb.Driver 58 | func (d Driver) LimitOffset(sql qb.SQL, limit, offset int) { 59 | if limit > 0 { 60 | sql.WriteLine(`LIMIT ` + strconv.Itoa(limit)) 61 | } 62 | if offset > 0 { 63 | sql.WriteLine(`OFFSET ` + strconv.Itoa(offset)) 64 | } 65 | } 66 | 67 | // Returning implements qb.Driver 68 | func (d Driver) Returning(_ qb.SQLBuilder, _ qb.Query, _ []qb.Field) (string, []interface{}) { 69 | panic(`mysql does not support RETURNING`) 70 | } 71 | 72 | // LateralJoin implements qb.Driver 73 | func (d Driver) LateralJoin(_ *qb.Context, _ *qb.SubQuery) string { 74 | panic(`mysql does not support lateral joins`) 75 | } 76 | 77 | var types = map[qb.DataType]string{ 78 | qb.Int: `int`, 79 | qb.String: `text`, 80 | qb.Bool: `boolean`, 81 | qb.Float: `double`, 82 | qb.Date: `date`, 83 | qb.Time: `datetime`, 84 | } 85 | 86 | // TypeName implements qb.Driver 87 | func (d Driver) TypeName(t qb.DataType) string { 88 | if s, ok := types[t]; ok { 89 | return s 90 | } 91 | panic(`Unknown type`) 92 | } 93 | 94 | var override = qb.OverrideMap{} 95 | 96 | func init() { 97 | override.Add(qf.Excluded, myqf.Values) 98 | } 99 | 100 | // Override implements qb.Driver 101 | func (d Driver) Override() qb.OverrideMap { 102 | return override 103 | } 104 | -------------------------------------------------------------------------------- /driver/myqb/myqf/functions.go: -------------------------------------------------------------------------------- 1 | package myqf // import "git.ultraware.nl/Ultraware/qb/v2/driver/myqb/myqf" 2 | 3 | import ( 4 | "git.ultraware.nl/Ultraware/qb/v2" 5 | "git.ultraware.nl/Ultraware/qb/v2/qf" 6 | ) 7 | 8 | // Values is a mysql-specific version of qf.Excluded 9 | func Values(f qb.Field) qb.Field { 10 | return qf.NewCalculatedField(`VALUES(`, f, `)`) 11 | } 12 | -------------------------------------------------------------------------------- /driver/pgqb/main.go: -------------------------------------------------------------------------------- 1 | package pgqb // import "git.ultraware.nl/Ultraware/qb/v2/driver/pgqb" 2 | 3 | import ( 4 | "database/sql" 5 | "strconv" 6 | "strings" 7 | 8 | "git.ultraware.nl/Ultraware/qb/v2" 9 | "git.ultraware.nl/Ultraware/qb/v2/driver/pgqb/pgqc" 10 | "git.ultraware.nl/Ultraware/qb/v2/driver/pgqb/pgqf" 11 | "git.ultraware.nl/Ultraware/qb/v2/qbdb" 12 | "git.ultraware.nl/Ultraware/qb/v2/qc" 13 | "git.ultraware.nl/Ultraware/qb/v2/qf" 14 | ) 15 | 16 | // Driver implements PostgreSQL-specific features 17 | type Driver struct{} 18 | 19 | // New returns the driver 20 | func New(db *sql.DB) qbdb.DB { 21 | return qbdb.New(Driver{}, db) 22 | } 23 | 24 | // ValueString returns a the SQL for a parameter value 25 | func (d Driver) ValueString(c int) string { 26 | return `$` + strconv.Itoa(c) 27 | } 28 | 29 | // BoolString formats a boolean in a format supported by PostgreSQL 30 | func (d Driver) BoolString(v bool) string { 31 | return strconv.FormatBool(v) 32 | } 33 | 34 | // EscapeCharacter returns the correct escape character for PostgreSQL 35 | func (d Driver) EscapeCharacter() string { 36 | return `"` 37 | } 38 | 39 | // UpsertSQL implements qb.Driver 40 | func (d Driver) UpsertSQL(t *qb.Table, conflict []qb.Field, q qb.Query) (string, []interface{}) { 41 | c := qb.NewContext(d, qb.NoAlias()) 42 | sql := `` 43 | for k, v := range conflict { 44 | if k > 0 { 45 | sql += qb.COMMA 46 | } 47 | sql += v.QueryString(c) 48 | } 49 | 50 | usql, values := q.SQL(qb.NewSQLBuilder(d)) 51 | if !strings.HasPrefix(usql, `UPDATE `+t.Name) { 52 | panic(`Update does not update the correct table`) 53 | } 54 | usql = strings.ReplaceAll(usql, `UPDATE `+t.Name, `UPDATE`) 55 | 56 | return `ON CONFLICT (` + sql + `) DO ` + usql, values 57 | } 58 | 59 | // IgnoreConflictSQL implements qb.Driver 60 | func (d Driver) IgnoreConflictSQL(_ *qb.Table, conflict []qb.Field) (string, []interface{}) { 61 | c := qb.NewContext(d, qb.NoAlias()) 62 | sql := `` 63 | for k, v := range conflict { 64 | if k > 0 { 65 | sql += qb.COMMA 66 | } 67 | sql += v.QueryString(c) 68 | } 69 | 70 | return `ON CONFLICT (` + sql + ") DO NOTHING\n", *c.Values 71 | } 72 | 73 | // LimitOffset implements qb.Driver 74 | func (d Driver) LimitOffset(sql qb.SQL, limit, offset int) { 75 | if limit > 0 { 76 | sql.WriteLine(`LIMIT ` + strconv.Itoa(limit)) 77 | } 78 | if offset > 0 { 79 | sql.WriteLine(`OFFSET ` + strconv.Itoa(offset)) 80 | } 81 | } 82 | 83 | // Returning implements qb.Driver 84 | func (d Driver) Returning(b qb.SQLBuilder, q qb.Query, f []qb.Field) (string, []interface{}) { 85 | s, v := q.SQL(b) 86 | 87 | line := `` 88 | for k, field := range f { 89 | if k > 0 { 90 | line += `, ` 91 | } 92 | line += field.QueryString(b.Context) 93 | } 94 | 95 | return s + `RETURNING ` + line + qb.NEWLINE, append(v, *b.Context.Values...) 96 | } 97 | 98 | // LateralJoin implements qb.Driver 99 | func (d Driver) LateralJoin(c *qb.Context, s *qb.SubQuery) string { 100 | return `LATERAL ` + s.TableString(c) 101 | } 102 | 103 | var types = map[qb.DataType]string{ 104 | qb.Int: `int`, 105 | qb.String: `text`, 106 | qb.Bool: `boolean`, 107 | qb.Float: `float`, 108 | qb.Date: `date`, 109 | qb.Time: `timestamptz`, 110 | } 111 | 112 | // TypeName implements qb.Driver 113 | func (d Driver) TypeName(t qb.DataType) string { 114 | if s, ok := types[t]; ok { 115 | return s 116 | } 117 | panic(`Unknown type`) 118 | } 119 | 120 | var override = qb.OverrideMap{} 121 | 122 | func init() { 123 | override.Add(qf.Excluded, pgqf.Excluded) 124 | 125 | override.Add(qc.Like, pgqc.ILike) 126 | } 127 | 128 | // Override implements qb.Driver 129 | func (d Driver) Override() qb.OverrideMap { 130 | return override 131 | } 132 | -------------------------------------------------------------------------------- /driver/pgqb/pgqc/conditions.go: -------------------------------------------------------------------------------- 1 | package pgqc // import "git.ultraware.nl/Ultraware/qb/v2/driver/pgqb/pgqc" 2 | 3 | import ( 4 | "git.ultraware.nl/Ultraware/qb/v2" 5 | "git.ultraware.nl/Ultraware/qb/v2/qc" 6 | ) 7 | 8 | // ILike is a postgres-specific version of qc.Like 9 | func ILike(f1 qb.Field, s string) qb.Condition { 10 | f2 := qb.MakeField(s) 11 | return qc.NewCondition(f1, ` ILIKE `, f2) 12 | } 13 | -------------------------------------------------------------------------------- /driver/pgqb/pgqf/functions.go: -------------------------------------------------------------------------------- 1 | package pgqf // import "git.ultraware.nl/Ultraware/qb/v2/driver/pgqb/pgqf" 2 | 3 | import ( 4 | "git.ultraware.nl/Ultraware/qb/v2" 5 | "git.ultraware.nl/Ultraware/qb/v2/qf" 6 | ) 7 | 8 | // Excluded is a postgres-specific version of qf.Excluded 9 | func Excluded(f qb.Field) qb.Field { 10 | return qf.NewCalculatedField(`EXCLUDED.`, f) 11 | } 12 | 13 | // ArrayAgg is the postgres-specific function array_agg 14 | func ArrayAgg(f qb.Field) qb.Field { 15 | return qf.NewCalculatedField(`array_agg(`, f, `)`) 16 | } 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module git.ultraware.nl/Ultraware/qb/v2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | git.fuyu.moe/Fuyu/assert v0.2.1 7 | github.com/go-sql-driver/mysql v1.9.1 8 | github.com/lib/pq v1.10.9 9 | github.com/microsoft/go-mssqldb v1.8.0 10 | ) 11 | 12 | require ( 13 | filippo.io/edwards25519 v1.1.0 // indirect 14 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 15 | github.com/golang-sql/sqlexp v0.1.0 // indirect 16 | github.com/google/go-cmp v0.6.0 // indirect 17 | github.com/google/uuid v1.6.0 // indirect 18 | golang.org/x/crypto v0.36.0 // indirect 19 | golang.org/x/text v0.23.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | git.fuyu.moe/Fuyu/assert v0.2.1 h1:F8faRzPp0AhzGDYzv0S6eehbRTb72dc0V2ZMMo39ryo= 4 | git.fuyu.moe/Fuyu/assert v0.2.1/go.mod h1:Q/wfl8GbNF2ZRkaVOd4zcxr+1MTQ3UZLywWwaxxBzkk= 5 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= 6 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= 7 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= 8 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= 9 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= 10 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= 11 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= 12 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= 13 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= 14 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= 15 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= 16 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI= 20 | github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 21 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 22 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 23 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 24 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 25 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 26 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 27 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 29 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 30 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 31 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 32 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 33 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 34 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 35 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 36 | github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= 37 | github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= 38 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 39 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 43 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 44 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 45 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 46 | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= 47 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 48 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 49 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 50 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 51 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 52 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 54 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | -------------------------------------------------------------------------------- /insert.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | type defaultField struct{} 4 | 5 | // Default uses a field's default value 6 | func Default() Field { 7 | return defaultField{} 8 | } 9 | 10 | // QueryString implements Field 11 | func (f defaultField) QueryString(_ *Context) string { 12 | return `DEFAULT` 13 | } 14 | 15 | // InsertBuilder builds an INSERT query 16 | type InsertBuilder struct { 17 | table *Table 18 | fields []Field 19 | values [][]Field 20 | 21 | conflict []Field 22 | update Query 23 | ignoreConflict bool 24 | } 25 | 26 | // Values adds values to the query 27 | func (q *InsertBuilder) Values(values ...interface{}) *InsertBuilder { 28 | if len(values) != len(q.fields) { 29 | panic(`Number of values has to match the number of fields`) 30 | } 31 | 32 | list := make([]Field, len(values)) 33 | for k, v := range values { 34 | list[k] = MakeField(v) 35 | } 36 | 37 | q.values = append(q.values, list) 38 | 39 | return q 40 | } 41 | 42 | // Upsert turns the INSERT query into an upsert query, only usable if your driver supports it 43 | func (q *InsertBuilder) Upsert(query Query, conflict ...Field) *InsertBuilder { 44 | if q.ignoreConflict { 45 | panic(`can't upsert and ignore conflicts at the same time`) 46 | } 47 | 48 | q.update = query 49 | q.conflict = conflict 50 | 51 | return q 52 | } 53 | 54 | // IgnoreConflict ignores conflicts from the insert query 55 | func (q *InsertBuilder) IgnoreConflict(conflict ...Field) *InsertBuilder { 56 | if q.update != nil { 57 | panic(`can't upsert and ignore conflicts at the same time`) 58 | } 59 | 60 | q.ignoreConflict = true 61 | q.conflict = conflict 62 | 63 | return q 64 | } 65 | 66 | // SQL returns a query string and a list of values 67 | func (q *InsertBuilder) SQL(b SQLBuilder) (string, []interface{}) { 68 | if len(q.values) == 0 { 69 | panic(`Cannot exectue insert without values`) 70 | } 71 | 72 | b.Insert(q.table, q.fields) 73 | b.Values(q.values) 74 | 75 | sql := b.w.String() 76 | if q.update != nil { 77 | s, v := b.Context.Driver.UpsertSQL(q.table, q.conflict, q.update) 78 | b.Context.Add(v...) 79 | sql += s 80 | } else if q.ignoreConflict { 81 | s, v := b.Context.Driver.IgnoreConflictSQL(q.table, q.conflict) 82 | b.Context.Add(v...) 83 | sql += s 84 | } 85 | 86 | return sql, *b.Context.Values 87 | } 88 | -------------------------------------------------------------------------------- /internal/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | ) 8 | 9 | // Filters is a list of regular expressions 10 | type Filters []*regexp.Regexp 11 | 12 | // Set implements flag.Value 13 | func (f *Filters) Set(value string) error { 14 | re, err := regexp.Compile(value) 15 | if err != nil { 16 | println(`Invalid regular expression: ` + value + "\n" + err.Error()) 17 | os.Exit(2) 18 | } 19 | 20 | *f = append(*f, re) 21 | 22 | return nil 23 | } 24 | 25 | // String implements flag.Value 26 | func (f Filters) String() string { 27 | return fmt.Sprint([]*regexp.Regexp(f)) 28 | } 29 | -------------------------------------------------------------------------------- /internal/tests/db_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "database/sql" 5 | "strings" 6 | 7 | _ "github.com/go-sql-driver/mysql" // database driver 8 | _ "github.com/lib/pq" // database driver 9 | _ "github.com/microsoft/go-mssqldb" // database driver for Microsoft MSSQL 10 | ) 11 | 12 | func initDatabase(driverName, connectionString string) *sql.DB { 13 | db, err := sql.Open(driverName, connectionString) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | dropQuery := `DROP TABLE IF EXISTS one, "two $#!", three` 19 | sql := createSQL 20 | if driverName != `postgres` { 21 | sql = strings.ReplaceAll(sql, `timestamp`, `datetime`) 22 | } 23 | if driverName == `mysql` { 24 | sql = strings.ReplaceAll(sql, `"`, "`") 25 | dropQuery = strings.ReplaceAll(dropQuery, `"`, "`") 26 | } 27 | 28 | _, err = db.Exec(dropQuery) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | _, err = db.Exec(sql) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | return db 39 | } 40 | 41 | func initPostgres() *sql.DB { 42 | return initDatabase(`postgres`, getPostgresDBString()) 43 | } 44 | 45 | func initMysql() *sql.DB { 46 | return initDatabase(`mysql`, getMysqlDBString()) 47 | } 48 | 49 | func initMssql() *sql.DB { 50 | return initDatabase(`sqlserver`, getMssqlDBString()) 51 | } 52 | -------------------------------------------------------------------------------- /internal/tests/dbstring_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func getHost(prefix string) string { 9 | if e := os.Getenv(prefix + `_TESTHOST`); e != `` { 10 | return e 11 | } 12 | if e := os.Getenv(`TESTHOST`); e != `` { 13 | return e 14 | } 15 | return `127.0.0.1` 16 | } 17 | 18 | func getPostgresDBString() string { 19 | return fmt.Sprintf("host=%s dbname=qb_test user=qb_test sslmode=disable", getHost(`POSTGRES`)) 20 | } 21 | 22 | func getMysqlDBString() string { 23 | return fmt.Sprintf("qb_test:qb_test@tcp(%s:3306)/qb_test?multiStatements=true&parseTime=true", getHost(`MYSQL`)) 24 | } 25 | 26 | func getMssqlDBString() string { 27 | return fmt.Sprintf("sqlserver://qb_test:qb_testA1@%s:1433?database=qb_test", getHost(`MSSQL`)) 28 | } 29 | 30 | const createSQL = ` 31 | CREATE TABLE one ( 32 | ID int PRIMARY KEY, 33 | "$Name. #()" varchar(50) NOT NULL, 34 | created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP 35 | ); 36 | 37 | CREATE TABLE "two $#!" ( 38 | OneID int, 39 | Number int, 40 | Comment varchar(100) NOT NULL, 41 | ModifiedAt timestamp, 42 | PRIMARY KEY (OneID, Number) 43 | ); 44 | 45 | CREATE TABLE three ( 46 | OneID int REFERENCES one(ID), 47 | Field3 int 48 | );` 49 | -------------------------------------------------------------------------------- /internal/tests/internal/model/db.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "one", 4 | "fields": [ 5 | {"name": "ID", "read_only": true, "data_type": "int"}, 6 | {"name": "$Name. #()", "data_type": "varchar", "size": 50}, 7 | {"name": "created_at", "read_only": true, "data_type": "timestamp"} 8 | ] 9 | }, 10 | { 11 | "name": "two $#!", 12 | "alias": "tw", 13 | "fields": [ 14 | {"name": "OneID", "data_type": "int"}, 15 | {"name": "Number", "data_type": "int"}, 16 | {"name": "Comment", "data_type": "varchar", "size": 100}, 17 | {"name": "ModifiedAt", "null": true, "data_type": "timestamp"} 18 | ] 19 | }, 20 | { 21 | "name": "three", 22 | "fields": [ 23 | {"name": "OneID", "data_type": "int"}, 24 | {"name": "Field3", "data_type": "int"} 25 | ] 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /internal/tests/internal/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | //go:generate qb-generator db.json tables.go 4 | -------------------------------------------------------------------------------- /internal/tests/internal/model/tables.go: -------------------------------------------------------------------------------- 1 | // Code generated by qb-generator; DO NOT EDIT. 2 | 3 | package model 4 | 5 | import "git.ultraware.nl/Ultraware/qb/v2" 6 | 7 | ///// One ///// 8 | var ( 9 | qbOneTable = qb.Table{Name: `one`} 10 | 11 | qbOneFID = qb.TableField{Parent: &qbOneTable, Name: `ID`, ReadOnly: true, Type: qb.Int, Size: 32} 12 | qbOneFName = qb.TableField{Parent: &qbOneTable, Name: `$Name. #()`, Escape: true, Type: qb.String, Size: 50} 13 | qbOneFCreatedAt = qb.TableField{Parent: &qbOneTable, Name: `created_at`, ReadOnly: true, Type: qb.Time} 14 | ) 15 | 16 | // OneType represents the table "One" 17 | type OneType struct { 18 | ID qb.Field 19 | Name qb.Field 20 | CreatedAt qb.Field 21 | table *qb.Table 22 | } 23 | 24 | // GetTable returns an object with info about the table 25 | func (t *OneType) GetTable() *qb.Table { 26 | return t.table 27 | } 28 | 29 | // Select starts a SELECT query 30 | func (t *OneType) Select(f ...qb.Field) *qb.SelectBuilder { 31 | return t.table.Select(f) 32 | } 33 | 34 | // Delete creates a DELETE query 35 | func (t *OneType) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 36 | return t.table.Delete(c1, c...) 37 | } 38 | 39 | // Update starts an UPDATE query 40 | func (t *OneType) Update() *qb.UpdateBuilder { 41 | return t.table.Update() 42 | } 43 | 44 | // Insert starts an INSERT query 45 | func (t *OneType) Insert(f ...qb.Field) *qb.InsertBuilder { 46 | return t.table.Insert(f) 47 | } 48 | 49 | // One returns a new OneType 50 | func One() *OneType { 51 | table := qbOneTable 52 | return &OneType{ 53 | qbOneFID.Copy(&table), 54 | qbOneFName.Copy(&table), 55 | qbOneFCreatedAt.Copy(&table), 56 | &table, 57 | } 58 | } 59 | 60 | ///// Two ///// 61 | var ( 62 | qbTwoTable = qb.Table{Name: `two $#!`, Alias: `tw`, Escape: true} 63 | 64 | qbTwoFOneID = qb.TableField{Parent: &qbTwoTable, Name: `OneID`, Type: qb.Int, Size: 32} 65 | qbTwoFNumber = qb.TableField{Parent: &qbTwoTable, Name: `Number`, Type: qb.Int, Size: 32} 66 | qbTwoFComment = qb.TableField{Parent: &qbTwoTable, Name: `Comment`, Type: qb.String, Size: 100} 67 | qbTwoFModifiedAt = qb.TableField{Parent: &qbTwoTable, Name: `ModifiedAt`, Type: qb.Time, Nullable: true} 68 | ) 69 | 70 | // TwoType represents the table "Two" 71 | type TwoType struct { 72 | OneID qb.Field 73 | Number qb.Field 74 | Comment qb.Field 75 | ModifiedAt qb.Field 76 | table *qb.Table 77 | } 78 | 79 | // GetTable returns an object with info about the table 80 | func (t *TwoType) GetTable() *qb.Table { 81 | return t.table 82 | } 83 | 84 | // Select starts a SELECT query 85 | func (t *TwoType) Select(f ...qb.Field) *qb.SelectBuilder { 86 | return t.table.Select(f) 87 | } 88 | 89 | // Delete creates a DELETE query 90 | func (t *TwoType) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 91 | return t.table.Delete(c1, c...) 92 | } 93 | 94 | // Update starts an UPDATE query 95 | func (t *TwoType) Update() *qb.UpdateBuilder { 96 | return t.table.Update() 97 | } 98 | 99 | // Insert starts an INSERT query 100 | func (t *TwoType) Insert(f ...qb.Field) *qb.InsertBuilder { 101 | return t.table.Insert(f) 102 | } 103 | 104 | // Two returns a new TwoType 105 | func Two() *TwoType { 106 | table := qbTwoTable 107 | return &TwoType{ 108 | qbTwoFOneID.Copy(&table), 109 | qbTwoFNumber.Copy(&table), 110 | qbTwoFComment.Copy(&table), 111 | qbTwoFModifiedAt.Copy(&table), 112 | &table, 113 | } 114 | } 115 | 116 | ///// Three ///// 117 | var ( 118 | qbThreeTable = qb.Table{Name: `three`} 119 | 120 | qbThreeFOneID = qb.TableField{Parent: &qbThreeTable, Name: `OneID`, Type: qb.Int, Size: 32} 121 | qbThreeFField3 = qb.TableField{Parent: &qbThreeTable, Name: `Field3`, Type: qb.Int, Size: 32} 122 | ) 123 | 124 | // ThreeType represents the table "Three" 125 | type ThreeType struct { 126 | OneID qb.Field 127 | Field3 qb.Field 128 | table *qb.Table 129 | } 130 | 131 | // GetTable returns an object with info about the table 132 | func (t *ThreeType) GetTable() *qb.Table { 133 | return t.table 134 | } 135 | 136 | // Select starts a SELECT query 137 | func (t *ThreeType) Select(f ...qb.Field) *qb.SelectBuilder { 138 | return t.table.Select(f) 139 | } 140 | 141 | // Delete creates a DELETE query 142 | func (t *ThreeType) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 143 | return t.table.Delete(c1, c...) 144 | } 145 | 146 | // Update starts an UPDATE query 147 | func (t *ThreeType) Update() *qb.UpdateBuilder { 148 | return t.table.Update() 149 | } 150 | 151 | // Insert starts an INSERT query 152 | func (t *ThreeType) Insert(f ...qb.Field) *qb.InsertBuilder { 153 | return t.table.Insert(f) 154 | } 155 | 156 | // Three returns a new ThreeType 157 | func Three() *ThreeType { 158 | table := qbThreeTable 159 | return &ThreeType{ 160 | qbThreeFOneID.Copy(&table), 161 | qbThreeFField3.Copy(&table), 162 | &table, 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /internal/tests/scripts/sqlserver/create_database.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE qb_test; 2 | -------------------------------------------------------------------------------- /internal/tests/scripts/sqlserver/create_login.sql: -------------------------------------------------------------------------------- 1 | CREATE LOGIN qb_test WITH PASSWORD = 'qb_testA1', CHECK_POLICY = OFF; 2 | CREATE USER qb_test FOR LOGIN qb_test; 3 | EXEC sp_addrolemember 'db_owner', 'qb_test'; 4 | -------------------------------------------------------------------------------- /internal/tests/tests_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "git.fuyu.moe/Fuyu/assert" 12 | "git.ultraware.nl/Ultraware/qb/v2" 13 | "git.ultraware.nl/Ultraware/qb/v2/driver/autoqb" 14 | "git.ultraware.nl/Ultraware/qb/v2/internal/tests/internal/model" 15 | "git.ultraware.nl/Ultraware/qb/v2/internal/testutil" 16 | "git.ultraware.nl/Ultraware/qb/v2/qbdb" 17 | "git.ultraware.nl/Ultraware/qb/v2/qc" 18 | "git.ultraware.nl/Ultraware/qb/v2/qf" 19 | ) 20 | 21 | var ( 22 | db qbdb.DB 23 | driver string 24 | ) 25 | 26 | func TestEndToEnd(t *testing.T) { 27 | if testing.Short() { 28 | t.Skip("skipping test in short mode.") 29 | } 30 | 31 | var db *sql.DB 32 | switch os.Getenv(`TYPE`) { 33 | case `postgres`: 34 | db = initPostgres() 35 | case `mysql`: 36 | db = initMysql() 37 | case `mssql`: 38 | db = initMssql() 39 | default: 40 | t.Error(`Missing TYPE`) 41 | t.FailNow() 42 | } 43 | 44 | startTests(t, db) 45 | } 46 | 47 | func startTests(t *testing.T, d *sql.DB) { 48 | db = autoqb.New(d) 49 | driver = reflect.TypeOf(db.Driver()).String() 50 | 51 | if testing.Verbose() { 52 | db.SetDebug(true) 53 | fmt.Println() 54 | fmt.Println(testutil.Info(`Testing with:`, driver)) 55 | fmt.Println() 56 | } 57 | 58 | runTests(t) 59 | 60 | if testing.Verbose() { 61 | fmt.Println(testutil.Info(`Finished testing:`, driver)) 62 | fmt.Println() 63 | } 64 | } 65 | 66 | func runTests(t *testing.T) { 67 | if driver == `msqb.Driver` { 68 | testUpsertSeparate(t) 69 | testInsert(t) 70 | testUpsertSeparate(t) 71 | } else { 72 | testUpsert(t) 73 | testInsert(t) 74 | testUpsert(t) 75 | } 76 | if driver == `pgqb.Driver` { 77 | testIgnoreConflicts(t) 78 | } 79 | 80 | if driver == `myqb.Driver` { 81 | testUpdate(t) 82 | } else { 83 | testUpdateReturning(t) 84 | } 85 | 86 | testRollback(t) 87 | 88 | testSubQueryField(t) 89 | 90 | testSelect(t) 91 | testSelectOffset(t) 92 | testInQuery(t) 93 | testExists(t) 94 | testPrepare(t) 95 | testSubQuery(t) 96 | if driver == `pgqb.Driver` { 97 | testInnerJoinLateral(t) 98 | testLateralJoinAlias(t) 99 | } 100 | 101 | testCTE(t) 102 | testUnionAll(t) 103 | if driver == `myqb.Driver` { 104 | testDelete(t) 105 | } else { 106 | testDeleteReturning(t) 107 | } 108 | 109 | testLeftJoin(t) 110 | 111 | testNilArg(t) 112 | } 113 | 114 | func testUpsert(test *testing.T) { 115 | o := model.One() 116 | 117 | q := o.Insert(o.ID, o.Name). 118 | Values(1, `Test 1`). 119 | Values(2, `Test 2`) 120 | q.Upsert( 121 | o.Update(). 122 | Set(o.Name, qf.Concat(qf.Excluded(o.Name), `.1`)), 123 | o.ID, 124 | ) 125 | res := db.MustExec(q) 126 | 127 | assert := assert.New(test) 128 | assert.True(res.MustRowsAffected() >= 2) 129 | } 130 | 131 | func testUpsertSeparate(test *testing.T) { 132 | o := model.One() 133 | 134 | iq := o.Insert(o.ID, o.Name). 135 | Values(1, `Test 1`). 136 | Values(2, `Test 2`) 137 | res, err := db.Exec(iq) 138 | 139 | assert := assert.New(test) 140 | if err == nil { 141 | assert.Eq(int64(2), res.MustRowsAffected()) 142 | return 143 | } 144 | 145 | uq := o.Update(). 146 | Set(o.Name, qf.Concat(o.Name, `.1`)) 147 | 148 | res = db.MustExec(uq) 149 | 150 | assert.Eq(int64(2), res.MustRowsAffected()) 151 | } 152 | 153 | func testIgnoreConflicts(test *testing.T) { 154 | o := model.One() 155 | 156 | q := o.Insert(o.ID, o.Name). 157 | Values(1, `Test 1`). 158 | IgnoreConflict(o.ID) 159 | res := db.MustExec(q) 160 | 161 | assert := assert.New(test) 162 | assert.Eq(int64(0), res.MustRowsAffected()) 163 | } 164 | 165 | func testInsert(test *testing.T) { 166 | t := model.Two() 167 | 168 | tx := db.MustBegin() 169 | 170 | q := t.Insert(t.OneID, t.Number, t.Comment). 171 | Values(1, 1, `Test comment`). 172 | Values(1, 2, `Test comment 2`) 173 | 174 | res := tx.MustExec(q) 175 | 176 | assert := assert.New(test) 177 | assert.Eq(int64(2), res.MustRowsAffected()) 178 | 179 | tx.MustCommit() 180 | } 181 | 182 | func testUpdate(test *testing.T) { 183 | t := model.Two() 184 | 185 | q := t.Update(). 186 | Set(t.Comment, qf.Concat(t.Comment, ` v2`)). 187 | Where(qc.Eq(t.OneID, 1)) 188 | 189 | res := db.MustExec(q) 190 | 191 | assert := assert.New(test) 192 | assert.Eq(int64(2), res.MustRowsAffected()) 193 | } 194 | 195 | func testUpdateReturning(test *testing.T) { 196 | t := model.Two() 197 | 198 | q := t.Update(). 199 | Set(t.Comment, qf.Concat(t.Comment, ` v2`)). 200 | Where(qc.Eq(t.OneID, 1)) 201 | 202 | r := db.MustQuery(qb.Returning(q, t.Comment, t.Number)) 203 | 204 | assert := assert.New(test) 205 | 206 | var ( 207 | comment = `` 208 | number = 0 209 | ) 210 | 211 | assert.True(r.Next()) 212 | assert.NoError(r.Scan(&comment, &number)) 213 | assert.Eq(`Test comment v2`, comment) 214 | assert.Eq(1, number) 215 | 216 | assert.True(r.Next()) 217 | r.MustScan(&comment, &number) 218 | assert.Eq(`Test comment 2 v2`, comment) 219 | assert.Eq(2, number) 220 | 221 | assert.False(r.Next()) 222 | } 223 | 224 | func testSubQueryField(test *testing.T) { 225 | o, t := model.One(), model.Two() 226 | 227 | sq := o.Select(o.ID).Where(qc.Eq(o.Name, `Test 1.1`)) 228 | 229 | q := t.Select(t.Number, t.Comment, t.ModifiedAt). 230 | Where(qc.Eq(t.OneID, sq)) 231 | r := db.QueryRow(q) 232 | 233 | var ( 234 | number int 235 | comment string 236 | modified *time.Time 237 | assert = assert.New(test) 238 | ) 239 | 240 | assert.True(r.MustScan(&number, &comment, &modified)) 241 | 242 | assert.Eq(1, number) 243 | assert.Eq(`Test comment v2`, comment) 244 | assert.Nil(modified) 245 | } 246 | 247 | func testSelect(test *testing.T) { 248 | o, t := model.One(), model.Two() 249 | 250 | q := o.Select(o.ID, o.Name, qf.Year(o.CreatedAt), t.Number, t.Comment, t.ModifiedAt). 251 | InnerJoin(t.OneID, o.ID). 252 | Where(qc.Eq(o.ID, 1)) 253 | r := db.QueryRow(q) 254 | 255 | var ( 256 | id int 257 | name string 258 | year, number int 259 | comment string 260 | modified *time.Time 261 | assert = assert.New(test) 262 | ) 263 | 264 | assert.True(r.MustScan(&id, &name, &year, &number, &comment, &modified)) 265 | 266 | assert.Eq(1, id) 267 | assert.Eq(`Test 1.1`, name) 268 | assert.Eq(time.Now().Year(), year) 269 | 270 | assert.Eq(1, number) 271 | assert.Eq(`Test comment v2`, comment) 272 | assert.Nil(modified) 273 | } 274 | 275 | func testNilArg(test *testing.T) { 276 | assert := assert.New(test) 277 | 278 | defer func() { 279 | assert.Nil(recover(), `test failed, expected no panic`) 280 | }() 281 | 282 | o := model.One() 283 | 284 | q := o.Select(o.ID, o.Name). 285 | Where( 286 | qc.Eq(o.ID, nil), 287 | ) 288 | 289 | r := db.QueryRow(q) 290 | 291 | var ( 292 | id int 293 | name string 294 | ) 295 | 296 | r.MustScan(&id, &name) 297 | } 298 | 299 | func testSelectOffset(test *testing.T) { 300 | o := model.One() 301 | 302 | q := o.Select(o.ID, o.Name, qf.Year(o.CreatedAt)). 303 | OrderBy(qb.Asc(o.ID)). 304 | Limit(2). 305 | Offset(1) 306 | r := db.QueryRow(q) 307 | 308 | var ( 309 | id int 310 | name string 311 | year int 312 | assert = assert.New(test) 313 | ) 314 | 315 | assert.True(r.MustScan(&id, &name, &year)) 316 | 317 | assert.Eq(2, id) 318 | assert.Eq(`Test 2.1`, name) 319 | assert.Eq(time.Now().Year(), year) 320 | } 321 | 322 | func testInQuery(test *testing.T) { 323 | o, o2 := model.One(), model.One() 324 | 325 | sq := o2.Select(o2.ID).Where(qc.Eq(o2.ID, 1)) 326 | 327 | q := o.Select(o.Name). 328 | Where(qc.InQuery(o.ID, sq)) 329 | row := db.QueryRow(q) 330 | 331 | var name string 332 | assert := assert.New(test) 333 | 334 | assert.True(row.MustScan(&name)) 335 | 336 | assert.Eq(`Test 1.1`, name) 337 | } 338 | 339 | func testExists(test *testing.T) { 340 | o, t, t2 := model.One(), model.Two(), model.Two() 341 | 342 | sq := t2.Select(t2.OneID).Where(qc.Eq(t2.OneID, o.ID), qc.Eq(t2.OneID, t.OneID)) 343 | 344 | q := o.Select(qf.Count(qf.Distinct(o.Name)), qf.Count(t.OneID)). 345 | LeftJoin(t.OneID, o.ID). 346 | Where(qc.Exists(sq)) 347 | row := db.QueryRow(q) 348 | 349 | var names, count int 350 | assert := assert.New(test) 351 | 352 | assert.True(row.MustScan(&names, &count)) 353 | 354 | assert.Eq(1, names) 355 | assert.Eq(2, count) 356 | } 357 | 358 | func testPrepare(test *testing.T) { 359 | o := model.One() 360 | 361 | oneid := 0 362 | 363 | q := o.Select(o.ID). 364 | Where(qc.Eq(o.ID, &oneid)) 365 | 366 | stmt, err := db.Prepare(q) 367 | if err != nil { 368 | panic(err) 369 | } 370 | 371 | assert := assert.New(test) 372 | out := 0 373 | 374 | assert.Eq(sql.ErrNoRows, stmt.QueryRow().Scan(&out)) 375 | 376 | oneid = 1 377 | assert.NoError(stmt.QueryRow().Scan(&out)) 378 | assert.Eq(oneid, out) 379 | 380 | oneid = 2 381 | assert.True(stmt.QueryRow().MustScan(&out)) 382 | assert.Eq(oneid, out) 383 | 384 | oneid = 3 385 | assert.False(stmt.QueryRow().MustScan(&out)) 386 | } 387 | 388 | type twoSQ struct { 389 | One qb.Field 390 | Count qb.Field 391 | } 392 | 393 | func testSubQuery(test *testing.T) { 394 | testSub(test, func(q *qb.SelectBuilder, sq *twoSQ) { 395 | q.SubQuery(&sq.One, &sq.Count) 396 | }) 397 | } 398 | 399 | func testCTE(test *testing.T) { 400 | testSub(test, func(q *qb.SelectBuilder, sq *twoSQ) { 401 | q.CTE(&sq.One, &sq.Count) 402 | }) 403 | } 404 | 405 | func testSub(test *testing.T, sqf func(*qb.SelectBuilder, *twoSQ)) { 406 | o, t := model.One(), model.Two() 407 | 408 | var sq twoSQ 409 | sqf(t.Select(t.OneID, qf.CountAll()).GroupBy(t.OneID), &sq) 410 | 411 | q := o.Select(o.ID, sq.Count). 412 | InnerJoin(sq.One, o.ID) 413 | r := db.QueryRow(q) 414 | 415 | var id, count int 416 | assert := assert.New(test) 417 | 418 | assert.True(r.MustScan(&id, &count)) 419 | 420 | assert.Eq(1, id) 421 | assert.Eq(2, count) 422 | 423 | t.Select(t.OneID).SubQuery() // No fields should pass 424 | } 425 | 426 | func testUnionAll(test *testing.T) { 427 | o := model.One() 428 | 429 | sq1 := o.Select(o.ID).Where(qc.Eq(o.ID, 1)) 430 | sq2 := o.Select(o.ID).Where(qc.Eq(o.Name, `Test 1.1`)) 431 | 432 | q := qb.UnionAll(sq1, sq2) 433 | r, err := db.Query(q) 434 | if err != nil { 435 | panic(err) 436 | } 437 | 438 | var ( 439 | id int 440 | assert = assert.New(test) 441 | ) 442 | 443 | assert.True(r.Next()) 444 | r.MustScan(&id) 445 | assert.Eq(1, id) 446 | 447 | assert.True(r.Next()) 448 | r.MustScan(&id) 449 | assert.Eq(1, id) 450 | 451 | assert.False(r.Next()) 452 | } 453 | 454 | func testDeleteReturning(test *testing.T) { 455 | t := model.Two() 456 | 457 | q := t.Delete(qc.Eq(t.OneID, 1)) 458 | r, err := db.Query(qb.Returning(q, t.Number)) 459 | if err != nil { 460 | panic(err) 461 | } 462 | 463 | var number int 464 | 465 | assert := assert.New(test) 466 | 467 | assert.True(r.Next()) 468 | r.MustScan(&number) 469 | assert.Eq(1, number) 470 | 471 | assert.True(r.Next()) 472 | r.MustScan(&number) 473 | assert.Eq(2, number) 474 | 475 | assert.False(r.Next()) 476 | } 477 | 478 | func testDelete(test *testing.T) { 479 | t := model.Two() 480 | 481 | q := t.Delete(qc.Eq(t.OneID, 1)) 482 | _, err := db.Exec(q) 483 | 484 | assert := assert.New(test) 485 | assert.NoError(err) 486 | } 487 | 488 | func testLeftJoin(test *testing.T) { 489 | o, t := model.One(), model.Two() 490 | 491 | q := o.Select(o.ID, t.OneID). 492 | LeftJoin(t.OneID, o.ID). 493 | Where(qc.Eq(o.ID, 1)) 494 | r := db.QueryRow(q) 495 | 496 | var ( 497 | id int 498 | oneid *int 499 | assert = assert.New(test) 500 | ) 501 | 502 | assert.True(r.MustScan(&id, &oneid)) 503 | assert.Eq(1, id) 504 | assert.Nil(oneid) 505 | } 506 | 507 | func testInnerJoinLateral(test *testing.T) { 508 | assert := assert.New(test) 509 | 510 | o, t := model.One(), model.Two() 511 | 512 | var fa, fb qb.Field 513 | sq := t.Select(t.Comment, qf.NewCalculatedField("2")). 514 | Where(qc.Eq(t.OneID, o.ID)). 515 | SubQuery(&fa, &fb).Lateral() 516 | 517 | db.SetDebug(true) 518 | q := o.Select(o.ID, fa, fb).InnerJoinLateral(sq, qc.Eq(1, 1)) 519 | r := db.QueryRow(q) 520 | 521 | var ( 522 | id int 523 | comment string 524 | number int 525 | ) 526 | assert.True(r.MustScan(&id, &comment, &number)) 527 | assert.Eq(1, id) 528 | assert.Eq(`Test comment v2`, comment) 529 | assert.Eq(2, number) 530 | 531 | // test result sql (with proper field naming) of subquery 532 | b := qb.NewSQLBuilder(db.Driver()) 533 | s, _ := q.SQL(b) 534 | assert.Eq(`SELECT o.ID, sq2.Comment0, sq2.f1 535 | FROM one AS o 536 | INNER JOIN LATERAL ( 537 | SELECT tw.Comment Comment0, 2 f1 538 | FROM "two $#!" AS tw 539 | WHERE tw.OneID = o.ID 540 | ) sq2 ON (? = ?) 541 | LIMIT 1 542 | `, s) 543 | } 544 | 545 | func testLateralJoinAlias(t *testing.T) { 546 | o, t2, t3 := model.One(), model.Two(), model.Three() 547 | // force auto aliasing 548 | o.GetTable().Alias = `` 549 | t2.GetTable().Alias = `` 550 | t3.GetTable().Alias = `` 551 | 552 | // create test data temporary 553 | tx := db.MustBegin() 554 | defer tx.Rollback() 555 | i := o.Insert(o.ID, o.Name).Values(3, `Test 3`) 556 | tx.MustExec(i) 557 | i = t2.Insert(t2.OneID, t2.Number, t2.Comment).Values(3, 3*3, `Test comment 3`) 558 | tx.MustExec(i) 559 | i = t3.Insert(t3.OneID, t3.Field3).Values(3, 3*3*3) 560 | tx.MustExec(i) 561 | 562 | t.Run(`tables with same alias "t"`, func(t *testing.T) { 563 | assert := assert.New(t) 564 | 565 | var fa qb.Field 566 | sq := t2.Select(t2.Comment). 567 | Where(qc.Eq(t2.OneID, t3.OneID)). 568 | SubQuery(&fa).Lateral() 569 | // lateral subquery must use global alias generator, otherwise this error will occur: 570 | // panic: pq: missing FROM-clause entry for table "t2" 571 | q := t3.Select(t3.Field3, fa).InnerJoinLateral(sq, qc.Eq(1, 1)) 572 | r := tx.QueryRow(q) 573 | 574 | var field int 575 | var comment string 576 | assert.True(r.MustScan(&field, &comment)) 577 | assert.Eq(3*3*3, field) 578 | assert.Eq(`Test comment 3`, comment) 579 | }) 580 | 581 | t.Run(`lateral subqueries must be able to use each other fields`, func(t *testing.T) { 582 | assert := assert.New(t) 583 | 584 | var fa, fa2 qb.Field 585 | sq := t2.Select(t2.Comment). 586 | Where(qc.Eq(t2.OneID, t3.OneID)). 587 | SubQuery(&fa).Lateral() 588 | sq2 := t2.Select(t2.Comment). 589 | Where( 590 | qc.Eq(fa, ``), 591 | qc.Eq(t2.OneID, 1), 592 | ). 593 | SubQuery(&fa2).Lateral() 594 | // lateral subquery must use global alias generator, otherwise this error will occur: 595 | // panic: pq: missing FROM-clause entry for table "sq" 596 | q := t3.Select(t3.Field3, fa, fa2). 597 | InnerJoinLateral(sq, qc.Eq(1, 1)). 598 | LeftJoinLateral(sq2, qc.Eq(1, 1)) 599 | r := tx.QueryRow(q) 600 | 601 | var field int 602 | var comment string 603 | var comment2 *string 604 | assert.True(r.MustScan(&field, &comment, &comment2)) 605 | assert.Eq(3*3*3, field) 606 | assert.Eq(`Test comment 3`, comment) 607 | assert.Nil(comment2) 608 | }) 609 | } 610 | 611 | func testRollback(test *testing.T) { 612 | o := model.One() 613 | 614 | tx := db.MustBegin() 615 | 616 | q := o.Delete(qc.Eq(1, 1)) 617 | tx.MustExec(q) 618 | 619 | assert := assert.New(test) 620 | assert.NoError(tx.Rollback()) 621 | } 622 | -------------------------------------------------------------------------------- /internal/testutil/test_helpers.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | var ( 11 | // Warn is used when printing warnings 12 | Warn = colored(31) 13 | // Okay is used when printing text signifying a passed test 14 | Okay = colored(32) 15 | // Info is used for info messages 16 | Info = colored(35) 17 | // Notice is used for less important info messages 18 | Notice = colored(36) 19 | // String is used when printing strings 20 | String = colored(33) 21 | ) 22 | 23 | func colored(code int) func(v ...interface{}) string { 24 | return func(v ...interface{}) string { 25 | return fmt.Sprint(append([]interface{}{shell(code)}, append(v, shell(0))...)...) 26 | } 27 | } 28 | 29 | func shell(i int) string { 30 | return "\x1B[" + strconv.Itoa(i) + "m" 31 | } 32 | 33 | func quoted(s string) string { 34 | var n string 35 | if len(strings.Split(s, "\n")) > 1 { 36 | n = "\n" 37 | } 38 | return fmt.Sprint(n, `"`, String(s), `"`) 39 | } 40 | 41 | // Compare data with the expected data and prints test output 42 | func Compare(t *testing.T, expected string, out string) { 43 | if out != expected { 44 | t.Error(Warn(`FAIL!`), "\n\n"+ 45 | `Got: `+quoted(out)+"\n"+ 46 | `Expected: `+quoted(expected)+"\n", 47 | ) 48 | } else { 49 | t.Log(Okay(`PASS:`), quoted(strings.TrimSuffix(out, "\n"))) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /join.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | type join struct { 4 | Type Join 5 | New Source 6 | Conditions []Condition 7 | } 8 | 9 | // Join is the type of join 10 | type Join string 11 | 12 | // All possible join types 13 | const ( 14 | JoinInner Join = `INNER` 15 | JoinLeft Join = `LEFT` 16 | JoinRight Join = `RIGHT` 17 | JoinCross Join = `CROSS` 18 | ) 19 | -------------------------------------------------------------------------------- /override.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import ( 4 | "reflect" 5 | "runtime" 6 | ) 7 | 8 | // GetFuncFrame returns a function 9 | func GetFuncFrame() string { 10 | pc := make([]uintptr, 1) 11 | runtime.Callers(3, pc) 12 | frame, _ := runtime.CallersFrames(pc).Next() 13 | fn := frame.Function 14 | 15 | return fn 16 | } 17 | 18 | // OverrideMap allows a driver to override functions from qf and qc. 19 | // This type is not intended to be used directly 20 | type OverrideMap map[string]interface{} 21 | 22 | // Add adds an override to the map 23 | func (m OverrideMap) Add(target, nv interface{}) { 24 | rt, rn := reflect.TypeOf(target), reflect.TypeOf(nv) 25 | 26 | if rt.Kind() != reflect.Func || rn.Kind() != reflect.Func { 27 | panic(`Cannot use non-function arguments in OverrideMap.Add`) 28 | } 29 | 30 | if rt != rn { 31 | panic(`Arguments in OverrideMap.Add must be the same type`) 32 | } 33 | 34 | if !isQbType(rt) { 35 | panic(`Arguments must be qb types`) 36 | } 37 | 38 | m[runtime.FuncForPC(reflect.ValueOf(target).Pointer()).Name()] = nv 39 | } 40 | 41 | func isQbType(rt reflect.Type) bool { 42 | field := reflect.TypeOf((*Field)(nil)).Elem() 43 | condition := reflect.TypeOf((*Condition)(nil)).Elem() 44 | 45 | return rt.NumOut() == 1 && (rt.Out(0).Implements(field) || rt.Out(0) == condition) 46 | } 47 | 48 | // Condition gets an override for qc, if there is no entry in the map fallback will be used 49 | func (m OverrideMap) Condition(source string, fallback interface{}, in []interface{}) Condition { 50 | return m.execute(source, fallback, in).(Condition) 51 | } 52 | 53 | // Field gets an override for qf, if there is no entry in the map fallback will be used 54 | func (m OverrideMap) Field(source string, fallback interface{}, in []interface{}) Field { 55 | return m.execute(source, fallback, in).(Field) 56 | } 57 | 58 | func (m OverrideMap) execute(source string, fallback interface{}, in []interface{}) interface{} { 59 | values := make([]reflect.Value, len(in)) 60 | for k, v := range in { 61 | if v, ok := v.(reflect.Value); ok { 62 | values[k] = v 63 | continue 64 | } 65 | if v == nil { 66 | values[k] = reflect.ValueOf(&in[k]).Elem() 67 | continue 68 | } 69 | values[k] = reflect.ValueOf(v) 70 | } 71 | 72 | if v, ok := m[source]; ok { 73 | return reflect.ValueOf(v).Call(values)[0].Interface() 74 | } 75 | 76 | if fallback == nil { 77 | panic(`Function "` + source + `" not implemented by driver`) 78 | } 79 | 80 | return reflect.ValueOf(fallback).Call(values)[0].Interface() 81 | } 82 | -------------------------------------------------------------------------------- /override_test.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import ( 4 | "testing" 5 | 6 | "git.fuyu.moe/Fuyu/assert" 7 | ) 8 | 9 | var override = OverrideMap{} 10 | 11 | func TField() Field { 12 | return func() Field { 13 | return override.Field(GetFuncFrame(), field1, nil) 14 | }() 15 | } 16 | 17 | type calculatedField func(*Context) string 18 | 19 | func (f calculatedField) QueryString(c *Context) string { 20 | return f(c) 21 | } 22 | 23 | func field1() Field { 24 | return calculatedField(func(_ *Context) string { 25 | return `test` 26 | }) 27 | } 28 | 29 | func field2() Field { 30 | return calculatedField(func(_ *Context) string { 31 | return `test2` 32 | }) 33 | } 34 | 35 | func TestOverrideField(t *testing.T) { 36 | assert := assert.New(t) 37 | 38 | assert.Cmp(TField().QueryString(nil), field1().QueryString(nil)) 39 | 40 | override.Add(TField, field2) 41 | 42 | assert.Cmp(TField().QueryString(nil), field2().QueryString(nil)) 43 | } 44 | -------------------------------------------------------------------------------- /qb-architect/README.md: -------------------------------------------------------------------------------- 1 | # qb-architect 2 | 3 | qb-architect is used to generate `db.json` for qb. 4 | 5 | Assume a database `example` with table: 6 | 7 | ```SQL 8 | CREATE TYPE e_type AS ENUM ( 9 | 'a', 10 | 'b' 11 | ); 12 | 13 | CREATE TABLE example ( 14 | pk serial PRIMARY KEY, 15 | t varchar(50) NOT NULL, 16 | i integer NOT NULL, 17 | e e_type NOT NULL, 18 | b boolean NOT NULL, 19 | n int NULL 20 | ); 21 | ``` 22 | 23 | This database will render this `db.json`: 24 | 25 | ```json 26 | [ 27 | { 28 | "name": "public.example", 29 | "fields": [ 30 | { 31 | "name": "b", 32 | "data_type": "boolean", 33 | "size": 1 34 | }, 35 | { 36 | "name": "e", 37 | "data_type": "e_type", 38 | "size": 4 39 | }, 40 | { 41 | "name": "i", 42 | "data_type": "integer", 43 | "size": 4 44 | }, 45 | { 46 | "name": "n", 47 | "data_type": "integer", 48 | "null": true, 49 | "size": 4 50 | }, 51 | { 52 | "name": "pk", 53 | "data_type": "integer", 54 | "size": 4 55 | }, 56 | { 57 | "name": "t", 58 | "data_type": "character varying", 59 | "size": 50 60 | } 61 | ] 62 | } 63 | ] 64 | ``` 65 | 66 | qb-architect it the tool that will generate this output for you 67 | 68 | ## Example useage 69 | 70 | To generate a db.json, qb-architect needs to know which database to generate the 71 | json from and the connection string 72 | 73 | ```bash 74 | $ qb-architect -help 75 | Usage of qb-architect: 76 | -dbms string 77 | Database type to use: psql, mysql, mssql 78 | -fexclude value 79 | Regular expressions to exclude fields 80 | -fonly value 81 | Regular expressions to whitelist fields, only tables that match at least one are returned 82 | -texclude value 83 | Regular expressions to exclude tables 84 | -tonly value 85 | Regular expressions to whitelist tables, only tables that match at least one are returned 86 | ``` 87 | 88 | To render the above example the next command will be used 89 | 90 | ```bash 91 | $ qb-architect -dbms psql "host=/tmp dbname=example" 92 | ``` 93 | 94 | #### Exclude and only 95 | 96 | If for some reason you would not want or only want a table `t` or field `f` you can 97 | use the exclude and only options 98 | 99 | ```bash 100 | -fexclude value 101 | Regular expressions to exclude fields 102 | -fonly value 103 | Regular expressions to whitelist fields, only tables that match at least one are returned 104 | -texclude value 105 | Regular expressions to exclude tables 106 | -tonly value 107 | Regular expressions to whitelist tables, only tables that match at least one are returned 108 | ``` 109 | -------------------------------------------------------------------------------- /qb-architect/internal/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | // Driver is the minimal interface needed for db.json generation 4 | type Driver interface { 5 | GetTables() []string 6 | GetFields(string) []Field 7 | } 8 | 9 | // Table represents the basic structure of db.json 10 | type Table struct { 11 | Name string `json:"name"` 12 | Fields []Field `json:"fields"` 13 | } 14 | 15 | // Field represents the structure for a db.json field 16 | type Field struct { 17 | Name string `json:"name"` 18 | Type string `json:"data_type,omitempty"` 19 | Nullable bool `json:"null,omitempty"` 20 | Size int `json:"size,omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /qb-architect/internal/db/msarchitect/msarchitect.go: -------------------------------------------------------------------------------- 1 | package msarchitect 2 | 3 | import ( 4 | "database/sql" 5 | "strings" 6 | 7 | "git.ultraware.nl/Ultraware/qb/v2" 8 | "git.ultraware.nl/Ultraware/qb/v2/driver/msqb" 9 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db" 10 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db/msarchitect/msmodel" 11 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/util" 12 | "git.ultraware.nl/Ultraware/qb/v2/qbdb" 13 | "git.ultraware.nl/Ultraware/qb/v2/qc" 14 | "git.ultraware.nl/Ultraware/qb/v2/qf" 15 | 16 | // mssql driver 17 | _ "github.com/microsoft/go-mssqldb" // database driver for Microsoft MSSQL 18 | ) 19 | 20 | // Driver implements db.Driver 21 | type Driver struct { 22 | DB qbdb.DB 23 | } 24 | 25 | // New opens a database connection and returns a Driver 26 | func New(dsn string) db.Driver { 27 | d, err := sql.Open(`sqlserver`, dsn) 28 | util.PanicOnErr(err) 29 | 30 | return Driver{msqb.New(d)} 31 | } 32 | 33 | func database() qb.Field { 34 | return qf.NewCalculatedField(`DB_NAME()`) 35 | } 36 | 37 | // GetTables returns all tables in the database 38 | func (d Driver) GetTables() []string { 39 | it := msmodel.Tables() 40 | 41 | q := it.Select(it.TableSchema, it.TableName). 42 | Where( 43 | qc.Eq(it.TableType, `BASE TABLE`), 44 | qc.Eq(it.TableCatalog, database()), 45 | ). 46 | GroupBy(it.TableSchema, it.TableName) 47 | 48 | rows, err := d.DB.Query(q) 49 | util.PanicOnErr(err) 50 | 51 | var tables []string 52 | for rows.Next() { 53 | var schema, table string 54 | err := rows.Scan(&schema, &table) 55 | util.PanicOnErr(err) 56 | 57 | tables = append(tables, schema+`.`+table) 58 | } 59 | 60 | return tables 61 | } 62 | 63 | // GetFields returns all fields in a table 64 | func (d Driver) GetFields(table string) []db.Field { 65 | sp := strings.Split(table, `.`) 66 | schema := sp[0] 67 | table = sp[1] 68 | 69 | c := msmodel.Columns() 70 | 71 | q := c.Select(c.ColumnName, c.DataType, c.IsNullable). 72 | Where( 73 | qc.Eq(c.TableCatalog, database()), 74 | qc.Eq(c.TableName, table), 75 | qc.Eq(c.TableSchema, schema), 76 | ). 77 | GroupBy(c.ColumnName, c.DataType, c.IsNullable) 78 | 79 | rows, err := d.DB.Query(q) 80 | util.PanicOnErr(err) 81 | 82 | var fields []db.Field 83 | 84 | for rows.Next() { 85 | f := db.Field{} 86 | 87 | var isNullable string 88 | 89 | err := rows.Scan(&f.Name, &f.Type, &isNullable) 90 | util.PanicOnErr(err) 91 | 92 | f.Nullable = isNullable == `YES` 93 | 94 | fields = append(fields, f) 95 | } 96 | 97 | return fields 98 | } 99 | -------------------------------------------------------------------------------- /qb-architect/internal/db/msarchitect/msmodel/db.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "information_schema.tables", 4 | "fields": [ 5 | {"name": "table_name"}, 6 | {"name": "table_schema"}, 7 | {"name": "table_catalog"}, 8 | {"name": "table_type"} 9 | ] 10 | }, 11 | { 12 | "name": "information_schema.columns", 13 | "fields": [ 14 | {"name": "column_name"}, 15 | {"name": "table_schema"}, 16 | {"name": "data_type"}, 17 | {"name": "is_nullable"}, 18 | {"name": "character_maximum_length"}, 19 | {"name": "table_catalog"}, 20 | {"name": "table_name"} 21 | ] 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /qb-architect/internal/db/msarchitect/msmodel/model.go: -------------------------------------------------------------------------------- 1 | package msmodel 2 | 3 | //go:generate qb-generator -package=msmodel db.json tables.go 4 | -------------------------------------------------------------------------------- /qb-architect/internal/db/msarchitect/msmodel/tables.go: -------------------------------------------------------------------------------- 1 | // Code generated by qb-generator; DO NOT EDIT. 2 | 3 | package msmodel 4 | 5 | import "git.ultraware.nl/Ultraware/qb/v2" 6 | 7 | ///// Tables ///// 8 | var ( 9 | qbTablesTable = qb.Table{Name: `information_schema.tables`} 10 | 11 | qbTablesFTableName = qb.TableField{Parent: &qbTablesTable, Name: `table_name`} 12 | qbTablesFTableSchema = qb.TableField{Parent: &qbTablesTable, Name: `table_schema`} 13 | qbTablesFTableCatalog = qb.TableField{Parent: &qbTablesTable, Name: `table_catalog`} 14 | qbTablesFTableType = qb.TableField{Parent: &qbTablesTable, Name: `table_type`} 15 | ) 16 | 17 | // TablesType represents the table "Tables" 18 | type TablesType struct { 19 | TableName qb.Field 20 | TableSchema qb.Field 21 | TableCatalog qb.Field 22 | TableType qb.Field 23 | table *qb.Table 24 | } 25 | 26 | // GetTable returns an object with info about the table 27 | func (t *TablesType) GetTable() *qb.Table { 28 | return t.table 29 | } 30 | 31 | // Select starts a SELECT query 32 | func (t *TablesType) Select(f ...qb.Field) *qb.SelectBuilder { 33 | return t.table.Select(f) 34 | } 35 | 36 | // Delete creates a DELETE query 37 | func (t *TablesType) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 38 | return t.table.Delete(c1, c...) 39 | } 40 | 41 | // Update starts an UPDATE query 42 | func (t *TablesType) Update() *qb.UpdateBuilder { 43 | return t.table.Update() 44 | } 45 | 46 | // Insert starts an INSERT query 47 | func (t *TablesType) Insert(f ...qb.Field) *qb.InsertBuilder { 48 | return t.table.Insert(f) 49 | } 50 | 51 | // Tables returns a new TablesType 52 | func Tables() *TablesType { 53 | table := qbTablesTable 54 | return &TablesType{ 55 | qbTablesFTableName.Copy(&table), 56 | qbTablesFTableSchema.Copy(&table), 57 | qbTablesFTableCatalog.Copy(&table), 58 | qbTablesFTableType.Copy(&table), 59 | &table, 60 | } 61 | } 62 | 63 | ///// Columns ///// 64 | var ( 65 | qbColumnsTable = qb.Table{Name: `information_schema.columns`} 66 | 67 | qbColumnsFColumnName = qb.TableField{Parent: &qbColumnsTable, Name: `column_name`} 68 | qbColumnsFTableSchema = qb.TableField{Parent: &qbColumnsTable, Name: `table_schema`} 69 | qbColumnsFDataType = qb.TableField{Parent: &qbColumnsTable, Name: `data_type`} 70 | qbColumnsFIsNullable = qb.TableField{Parent: &qbColumnsTable, Name: `is_nullable`} 71 | qbColumnsFCharacterMaximumLength = qb.TableField{Parent: &qbColumnsTable, Name: `character_maximum_length`} 72 | qbColumnsFTableCatalog = qb.TableField{Parent: &qbColumnsTable, Name: `table_catalog`} 73 | qbColumnsFTableName = qb.TableField{Parent: &qbColumnsTable, Name: `table_name`} 74 | ) 75 | 76 | // ColumnsType represents the table "Columns" 77 | type ColumnsType struct { 78 | ColumnName qb.Field 79 | TableSchema qb.Field 80 | DataType qb.Field 81 | IsNullable qb.Field 82 | CharacterMaximumLength qb.Field 83 | TableCatalog qb.Field 84 | TableName qb.Field 85 | table *qb.Table 86 | } 87 | 88 | // GetTable returns an object with info about the table 89 | func (t *ColumnsType) GetTable() *qb.Table { 90 | return t.table 91 | } 92 | 93 | // Select starts a SELECT query 94 | func (t *ColumnsType) Select(f ...qb.Field) *qb.SelectBuilder { 95 | return t.table.Select(f) 96 | } 97 | 98 | // Delete creates a DELETE query 99 | func (t *ColumnsType) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 100 | return t.table.Delete(c1, c...) 101 | } 102 | 103 | // Update starts an UPDATE query 104 | func (t *ColumnsType) Update() *qb.UpdateBuilder { 105 | return t.table.Update() 106 | } 107 | 108 | // Insert starts an INSERT query 109 | func (t *ColumnsType) Insert(f ...qb.Field) *qb.InsertBuilder { 110 | return t.table.Insert(f) 111 | } 112 | 113 | // Columns returns a new ColumnsType 114 | func Columns() *ColumnsType { 115 | table := qbColumnsTable 116 | return &ColumnsType{ 117 | qbColumnsFColumnName.Copy(&table), 118 | qbColumnsFTableSchema.Copy(&table), 119 | qbColumnsFDataType.Copy(&table), 120 | qbColumnsFIsNullable.Copy(&table), 121 | qbColumnsFCharacterMaximumLength.Copy(&table), 122 | qbColumnsFTableCatalog.Copy(&table), 123 | qbColumnsFTableName.Copy(&table), 124 | &table, 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /qb-architect/internal/db/myarchitect/myarchitect.go: -------------------------------------------------------------------------------- 1 | package myarchitect 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "git.ultraware.nl/Ultraware/qb/v2" 7 | "git.ultraware.nl/Ultraware/qb/v2/driver/myqb" 8 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db" 9 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db/myarchitect/mymodel" 10 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/util" 11 | "git.ultraware.nl/Ultraware/qb/v2/qbdb" 12 | "git.ultraware.nl/Ultraware/qb/v2/qc" 13 | "git.ultraware.nl/Ultraware/qb/v2/qf" 14 | 15 | // mysql driver 16 | _ "github.com/go-sql-driver/mysql" 17 | ) 18 | 19 | // Driver implements db.Driver 20 | type Driver struct { 21 | DB qbdb.DB 22 | } 23 | 24 | // New opens a database connection and returns a Driver 25 | func New(dsn string) db.Driver { 26 | d, err := sql.Open(`mysql`, dsn) 27 | util.PanicOnErr(err) 28 | 29 | return Driver{myqb.New(d)} 30 | } 31 | 32 | func database() qb.Field { 33 | return qf.NewCalculatedField(`database()`) 34 | } 35 | 36 | // GetTables returns all tables in the database 37 | func (d Driver) GetTables() []string { 38 | it := mymodel.Tables() 39 | 40 | q := it.Select(it.TableName). 41 | Where(qc.Eq(it.TableSchema, database())). 42 | GroupBy(it.TableSchema, it.TableName) 43 | 44 | rows, err := d.DB.Query(q) 45 | util.PanicOnErr(err) 46 | 47 | var tables []string 48 | for rows.Next() { 49 | var tbl string 50 | err := rows.Scan(&tbl) 51 | util.PanicOnErr(err) 52 | 53 | tables = append(tables, tbl) 54 | } 55 | 56 | return tables 57 | } 58 | 59 | // GetFields returns all fields in a table 60 | func (d Driver) GetFields(table string) []db.Field { 61 | c := mymodel.Columns() 62 | 63 | q := c.Select(c.ColumnName, c.DataType, c.IsNullable, c.CharacterMaximumLength). 64 | Where( 65 | qc.Eq(c.TableSchema, database()), 66 | qc.Eq(c.TableName, table), 67 | ). 68 | GroupBy(c.ColumnName, c.DataType, c.IsNullable, c.CharacterMaximumLength) 69 | 70 | rows, err := d.DB.Query(q) 71 | util.PanicOnErr(err) 72 | 73 | var fields []db.Field 74 | for rows.Next() { 75 | f := db.Field{} 76 | 77 | var isNullable string 78 | var characterMaximumLength sql.NullInt64 79 | 80 | err := rows.Scan(&f.Name, &f.Type, &isNullable, &characterMaximumLength) 81 | util.PanicOnErr(err) 82 | 83 | f.Nullable = isNullable == `YES` 84 | if characterMaximumLength.Valid { 85 | f.Size = int(characterMaximumLength.Int64) 86 | } 87 | 88 | fields = append(fields, f) 89 | } 90 | 91 | return fields 92 | } 93 | -------------------------------------------------------------------------------- /qb-architect/internal/db/myarchitect/mymodel/db.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "information_schema.tables", 4 | "fields": [ 5 | {"name": "table_name", "type": "string", "read_only": true}, 6 | {"name": "table_schema", "type": "string", "read_only": true} 7 | ] 8 | }, 9 | { 10 | "name": "information_schema.columns", 11 | "fields": [ 12 | {"name": "column_name", "type": "string", "read_only": true}, 13 | {"name": "data_type", "type": "string", "read_only": true}, 14 | {"name": "is_nullable", "type": "string", "read_only": true}, 15 | {"name": "character_maximum_length", "type": "int", "read_only": true}, 16 | {"name": "table_schema", "type": "string", "read_only": true}, 17 | {"name": "table_name", "type": "string", "read_only": true} 18 | ] 19 | } 20 | ] 21 | 22 | -------------------------------------------------------------------------------- /qb-architect/internal/db/myarchitect/mymodel/model.go: -------------------------------------------------------------------------------- 1 | package mymodel 2 | 3 | //go:generate qb-generator -package=mymodel db.json tables.go 4 | -------------------------------------------------------------------------------- /qb-architect/internal/db/myarchitect/mymodel/tables.go: -------------------------------------------------------------------------------- 1 | // Code generated by qb-generator; DO NOT EDIT. 2 | 3 | package mymodel 4 | 5 | import "git.ultraware.nl/Ultraware/qb/v2" 6 | 7 | ///// Tables ///// 8 | var ( 9 | qbTablesTable = qb.Table{Name: `information_schema.tables`} 10 | 11 | qbTablesFTableName = qb.TableField{Parent: &qbTablesTable, Name: `table_name`, ReadOnly: true} 12 | qbTablesFTableSchema = qb.TableField{Parent: &qbTablesTable, Name: `table_schema`, ReadOnly: true} 13 | ) 14 | 15 | // TablesType represents the table "Tables" 16 | type TablesType struct { 17 | TableName qb.Field 18 | TableSchema qb.Field 19 | table *qb.Table 20 | } 21 | 22 | // GetTable returns an object with info about the table 23 | func (t *TablesType) GetTable() *qb.Table { 24 | return t.table 25 | } 26 | 27 | // Select starts a SELECT query 28 | func (t *TablesType) Select(f ...qb.Field) *qb.SelectBuilder { 29 | return t.table.Select(f) 30 | } 31 | 32 | // Delete creates a DELETE query 33 | func (t *TablesType) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 34 | return t.table.Delete(c1, c...) 35 | } 36 | 37 | // Update starts an UPDATE query 38 | func (t *TablesType) Update() *qb.UpdateBuilder { 39 | return t.table.Update() 40 | } 41 | 42 | // Insert starts an INSERT query 43 | func (t *TablesType) Insert(f ...qb.Field) *qb.InsertBuilder { 44 | return t.table.Insert(f) 45 | } 46 | 47 | // Tables returns a new TablesType 48 | func Tables() *TablesType { 49 | table := qbTablesTable 50 | return &TablesType{ 51 | qbTablesFTableName.Copy(&table), 52 | qbTablesFTableSchema.Copy(&table), 53 | &table, 54 | } 55 | } 56 | 57 | ///// Columns ///// 58 | var ( 59 | qbColumnsTable = qb.Table{Name: `information_schema.columns`} 60 | 61 | qbColumnsFColumnName = qb.TableField{Parent: &qbColumnsTable, Name: `column_name`, ReadOnly: true} 62 | qbColumnsFDataType = qb.TableField{Parent: &qbColumnsTable, Name: `data_type`, ReadOnly: true} 63 | qbColumnsFIsNullable = qb.TableField{Parent: &qbColumnsTable, Name: `is_nullable`, ReadOnly: true} 64 | qbColumnsFCharacterMaximumLength = qb.TableField{Parent: &qbColumnsTable, Name: `character_maximum_length`, ReadOnly: true} 65 | qbColumnsFTableSchema = qb.TableField{Parent: &qbColumnsTable, Name: `table_schema`, ReadOnly: true} 66 | qbColumnsFTableName = qb.TableField{Parent: &qbColumnsTable, Name: `table_name`, ReadOnly: true} 67 | ) 68 | 69 | // ColumnsType represents the table "Columns" 70 | type ColumnsType struct { 71 | ColumnName qb.Field 72 | DataType qb.Field 73 | IsNullable qb.Field 74 | CharacterMaximumLength qb.Field 75 | TableSchema qb.Field 76 | TableName qb.Field 77 | table *qb.Table 78 | } 79 | 80 | // GetTable returns an object with info about the table 81 | func (t *ColumnsType) GetTable() *qb.Table { 82 | return t.table 83 | } 84 | 85 | // Select starts a SELECT query 86 | func (t *ColumnsType) Select(f ...qb.Field) *qb.SelectBuilder { 87 | return t.table.Select(f) 88 | } 89 | 90 | // Delete creates a DELETE query 91 | func (t *ColumnsType) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 92 | return t.table.Delete(c1, c...) 93 | } 94 | 95 | // Update starts an UPDATE query 96 | func (t *ColumnsType) Update() *qb.UpdateBuilder { 97 | return t.table.Update() 98 | } 99 | 100 | // Insert starts an INSERT query 101 | func (t *ColumnsType) Insert(f ...qb.Field) *qb.InsertBuilder { 102 | return t.table.Insert(f) 103 | } 104 | 105 | // Columns returns a new ColumnsType 106 | func Columns() *ColumnsType { 107 | table := qbColumnsTable 108 | return &ColumnsType{ 109 | qbColumnsFColumnName.Copy(&table), 110 | qbColumnsFDataType.Copy(&table), 111 | qbColumnsFIsNullable.Copy(&table), 112 | qbColumnsFCharacterMaximumLength.Copy(&table), 113 | qbColumnsFTableSchema.Copy(&table), 114 | qbColumnsFTableName.Copy(&table), 115 | &table, 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /qb-architect/internal/db/pgarchitect/pgarchitect.go: -------------------------------------------------------------------------------- 1 | package pgarchitect 2 | 3 | import ( 4 | "database/sql" 5 | "strings" 6 | 7 | "git.ultraware.nl/Ultraware/qb/v2" 8 | "git.ultraware.nl/Ultraware/qb/v2/driver/pgqb" 9 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db" 10 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db/pgarchitect/pgmodel" 11 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/util" 12 | "git.ultraware.nl/Ultraware/qb/v2/qbdb" 13 | "git.ultraware.nl/Ultraware/qb/v2/qc" 14 | "git.ultraware.nl/Ultraware/qb/v2/qf" 15 | 16 | // pgsql driver 17 | _ "github.com/lib/pq" 18 | ) 19 | 20 | // Driver implements db.Driver 21 | type Driver struct { 22 | DB qbdb.DB 23 | } 24 | 25 | // New opens a database connection and returns a Driver 26 | func New(dsn string) db.Driver { 27 | d, err := sql.Open("postgres", dsn) 28 | util.PanicOnErr(err) 29 | 30 | return Driver{pgqb.New(d)} 31 | } 32 | 33 | func schemas() qb.Field { 34 | return qf.NewCalculatedField(`current_schemas(false)`) 35 | } 36 | 37 | func regtype(f qb.Field) qb.Field { 38 | return qf.NewCalculatedField(f, `::regtype`) 39 | } 40 | 41 | func attrelid(schema, table string) qb.Field { 42 | return qf.NewCalculatedField(qb.Value(schema+`.`+table), `::regclass`) 43 | } 44 | 45 | func castAny(f qb.Field) qb.Field { 46 | return qf.NewCalculatedField(`ANY(`, f, `)`) 47 | } 48 | 49 | // GetTables returns all tables in the database 50 | func (d Driver) GetTables() []string { 51 | it := pgmodel.Tables() 52 | 53 | q := it.Select(it.TableSchema, it.TableName). 54 | Where(qc.Eq(it.TableSchema, castAny(schemas()))). 55 | GroupBy(it.TableSchema, it.TableName) 56 | 57 | rows, err := d.DB.Query(q) 58 | util.PanicOnErr(err) 59 | 60 | var tables []string 61 | for rows.Next() { 62 | var schema, table string 63 | err := rows.Scan(&schema, &table) 64 | util.PanicOnErr(err) 65 | 66 | tables = append(tables, schema+`.`+table) 67 | } 68 | 69 | return tables 70 | } 71 | 72 | // GetFields returns all fields in a table 73 | func (d Driver) GetFields(table string) []db.Field { 74 | sp := strings.Split(table, `.`) 75 | schema := sp[0] 76 | table = sp[1] 77 | 78 | pa := pgmodel.PgAttribute() 79 | c := pgmodel.Columns() 80 | 81 | q := pa.Select( 82 | pa.Attname, regtype(pa.Atttypid), pa.Attnotnull, 83 | qf.Case(). 84 | When(qc.Gt(pa.Attlen, -1), pa.Attlen). 85 | When(qc.NotNull(c.CharacterMaximumLength), c.CharacterMaximumLength). 86 | Else(0), 87 | ). 88 | InnerJoin(c.ColumnName, pa.Attname, 89 | qc.Eq(c.TableName, table), 90 | qc.Eq(c.TableSchema, schema), 91 | ). 92 | Where( 93 | qc.Eq(pa.Attrelid, attrelid(schema, table)), 94 | qc.Gt(pa.Attnum, 0), 95 | qc.Eq(pa.Attisdropped, false), 96 | ). 97 | GroupBy(pa.Attname, pa.Atttypid, pa.Attnotnull, pa.Attlen, c.CharacterMaximumLength) 98 | 99 | rows, err := d.DB.Query(q) 100 | if err != nil { 101 | panic(err) 102 | } 103 | 104 | var fields []db.Field 105 | for rows.Next() { 106 | f := db.Field{} 107 | 108 | var notNullable bool 109 | 110 | err := rows.Scan(&f.Name, &f.Type, ¬Nullable, &f.Size) 111 | util.PanicOnErr(err) 112 | 113 | f.Nullable = !notNullable 114 | 115 | fields = append(fields, f) 116 | } 117 | 118 | return fields 119 | } 120 | -------------------------------------------------------------------------------- /qb-architect/internal/db/pgarchitect/pgmodel/db.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "pg_attribute", 4 | "fields": [ 5 | {"name": "attname"}, 6 | {"name": "atttypid"}, 7 | {"name": "attlen"}, 8 | {"name": "atttypmod"}, 9 | {"name": "attrelid"}, 10 | {"name": "attnotnull"}, 11 | {"name": "attnum"}, 12 | {"name": "attisdropped"} 13 | ] 14 | }, 15 | { 16 | "name": "information_schema.tables", 17 | "fields": [ 18 | {"name": "table_name"}, 19 | {"name": "table_schema"} 20 | ] 21 | }, 22 | { 23 | "name": "information_schema.columns", 24 | "fields": [ 25 | {"name": "column_name"}, 26 | {"name": "table_schema"}, 27 | {"name": "table_name"}, 28 | {"name": "character_maximum_length"} 29 | ] 30 | } 31 | ] 32 | 33 | -------------------------------------------------------------------------------- /qb-architect/internal/db/pgarchitect/pgmodel/model.go: -------------------------------------------------------------------------------- 1 | package pgmodel 2 | 3 | //go:generate qb-generator -package=pgmodel db.json tables.go 4 | -------------------------------------------------------------------------------- /qb-architect/internal/db/pgarchitect/pgmodel/tables.go: -------------------------------------------------------------------------------- 1 | // Code generated by qb-generator; DO NOT EDIT. 2 | 3 | package pgmodel 4 | 5 | import "git.ultraware.nl/Ultraware/qb/v2" 6 | 7 | ///// PgAttribute ///// 8 | var ( 9 | qbPgAttributeTable = qb.Table{Name: `pg_attribute`} 10 | 11 | qbPgAttributeFAttname = qb.TableField{Parent: &qbPgAttributeTable, Name: `attname`} 12 | qbPgAttributeFAtttypid = qb.TableField{Parent: &qbPgAttributeTable, Name: `atttypid`} 13 | qbPgAttributeFAttlen = qb.TableField{Parent: &qbPgAttributeTable, Name: `attlen`} 14 | qbPgAttributeFAtttypmod = qb.TableField{Parent: &qbPgAttributeTable, Name: `atttypmod`} 15 | qbPgAttributeFAttrelid = qb.TableField{Parent: &qbPgAttributeTable, Name: `attrelid`} 16 | qbPgAttributeFAttnotnull = qb.TableField{Parent: &qbPgAttributeTable, Name: `attnotnull`} 17 | qbPgAttributeFAttnum = qb.TableField{Parent: &qbPgAttributeTable, Name: `attnum`} 18 | qbPgAttributeFAttisdropped = qb.TableField{Parent: &qbPgAttributeTable, Name: `attisdropped`} 19 | ) 20 | 21 | // PgAttributeType represents the table "PgAttribute" 22 | type PgAttributeType struct { 23 | Attname qb.Field 24 | Atttypid qb.Field 25 | Attlen qb.Field 26 | Atttypmod qb.Field 27 | Attrelid qb.Field 28 | Attnotnull qb.Field 29 | Attnum qb.Field 30 | Attisdropped qb.Field 31 | table *qb.Table 32 | } 33 | 34 | // GetTable returns an object with info about the table 35 | func (t *PgAttributeType) GetTable() *qb.Table { 36 | return t.table 37 | } 38 | 39 | // Select starts a SELECT query 40 | func (t *PgAttributeType) Select(f ...qb.Field) *qb.SelectBuilder { 41 | return t.table.Select(f) 42 | } 43 | 44 | // Delete creates a DELETE query 45 | func (t *PgAttributeType) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 46 | return t.table.Delete(c1, c...) 47 | } 48 | 49 | // Update starts an UPDATE query 50 | func (t *PgAttributeType) Update() *qb.UpdateBuilder { 51 | return t.table.Update() 52 | } 53 | 54 | // Insert starts an INSERT query 55 | func (t *PgAttributeType) Insert(f ...qb.Field) *qb.InsertBuilder { 56 | return t.table.Insert(f) 57 | } 58 | 59 | // PgAttribute returns a new PgAttributeType 60 | func PgAttribute() *PgAttributeType { 61 | table := qbPgAttributeTable 62 | return &PgAttributeType{ 63 | qbPgAttributeFAttname.Copy(&table), 64 | qbPgAttributeFAtttypid.Copy(&table), 65 | qbPgAttributeFAttlen.Copy(&table), 66 | qbPgAttributeFAtttypmod.Copy(&table), 67 | qbPgAttributeFAttrelid.Copy(&table), 68 | qbPgAttributeFAttnotnull.Copy(&table), 69 | qbPgAttributeFAttnum.Copy(&table), 70 | qbPgAttributeFAttisdropped.Copy(&table), 71 | &table, 72 | } 73 | } 74 | 75 | ///// Tables ///// 76 | var ( 77 | qbTablesTable = qb.Table{Name: `information_schema.tables`} 78 | 79 | qbTablesFTableName = qb.TableField{Parent: &qbTablesTable, Name: `table_name`} 80 | qbTablesFTableSchema = qb.TableField{Parent: &qbTablesTable, Name: `table_schema`} 81 | ) 82 | 83 | // TablesType represents the table "Tables" 84 | type TablesType struct { 85 | TableName qb.Field 86 | TableSchema qb.Field 87 | table *qb.Table 88 | } 89 | 90 | // GetTable returns an object with info about the table 91 | func (t *TablesType) GetTable() *qb.Table { 92 | return t.table 93 | } 94 | 95 | // Select starts a SELECT query 96 | func (t *TablesType) Select(f ...qb.Field) *qb.SelectBuilder { 97 | return t.table.Select(f) 98 | } 99 | 100 | // Delete creates a DELETE query 101 | func (t *TablesType) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 102 | return t.table.Delete(c1, c...) 103 | } 104 | 105 | // Update starts an UPDATE query 106 | func (t *TablesType) Update() *qb.UpdateBuilder { 107 | return t.table.Update() 108 | } 109 | 110 | // Insert starts an INSERT query 111 | func (t *TablesType) Insert(f ...qb.Field) *qb.InsertBuilder { 112 | return t.table.Insert(f) 113 | } 114 | 115 | // Tables returns a new TablesType 116 | func Tables() *TablesType { 117 | table := qbTablesTable 118 | return &TablesType{ 119 | qbTablesFTableName.Copy(&table), 120 | qbTablesFTableSchema.Copy(&table), 121 | &table, 122 | } 123 | } 124 | 125 | ///// Columns ///// 126 | var ( 127 | qbColumnsTable = qb.Table{Name: `information_schema.columns`} 128 | 129 | qbColumnsFColumnName = qb.TableField{Parent: &qbColumnsTable, Name: `column_name`} 130 | qbColumnsFTableSchema = qb.TableField{Parent: &qbColumnsTable, Name: `table_schema`} 131 | qbColumnsFTableName = qb.TableField{Parent: &qbColumnsTable, Name: `table_name`} 132 | qbColumnsFCharacterMaximumLength = qb.TableField{Parent: &qbColumnsTable, Name: `character_maximum_length`} 133 | ) 134 | 135 | // ColumnsType represents the table "Columns" 136 | type ColumnsType struct { 137 | ColumnName qb.Field 138 | TableSchema qb.Field 139 | TableName qb.Field 140 | CharacterMaximumLength qb.Field 141 | table *qb.Table 142 | } 143 | 144 | // GetTable returns an object with info about the table 145 | func (t *ColumnsType) GetTable() *qb.Table { 146 | return t.table 147 | } 148 | 149 | // Select starts a SELECT query 150 | func (t *ColumnsType) Select(f ...qb.Field) *qb.SelectBuilder { 151 | return t.table.Select(f) 152 | } 153 | 154 | // Delete creates a DELETE query 155 | func (t *ColumnsType) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 156 | return t.table.Delete(c1, c...) 157 | } 158 | 159 | // Update starts an UPDATE query 160 | func (t *ColumnsType) Update() *qb.UpdateBuilder { 161 | return t.table.Update() 162 | } 163 | 164 | // Insert starts an INSERT query 165 | func (t *ColumnsType) Insert(f ...qb.Field) *qb.InsertBuilder { 166 | return t.table.Insert(f) 167 | } 168 | 169 | // Columns returns a new ColumnsType 170 | func Columns() *ColumnsType { 171 | table := qbColumnsTable 172 | return &ColumnsType{ 173 | qbColumnsFColumnName.Copy(&table), 174 | qbColumnsFTableSchema.Copy(&table), 175 | qbColumnsFTableName.Copy(&table), 176 | qbColumnsFCharacterMaximumLength.Copy(&table), 177 | &table, 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /qb-architect/internal/util/panic.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // PanicOnErr panics if err is not nil 4 | func PanicOnErr(err interface{}) { 5 | if err != nil { 6 | panic(err) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /qb-architect/main.go: -------------------------------------------------------------------------------- 1 | package main // import "git.ultraware.nl/Ultraware/qb/v2/qb-architect" 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "sort" 10 | "strings" 11 | 12 | "git.ultraware.nl/Ultraware/qb/v2/internal/filter" 13 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db" 14 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db/msarchitect" 15 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db/myarchitect" 16 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db/pgarchitect" 17 | ) 18 | 19 | var ( 20 | errNoTablesFoundInDatabase = errors.New(`no tables found in this database`) 21 | 22 | errString = `Please specify a %s, example:` + "\n\t" + 23 | `qb-architect -dbms psql "host=/tmp user=qb database=architect" > db.json` 24 | ) 25 | 26 | func main() { 27 | dbms := flag.String(`dbms`, ``, `Database type to use: psql, mysql, mssql`) 28 | 29 | var tExclude, tOnly filter.Filters 30 | flag.Var(&tExclude, `texclude`, `Regular expressions to exclude tables`) 31 | flag.Var(&tOnly, `tonly`, `Regular expressions to whitelist tables, only tables that match at least one are returned`) 32 | 33 | var fExclude, fOnly filter.Filters 34 | flag.Var(&fExclude, `fexclude`, `Regular expressions to exclude fields`) 35 | flag.Var(&fOnly, `fonly`, `Regular expressions to whitelist fields, only tables that match at least one are returned`) 36 | 37 | flag.Parse() 38 | 39 | dsn := strings.Join(flag.Args(), ` `) 40 | if dsn == `` { 41 | println(fmt.Sprintf(errString, `connection string`)) 42 | os.Exit(2) 43 | } 44 | 45 | var driver db.Driver 46 | switch strings.ToLower(*dbms) { 47 | case ``: 48 | println(fmt.Sprintf(errString, `dbms`)) 49 | os.Exit(2) 50 | case `psql`, `postgres`, `postgresql`: 51 | driver = pgarchitect.New(dsn) 52 | case `mssql`, `sqlserver`: 53 | driver = msarchitect.New(dsn) 54 | case `mysql`: 55 | driver = myarchitect.New(dsn) 56 | default: 57 | println(`"` + *dbms + `" is not supported`) 58 | os.Exit(2) 59 | } 60 | 61 | filtered := filterTables(driver.GetTables(), tOnly, tExclude) 62 | 63 | tables := make([]db.Table, 0, len(filtered)) 64 | for _, v := range filtered { 65 | tables = append(tables, db.Table{ 66 | Name: v, 67 | Fields: filterFields(driver.GetFields(v), fOnly, fExclude), 68 | }) 69 | } 70 | 71 | err := output(tables) 72 | if err != nil { 73 | println(err.Error()) 74 | os.Exit(1) 75 | } 76 | } 77 | 78 | func filterTables(tables []string, only, exclude filter.Filters) []string { 79 | var out []string 80 | 81 | for _, v := range tables { 82 | if applyFilters(v, only, exclude) { 83 | out = append(out, v) 84 | } 85 | } 86 | 87 | sort.Strings(out) 88 | 89 | return out 90 | } 91 | 92 | func filterFields(field []db.Field, only, exclude filter.Filters) []db.Field { 93 | var out []db.Field 94 | 95 | for _, v := range field { 96 | if applyFilters(v.Name, only, exclude) { 97 | out = append(out, v) 98 | } 99 | } 100 | 101 | sort.Sort(fields(out)) 102 | 103 | return out 104 | } 105 | 106 | func applyFilters(name string, only, exclude filter.Filters) bool { 107 | pass := false 108 | for _, re := range only { 109 | if re.MatchString(name) { 110 | pass = true 111 | break 112 | } 113 | } 114 | 115 | if !pass && len(only) > 0 { 116 | return false 117 | } 118 | 119 | for _, re := range exclude { 120 | if re.MatchString(name) { 121 | return false 122 | } 123 | } 124 | 125 | return true 126 | } 127 | 128 | func output(tables []db.Table) error { 129 | if len(tables) == 0 { 130 | return errNoTablesFoundInDatabase 131 | } 132 | 133 | enc := json.NewEncoder(os.Stdout) 134 | enc.SetIndent("", "\t") 135 | 136 | return enc.Encode(tables) 137 | } 138 | -------------------------------------------------------------------------------- /qb-architect/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | 6 | "git.ultraware.nl/Ultraware/qb/v2/qb-architect/internal/db" 7 | ) 8 | 9 | type fields []db.Field 10 | 11 | func (f fields) Len() int { 12 | return len(f) 13 | } 14 | 15 | func (f fields) Less(i, j int) bool { 16 | return sort.StringsAreSorted([]string{f[i].Name, f[j].Name}) 17 | } 18 | 19 | func (f fields) Swap(i, j int) { 20 | f[i], f[j] = f[j], f[i] 21 | } 22 | -------------------------------------------------------------------------------- /qb-generator/main.go: -------------------------------------------------------------------------------- 1 | package main // import "git.ultraware.nl/Ultraware/qb/v2/qb-generator" 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "io" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "regexp" 11 | "strings" 12 | "text/template" 13 | 14 | "git.ultraware.nl/Ultraware/qb/v2/internal/filter" 15 | ) 16 | 17 | type inputTable struct { 18 | String string `json:"name"` 19 | Alias string `json:"alias"` 20 | Fields []inputField `json:"fields"` 21 | } 22 | 23 | type inputField struct { 24 | String string `json:"name"` 25 | Nullable bool `json:"null"` 26 | ReadOnly bool `json:"read_only"` 27 | DataType string `json:"data_type"` 28 | Size int `json:"size"` 29 | } 30 | 31 | type table struct { 32 | Table string 33 | TableString string 34 | Alias string 35 | Escape bool 36 | Fields []field 37 | } 38 | 39 | type field struct { 40 | Name string 41 | String string 42 | Escape bool 43 | ReadOnly bool 44 | DataType dataType 45 | } 46 | 47 | var fullUpperList = []string{ 48 | `acl`, 49 | `api`, 50 | `ascii`, 51 | `cpu`, 52 | `css`, 53 | `dns`, 54 | `eof`, 55 | `guid`, 56 | `html`, 57 | `http`, 58 | `https`, 59 | `id`, 60 | `ip`, 61 | `json`, 62 | `lhs`, 63 | `qps`, 64 | `ram`, 65 | `rhs`, 66 | `rpc`, 67 | `sla`, 68 | `smtp`, 69 | `sql`, 70 | `ssh`, 71 | `tcp`, 72 | `tls`, 73 | `ttl`, 74 | `udp`, 75 | `ui`, 76 | `uid`, 77 | `uuid`, 78 | `uri`, 79 | `url`, 80 | `utf8`, 81 | `vm`, 82 | `xml`, 83 | `xmpp`, 84 | `xsrf`, 85 | `xss`, 86 | } 87 | 88 | type dataType struct { 89 | Name string 90 | Size int 91 | Null bool 92 | } 93 | 94 | var dataTypes = map[string]dataType{ 95 | `char`: {`String`, 0, false}, 96 | `character`: {`String`, 0, false}, 97 | `character varying`: {`String`, 0, false}, 98 | `varchar`: {`String`, 0, false}, 99 | `tinyint`: {`Int`, 8, false}, 100 | `smallint`: {`Int`, 16, false}, 101 | `int`: {`Int`, 32, false}, 102 | `integer`: {`Int`, 32, false}, 103 | `bigint`: {`Int`, 64, false}, 104 | `real`: {`Float`, 32, false}, 105 | `float`: {`Float`, 64, false}, 106 | `double`: {`Float`, 64, false}, 107 | `time`: {`Time`, 0, false}, 108 | `date`: {`Date`, 0, false}, 109 | `datetime`: {`Time`, 0, false}, 110 | `timestamp`: {`Time`, 0, false}, 111 | `boolean`: {`Bool`, 0, false}, 112 | `bool`: {`Bool`, 0, false}, 113 | } 114 | 115 | func getDataType(t string, size int, null bool) dataType { 116 | if v, ok := dataTypes[strings.Split(t, ` `)[0]]; ok { 117 | if v.Size == 0 { 118 | v.Size = size 119 | } 120 | v.Null = null 121 | return v 122 | } 123 | return dataType{``, size, null} 124 | } 125 | 126 | var escapeRE = regexp.MustCompile(`[^a-zA-Z0-9_$]`) 127 | 128 | func shouldEscape(s string) bool { 129 | return escapeRE.MatchString(s) 130 | } 131 | 132 | func newField(f inputField) field { 133 | return field{cleanName(f.String, fTrim), f.String, shouldEscape(f.String), f.ReadOnly, getDataType(f.DataType, f.Size, f.Nullable)} 134 | } 135 | 136 | func removeSchema(s string) string { 137 | parts := strings.Split(s, `.`) 138 | return parts[len(parts)-1] 139 | } 140 | 141 | var nameRE = regexp.MustCompile(`[^a-zA-Z0-9_]`) 142 | 143 | func cleanName(s string, f filter.Filters) string { 144 | for _, re := range f { 145 | s = re.ReplaceAllString(s, ``) 146 | } 147 | 148 | s = nameRE.ReplaceAllString(s, `_`) 149 | 150 | parts := strings.Split(s, `_`) 151 | for k := range parts { 152 | upper := false 153 | for _, v := range fullUpperList { 154 | if v == parts[k] { 155 | upper = true 156 | break 157 | } 158 | } 159 | 160 | if upper || len(parts[k]) <= 1 { 161 | parts[k] = strings.ToUpper(parts[k]) 162 | continue 163 | } 164 | 165 | parts[k] = strings.ToUpper(string(parts[k][0])) + parts[k][1:] 166 | } 167 | return strings.Join(parts, ``) 168 | } 169 | 170 | var ( 171 | pkg string 172 | tTrim, fTrim filter.Filters 173 | ) 174 | 175 | func initFlags() { 176 | log.SetFlags(0) 177 | 178 | flag.StringVar(&pkg, `package`, `model`, `The package name for the output file`) 179 | flag.Var(&tTrim, `ttrim`, `Regular expressions to clean up table names (runs after removing schema)`) 180 | flag.Var(&fTrim, `ftrim`, `Regular expressions to clean up field names`) 181 | flag.Parse() 182 | 183 | if len(flag.Args()) != 2 { 184 | log.Println(`Usage: qbgenerate [options] input.json output.go`) 185 | os.Exit(2) 186 | } 187 | } 188 | 189 | func main() { 190 | initFlags() 191 | 192 | in, err := os.Open(flag.Arg(0)) 193 | if err != nil { 194 | log.Fatal(`Failed to open input file. `, err) 195 | } 196 | 197 | input := []inputTable{} 198 | 199 | err = json.NewDecoder(in).Decode(&input) 200 | if err != nil { 201 | log.Fatal(`Failed to parse input file. `, err) 202 | } 203 | 204 | out, err := os.OpenFile(flag.Arg(1), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) 205 | if err != nil { 206 | log.Fatal(`Failed to open output file. `, err) 207 | } 208 | 209 | err = generateCode(out, input) 210 | if err != nil { 211 | log.Fatal(`Failed to generate code. `, err) 212 | } 213 | 214 | _ = out.Close() 215 | err = exec.Command(`goimports`, `-w`, out.Name()).Run() 216 | if err != nil { 217 | log.Fatal(`Failed to exectue goimports. `, err) 218 | } 219 | } 220 | 221 | func generateCode(out io.Writer, input []inputTable) error { 222 | tables := make([]table, len(input)) 223 | for k, v := range input { 224 | t := &tables[k] 225 | t.Table = cleanName(removeSchema(v.String), tTrim) 226 | t.Alias = v.Alias 227 | t.TableString = v.String 228 | t.Escape = shouldEscape(removeSchema(v.String)) 229 | 230 | for _, f := range v.Fields { 231 | t.Fields = append(t.Fields, newField(f)) 232 | } 233 | } 234 | 235 | t, err := template.New(`code`).Parse(codeTemplate) 236 | if err != nil { 237 | return err 238 | } 239 | 240 | _, _ = io.WriteString(out, "// Code generated by qb-generator; DO NOT EDIT.\n\n") 241 | 242 | _, _ = io.WriteString(out, `package `+pkg+"\n\n") 243 | _, _ = io.WriteString(out, `import "git.ultraware.nl/Ultraware/qb/v2"`+"\n\n") 244 | 245 | for _, v := range tables { 246 | if err := t.Execute(out, v); err != nil { 247 | return err 248 | } 249 | } 250 | return nil 251 | } 252 | -------------------------------------------------------------------------------- /qb-generator/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "git.fuyu.moe/Fuyu/assert" 8 | "git.ultraware.nl/Ultraware/qb/v2/internal/filter" 9 | ) 10 | 11 | func expectCleanName(input, expected string) func(*testing.T) { 12 | return func(t *testing.T) { 13 | actual := cleanName(input, nil) 14 | if actual != expected { 15 | t.Errorf(`actual: "%s" does not match expected: "%s"`, actual, expected) 16 | } 17 | } 18 | } 19 | 20 | func TestCleanName(t *testing.T) { 21 | t.Run(`single space`, expectCleanName(`single table`, `SingleTable`)) 22 | t.Run(`multiple space`, expectCleanName(`space multiple table`, `SpaceMultipleTable`)) 23 | t.Run(`repeating space`, expectCleanName(`space repeating table`, `SpaceRepeatingTable`)) 24 | 25 | t.Run(`single $`, expectCleanName(`single$table`, `SingleTable`)) 26 | t.Run(`multiple $`, expectCleanName(`dollar$multiple$table`, `DollarMultipleTable`)) 27 | t.Run(`repeating $`, expectCleanName(`dollar$$repeating$$table`, `DollarRepeatingTable`)) 28 | 29 | t.Run(`single _`, expectCleanName(`single_table`, `SingleTable`)) 30 | t.Run(`multiple _`, expectCleanName(`underscore_multiple_table`, `UnderscoreMultipleTable`)) 31 | t.Run(`repeating _`, expectCleanName(`underscore__repeating__table`, `UnderscoreRepeatingTable`)) 32 | 33 | t.Run(`single -`, expectCleanName(`single-table`, `SingleTable`)) 34 | t.Run(`multiple -`, expectCleanName(`hyphen-multiple-table`, `HyphenMultipleTable`)) 35 | t.Run(`repeating -`, expectCleanName(`hyphen--repeating--table`, `HyphenRepeatingTable`)) 36 | 37 | t.Run(`replace all`, expectCleanName(`$makes_$no-sense at - all`, `MakesNoSenseAtAll`)) 38 | t.Run(`preserve casing`, expectCleanName(`preServe$CASING`, `PreServeCASING`)) 39 | t.Run(`special uppercase`, expectCleanName(`schema$base_url`, `SchemaBaseURL`)) 40 | } 41 | 42 | func TestCleanNameTrim(t *testing.T) { 43 | tblRe := regexp.MustCompile(`_tbl$`) 44 | dollarRe := regexp.MustCompile(`.*\$`) 45 | 46 | cases := []struct { 47 | re filter.Filters 48 | expected string 49 | }{ 50 | { 51 | re: nil, 52 | expected: `SchemaUserTbl`, 53 | }, 54 | { 55 | re: filter.Filters{tblRe}, 56 | expected: `SchemaUser`, 57 | }, 58 | { 59 | re: filter.Filters{dollarRe}, 60 | expected: `UserTbl`, 61 | }, 62 | { 63 | re: filter.Filters{dollarRe, tblRe}, 64 | expected: `User`, 65 | }, 66 | { // Regexes should be executed in the correct order 67 | re: filter.Filters{regexp.MustCompile(`bl$`), tblRe}, 68 | expected: `SchemaUserT`, 69 | }, 70 | } 71 | 72 | for _, v := range cases { 73 | c := v 74 | t.Run(c.re.String(), func(t *testing.T) { 75 | assert := assert.New(t) 76 | 77 | out := cleanName(`schema$user_tbl`, c.re) 78 | assert.Eq(c.expected, out) 79 | }) 80 | } 81 | } 82 | 83 | func TestRemoveSchema(t *testing.T) { 84 | assert := assert.New(t) 85 | 86 | out := removeSchema(`public.tbl`) 87 | assert.Eq(`tbl`, out) 88 | 89 | out = removeSchema(`dbo.something.idk.tbl`) 90 | assert.Eq(`tbl`, out) 91 | } 92 | -------------------------------------------------------------------------------- /qb-generator/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var codeTemplate = `///// {{.Table}} ///// 4 | var ( 5 | qb{{.Table}}Table = qb.Table{Name: ` + "`" + `{{.TableString}}` + "`" + ` 6 | {{- if .Alias }}, Alias: ` + "`" + `{{.Alias}}` + "`" + `{{end -}} 7 | {{- if .Escape }}, Escape: true{{end -}} 8 | } 9 | 10 | {{range .Fields -}} 11 | qb{{$.Table}}F{{.Name}} = qb.TableField{Parent: &qb{{$.Table}}Table, Name: ` + "`" + `{{.String}}` + "`" + `, 12 | {{- if .Escape }}Escape: true,{{end -}} 13 | {{- if .ReadOnly }}ReadOnly: true,{{end -}} 14 | {{- if .DataType.Name }}Type: qb.{{.DataType.Name}},{{end -}} 15 | {{- if .DataType.Size }}Size: {{.DataType.Size}},{{end -}} 16 | {{- if .DataType.Null }}Nullable: true,{{end -}} 17 | } 18 | {{end}} 19 | ) 20 | 21 | // {{.Table}}Type represents the table "{{.Table}}" 22 | type {{.Table}}Type struct { 23 | {{- range .Fields}} 24 | {{.Name}} qb.Field 25 | {{- end}} 26 | table *qb.Table 27 | } 28 | 29 | // GetTable returns an object with info about the table 30 | func (t *{{.Table}}Type) GetTable() *qb.Table { 31 | return t.table 32 | } 33 | 34 | // Select starts a SELECT query 35 | func (t *{{.Table}}Type) Select(f ...qb.Field) *qb.SelectBuilder { 36 | return t.table.Select(f) 37 | } 38 | 39 | // Delete creates a DELETE query 40 | func (t *{{.Table}}Type) Delete(c1 qb.Condition, c ...qb.Condition) qb.Query { 41 | return t.table.Delete(c1, c...) 42 | } 43 | 44 | // Update starts an UPDATE query 45 | func (t *{{.Table}}Type) Update() *qb.UpdateBuilder { 46 | return t.table.Update() 47 | } 48 | 49 | // Insert starts an INSERT query 50 | func (t *{{.Table}}Type) Insert(f ...qb.Field) *qb.InsertBuilder { 51 | return t.table.Insert(f) 52 | } 53 | 54 | // {{.Table}} returns a new {{.Table}}Type 55 | func {{.Table}}() *{{.Table}}Type { 56 | table := qb{{$.Table}}Table 57 | return &{{.Table}}Type{ 58 | {{- range .Fields}} 59 | qb{{$.Table}}F{{.Name}}.Copy(&table), 60 | {{- end}} 61 | &table, 62 | } 63 | } 64 | ` 65 | -------------------------------------------------------------------------------- /qb.go: -------------------------------------------------------------------------------- 1 | package qb // import "git.ultraware.nl/Ultraware/qb/v2" 2 | -------------------------------------------------------------------------------- /qbdb/driver.go: -------------------------------------------------------------------------------- 1 | package qbdb 2 | 3 | import ( 4 | "strconv" 5 | 6 | "git.ultraware.nl/Ultraware/qb/v2" 7 | ) 8 | 9 | // Driver is a default driver used for tests 10 | type Driver struct{} 11 | 12 | // ValueString returns the placeholder for prepare values 13 | func (d Driver) ValueString(_ int) string { 14 | return `@@` 15 | } 16 | 17 | // BoolString returns the notation for boolean values 18 | func (d Driver) BoolString(v bool) string { 19 | if v { 20 | return `t` 21 | } 22 | return `f` 23 | } 24 | 25 | // EscapeCharacter returns the character to escape table and field names 26 | func (d Driver) EscapeCharacter() string { 27 | return `"` 28 | } 29 | 30 | // UpsertSQL implements qb.Driver 31 | func (d Driver) UpsertSQL(_ *qb.Table, _ []qb.Field, _ qb.Query) (string, []interface{}) { 32 | panic(`This should not be used`) 33 | } 34 | 35 | // IgnoreConflictSQL implements qb.Driver 36 | func (d Driver) IgnoreConflictSQL(_ *qb.Table, _ []qb.Field) (string, []interface{}) { 37 | panic(`This should not be used`) 38 | } 39 | 40 | // LimitOffset implements qb.Driver 41 | func (d Driver) LimitOffset(sql qb.SQL, limit, offset int) { 42 | if limit > 0 { 43 | sql.WriteLine(`LIMIT ` + strconv.Itoa(limit)) 44 | } 45 | if offset > 0 { 46 | sql.WriteLine(`OFFSET ` + strconv.Itoa(offset)) 47 | } 48 | } 49 | 50 | // Returning implements qb.Driver 51 | func (d Driver) Returning(_ qb.SQLBuilder, _ qb.Query, _ []qb.Field) (string, []interface{}) { 52 | panic(`This should not be used`) 53 | } 54 | 55 | // LateralJoin implements qb.Driver 56 | func (d Driver) LateralJoin(_ *qb.Context, _ *qb.SubQuery) string { 57 | panic(`This should not be used`) 58 | } 59 | 60 | var types = map[qb.DataType]string{ 61 | qb.Int: `int`, 62 | qb.String: `string`, 63 | qb.Bool: `boolean`, 64 | qb.Float: `float`, 65 | qb.Date: `date`, 66 | qb.Time: `time`, 67 | } 68 | 69 | // TypeName returns the sql name for a type 70 | func (d Driver) TypeName(t qb.DataType) string { 71 | if s, ok := types[t]; ok { 72 | return s 73 | } 74 | panic(`Unknown type`) 75 | } 76 | 77 | // Override returns the override map 78 | func (d Driver) Override() qb.OverrideMap { 79 | return qb.OverrideMap{} 80 | } 81 | -------------------------------------------------------------------------------- /qbdb/driver_test.go: -------------------------------------------------------------------------------- 1 | package qbdb 2 | 3 | import ( 4 | "database/sql/driver" 5 | "testing" 6 | 7 | "git.ultraware.nl/Ultraware/qb/v2/internal/testutil" 8 | ) 9 | 10 | var database = New(Driver{}, nil).(*db) 11 | 12 | type Valuer int 13 | 14 | func (v Valuer) Value() (driver.Value, error) { 15 | return v + 1, nil 16 | } 17 | 18 | func TestPrepareSQL(t *testing.T) { 19 | test := `SELECT a + ?, ? FROM tbl WHERE id = ?` 20 | testIn := [][]interface{}{ 21 | {`abc`, true, 1}, 22 | {3, nil, 123}, 23 | } 24 | testOut := []string{ 25 | `SELECT a + @@, @@ FROM tbl WHERE id = @@`, 26 | `SELECT a + @@, @@ FROM tbl WHERE id = @@`, 27 | } 28 | 29 | for k, v := range testIn { 30 | out, _ := database.prepareSQL(test, v) 31 | testutil.Compare(t, testOut[k], out) 32 | } 33 | } 34 | 35 | func BenchmarkPrepareSQL(b *testing.B) { 36 | test := `SELECT a + ?, ? FROM tbl WHERE str IN (?,?,?,?,?,?)` 37 | 38 | for i := 0; i < b.N; i++ { 39 | database.prepareSQL(test, []interface{}{`abc`, true, 1, `defg`, `hijk`, `lmnop`, `qrstuvw`, `xyz`}) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /qbdb/main.go: -------------------------------------------------------------------------------- 1 | package qbdb // import "git.ultraware.nl/Ultraware/qb/v2/qbdb" 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "git.ultraware.nl/Ultraware/qb/v2" 9 | ) 10 | 11 | // Render returns the generated SQL and values without executing the query 12 | func (db queryTarget) Render(q qb.Query) (string, []interface{}) { 13 | return db.prepare(q) 14 | } 15 | 16 | func (db queryTarget) ctes(ctes []*qb.CTE, done map[*qb.CTE]bool, b qb.SQLBuilder) []string { 17 | var list []string 18 | 19 | for _, v := range ctes { 20 | if _, ok := done[v]; ok { 21 | continue 22 | } 23 | done[v] = true 24 | 25 | tmp := b.Context.Values 26 | 27 | newValues, newCTEs := []interface{}{}, []*qb.CTE{} 28 | b.Context.Values, b.Context.CTEs = &newValues, &newCTEs 29 | 30 | newWith := v.With(b) 31 | 32 | b.Context.Values = tmp 33 | list = append(append(list, db.ctes(*b.Context.CTEs, done, b)...), newWith) 34 | 35 | b.Context.Add(newValues...) 36 | } 37 | 38 | return list 39 | } 40 | 41 | func shouldContext(q qb.Query) bool { 42 | switch t := q.(type) { 43 | case *qb.UpdateBuilder, qb.DeleteBuilder: 44 | return true 45 | case qb.ReturningBuilder: 46 | return shouldContext(t.Query) 47 | } 48 | 49 | return false 50 | } 51 | 52 | func (db queryTarget) prepare(q qb.Query) (string, []interface{}) { 53 | b := qb.NewSQLBuilder(db.driver) 54 | 55 | if shouldContext(q) { 56 | b.Context = qb.NewContext(b.Context.Driver, qb.AliasGenerator()) 57 | } 58 | 59 | s, v := q.SQL(b) 60 | 61 | if _, ok := q.(qb.SelectQuery); ok { 62 | ctes := b.Context.CTEs 63 | 64 | newList := []interface{}{} 65 | b.Context.Values = &newList 66 | if len(*ctes) > 0 { 67 | done := make(map[*qb.CTE]bool) 68 | 69 | s = `WITH ` + strings.Join(db.ctes(*ctes, done, b), `, `) + "\n\n" + s 70 | } 71 | v = append(*b.Context.Values, v...) 72 | } 73 | 74 | return db.prepareSQL(s, v) 75 | } 76 | 77 | func (db queryTarget) prepareSQL(s string, v []interface{}) (string, []interface{}) { 78 | c := 0 79 | b := &strings.Builder{} 80 | 81 | newValue := []interface{}{} 82 | for _, chr := range s { 83 | if chr != '?' { 84 | if _, err := b.WriteRune(chr); err != nil { 85 | panic(err) 86 | } 87 | continue 88 | } 89 | 90 | newValue = append(newValue, v[c]) 91 | c++ 92 | if _, err := b.WriteString(db.driver.ValueString(c)); err != nil { 93 | panic(err) 94 | } 95 | } 96 | 97 | db.log(b.String(), newValue) 98 | return b.String(), newValue 99 | } 100 | 101 | func (db queryTarget) log(s string, v []interface{}) { 102 | if db.debug { 103 | fmt.Printf("-- Running query:\n%s-- With values: %v\n\n", s, v) 104 | } 105 | } 106 | 107 | // Query executes the given SelectQuery on the database 108 | func (db queryTarget) Query(q qb.SelectQuery) (Rows, error) { 109 | return db.QueryContext(context.Background(), q) 110 | } 111 | 112 | // QueryContext executes the given SelectQuery on the database 113 | func (db queryTarget) QueryContext(c context.Context, q qb.SelectQuery) (Rows, error) { 114 | s, v := db.prepare(q) 115 | r, err := db.RawQueryContext(c, s, v...) 116 | return r, err 117 | } 118 | 119 | // MustQuery executes the given SelectQuery on the database 120 | // If an error occurs returned it will panic 121 | func (db queryTarget) MustQuery(q qb.SelectQuery) Rows { 122 | return db.MustQueryContext(context.Background(), q) 123 | } 124 | 125 | // MustQueryContext executes the given SelectQuery on the database 126 | // If an error occurs returned it will panic 127 | func (db queryTarget) MustQueryContext(c context.Context, q qb.SelectQuery) Rows { 128 | return mustRows(db.QueryContext(c, q)) 129 | } 130 | 131 | // RawQuery executes the given raw query on the database 132 | func (db queryTarget) RawQuery(s string, v ...interface{}) (Rows, error) { 133 | return db.RawQueryContext(context.Background(), s, v...) 134 | } 135 | 136 | // RawQueryContext executes the given raw query on the database 137 | func (db queryTarget) RawQueryContext(c context.Context, s string, v ...interface{}) (Rows, error) { 138 | r, err := db.src.QueryContext(c, s, v...) 139 | return Rows{r}, err 140 | } 141 | 142 | // MustRawQuery executes the given raw query on the database 143 | // If an error occurs returned it will panic 144 | func (db queryTarget) MustRawQuery(s string, v ...interface{}) Rows { 145 | return db.MustRawQueryContext(context.Background(), s, v...) 146 | } 147 | 148 | // MustRawQueryContext executes the given raw query on the database 149 | // If an error occurs returned it will panic 150 | func (db queryTarget) MustRawQueryContext(c context.Context, s string, v ...interface{}) Rows { 151 | return mustRows(db.RawQueryContext(c, s, v...)) 152 | } 153 | 154 | // QueryRow executes the given SelectQuery on the database, only returns one row 155 | func (db queryTarget) QueryRow(q qb.SelectQuery) Row { 156 | return db.QueryRowContext(context.Background(), q) 157 | } 158 | 159 | // QueryRowContext executes the given SelectQuery on the database, only returns one row 160 | func (db queryTarget) QueryRowContext(c context.Context, q qb.SelectQuery) Row { 161 | if sq, ok := q.(*qb.SelectBuilder); ok { 162 | sq.Limit(1) 163 | } 164 | 165 | s, v := db.prepare(q) 166 | return db.RawQueryRowContext(c, s, v...) 167 | } 168 | 169 | // RawQueryRow executes the given raw query on the database, only returns one row 170 | func (db queryTarget) RawQueryRow(s string, v ...interface{}) Row { 171 | return db.RawQueryRowContext(context.Background(), s, v...) 172 | } 173 | 174 | // RawQueryRowContext executes the given raw query on the database, only returns one row 175 | func (db queryTarget) RawQueryRowContext(c context.Context, s string, v ...interface{}) Row { 176 | return Row{db.src.QueryRowContext(c, s, v...)} 177 | } 178 | 179 | // Exec executes the given query, returns only an error 180 | func (db queryTarget) Exec(q qb.Query) (Result, error) { 181 | return db.ExecContext(context.Background(), q) 182 | } 183 | 184 | // ExecContext executes the given query, returns only an error 185 | func (db queryTarget) ExecContext(c context.Context, q qb.Query) (Result, error) { 186 | s, v := db.prepare(q) 187 | return db.RawExecContext(c, s, v...) 188 | } 189 | 190 | // MustExec executes the given query 191 | // If an error occurs returned it will panic 192 | func (db queryTarget) MustExec(q qb.Query) Result { 193 | return db.MustExecContext(context.Background(), q) 194 | } 195 | 196 | // MustExecContext executes the given query with context 197 | // If an error occurs returned it will panic 198 | func (db queryTarget) MustExecContext(c context.Context, q qb.Query) Result { 199 | return mustResult(db.ExecContext(c, q)) 200 | } 201 | 202 | // RawExec executes the given SQL with the given params directly on the database 203 | func (db queryTarget) RawExec(s string, v ...interface{}) (Result, error) { 204 | return db.RawExecContext(context.Background(), s, v...) 205 | } 206 | 207 | // RawExecContext executes the given SQL with the given params directly on the database 208 | func (db queryTarget) RawExecContext(c context.Context, s string, v ...interface{}) (Result, error) { 209 | r, err := db.src.ExecContext(c, s, v...) 210 | return Result{r}, err 211 | } 212 | 213 | // MustRawExec executes the given SQL with the given params directly on the database 214 | // If an error occurs returned it will panic 215 | func (db queryTarget) MustRawExec(s string, v ...interface{}) Result { 216 | return db.MustRawExecContext(context.Background(), s, v...) 217 | } 218 | 219 | // MustRawExecContext executes the given SQL with the given params directly on the database 220 | // If an error occurs returned it will panic 221 | func (db queryTarget) MustRawExecContext(c context.Context, s string, v ...interface{}) Result { 222 | return mustResult(db.RawExecContext(c, s, v...)) 223 | } 224 | 225 | // Prepare prepares a query for efficient repeated executions 226 | func (db queryTarget) Prepare(q qb.Query) (*Stmt, error) { 227 | return db.PrepareContext(context.Background(), q) 228 | } 229 | 230 | // PrepareContext prepares a query for efficient repeated executions 231 | func (db queryTarget) PrepareContext(c context.Context, q qb.Query) (*Stmt, error) { 232 | s, v := db.prepare(q) 233 | 234 | stmt, err := db.src.PrepareContext(c, s) 235 | if err != nil { 236 | return nil, err 237 | } 238 | 239 | return &Stmt{stmt, v}, nil 240 | } 241 | 242 | // MustPrepare prepares a query for efficient repeated executions 243 | // If an error occurs returned it will panic 244 | func (db queryTarget) MustPrepare(q qb.Query) *Stmt { 245 | return db.MustPrepareContext(context.Background(), q) 246 | } 247 | 248 | // MustPrepareContext prepares a query for efficient repeated executions 249 | // If an error occurs returned it will panic 250 | func (db queryTarget) MustPrepareContext(ctx context.Context, q qb.Query) *Stmt { 251 | stmt, err := db.PrepareContext(ctx, q) 252 | if err != nil { 253 | panic(err) 254 | } 255 | return stmt 256 | } 257 | 258 | func mustRows(r Rows, err error) Rows { 259 | if err != nil { 260 | panic(err) 261 | } 262 | return r 263 | } 264 | 265 | func mustResult(r Result, err error) Result { 266 | if err != nil { 267 | panic(err) 268 | } 269 | return r 270 | } 271 | -------------------------------------------------------------------------------- /qbdb/stmt.go: -------------------------------------------------------------------------------- 1 | package qbdb 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | // Stmt represents a prepared statement in the database 9 | type Stmt struct { 10 | stmt *sql.Stmt 11 | args []interface{} 12 | } 13 | 14 | // Close closes the prepared statement 15 | func (s *Stmt) Close() error { 16 | return s.stmt.Close() 17 | } 18 | 19 | // Query executes the prepared statement on the database 20 | func (s *Stmt) Query() (Rows, error) { 21 | return s.QueryContext(context.Background()) 22 | } 23 | 24 | // QueryContext executes the prepared statement on the database 25 | func (s *Stmt) QueryContext(c context.Context) (Rows, error) { 26 | r, err := s.stmt.QueryContext(c, s.args...) 27 | return Rows{r}, err 28 | } 29 | 30 | // MustQuery executes the prepared statement on the database 31 | func (s *Stmt) MustQuery() Rows { 32 | r, err := s.Query() 33 | if err != nil { 34 | panic(err) 35 | } 36 | return r 37 | } 38 | 39 | // QueryRow executes the prepared statement on the database, only returns one row 40 | func (s *Stmt) QueryRow() Row { 41 | return s.QueryRowContext(context.Background()) 42 | } 43 | 44 | // QueryRowContext executes the prepared statement on the database, only returns one row 45 | func (s *Stmt) QueryRowContext(c context.Context) Row { 46 | return Row{s.stmt.QueryRowContext(c, s.args...)} 47 | } 48 | 49 | // Exec executes the prepared statement 50 | func (s *Stmt) Exec() (Result, error) { 51 | return s.ExecContext(context.Background()) 52 | } 53 | 54 | // ExecContext executes the prepared statement 55 | func (s *Stmt) ExecContext(c context.Context) (Result, error) { 56 | r, err := s.stmt.ExecContext(c, s.args...) 57 | 58 | return Result{r}, err 59 | } 60 | 61 | // MustExec executes the given SelectQuery on the database 62 | func (s *Stmt) MustExec() Result { 63 | r, err := s.Exec() 64 | if err != nil { 65 | panic(err) 66 | } 67 | return r 68 | } 69 | -------------------------------------------------------------------------------- /qbdb/types.go: -------------------------------------------------------------------------------- 1 | package qbdb 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | 8 | "git.ultraware.nl/Ultraware/qb/v2" 9 | ) 10 | 11 | // Target is a target for a query, either a plain DB or a Tx 12 | type Target interface { 13 | Render(qb.Query) (string, []interface{}) 14 | Query(qb.SelectQuery) (Rows, error) 15 | QueryContext(ctx context.Context, q qb.SelectQuery) (Rows, error) 16 | RawQuery(string, ...interface{}) (Rows, error) 17 | RawQueryContext(ctx context.Context, s string, v ...interface{}) (Rows, error) 18 | MustQuery(qb.SelectQuery) Rows 19 | MustRawQuery(string, ...interface{}) Rows 20 | MustQueryContext(ctx context.Context, q qb.SelectQuery) Rows 21 | MustRawQueryContext(ctx context.Context, s string, v ...interface{}) Rows 22 | QueryRow(qb.SelectQuery) Row 23 | QueryRowContext(ctx context.Context, q qb.SelectQuery) Row 24 | RawQueryRow(string, ...interface{}) Row 25 | RawQueryRowContext(ctx context.Context, s string, v ...interface{}) Row 26 | Exec(q qb.Query) (Result, error) 27 | ExecContext(ctx context.Context, q qb.Query) (Result, error) 28 | RawExec(string, ...interface{}) (Result, error) 29 | RawExecContext(ctx context.Context, s string, v ...interface{}) (Result, error) 30 | MustExec(q qb.Query) Result 31 | MustExecContext(ctx context.Context, q qb.Query) Result 32 | MustRawExec(string, ...interface{}) Result 33 | MustRawExecContext(ctx context.Context, s string, v ...interface{}) Result 34 | Prepare(qb.Query) (*Stmt, error) 35 | PrepareContext(ctx context.Context, q qb.Query) (*Stmt, error) 36 | MustPrepare(qb.Query) *Stmt 37 | MustPrepareContext(ctx context.Context, q qb.Query) *Stmt 38 | Driver() qb.Driver 39 | SetDebug(bool) 40 | } 41 | 42 | // Tx is a transaction 43 | type Tx interface { 44 | Target 45 | Commit() error 46 | MustCommit() 47 | Rollback() error 48 | } 49 | 50 | type tx struct { 51 | queryTarget 52 | tx *sql.Tx 53 | } 54 | 55 | // Commit applies all the changes from the transaction 56 | func (t *tx) Commit() error { 57 | return t.tx.Commit() 58 | } 59 | 60 | // MustCommit is the same as Commit, but it panics if an error occurred 61 | func (t *tx) MustCommit() { 62 | err := t.Commit() 63 | if err != nil { 64 | panic(err) 65 | } 66 | } 67 | 68 | // Rollback reverts all the changes from the transaction 69 | func (t *tx) Rollback() error { 70 | return t.tx.Rollback() 71 | } 72 | 73 | type queryTarget struct { 74 | src interface { 75 | Exec(string, ...interface{}) (sql.Result, error) 76 | Query(string, ...interface{}) (*sql.Rows, error) 77 | QueryRow(string, ...interface{}) *sql.Row 78 | Prepare(string) (*sql.Stmt, error) 79 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 80 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 81 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 82 | PrepareContext(context.Context, string) (*sql.Stmt, error) 83 | } 84 | driver qb.Driver 85 | debug bool 86 | } 87 | 88 | func (db *queryTarget) Driver() qb.Driver { 89 | return db.driver 90 | } 91 | 92 | func (db *queryTarget) SetDebug(b bool) { 93 | db.debug = b 94 | } 95 | 96 | // DB is a database connection 97 | type DB interface { 98 | Target 99 | Begin() (Tx, error) 100 | BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) 101 | MustBegin() Tx 102 | } 103 | 104 | type db struct { 105 | queryTarget 106 | DB *sql.DB 107 | } 108 | 109 | // Begin starts a transaction 110 | func (db *db) Begin() (Tx, error) { 111 | return db.BeginTx(context.Background(), nil) 112 | } 113 | 114 | // BeginTx starts a transaction 115 | func (db *db) BeginTx(c context.Context, o *sql.TxOptions) (Tx, error) { 116 | rawTx, err := db.DB.BeginTx(c, o) 117 | return &tx{queryTarget{rawTx, db.queryTarget.driver, db.queryTarget.debug}, rawTx}, err 118 | } 119 | 120 | // MustBegin is the same as Begin, but it panics if an error occurred 121 | func (db *db) MustBegin() Tx { 122 | tx, err := db.Begin() 123 | if err != nil { 124 | panic(err) 125 | } 126 | return tx 127 | } 128 | 129 | // New returns a new DB 130 | func New(driver qb.Driver, database *sql.DB) DB { 131 | return &db{queryTarget{database, driver, false}, database} 132 | } 133 | 134 | ///////// Wrappers for Must functions ///////// 135 | 136 | // Rows is a wrapper for sql.Rows that adds MustScan 137 | type Rows struct { 138 | *sql.Rows 139 | } 140 | 141 | // MustScan is the same as Scan except if an error occurs returned it will panic 142 | func (r Rows) MustScan(dest ...interface{}) { 143 | err := r.Scan(dest...) 144 | if err != nil { 145 | panic(err) 146 | } 147 | } 148 | 149 | // Row is a wrapper for sql.Row that adds MustScan 150 | type Row struct { 151 | *sql.Row 152 | } 153 | 154 | // MustScan returns true if there was a row. 155 | // If an error occurs it will panic 156 | func (r Row) MustScan(dest ...interface{}) bool { 157 | err := r.Scan(dest...) 158 | if err != nil && !errors.Is(err, sql.ErrNoRows) { 159 | panic(err) 160 | } 161 | return err == nil 162 | } 163 | 164 | // Result is a wrapper for sql.Result that adds MustLastInsertId and MustRowsAffected 165 | type Result struct { 166 | sql.Result 167 | } 168 | 169 | // MustLastInsertId is the same as LastInsertId except if an error occurs returned it will panic 170 | func (r Result) MustLastInsertId() int64 { //nolint:revive,stylecheck 171 | id, err := r.LastInsertId() 172 | if err != nil { 173 | panic(err) 174 | } 175 | return id 176 | } 177 | 178 | // MustRowsAffected is the same as RowsAffected except if an error occurs returned it will panic 179 | func (r Result) MustRowsAffected() int64 { 180 | rows, err := r.RowsAffected() 181 | if err != nil { 182 | panic(err) 183 | } 184 | return rows 185 | } 186 | -------------------------------------------------------------------------------- /qbdb/types_test.go: -------------------------------------------------------------------------------- 1 | package qbdb 2 | 3 | import ( 4 | "testing" 5 | 6 | "git.fuyu.moe/Fuyu/assert" 7 | ) 8 | 9 | func TestImplements(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | _, ok := interface{}(&db{}).(DB) 13 | assert.True(ok) 14 | 15 | _, ok = interface{}(&tx{}).(Tx) 16 | assert.True(ok) 17 | 18 | _, ok = interface{}(&db{}).(Target) 19 | assert.True(ok) 20 | _, ok = interface{}(&tx{}).(Target) 21 | assert.True(ok) 22 | 23 | _, ok = interface{}(Result{}).(Result) 24 | assert.True(ok) 25 | } 26 | -------------------------------------------------------------------------------- /qc/condition.go: -------------------------------------------------------------------------------- 1 | package qc // import "git.ultraware.nl/Ultraware/qb/v2/qc" 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "git.ultraware.nl/Ultraware/qb/v2" 8 | ) 9 | 10 | func createOperatorCondition(i1, i2 interface{}, operator string) qb.Condition { 11 | f1 := qb.MakeField(i1) 12 | f2 := qb.MakeField(i2) 13 | return NewCondition(f1, ` `+operator+` `, f2) 14 | } 15 | 16 | // Eq checks if the values are equal (=) 17 | func Eq(i1, i2 interface{}) qb.Condition { 18 | return useOverride(eq, i1, i2) 19 | } 20 | 21 | func eq(i1, i2 interface{}) qb.Condition { 22 | return createOperatorCondition(i1, i2, `=`) 23 | } 24 | 25 | // Ne checks if the values are unequal (!=) 26 | func Ne(i1, i2 interface{}) qb.Condition { 27 | return useOverride(ne, i1, i2) 28 | } 29 | 30 | func ne(i1, i2 interface{}) qb.Condition { 31 | return createOperatorCondition(i1, i2, `!=`) 32 | } 33 | 34 | // Gt checks if i1 is greater than i2 (>) 35 | func Gt(i1, i2 interface{}) qb.Condition { 36 | return useOverride(gt, i1, i2) 37 | } 38 | 39 | func gt(i1, i2 interface{}) qb.Condition { 40 | return createOperatorCondition(i1, i2, `>`) 41 | } 42 | 43 | // Gte checks if i1 is greater or equal to i2 (>=) 44 | func Gte(i1, i2 interface{}) qb.Condition { 45 | return useOverride(gte, i1, i2) 46 | } 47 | 48 | func gte(i1, i2 interface{}) qb.Condition { 49 | return createOperatorCondition(i1, i2, `>=`) 50 | } 51 | 52 | // Lt checks if i1 is less than i2 (<) 53 | func Lt(i1, i2 interface{}) qb.Condition { 54 | return useOverride(lt, i1, i2) 55 | } 56 | 57 | func lt(i1, i2 interface{}) qb.Condition { 58 | return createOperatorCondition(i1, i2, `<`) 59 | } 60 | 61 | // Lte checks if i1 is less than or equal to i2 (<=) 62 | func Lte(i1, i2 interface{}) qb.Condition { 63 | return useOverride(lte, i1, i2) 64 | } 65 | 66 | func lte(i1, i2 interface{}) qb.Condition { 67 | return createOperatorCondition(i1, i2, `<=`) 68 | } 69 | 70 | // Between checks if f1 is between i1 and i2 71 | func Between(f1 qb.Field, i1, i2 interface{}) qb.Condition { 72 | return useOverride(between, f1, i1, i2) 73 | } 74 | 75 | func between(f1 qb.Field, i1, i2 interface{}) qb.Condition { 76 | f2 := qb.MakeField(i1) 77 | f3 := qb.MakeField(i2) 78 | return NewCondition(f1, ` BETWEEN `, f2, ` AND `, f3) 79 | } 80 | 81 | // IsNull checks if the field is NULL 82 | func IsNull(f1 qb.Field) qb.Condition { 83 | return useOverride(isNull, f1) 84 | } 85 | 86 | func isNull(f1 qb.Field) qb.Condition { 87 | return NewCondition(f1, ` IS NULL`) 88 | } 89 | 90 | // NotNull checks if the field is not NULL 91 | func NotNull(f1 qb.Field) qb.Condition { 92 | return useOverride(notNull, f1) 93 | } 94 | 95 | func notNull(f1 qb.Field) qb.Condition { 96 | return NewCondition(f1, ` IS NOT NULL`) 97 | } 98 | 99 | // Like checks if the f1 is like s 100 | func Like(f1 qb.Field, s string) qb.Condition { 101 | return useOverride(like, f1, s) 102 | } 103 | 104 | func like(f1 qb.Field, s string) qb.Condition { 105 | f2 := qb.MakeField(s) 106 | return NewCondition(f1, ` LIKE `, f2) 107 | } 108 | 109 | // In checks if f1 is in the list 110 | func In(f1 qb.Field, args ...interface{}) qb.Condition { 111 | if len(args) == 0 { 112 | panic(`Cannot call qc.In with zero in values`) 113 | } 114 | 115 | return useOverride(in, append([]interface{}{f1}, args...)...) 116 | } 117 | 118 | func in(f1 qb.Field, args ...interface{}) qb.Condition { 119 | list := strings.TrimSuffix(strings.Repeat(`?, `, len(args)), `, `) 120 | return func(c *qb.Context) string { 121 | c.Add(args...) 122 | return qb.ConcatQuery(c, f1, ` IN (`+list+`)`) 123 | } 124 | } 125 | 126 | // InQuery checks if f1 is in the subquery's result 127 | func InQuery(f qb.Field, q qb.SelectQuery) qb.Condition { 128 | return useOverride(inQuery, f, q) 129 | } 130 | 131 | func inQuery(f qb.Field, q qb.SelectQuery) qb.Condition { 132 | return func(c *qb.Context) string { 133 | return qb.ConcatQuery(c, f, ` IN `, q) 134 | } 135 | } 136 | 137 | // Exists checks if the subquery returns rows 138 | func Exists(q qb.SelectQuery) qb.Condition { 139 | return useOverride(exists, q) 140 | } 141 | 142 | func exists(q qb.SelectQuery) qb.Condition { 143 | return func(c *qb.Context) string { 144 | return qb.ConcatQuery(c, `EXISTS `, q) 145 | } 146 | } 147 | 148 | // Not reverses a boolean (!) 149 | func Not(c qb.Condition) qb.Condition { 150 | return useOverride(not, c) 151 | } 152 | 153 | func not(c qb.Condition) qb.Condition { 154 | return func(ctx *qb.Context) string { 155 | return `NOT (` + c(ctx) + `)` 156 | } 157 | } 158 | 159 | func createLogicalCondition(operator string, conditions ...qb.Condition) qb.Condition { 160 | return func(ctx *qb.Context) string { 161 | s := strings.Builder{} 162 | s.WriteString(`(`) 163 | for k, c := range conditions { 164 | if k > 0 { 165 | s.WriteString(` ` + operator + ` `) 166 | } 167 | s.WriteString(c(ctx)) 168 | } 169 | s.WriteString(`)`) 170 | return s.String() 171 | } 172 | } 173 | 174 | // And requires both conditions to be true 175 | func And(c ...qb.Condition) qb.Condition { 176 | list := make([]interface{}, len(c)) 177 | for k, v := range c { 178 | list[k] = reflect.ValueOf(v) 179 | } 180 | 181 | return useOverride(and, list...) 182 | } 183 | 184 | func and(c ...qb.Condition) qb.Condition { 185 | return createLogicalCondition(`AND`, c...) 186 | } 187 | 188 | // Or requires one of the conditions to be true 189 | func Or(c ...qb.Condition) qb.Condition { 190 | list := make([]interface{}, len(c)) 191 | for k, v := range c { 192 | list[k] = reflect.ValueOf(v) 193 | } 194 | 195 | return useOverride(or, list...) 196 | } 197 | 198 | func or(c ...qb.Condition) qb.Condition { 199 | return createLogicalCondition(`OR`, c...) 200 | } 201 | -------------------------------------------------------------------------------- /qc/condition_test.go: -------------------------------------------------------------------------------- 1 | package qc 2 | 3 | import ( 4 | "testing" 5 | 6 | "git.ultraware.nl/Ultraware/qb/v2" 7 | "git.ultraware.nl/Ultraware/qb/v2/internal/testutil" 8 | "git.ultraware.nl/Ultraware/qb/v2/qbdb" 9 | ) 10 | 11 | func TestAll(t *testing.T) { 12 | qb.NEWLINE, qb.INDENT = ` `, `` 13 | 14 | tb := &qb.Table{Name: `test`} 15 | 16 | f1 := &qb.TableField{Name: `A`, Parent: tb} 17 | f2 := &qb.TableField{Name: `B`, Parent: tb} 18 | 19 | qry := tb.Select([]qb.Field{f1}) 20 | 21 | check(t, Eq(f1, f2), `A = B`) 22 | check(t, Ne(f1, f2), `A != B`) 23 | check(t, Gt(f1, f2), `A > B`) 24 | check(t, Gte(f1, f2), `A >= B`) 25 | check(t, Lt(f1, f2), `A < B`) 26 | check(t, Lte(f1, f2), `A <= B`) 27 | 28 | check(t, Between(f1, 1, 2), `A BETWEEN ? AND ?`) 29 | check(t, IsNull(f1), `A IS NULL`) 30 | check(t, NotNull(f1), `A IS NOT NULL`) 31 | check(t, Like(f1, `%a%`), `A LIKE ?`) 32 | check(t, In(f1, 1, 2, 3), `A IN (?, ?, ?)`) 33 | 34 | check(t, InQuery(f1, qry), `A IN ( SELECT t.A FROM test AS t )`) 35 | check(t, Exists(qry), `EXISTS ( SELECT t.A FROM test AS t )`) 36 | 37 | check(t, Not(Eq(f1, f2)), `NOT (A = B)`) 38 | check(t, And(Eq(f1, f2), NotNull(f1)), `(A = B AND A IS NOT NULL)`) 39 | check(t, Or(Eq(f1, f2), IsNull(f1)), `(A = B OR A IS NULL)`) 40 | 41 | if !fails(func() { In(f1) }) { 42 | t.Error(`Expected In to panic when no parameters are provided`) 43 | } 44 | } 45 | 46 | var ctx = qb.NewContext(qbdb.Driver{}, qb.NoAlias()) 47 | 48 | func check(t *testing.T, c qb.Condition, expectedSQL string) { 49 | sql := c(ctx) 50 | 51 | testutil.Compare(t, expectedSQL, sql) 52 | } 53 | 54 | func fails(f func()) (failed bool) { 55 | defer func() { 56 | if recover() != nil { 57 | failed = true 58 | } 59 | }() 60 | 61 | f() 62 | 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /qc/helper.go: -------------------------------------------------------------------------------- 1 | package qc 2 | 3 | import ( 4 | "git.ultraware.nl/Ultraware/qb/v2" 5 | ) 6 | 7 | // NewCondition returns a new Condition 8 | func NewCondition(values ...interface{}) qb.Condition { 9 | return func(c *qb.Context) string { 10 | return qb.ConcatQuery(c, values...) 11 | } 12 | } 13 | 14 | func useOverride(fallback interface{}, in ...interface{}) qb.Condition { 15 | fn := qb.GetFuncFrame() 16 | 17 | return func(c *qb.Context) string { 18 | return c.Driver.Override().Condition(fn, fallback, in)(c) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /qf/case.go: -------------------------------------------------------------------------------- 1 | package qf 2 | 3 | import ( 4 | "strings" 5 | 6 | "git.ultraware.nl/Ultraware/qb/v2" 7 | ) 8 | 9 | type when struct { 10 | C qb.Condition 11 | F qb.Field 12 | } 13 | 14 | // WhenList contains a list of CASE when statements 15 | type WhenList []when 16 | 17 | // Case returns a type that allows you to build a when statement 18 | func Case() WhenList { 19 | return WhenList{} 20 | } 21 | 22 | // When adds a statement to the list 23 | func (l WhenList) When(c qb.Condition, v interface{}) WhenList { 24 | var ( 25 | f qb.Field 26 | ok bool 27 | ) 28 | if f, ok = v.(qb.Field); !ok { 29 | f = qb.Value(v) 30 | } 31 | 32 | return append(l, when{C: c, F: f}) 33 | } 34 | 35 | // Else returns a valid Field to finish the case 36 | func (l WhenList) Else(v interface{}) CaseField { 37 | var ( 38 | f qb.Field 39 | ok bool 40 | ) 41 | if f, ok = v.(qb.Field); !ok { 42 | f = qb.Value(v) 43 | } 44 | 45 | return CaseField{When: []when(l), Else: f} 46 | } 47 | 48 | // CaseField is a qb.Field that generates a case statement 49 | type CaseField struct { 50 | When []when 51 | Else qb.Field 52 | } 53 | 54 | // QueryString returns a string for use in queries 55 | func (f CaseField) QueryString(c *qb.Context) string { 56 | s := strings.Builder{} 57 | s.WriteString(`CASE`) 58 | 59 | for _, v := range f.When { 60 | s.WriteString(` WHEN ` + v.C(c) + ` THEN ` + v.F.QueryString(c)) 61 | } 62 | 63 | s.WriteString(` ELSE ` + f.Else.QueryString(c) + ` END`) 64 | return s.String() 65 | } 66 | -------------------------------------------------------------------------------- /qf/case_test.go: -------------------------------------------------------------------------------- 1 | package qf 2 | 3 | import ( 4 | "testing" 5 | 6 | "git.ultraware.nl/Ultraware/qb/v2" 7 | "git.ultraware.nl/Ultraware/qb/v2/internal/testutil" 8 | ) 9 | 10 | var ( 11 | c1 = func(_ *qb.Context) string { return `A` } 12 | c2 = func(_ *qb.Context) string { return `B` } 13 | ) 14 | 15 | func TestCase(t *testing.T) { 16 | c := Case().When(c1, 1).When(c2, 2).Else(3) 17 | expected := `CASE WHEN A THEN ? WHEN B THEN ? ELSE ? END` 18 | 19 | ctx := qb.NewContext(nil, qb.NoAlias()) 20 | 21 | sql := c.QueryString(ctx) 22 | 23 | if len(*ctx.Values) != 3 || (*ctx.Values)[0] != 1 || (*ctx.Values)[1] != 2 || (*ctx.Values)[2] != 3 { 24 | t.Errorf(`Expected values [1, 2, 3]. Got: %v`, *ctx.Values) 25 | } 26 | 27 | testutil.Compare(t, expected, sql) 28 | } 29 | -------------------------------------------------------------------------------- /qf/field.go: -------------------------------------------------------------------------------- 1 | package qf 2 | 3 | import ( 4 | "git.ultraware.nl/Ultraware/qb/v2" 5 | ) 6 | 7 | // CalculatedField is a field created by running functions on a TableField 8 | type CalculatedField func(c *qb.Context) string 9 | 10 | // QueryString implements qb.Field 11 | func (f CalculatedField) QueryString(c *qb.Context) string { 12 | return f(c) 13 | } 14 | 15 | // NewCalculatedField returns a new CalculatedField 16 | func NewCalculatedField(args ...interface{}) CalculatedField { 17 | return func(c *qb.Context) string { return qb.ConcatQuery(c, args...) } 18 | } 19 | 20 | func useOverride(fallback interface{}, in ...interface{}) qb.Field { 21 | fn := qb.GetFuncFrame() 22 | 23 | return CalculatedField(func(c *qb.Context) string { 24 | return c.Driver.Override().Field(fn, fallback, in).QueryString(c) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /qf/functions.go: -------------------------------------------------------------------------------- 1 | package qf // import "git.ultraware.nl/Ultraware/qb/v2/qf" 2 | 3 | import "git.ultraware.nl/Ultraware/qb/v2" 4 | 5 | ///// General functions ///// 6 | 7 | // Excluded uses a value from the INSERT, only usable in an upsert query 8 | func Excluded(f qb.Field) qb.Field { 9 | return useOverride(nil, f) 10 | } 11 | 12 | // Cast casts a value to a different type 13 | func Cast(f qb.Field, t qb.DataType) qb.Field { 14 | return useOverride(cast, f, t) 15 | } 16 | 17 | func cast(f qb.Field, t qb.DataType) qb.Field { 18 | return CalculatedField(func(c *qb.Context) string { 19 | return qb.ConcatQuery(c, `CAST(`, f, ` AS `, c.Driver.TypeName(t), `)`) 20 | }) 21 | } 22 | 23 | // Distinct removes all duplicate values for this field 24 | func Distinct(f qb.Field) qb.Field { 25 | return useOverride(distinct, f) 26 | } 27 | 28 | func distinct(f qb.Field) qb.Field { 29 | return NewCalculatedField(`DISTINCT `, f) 30 | } 31 | 32 | // CountAll counts the number of rows 33 | func CountAll() qb.Field { 34 | return useOverride(countAll) 35 | } 36 | 37 | func countAll() qb.Field { 38 | return NewCalculatedField(`count(1)`) 39 | } 40 | 41 | // Count counts the number of non-NULL values for this field 42 | func Count(f qb.Field) qb.Field { 43 | return useOverride(count, f) 44 | } 45 | 46 | func count(f qb.Field) qb.Field { 47 | return NewCalculatedField(`count(`, f, `)`) 48 | } 49 | 50 | // Sum calculates the sum of all values in this field 51 | func Sum(f qb.Field) qb.Field { 52 | return useOverride(sum, f) 53 | } 54 | 55 | func sum(f qb.Field) qb.Field { 56 | return NewCalculatedField(`sum(`, f, `)`) 57 | } 58 | 59 | // Average calculates the average of all values in this field 60 | func Average(f qb.Field) qb.Field { 61 | return useOverride(average, f) 62 | } 63 | 64 | func average(f qb.Field) qb.Field { 65 | return NewCalculatedField(`avg(`, f, `)`) 66 | } 67 | 68 | // Min calculates the minimum value in this field 69 | func Min(f qb.Field) qb.Field { 70 | return useOverride(minFunc, f) 71 | } 72 | 73 | func minFunc(f qb.Field) qb.Field { 74 | return NewCalculatedField(`min(`, f, `)`) 75 | } 76 | 77 | // Max calculates the maximum value in this field 78 | func Max(f qb.Field) qb.Field { 79 | return useOverride(maxFunc, f) 80 | } 81 | 82 | func maxFunc(f qb.Field) qb.Field { 83 | return NewCalculatedField(`max(`, f, `)`) 84 | } 85 | 86 | // Coalesce returns the second argument if the first one is NULL 87 | func Coalesce(f1 qb.Field, i interface{}) qb.Field { 88 | return useOverride(coalesce, f1, i) 89 | } 90 | 91 | func coalesce(f1 qb.Field, i interface{}) qb.Field { 92 | f2 := qb.MakeField(i) 93 | return NewCalculatedField(`coalesce(`, f1, `, `, f2, `)`) 94 | } 95 | 96 | ///// String functions ///// 97 | 98 | // Lower returns the value as a lowercase string 99 | func Lower(f qb.Field) qb.Field { 100 | return useOverride(lower, f) 101 | } 102 | 103 | func lower(f qb.Field) qb.Field { 104 | return NewCalculatedField(`lower(`, f, `)`) 105 | } 106 | 107 | // Concat combines fields and strings into a single string 108 | func Concat(i ...interface{}) qb.Field { 109 | return useOverride(concat, i...) 110 | } 111 | 112 | func concat(i ...interface{}) qb.Field { 113 | return CalculatedField(func(c *qb.Context) string { 114 | return qb.JoinQuery(c, ` || `, i) 115 | }) 116 | } 117 | 118 | // Replace replaces values in a string 119 | func Replace(f qb.Field, from, to interface{}) qb.Field { 120 | return useOverride(replace, f, from, to) 121 | } 122 | 123 | func replace(f qb.Field, from, to interface{}) qb.Field { 124 | f1, f2 := qb.MakeField(from), qb.MakeField(to) 125 | return NewCalculatedField(`replace(`, f, `, `, f1, `, `, f2, `)`) 126 | } 127 | 128 | // Substring retrieves a part of a string 129 | func Substring(f qb.Field, from, length interface{}) qb.Field { 130 | return useOverride(substring, f, from, length) 131 | } 132 | 133 | func substring(f qb.Field, from, length interface{}) qb.Field { 134 | f1 := qb.MakeField(from) 135 | if length == nil { 136 | return NewCalculatedField(`substring(`, f, `, `, f1, `)`) 137 | } 138 | f2 := qb.MakeField(length) 139 | return NewCalculatedField(`substring(`, f, `, `, f1, `, `, f2, `)`) 140 | } 141 | 142 | ///// Date functions ///// 143 | 144 | // Now retrieves the current time 145 | func Now() qb.Field { 146 | return useOverride(now) 147 | } 148 | 149 | func now() qb.Field { 150 | return NewCalculatedField(`now()`) 151 | } 152 | 153 | // Extract retrieves a the given part of a date 154 | func Extract(f qb.Field, part string) qb.Field { 155 | return useOverride(extract, f, part) 156 | } 157 | 158 | func extract(f qb.Field, part string) qb.Field { 159 | return NewCalculatedField(`EXTRACT(`, part, ` FROM `, f, `)`) 160 | } 161 | 162 | // Second retrieves the second from a date 163 | func Second(f qb.Field) qb.Field { 164 | return useOverride(second, f) 165 | } 166 | 167 | func second(f qb.Field) qb.Field { 168 | return Extract(f, `second`) 169 | } 170 | 171 | // Minute retrieves the minute from a date 172 | func Minute(f qb.Field) qb.Field { 173 | return useOverride(minute, f) 174 | } 175 | 176 | func minute(f qb.Field) qb.Field { 177 | return Extract(f, `minute`) 178 | } 179 | 180 | // Hour retrieves the hour from a date 181 | func Hour(f qb.Field) qb.Field { 182 | return useOverride(hour, f) 183 | } 184 | 185 | func hour(f qb.Field) qb.Field { 186 | return Extract(f, `hour`) 187 | } 188 | 189 | // Day retrieves the day from a date 190 | func Day(f qb.Field) qb.Field { 191 | return useOverride(day, f) 192 | } 193 | 194 | func day(f qb.Field) qb.Field { 195 | return Extract(f, `day`) 196 | } 197 | 198 | // Week retrieves the week from a date 199 | func Week(f qb.Field) qb.Field { 200 | return useOverride(week, f) 201 | } 202 | 203 | func week(f qb.Field) qb.Field { 204 | return Extract(f, `week`) 205 | } 206 | 207 | // Month retrieves the month from a date 208 | func Month(f qb.Field) qb.Field { 209 | return useOverride(month, f) 210 | } 211 | 212 | func month(f qb.Field) qb.Field { 213 | return Extract(f, `month`) 214 | } 215 | 216 | // Year retrieves the year from a date 217 | func Year(f qb.Field) qb.Field { 218 | return useOverride(year, f) 219 | } 220 | 221 | func year(f qb.Field) qb.Field { 222 | return Extract(f, `year`) 223 | } 224 | 225 | ///// Mathmatical functions ///// 226 | 227 | // Abs returns the absolute value, turning all negatieve numbers into positive numbers 228 | func Abs(f qb.Field) qb.Field { 229 | return useOverride(abs, f) 230 | } 231 | 232 | func abs(f qb.Field) qb.Field { 233 | return NewCalculatedField(`abs(`, f, `)`) 234 | } 235 | 236 | // Ceil rounds a value up 237 | func Ceil(f qb.Field) qb.Field { 238 | return useOverride(ceil, f) 239 | } 240 | 241 | func ceil(f qb.Field) qb.Field { 242 | return NewCalculatedField(`ceil(`, f, `)`) 243 | } 244 | 245 | // Floor rounds a value down 246 | func Floor(f qb.Field) qb.Field { 247 | return useOverride(floor, f) 248 | } 249 | 250 | func floor(f qb.Field) qb.Field { 251 | return NewCalculatedField(`floor(`, f, `)`) 252 | } 253 | 254 | // Round rounds a value to the specified precision 255 | func Round(f1 qb.Field, precision int) qb.Field { 256 | return useOverride(round, f1, precision) 257 | } 258 | 259 | func round(f1 qb.Field, precision int) qb.Field { 260 | f2 := qb.MakeField(precision) 261 | return NewCalculatedField(`round(`, f1, `, `, f2, `)`) 262 | } 263 | 264 | ///// Mathmatical expressions ///// 265 | 266 | // Add adds the values (+) 267 | func Add(f1 qb.Field, i interface{}) qb.Field { 268 | return useOverride(add, f1, i) 269 | } 270 | 271 | func add(f1 qb.Field, i interface{}) qb.Field { 272 | f2 := qb.MakeField(i) 273 | return NewCalculatedField(`(`, f1, ` + `, f2, `)`) 274 | } 275 | 276 | // Sub subtracts the values (-) 277 | func Sub(f1 qb.Field, i interface{}) qb.Field { 278 | return useOverride(sub, f1, i) 279 | } 280 | 281 | func sub(f1 qb.Field, i interface{}) qb.Field { 282 | f2 := qb.MakeField(i) 283 | return NewCalculatedField(`(`, f1, ` - `, f2, `)`) 284 | } 285 | 286 | // Mult multiplies the values (*) 287 | func Mult(f1 qb.Field, i interface{}) qb.Field { 288 | return useOverride(mult, f1, i) 289 | } 290 | 291 | func mult(f1 qb.Field, i interface{}) qb.Field { 292 | f2 := qb.MakeField(i) 293 | return NewCalculatedField(`(`, f1, ` * `, f2, `)`) 294 | } 295 | 296 | // Div divides the values (/) 297 | func Div(f1 qb.Field, i interface{}) qb.Field { 298 | return useOverride(div, f1, i) 299 | } 300 | 301 | func div(f1 qb.Field, i interface{}) qb.Field { 302 | f2 := qb.MakeField(i) 303 | return NewCalculatedField(`(`, f1, ` / `, f2, `)`) 304 | } 305 | 306 | // Mod gets the remainder of the division (%) 307 | func Mod(f1 qb.Field, i interface{}) qb.Field { 308 | return useOverride(mod, f1, i) 309 | } 310 | 311 | func mod(f1 qb.Field, i interface{}) qb.Field { 312 | f2 := qb.MakeField(i) 313 | return NewCalculatedField(`(`, f1, ` % `, f2, `)`) 314 | } 315 | 316 | // Pow calculates the power of a number (^) 317 | func Pow(f1 qb.Field, i interface{}) qb.Field { 318 | return useOverride(pow, f1, i) 319 | } 320 | 321 | func pow(f1 qb.Field, i interface{}) qb.Field { 322 | f2 := qb.MakeField(i) 323 | return NewCalculatedField(`(`, f1, ` ^ `, f2, `)`) 324 | } 325 | -------------------------------------------------------------------------------- /qf/functions_test.go: -------------------------------------------------------------------------------- 1 | package qf 2 | 3 | import ( 4 | "testing" 5 | 6 | "git.ultraware.nl/Ultraware/qb/v2" 7 | "git.ultraware.nl/Ultraware/qb/v2/internal/testutil" 8 | "git.ultraware.nl/Ultraware/qb/v2/qbdb" 9 | ) 10 | 11 | func TestAll(t *testing.T) { 12 | tb := &qb.Table{Name: `test`} 13 | 14 | f1 := &qb.TableField{Name: `A`, Parent: tb} 15 | f2 := &qb.TableField{Name: `B`, Parent: tb} 16 | f3 := &qb.TableField{Name: `C`, Parent: tb} 17 | 18 | check(t, Cast(f1, qb.Int), `CAST(A AS int)`) 19 | check(t, Cast(f1, qb.String), `CAST(A AS string)`) 20 | 21 | check(t, Distinct(f1), `DISTINCT A`) 22 | check(t, CountAll(), `count(1)`) 23 | check(t, Count(f1), `count(A)`) 24 | 25 | check(t, Sum(f1), `sum(A)`) 26 | check(t, Average(f1), `avg(A)`) 27 | check(t, Min(f1), `min(A)`) 28 | check(t, Max(f1), `max(A)`) 29 | 30 | check(t, Coalesce(f1, f2), `coalesce(A, B)`) 31 | 32 | check(t, Lower(f1), `lower(A)`) 33 | check(t, Concat(f3, `B`, `A`), `C || ? || ?`) 34 | check(t, Replace(f1, f2, `C`), `replace(A, B, ?)`) 35 | check(t, Substring(f1, 1, 4), `substring(A, ?, ?)`) 36 | check(t, Substring(f1, 1, nil), `substring(A, ?)`) 37 | 38 | check(t, Now(), `now()`) 39 | 40 | check(t, Second(f1), `EXTRACT(second FROM A)`) 41 | check(t, Minute(f1), `EXTRACT(minute FROM A)`) 42 | check(t, Hour(f1), `EXTRACT(hour FROM A)`) 43 | check(t, Day(f1), `EXTRACT(day FROM A)`) 44 | check(t, Week(f1), `EXTRACT(week FROM A)`) 45 | check(t, Month(f1), `EXTRACT(month FROM A)`) 46 | check(t, Year(f1), `EXTRACT(year FROM A)`) 47 | 48 | check(t, Abs(f1), `abs(A)`) 49 | check(t, Ceil(f1), `ceil(A)`) 50 | check(t, Floor(f1), `floor(A)`) 51 | check(t, Round(f1, 2), `round(A, ?)`) 52 | 53 | check(t, Add(f1, f2), `(A + B)`) 54 | check(t, Sub(f1, f2), `(A - B)`) 55 | check(t, Mult(f1, f2), `(A * B)`) 56 | check(t, Div(f1, f2), `(A / B)`) 57 | check(t, Mod(f1, f2), `(A % B)`) 58 | check(t, Pow(f1, f2), `(A ^ B)`) 59 | 60 | fails(func() { 61 | Excluded(f1) 62 | }) 63 | } 64 | 65 | var c = qb.NewContext(qbdb.Driver{}, qb.NoAlias()) 66 | 67 | func check(t *testing.T, f qb.Field, expectedSQL string) { 68 | sql := f.QueryString(c) 69 | 70 | testutil.Compare(t, expectedSQL, sql) 71 | } 72 | 73 | func fails(f func()) (failed bool) { 74 | defer func() { 75 | if recover() != nil { 76 | failed = true 77 | } 78 | }() 79 | 80 | f() 81 | 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /select.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | // SelectQuery represents a query that returns data 4 | type SelectQuery interface { 5 | Query 6 | getSQL(SQLBuilder, bool) (string, []interface{}) 7 | SubQuery(...*Field) *SubQuery 8 | CTE(...*Field) *CTE 9 | Fields() []Field 10 | } 11 | 12 | // ReturningBuilder builds a query with a RETURNING statement 13 | type ReturningBuilder struct { 14 | Query Query 15 | fields []Field 16 | } 17 | 18 | // Returning creates a RETURNING or OUTPUT query 19 | func Returning(q Query, f ...Field) SelectQuery { 20 | return ReturningBuilder{q, f} 21 | } 22 | 23 | // SQL returns a query string and a list of values 24 | func (q ReturningBuilder) SQL(b SQLBuilder) (string, []interface{}) { 25 | return q.getSQL(b, false) 26 | } 27 | 28 | func (q ReturningBuilder) getSQL(b SQLBuilder, _ bool) (string, []interface{}) { 29 | s, v := b.Context.Driver.Returning(b, q.Query, q.fields) 30 | return s, append(v, *b.Context.Values...) 31 | } 32 | 33 | // Fields returns a list of the fields used in the query 34 | func (q ReturningBuilder) Fields() []Field { 35 | return q.fields 36 | } 37 | 38 | // SubQuery converts the SelectQuery to a SubQuery for use in further queries 39 | func (q ReturningBuilder) SubQuery(fields ...*Field) *SubQuery { 40 | return newSubQuery(q, fields) 41 | } 42 | 43 | // CTE creates a new CTE (WITH) Query 44 | func (q ReturningBuilder) CTE(fields ...*Field) *CTE { 45 | return newCTE(q, fields) 46 | } 47 | 48 | // SelectBuilder builds a SELECT query 49 | type SelectBuilder struct { 50 | source Source 51 | fields []Field 52 | where []Condition 53 | joins []join 54 | order []FieldOrder 55 | group []Field 56 | having []Condition 57 | tables []Source 58 | limit int 59 | offset int 60 | } 61 | 62 | // NewSelectBuilder retruns a new SelectBuilder 63 | func NewSelectBuilder(f []Field, src Source) *SelectBuilder { 64 | return &SelectBuilder{fields: f, source: src, tables: []Source{src}} 65 | } 66 | 67 | // Where adds conditions to the WHERE clause 68 | func (q *SelectBuilder) Where(c ...Condition) *SelectBuilder { 69 | q.where = append(q.where, c...) 70 | return q 71 | } 72 | 73 | // InnerJoin adds an INNER JOIN clause to the query 74 | func (q *SelectBuilder) InnerJoin(f1, f2 Field, c ...Condition) *SelectBuilder { 75 | return q.join(JoinInner, f1, f2, c) 76 | } 77 | 78 | // CrossJoin adds a CROSS JOIN clause to the query 79 | func (q *SelectBuilder) CrossJoin(s Source) *SelectBuilder { 80 | q.joins = append(q.joins, join{JoinCross, s, nil}) 81 | q.tables = append(q.tables, s) 82 | 83 | return q 84 | } 85 | 86 | // LeftJoin adds a LEFT JOIN clause to the query 87 | func (q *SelectBuilder) LeftJoin(f1, f2 Field, c ...Condition) *SelectBuilder { 88 | return q.join(JoinLeft, f1, f2, c) 89 | } 90 | 91 | // RightJoin adds a RIGHT JOIN clause to the query 92 | func (q *SelectBuilder) RightJoin(f1, f2 Field, c ...Condition) *SelectBuilder { 93 | return q.join(JoinRight, f1, f2, c) 94 | } 95 | 96 | // ManualJoin manually joins a table 97 | // Only use this if you know what you are doing 98 | func (q *SelectBuilder) ManualJoin(t Join, s Source, c ...Condition) *SelectBuilder { 99 | q.joins = append(q.joins, join{t, s, c}) 100 | q.tables = append(q.tables, s) 101 | 102 | return q 103 | } 104 | 105 | // CrossJoinLateral adds an CORSS JOIN LATERAL clause to the query 106 | func (q *SelectBuilder) CrossJoinLateral(s LateralJoinSource) *SelectBuilder { 107 | return q.ManualJoin( 108 | JoinCross, 109 | s.lateralJoinSource(), 110 | make([]Condition, 0)..., 111 | ) 112 | } 113 | 114 | // InnerJoinLateral adds an INNER JOIN LATERAL clause to the query 115 | func (q *SelectBuilder) InnerJoinLateral(s LateralJoinSource, condition Condition, conditions ...Condition) *SelectBuilder { 116 | return q.ManualJoin( 117 | JoinInner, 118 | s.lateralJoinSource(), 119 | append([]Condition{condition}, conditions...)..., 120 | ) 121 | } 122 | 123 | // LeftJoinLateral adds a LEFT JOIN LATERAL clause to the query 124 | func (q *SelectBuilder) LeftJoinLateral(s LateralJoinSource, condition Condition, conditions ...Condition) *SelectBuilder { 125 | return q.ManualJoin( 126 | JoinLeft, 127 | s.lateralJoinSource(), 128 | append([]Condition{condition}, conditions...)..., 129 | ) 130 | } 131 | 132 | // RightJoinLateral adds a RIGHT JOIN LATERAL clause to the query 133 | func (q *SelectBuilder) RightJoinLateral(s LateralJoinSource, condition Condition, conditions ...Condition) *SelectBuilder { 134 | return q.ManualJoin( 135 | JoinRight, 136 | s.lateralJoinSource(), 137 | append([]Condition{condition}, conditions...)..., 138 | ) 139 | } 140 | 141 | func (q *SelectBuilder) join(t Join, f1, f2 Field, c []Condition) *SelectBuilder { 142 | var newSource Source 143 | exists := 0 144 | for _, v := range q.tables { 145 | if src := getParent(f1); src == v { 146 | exists++ 147 | newSource = getParent(f2) 148 | } 149 | if src := getParent(f2); src == v { 150 | exists++ 151 | newSource = getParent(f1) 152 | } 153 | } 154 | 155 | if exists == 0 { 156 | panic(`None of these tables are present in the query`) 157 | } 158 | if exists > 1 { 159 | panic(`Both tables already joined`) 160 | } 161 | 162 | return q.ManualJoin(t, newSource, append(c, eq(f1, f2))...) 163 | } 164 | 165 | // GroupBy adds a GROUP BY clause to the query 166 | func (q *SelectBuilder) GroupBy(f ...Field) *SelectBuilder { 167 | q.group = append(q.group, f...) 168 | return q 169 | } 170 | 171 | // Having adds a HAVING clause to the query 172 | func (q *SelectBuilder) Having(c ...Condition) *SelectBuilder { 173 | q.having = append(q.having, c...) 174 | return q 175 | } 176 | 177 | // OrderBy adds a ORDER BY clause to the query 178 | func (q *SelectBuilder) OrderBy(o ...FieldOrder) *SelectBuilder { 179 | q.order = append(q.order, o...) 180 | return q 181 | } 182 | 183 | // Limit adds a LIMIT clause to the query 184 | func (q *SelectBuilder) Limit(i int) *SelectBuilder { 185 | q.limit = i 186 | return q 187 | } 188 | 189 | // Offset adds a OFFSET clause to the query 190 | func (q *SelectBuilder) Offset(i int) *SelectBuilder { 191 | q.offset = i 192 | return q 193 | } 194 | 195 | // CTE creates a new CTE (WITH) Query 196 | func (q *SelectBuilder) CTE(fields ...*Field) *CTE { 197 | return newCTE(q, fields) 198 | } 199 | 200 | // SQL returns a query string and a list of values 201 | func (q *SelectBuilder) SQL(b SQLBuilder) (string, []interface{}) { 202 | return q.getSQL(b, false) 203 | } 204 | 205 | func (q *SelectBuilder) getSQL(b SQLBuilder, aliasFields bool) (string, []interface{}) { 206 | oldAlias := b.Context.alias 207 | if _, ok := oldAlias.(*noAlias); ok { 208 | b.Context.alias = AliasGenerator() 209 | } 210 | 211 | for _, v := range q.tables { 212 | _ = b.Context.Alias(v) 213 | } 214 | 215 | b.Select(aliasFields, q.fields...) 216 | b.From(q.source) 217 | b.Join(q.joins...) 218 | b.Where(q.where...) 219 | b.GroupBy(q.group...) 220 | b.Having(q.having...) 221 | b.OrderBy(q.order...) 222 | b.LimitOffset(q.limit, q.offset) 223 | 224 | b.Context.alias = oldAlias 225 | 226 | return b.w.String(), *b.Context.Values 227 | } 228 | 229 | // SubQuery converts the SelectQuery to a SubQuery for use in further queries 230 | func (q *SelectBuilder) SubQuery(fields ...*Field) *SubQuery { 231 | return newSubQuery(q, fields) 232 | } 233 | 234 | // Fields returns a list of the fields used in the query 235 | func (q *SelectBuilder) Fields() []Field { 236 | return q.fields 237 | } 238 | 239 | //////////////////////////// 240 | 241 | type combinedQuery struct { 242 | combineType string 243 | queries []SelectQuery 244 | } 245 | 246 | func (q combinedQuery) getSQL(b SQLBuilder, aliasFields bool) (string, []interface{}) { 247 | s := `` 248 | for k, v := range q.queries { 249 | var sql string 250 | 251 | if k == 0 { 252 | sql, _ = v.getSQL(b, aliasFields) 253 | } else { 254 | s += ` ` + q.combineType + ` ` 255 | sql, _ = v.getSQL(b, false) 256 | } 257 | s += getSubQuerySQL(sql) 258 | } 259 | 260 | return s + NEWLINE, *b.Context.Values 261 | } 262 | 263 | func (q combinedQuery) SQL(b SQLBuilder) (string, []interface{}) { 264 | return q.getSQL(b, false) 265 | } 266 | 267 | func (q combinedQuery) Fields() []Field { 268 | return q.queries[0].Fields() 269 | } 270 | 271 | func (q combinedQuery) CTE(fields ...*Field) *CTE { 272 | return newCTE(q, fields) 273 | } 274 | 275 | func (q combinedQuery) SubQuery(fields ...*Field) *SubQuery { 276 | return newSubQuery(q, fields) 277 | } 278 | 279 | //////////////////////// 280 | 281 | // UnionAll combines queries with an UNION ALL 282 | func UnionAll(q ...SelectQuery) SelectQuery { 283 | return combinedQuery{combineType: `UNION ALL`, queries: q} 284 | } 285 | 286 | // Union combines queries with an UNION 287 | func Union(q ...SelectQuery) SelectQuery { 288 | return combinedQuery{combineType: `UNION`, queries: q} 289 | } 290 | 291 | // ExceptAll combines queries with an EXCEPT ALL 292 | func ExceptAll(q1, q2 SelectQuery) SelectQuery { 293 | return combinedQuery{combineType: `EXCEPT ALL`, queries: []SelectQuery{q1, q2}} 294 | } 295 | 296 | // Except combines queries with an EXCEPT 297 | func Except(q1, q2 SelectQuery) SelectQuery { 298 | return combinedQuery{combineType: `EXCEPT`, queries: []SelectQuery{q1, q2}} 299 | } 300 | 301 | // IntersectAll combines queries with an INTERSECT ALL 302 | func IntersectAll(q1, q2 SelectQuery) SelectQuery { 303 | return combinedQuery{combineType: `INTERSECT ALL`, queries: []SelectQuery{q1, q2}} 304 | } 305 | 306 | // Intersect combines queries with an INTERSECT 307 | func Intersect(q1, q2 SelectQuery) SelectQuery { 308 | return combinedQuery{combineType: `INTERSECT`, queries: []SelectQuery{q1, q2}} 309 | } 310 | -------------------------------------------------------------------------------- /sql.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // SQL represents an SQL string. 10 | // Not intended to be used directly 11 | type SQL interface { 12 | WriteString(string) 13 | WriteLine(string) 14 | Rewrite(string) 15 | String() string 16 | } 17 | 18 | type sqlWriter struct { 19 | sql bytes.Buffer 20 | indent int 21 | } 22 | 23 | func (w *sqlWriter) WriteString(s string) { 24 | if w.sql.Len() == 0 || w.sql.Bytes()[w.sql.Len()-1] == '\n' { 25 | w.sql.WriteString(strings.Repeat(INDENT, w.indent)) 26 | } 27 | for k, v := range strings.Split(s, "\n") { 28 | if k > 0 && v != `` { 29 | w.sql.WriteString(NEWLINE + strings.Repeat(INDENT, w.indent)) 30 | } 31 | w.sql.WriteString(v) 32 | } 33 | if s[len(s)-1] == '\n' { 34 | w.sql.WriteString(NEWLINE) 35 | } 36 | } 37 | 38 | func (w *sqlWriter) WriteLine(s string) { 39 | w.WriteString(s + NEWLINE) 40 | } 41 | 42 | func (w *sqlWriter) Rewrite(s string) { 43 | w.sql = bytes.Buffer{} 44 | w.sql.WriteString(s) 45 | } 46 | 47 | func (w *sqlWriter) AddIndent() { 48 | w.indent++ 49 | } 50 | 51 | func (w *sqlWriter) SubIndent() { 52 | w.indent-- 53 | } 54 | 55 | func (w *sqlWriter) String() string { 56 | return w.sql.String() 57 | } 58 | 59 | // SQLBuilder contains data and methods to generate SQL. 60 | // This type is not intended to be used directly 61 | type SQLBuilder struct { 62 | w sqlWriter 63 | Context *Context 64 | } 65 | 66 | // NewSQLBuilder returns a new SQLBuilder 67 | func NewSQLBuilder(d Driver) SQLBuilder { 68 | alias := NoAlias() 69 | 70 | return SQLBuilder{sqlWriter{}, NewContext(d, alias)} 71 | } 72 | 73 | ///// Non-statements ///// 74 | 75 | // SourceToSQL converts a Source to a string 76 | func (b *SQLBuilder) SourceToSQL(s Source) string { 77 | return s.TableString(b.Context) 78 | } 79 | 80 | // FieldToSQL converts a Field to a string 81 | func (b *SQLBuilder) FieldToSQL(f Field) string { 82 | return f.QueryString(b.Context) 83 | } 84 | 85 | // List lists the given fields 86 | func (b *SQLBuilder) List(f []Field, withAlias bool) string { 87 | s := `` 88 | for k, v := range f { 89 | if k > 0 { 90 | s += COMMA 91 | } 92 | s += b.FieldToSQL(v) 93 | if withAlias { 94 | s += ` ` + getFieldName(v) + strconv.Itoa(k) 95 | } 96 | } 97 | 98 | return s 99 | } 100 | 101 | // Conditions generates valid SQL for the given list of conditions 102 | func (b *SQLBuilder) Conditions(c []Condition, newline bool) { 103 | fn := b.w.WriteString 104 | if newline { 105 | fn = b.w.WriteLine 106 | } 107 | 108 | if len(c) == 0 { 109 | return 110 | } 111 | fn(c[0](b.Context)) 112 | 113 | if newline { 114 | b.w.AddIndent() 115 | defer b.w.SubIndent() 116 | } 117 | 118 | for _, v := range c[1:] { 119 | if !newline { 120 | fn(` `) 121 | } 122 | fn(`AND ` + v(b.Context)) 123 | } 124 | } 125 | 126 | func eq(f1, f2 Field) Condition { 127 | return func(c *Context) string { 128 | return ConcatQuery(c, f1, ` = `, f2) 129 | } 130 | } 131 | 132 | ///// SQL statements ///// 133 | 134 | // Select generates a SQL SELECT line 135 | func (b *SQLBuilder) Select(withAlias bool, f ...Field) { 136 | b.w.WriteLine(`SELECT ` + b.List(f, withAlias)) 137 | } 138 | 139 | // From generates a SQL FROM line 140 | func (b *SQLBuilder) From(src Source) { 141 | b.w.WriteLine(`FROM ` + b.SourceToSQL(src)) 142 | } 143 | 144 | // Join generates SQL JOIN lines 145 | func (b *SQLBuilder) Join(j ...join) { 146 | b.w.AddIndent() 147 | defer b.w.SubIndent() 148 | 149 | for _, v := range j { 150 | b.w.WriteString(string(v.Type)) 151 | b.w.WriteString(` JOIN `) 152 | b.w.WriteString(b.SourceToSQL(v.New)) 153 | 154 | if len(v.Conditions) > 0 { 155 | b.w.WriteString(` ON (`) 156 | b.Conditions(v.Conditions, false) 157 | b.w.WriteString(`)`) 158 | } 159 | 160 | b.w.WriteLine(``) 161 | } 162 | } 163 | 164 | // Where generates SQL WHERE/AND lines 165 | func (b *SQLBuilder) Where(c ...Condition) { 166 | if len(c) == 0 { 167 | return 168 | } 169 | b.w.WriteString(`WHERE `) 170 | b.Conditions(c, true) 171 | } 172 | 173 | // GroupBy generates a SQL GROUP BY line 174 | func (b *SQLBuilder) GroupBy(f ...Field) { 175 | if len(f) == 0 { 176 | return 177 | } 178 | b.w.WriteLine(`GROUP BY ` + b.List(f, false)) 179 | } 180 | 181 | // Having generates a SQL HAVING line 182 | func (b *SQLBuilder) Having(c ...Condition) { 183 | if len(c) == 0 { 184 | return 185 | } 186 | b.w.WriteString(`HAVING `) 187 | b.Conditions(c, true) 188 | } 189 | 190 | // OrderBy generates a SQL ORDER BY line 191 | func (b *SQLBuilder) OrderBy(o ...FieldOrder) { 192 | if len(o) == 0 { 193 | return 194 | } 195 | s := `ORDER BY ` 196 | for k, v := range o { 197 | if k > 0 { 198 | s += COMMA 199 | } 200 | s += b.FieldToSQL(v.Field) + ` ` + v.Order 201 | } 202 | b.w.WriteLine(s) 203 | } 204 | 205 | // LimitOffset generates a SQL LIMIT and OFFSET line 206 | func (b *SQLBuilder) LimitOffset(l, o int) { 207 | if l == 0 && o == 0 { 208 | return 209 | } 210 | b.Context.Driver.LimitOffset(&b.w, l, o) 211 | } 212 | 213 | // Update generates a SQL UPDATE line 214 | func (b *SQLBuilder) Update(t *Table) { 215 | _ = t.Name 216 | b.w.WriteLine(`UPDATE ` + b.SourceToSQL(t)) 217 | } 218 | 219 | // Set generates a SQL SET line 220 | func (b *SQLBuilder) Set(sets []set) { 221 | if len(sets) == 0 { 222 | return 223 | } 224 | if len(sets) > 1 { 225 | b.w.WriteLine(`SET`) 226 | b.w.AddIndent() 227 | defer b.w.SubIndent() 228 | } else { 229 | b.w.WriteString(`SET `) 230 | } 231 | 232 | cField := *b.Context 233 | cField.alias = NoAlias() 234 | 235 | for k, v := range sets { 236 | comma := `,` 237 | if k == len(sets)-1 { 238 | comma = `` 239 | } 240 | b.w.WriteLine(v.Field.QueryString(&cField) + ` = ` + v.Value.QueryString(b.Context) + comma) 241 | } 242 | } 243 | 244 | // Delete generates a SQL DELETE FROM line 245 | func (b *SQLBuilder) Delete(t *Table) { 246 | b.w.WriteLine(`DELETE FROM ` + b.SourceToSQL(t)) 247 | } 248 | 249 | // Insert generates a SQL INSERT line 250 | func (b *SQLBuilder) Insert(t *Table, f []Field) { 251 | _ = t.Name 252 | s := `` 253 | for k, v := range f { 254 | if k > 0 { 255 | s += COMMA 256 | } 257 | s += b.FieldToSQL(v) 258 | } 259 | b.w.WriteLine(`INSERT INTO ` + b.SourceToSQL(t) + ` (` + s + `)`) 260 | } 261 | 262 | // Values generates a SQL VALUES line 263 | func (b *SQLBuilder) Values(f [][]Field) { 264 | if len(f) > 1 { 265 | b.w.WriteLine(`VALUES`) 266 | b.w.AddIndent() 267 | defer b.w.SubIndent() 268 | } else { 269 | b.w.WriteString(`VALUES `) 270 | } 271 | 272 | for k, v := range f { 273 | b.valueLine(v, k != len(f)-1) 274 | } 275 | } 276 | 277 | func (b *SQLBuilder) valueLine(f []Field, addComma bool) { 278 | comma := `,` 279 | if !addComma { 280 | comma = `` 281 | } 282 | 283 | s := strings.Builder{} 284 | for k, v := range f { 285 | if k > 0 { 286 | s.WriteString(COMMA) 287 | } 288 | s.WriteString(b.FieldToSQL(v)) 289 | } 290 | b.w.WriteLine(`(` + s.String() + `)` + comma) 291 | } 292 | -------------------------------------------------------------------------------- /sql_test.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import ( 4 | "testing" 5 | 6 | "git.ultraware.nl/Ultraware/qb/v2/internal/testutil" 7 | ) 8 | 9 | func BenchmarkSQLWrite(b *testing.B) { 10 | w := sqlWriter{} 11 | 12 | for i := 0; i < b.N; i++ { 13 | w.WriteLine("Test") 14 | } 15 | } 16 | 17 | func TestSQLWriter(t *testing.T) { 18 | w := sqlWriter{} 19 | 20 | w.WriteLine("0\n0") 21 | w.AddIndent() 22 | w.WriteString(`1`) 23 | w.WriteString("-\n1") 24 | w.AddIndent() 25 | w.WriteLine(`-`) 26 | w.WriteString(`2`) 27 | w.SubIndent() 28 | w.SubIndent() 29 | w.WriteLine(`-`) 30 | w.WriteString(`0`) 31 | 32 | testutil.Compare(t, "0\n0\n\t1-\n\t1-\n\t\t2-\n0", w.String()) 33 | } 34 | 35 | func newCheckOutput(t *testing.T, b *SQLBuilder) func(bool, string) { 36 | return func(newline bool, expected string) { 37 | out := b.w.String() 38 | b.w = sqlWriter{} 39 | 40 | if newline { 41 | expected += "\n" 42 | } 43 | 44 | testutil.Compare(t, expected, out) 45 | } 46 | } 47 | 48 | func info(t *testing.T, msg string) { 49 | t.Log(testutil.Notice(msg)) 50 | } 51 | 52 | func testBuilder(t *testing.T, alias bool) (*SQLBuilder, func(bool, string)) { 53 | b := &SQLBuilder{sqlWriter{}, NewContext(nil, NoAlias())} 54 | if alias { 55 | b.Context.alias = AliasGenerator() 56 | } 57 | return b, newCheckOutput(t, b) 58 | } 59 | 60 | // Tables 61 | 62 | var ( 63 | testTable = &Table{Name: `tmp`} 64 | testFieldA = &TableField{Name: `colA`, Parent: testTable} 65 | testFieldB = &TableField{Name: `colB`, Parent: testTable} 66 | ) 67 | 68 | var ( 69 | testTable2 = &Table{Name: `tmp2`} 70 | testFieldA2 = &TableField{Name: `colA2`, Parent: testTable2} 71 | ) 72 | 73 | func TestFrom(t *testing.T) { 74 | info(t, `-- Testing without alias`) 75 | b, check := testBuilder(t, false) 76 | 77 | b.From(testTable) 78 | check(true, `FROM tmp`) 79 | 80 | info(t, `-- Testing with alias`) 81 | b, check = testBuilder(t, true) 82 | 83 | b.From(testTable) 84 | check(true, `FROM tmp AS t`) 85 | } 86 | 87 | func TestDelete(t *testing.T) { 88 | b, check := testBuilder(t, false) 89 | 90 | b.Delete(testTable) 91 | check(true, `DELETE FROM tmp`) 92 | } 93 | 94 | func TestUpdate(t *testing.T) { 95 | b, check := testBuilder(t, false) 96 | 97 | b.Update(testTable) 98 | check(true, `UPDATE tmp`) 99 | } 100 | 101 | func TestInsert(t *testing.T) { 102 | b, check := testBuilder(t, false) 103 | 104 | b.Insert(testTable, nil) 105 | check(true, `INSERT INTO tmp ()`) 106 | 107 | b.Insert(testTable, []Field{testFieldA, testFieldB}) 108 | check(true, `INSERT INTO tmp (colA, colB)`) 109 | } 110 | 111 | func TestJoin(t *testing.T) { 112 | b, check := testBuilder(t, true) 113 | 114 | b.From(testTable) 115 | b.w = sqlWriter{} 116 | 117 | b.Join(join{JoinInner, testTable2, nil}) 118 | check(true, 119 | "\t"+`INNER JOIN tmp2 AS t2`, 120 | ) 121 | 122 | b.Join(join{JoinInner, testTable2, []Condition{eq(testFieldA, testFieldA2)}}) 123 | check(true, 124 | "\t"+`INNER JOIN tmp2 AS t2 ON (t.colA = t2.colA2)`, 125 | ) 126 | 127 | b.Join(join{JoinLeft, testTable2, []Condition{eq(testFieldA, testFieldA2), testCondition, testCondition2}}) 128 | check(true, 129 | "\t"+`LEFT JOIN tmp2 AS t2 ON (t.colA = t2.colA2 AND a AND b)`, 130 | ) 131 | 132 | b.Join( 133 | join{JoinInner, testTable2, []Condition{eq(testFieldA, testFieldA2)}}, 134 | join{JoinLeft, testTable2, []Condition{eq(testFieldA, testFieldA2), testCondition, testCondition2}}, 135 | ) 136 | check(true, 137 | "\t"+`INNER JOIN tmp2 AS t2 ON (t.colA = t2.colA2)`+"\n\t"+ 138 | `LEFT JOIN tmp2 AS t2 ON (t.colA = t2.colA2 AND a AND b)`, 139 | ) 140 | } 141 | 142 | // Fields 143 | 144 | func TestSelect(t *testing.T) { 145 | f1 := testFieldA 146 | f2 := testFieldB 147 | 148 | info(t, `-- Testing without alias`) 149 | b, check := testBuilder(t, false) 150 | 151 | b.Select(false, f1, f2) 152 | check(true, `SELECT colA, colB`) 153 | 154 | b.Select(true, f1, f2) 155 | check(true, `SELECT colA colA0, colB colB1`) 156 | 157 | info(t, `-- Testing with alias`) 158 | b, check = testBuilder(t, true) 159 | 160 | b.Select(false, f1, f2) 161 | check(true, `SELECT t.colA, t.colB`) 162 | 163 | b.Select(true, f1, f2) 164 | check(true, `SELECT t.colA colA0, t.colB colB1`) 165 | } 166 | 167 | func TestSet(t *testing.T) { 168 | b, check := testBuilder(t, false) 169 | 170 | b.Set([]set{}) 171 | check(false, ``) 172 | 173 | b.Set([]set{{testFieldA, Value(1)}}) 174 | check(true, `SET colA = ?`) 175 | 176 | b.Set([]set{{testFieldA, Value(1)}, {testFieldB, Value(3)}}) 177 | check(true, "SET\n\tcolA = ?,\n\tcolB = ?") 178 | } 179 | 180 | // Conditions 181 | 182 | var testCondition = func(_ *Context) string { 183 | return `a` 184 | } 185 | 186 | var testCondition2 = func(_ *Context) string { 187 | return `b` 188 | } 189 | 190 | func TestWhere(t *testing.T) { 191 | b, check := testBuilder(t, false) 192 | 193 | b.Where(testCondition, testCondition2) 194 | check(true, `WHERE a`+"\n\t"+`AND b`) 195 | 196 | b.Where() 197 | check(false, ``) 198 | } 199 | 200 | func TestHaving(t *testing.T) { 201 | b, check := testBuilder(t, false) 202 | 203 | b.Having(testCondition, testCondition2) 204 | check(true, `HAVING a`+"\n\t"+`AND b`) 205 | 206 | b.Having() 207 | check(false, ``) 208 | } 209 | 210 | // Other 211 | 212 | func TestGroupBy(t *testing.T) { 213 | b, check := testBuilder(t, false) 214 | 215 | b.GroupBy(testFieldA, testFieldB) 216 | check(true, `GROUP BY colA, colB`) 217 | 218 | b.GroupBy() 219 | check(false, ``) 220 | } 221 | 222 | func TestOrderBy(t *testing.T) { 223 | b, check := testBuilder(t, false) 224 | 225 | b.OrderBy(Asc(testFieldA), Desc(testFieldB)) 226 | check(true, `ORDER BY colA ASC, colB DESC`) 227 | 228 | b.OrderBy() 229 | check(false, ``) 230 | } 231 | 232 | func TestValues(t *testing.T) { 233 | b, check := testBuilder(t, false) 234 | 235 | line := []Field{Value(1), Value(2), Value(3)} 236 | 237 | b.Values([][]Field{line}) 238 | check(true, `VALUES (?, ?, ?)`) 239 | 240 | b.Values([][]Field{line, line}) 241 | check(true, `VALUES`+"\n\t"+ 242 | `(?, ?, ?),`+"\n\t"+ 243 | `(?, ?, ?)`, 244 | ) 245 | } 246 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // Driver implements databse-specific features 9 | type Driver interface { 10 | ValueString(int) string 11 | BoolString(bool) string 12 | EscapeCharacter() string 13 | UpsertSQL(*Table, []Field, Query) (string, []interface{}) 14 | IgnoreConflictSQL(*Table, []Field) (string, []interface{}) 15 | LimitOffset(SQL, int, int) 16 | Returning(SQLBuilder, Query, []Field) (string, []interface{}) 17 | LateralJoin(*Context, *SubQuery) string 18 | TypeName(DataType) string 19 | Override() OverrideMap 20 | } 21 | 22 | // Query generates SQL 23 | type Query interface { 24 | SQL(b SQLBuilder) (string, []interface{}) 25 | } 26 | 27 | // Alias generates table aliasses. 28 | // This type is not intended to be used directly 29 | type Alias interface { 30 | Get(Source) string 31 | } 32 | 33 | /// 34 | /// Source 35 | /// 36 | 37 | // Source represents a table or a subquery 38 | type Source interface { 39 | TableString(*Context) string 40 | aliasString() string 41 | } 42 | 43 | // LateralJoinSource represents a joinable lateral derived table or function 44 | type LateralJoinSource interface { 45 | lateralJoinSource() Source 46 | } 47 | 48 | // Table represents a table in the database. 49 | // This type is used by qb-generator's generated code and is not intended to be used manually 50 | type Table struct { 51 | Name string 52 | Alias string 53 | Escape bool 54 | } 55 | 56 | // TableString implements Source 57 | func (t *Table) TableString(c *Context) string { 58 | alias := c.Alias(t) 59 | if len(alias) > 0 { 60 | alias = ` AS ` + alias 61 | } 62 | name := t.Name 63 | if t.Escape { 64 | ec := c.Driver.EscapeCharacter() 65 | name = ec + name + ec 66 | } 67 | return name + alias 68 | } 69 | 70 | func (t *Table) aliasString() string { 71 | if t.Alias != `` { 72 | return t.Alias 73 | } 74 | parts := strings.Split(t.Name, `.`) 75 | return strings.ToLower(parts[len(parts)-1][0:1]) 76 | } 77 | 78 | // Select starts a SELECT query 79 | func (t *Table) Select(f []Field) *SelectBuilder { 80 | return NewSelectBuilder(f, t) 81 | } 82 | 83 | // Delete starts a DELETE query 84 | func (t *Table) Delete(c1 Condition, c ...Condition) Query { 85 | return DeleteBuilder{t, append(c, c1)} 86 | } 87 | 88 | // Update starts an UPDATE query 89 | func (t *Table) Update() *UpdateBuilder { 90 | return &UpdateBuilder{t, nil, nil} 91 | } 92 | 93 | // Insert starts an INSERT query 94 | func (t *Table) Insert(f []Field) *InsertBuilder { 95 | q := InsertBuilder{table: t, fields: f} 96 | return &q 97 | } 98 | 99 | // CTE is a type of subqueries 100 | type CTE struct { 101 | query SelectQuery 102 | F []Field 103 | } 104 | 105 | func newCTE(q SelectQuery, fields []*Field) *CTE { 106 | cte := &CTE{query: q} 107 | 108 | assignFields(&cte.F, cte, q, fields) 109 | 110 | return cte 111 | } 112 | 113 | func (cte *CTE) aliasString() string { 114 | return `ct` 115 | } 116 | 117 | // TableString implements Source 118 | func (cte *CTE) TableString(c *Context) string { 119 | alias := c.Alias(cte) 120 | if len(alias) > 0 { 121 | alias = ` ` + alias 122 | } 123 | 124 | return c.cteName(cte) + alias 125 | } 126 | 127 | // With generates the SQL for a WITH statement. 128 | // This function is not intended to be called directly 129 | func (cte *CTE) With(b SQLBuilder) string { 130 | s, _ := cte.query.getSQL(b, true) 131 | return b.Context.cteName(cte) + ` AS ` + getSubQuerySQL(s) 132 | } 133 | 134 | // Select starts a SELECT query 135 | func (cte *CTE) Select(f ...Field) *SelectBuilder { 136 | return NewSelectBuilder(f, cte) 137 | } 138 | 139 | // SubQuery represents a subquery 140 | type SubQuery struct { 141 | query SelectQuery 142 | F []Field 143 | } 144 | 145 | func newSubQuery(q SelectQuery, fields []*Field) *SubQuery { 146 | sq := &SubQuery{query: q} 147 | 148 | assignFields(&sq.F, sq, q, fields) 149 | 150 | return sq 151 | } 152 | 153 | func assignFields(dest *[]Field, parent Source, q SelectQuery, fields []*Field) { 154 | if fields != nil && len(q.Fields()) != len(fields) { 155 | panic(`Field count in CTE/SubQuery doesn't match`) 156 | } 157 | 158 | for k, f := range q.Fields() { 159 | f2 := &TableField{Name: getFieldName(f) + strconv.Itoa(k), Parent: parent} 160 | *dest = append(*dest, f2) 161 | 162 | if fields != nil { 163 | *fields[k] = f2 164 | } 165 | } 166 | } 167 | 168 | // TableString implements Source 169 | func (s *SubQuery) TableString(c *Context) string { 170 | alias := c.Alias(s) 171 | if len(alias) > 0 { 172 | alias = ` ` + alias 173 | } 174 | 175 | sql, v := s.query.getSQL(SQLBuilder{Context: c.clone(c.alias)}, true) 176 | c.Add(v...) 177 | 178 | return getSubQuerySQL(sql) + alias 179 | } 180 | 181 | // Lateral returns a LATERAL subquery 182 | func (s *SubQuery) Lateral() LateralSubQuery { 183 | return LateralSubQuery{s} 184 | } 185 | 186 | func getSubQuerySQL(sql string) string { 187 | return `(` + NEWLINE + INDENT + strings.ReplaceAll(strings.TrimSuffix(sql, "\n"), "\n", "\n"+INDENT) + NEWLINE + `)` 188 | } 189 | 190 | func (s *SubQuery) aliasString() string { 191 | return `sq` 192 | } 193 | 194 | // Select starts a SELECT query 195 | func (s *SubQuery) Select(f ...Field) *SelectBuilder { 196 | return NewSelectBuilder(f, s) 197 | } 198 | 199 | /// 200 | /// Field 201 | /// 202 | 203 | // Field represents a field in a query 204 | type Field interface { 205 | QueryString(*Context) string 206 | } 207 | 208 | func getFieldName(f Field) string { 209 | name := `f` 210 | if tf, ok := f.(*TableField); ok && tf.Name != `` { 211 | name = tf.Name 212 | } 213 | return name 214 | } 215 | 216 | // TableField represents a field in a table. 217 | // This type is used by qb-generator's generated code and is not intended to be used manually 218 | type TableField struct { 219 | Parent Source 220 | Name string 221 | Escape bool 222 | ReadOnly bool 223 | Nullable bool 224 | Type DataType 225 | Size int 226 | } 227 | 228 | // QueryString implements Field 229 | func (f TableField) QueryString(c *Context) string { 230 | alias := c.Alias(f.Parent) 231 | if alias != `` { 232 | alias += `.` 233 | } 234 | name := f.Name 235 | if f.Escape { 236 | ec := c.Driver.EscapeCharacter() 237 | name = ec + name + ec 238 | } 239 | return alias + name 240 | } 241 | 242 | // Copy creates a new instance of the field with a different Parent 243 | func (f TableField) Copy(src Source) *TableField { 244 | f.Parent = src 245 | return &f 246 | } 247 | 248 | func getParent(f Field) Source { 249 | if v, ok := f.(*TableField); ok { 250 | return v.Parent 251 | } 252 | panic(`Invalid use of a non-TableField field`) 253 | } 254 | 255 | type valueField struct { 256 | Value interface{} 257 | } 258 | 259 | func (f valueField) QueryString(c *Context) string { 260 | c.Add(f.Value) 261 | return VALUE 262 | } 263 | 264 | // Value creats a new Field 265 | func Value(v interface{}) Field { 266 | return valueField{v} 267 | } 268 | 269 | type subqueryField struct { 270 | sq SelectQuery 271 | } 272 | 273 | func (f subqueryField) QueryString(c *Context) string { 274 | sql, _ := f.sq.SQL(SQLBuilder{Context: c, w: sqlWriter{indent: 1}}) 275 | return `(` + strings.TrimSpace(sql) + `)` 276 | } 277 | 278 | /// 279 | /// Query types 280 | /// 281 | 282 | // Condition is used in the Where function 283 | type Condition func(c *Context) string 284 | 285 | // FieldOrder specifies the order in which fields should be sorted 286 | type FieldOrder struct { 287 | Field Field 288 | Order string 289 | } 290 | 291 | // Asc is used to sort in ascending order 292 | func Asc(f Field) FieldOrder { 293 | return FieldOrder{Field: f, Order: `ASC`} 294 | } 295 | 296 | // Desc is used to sort in descending order 297 | func Desc(f Field) FieldOrder { 298 | return FieldOrder{Field: f, Order: `DESC`} 299 | } 300 | 301 | // Context contains all the data needed to build parts of a query. 302 | // This type is not intended to be used directly 303 | type Context struct { 304 | Driver Driver 305 | alias Alias 306 | Values *[]interface{} 307 | cteNames map[*CTE]string 308 | cteCount *int 309 | CTEs *[]*CTE 310 | } 311 | 312 | func (c *Context) cteName(cte *CTE) string { 313 | if v, ok := c.cteNames[cte]; ok { 314 | return v 315 | } 316 | 317 | *c.CTEs = append(*c.CTEs, cte) 318 | *c.cteCount++ 319 | 320 | c.cteNames[cte] = `cte` + strconv.Itoa(*c.cteCount) 321 | return c.cteNames[cte] 322 | } 323 | 324 | // Add adds a value to Values 325 | func (c *Context) Add(v ...interface{}) { 326 | *c.Values = append(*c.Values, v...) 327 | } 328 | 329 | // Alias returns an alias for the given Source 330 | func (c *Context) Alias(src Source) string { 331 | return c.alias.Get(src) 332 | } 333 | 334 | func (c *Context) clone(alias Alias) *Context { 335 | nc := *c 336 | nc.alias = alias 337 | 338 | var values []interface{} 339 | nc.Values = &values 340 | 341 | return &nc 342 | } 343 | 344 | // NewContext returns a new *Context 345 | func NewContext(d Driver, a Alias) *Context { 346 | values, count, ctes := []interface{}{}, 0, []*CTE{} 347 | return &Context{d, a, &values, make(map[*CTE]string), &count, &ctes} 348 | } 349 | 350 | // LateralSubQuery adds LATERAL keyword to the SubQuery 351 | type LateralSubQuery struct { 352 | sq *SubQuery 353 | } 354 | 355 | func (ls LateralSubQuery) lateralJoinSource() Source { 356 | return &lateralSubQueryJoin{ls.sq} 357 | } 358 | 359 | type lateralSubQueryJoin struct { 360 | sq *SubQuery 361 | } 362 | 363 | // TableString implements Source 364 | func (t *lateralSubQueryJoin) TableString(c *Context) string { 365 | return c.Driver.LateralJoin(c, t.sq) 366 | } 367 | 368 | func (t *lateralSubQueryJoin) aliasString() string { 369 | return t.sq.aliasString() 370 | } 371 | -------------------------------------------------------------------------------- /update.go: -------------------------------------------------------------------------------- 1 | package qb 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | type set struct { 9 | Field Field 10 | Value Field 11 | } 12 | 13 | func newSet(f Field, v interface{}) set { 14 | f1, ok := f.(*TableField) 15 | if !ok { 16 | panic(`Cannot use non-table field in update`) 17 | } 18 | if f1.ReadOnly { 19 | panic(`Cannot update read-only field`) 20 | } 21 | f2 := MakeField(v) 22 | return set{f1, f2} 23 | } 24 | 25 | // UpdateBuilder builds an UPDATE query 26 | type UpdateBuilder struct { 27 | t *Table 28 | c []Condition 29 | set []set 30 | } 31 | 32 | // Set adds an update to the SET clause 33 | func (q *UpdateBuilder) Set(f Field, v interface{}) *UpdateBuilder { 34 | q.set = append(q.set, newSet(f, v)) 35 | return q 36 | } 37 | 38 | // Where adds conditions to the WHERE clause 39 | func (q *UpdateBuilder) Where(c ...Condition) *UpdateBuilder { 40 | q.c = append(q.c, c...) 41 | return q 42 | } 43 | 44 | // SQL returns a query string and a list of values 45 | func (q *UpdateBuilder) SQL(b SQLBuilder) (string, []interface{}) { 46 | if reflect.TypeOf(b.Context.alias) != reflect.TypeOf(NoAlias()) && strings.HasSuffix(reflect.TypeOf(b.Context.Driver).PkgPath(), `msqb`) { 47 | b.w.WriteLine(`UPDATE ` + q.t.aliasString()) 48 | b.Set(q.set) 49 | b.w.WriteLine(`FROM ` + b.SourceToSQL(q.t)) 50 | } else { 51 | b.Update(q.t) 52 | b.Set(q.set) 53 | } 54 | b.Where(q.c...) 55 | 56 | return b.w.String(), *b.Context.Values 57 | } 58 | --------------------------------------------------------------------------------