├── .circleci └── config.yml ├── .dockerignore ├── .github └── dependabot.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── builder ├── builder.go ├── builder_test.go ├── delete.go ├── delete_test.go ├── doc.go ├── insert.go ├── insert_test.go ├── select.go ├── select_test.go ├── update.go └── update_test.go ├── doc.go ├── docs └── images │ ├── LICENSE │ ├── banner.png │ ├── banner.svg │ ├── loukoum.png │ ├── loukoum.svg │ ├── square.jpg │ ├── square.png │ └── square.svg ├── examples ├── bootstrap.sql ├── go.mod ├── go.sum ├── named │ ├── comment.go │ ├── main.go │ ├── news.go │ └── user.go └── standard │ ├── comment.go │ ├── main.go │ ├── news.go │ └── user.go ├── format ├── doc.go └── format.go ├── go.mod ├── go.sum ├── lexer ├── iteratee.go ├── lexer.go └── lexer_test.go ├── loukoum.go ├── parser ├── join.go └── join_test.go ├── scripts ├── conf │ └── go │ │ └── Dockerfile ├── go-wrapper ├── lint └── test ├── stmt ├── aggregate.go ├── between.go ├── column.go ├── comment.go ├── conflict.go ├── delete.go ├── distinct_on.go ├── doc.go ├── encoder.go ├── expression.go ├── expression_test.go ├── from.go ├── groupby.go ├── having.go ├── in.go ├── infix.go ├── insert.go ├── into.go ├── join.go ├── limit.go ├── offset.go ├── on.go ├── operator.go ├── order.go ├── orderby.go ├── prefix.go ├── returning.go ├── select.go ├── set.go ├── stmt.go ├── subquery.go ├── suffix.go ├── table.go ├── update.go ├── using.go ├── values.go ├── where.go └── with.go ├── token └── token.go └── types ├── context.go ├── doc.go ├── join.go ├── kv.go ├── operator.go └── order.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | machine: 6 | image: circleci/classic:edge 7 | docker_layer_caching: true 8 | steps: 9 | - checkout 10 | - run: 11 | name: Checkout submodules 12 | command: | 13 | git submodule sync 14 | git submodule update --init 15 | 16 | - run: 17 | name: Run tests 18 | command: scripts/go-wrapper scripts/test 19 | no_output_timeout: 10m 20 | 21 | - run: 22 | name: Run linters 23 | command: scripts/go-wrapper scripts/lint 24 | no_output_timeout: 10m 25 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Circle CI directory 2 | .circleci 3 | 4 | # Documentation directory 5 | docs 6 | 7 | # Example directory 8 | examples 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | commit-message: 9 | prefix: "chore(go.mod):" 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Test directory 11 | /testdata/ 12 | 13 | # Vendor directory 14 | /vendor/ 15 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | concurrency: 4 3 | deadline: 1m 4 | issues-exit-code: 1 5 | tests: true 6 | 7 | 8 | output: 9 | format: colored-line-number 10 | print-issued-lines: true 11 | print-linter-name: true 12 | 13 | 14 | linters-settings: 15 | errcheck: 16 | check-type-assertions: false 17 | check-blank: false 18 | govet: 19 | check-shadowing: false 20 | use-installed-packages: false 21 | golint: 22 | min-confidence: 0.8 23 | gofmt: 24 | simplify: true 25 | gocyclo: 26 | min-complexity: 10 27 | maligned: 28 | suggest-new: true 29 | dupl: 30 | threshold: 80 31 | goconst: 32 | min-len: 3 33 | min-occurrences: 3 34 | misspell: 35 | locale: US 36 | lll: 37 | line-length: 120 38 | unused: 39 | check-exported: false 40 | unparam: 41 | algo: cha 42 | check-exported: false 43 | nakedret: 44 | max-func-lines: 30 45 | 46 | linters: 47 | enable: 48 | - megacheck 49 | - govet 50 | - errcheck 51 | - gas 52 | - structcheck 53 | - varcheck 54 | - ineffassign 55 | - deadcode 56 | - typecheck 57 | - golint 58 | - interfacer 59 | - unconvert 60 | - gocyclo 61 | - gofmt 62 | - misspell 63 | - lll 64 | - nakedret 65 | enable-all: false 66 | disable: 67 | - depguard 68 | - prealloc 69 | - dupl 70 | - maligned 71 | disable-all: false 72 | 73 | 74 | issues: 75 | exclude-use-default: false 76 | max-per-linter: 1024 77 | max-same: 1024 78 | exclude: 79 | - "G304" 80 | - "G101" 81 | - "G104" 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Ulule 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | scripts/test 3 | 4 | lint: 5 | scripts/lint 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loukoum 2 | 3 | [![CircleCI][circle-img]][circle-url] 4 | [![Documentation][godoc-img]][godoc-url] 5 | ![License][license-img] 6 | 7 | _A simple SQL Query Builder._ 8 | 9 | [![Loukoum][loukoum-img]][loukoum-url] 10 | 11 | ## Introduction 12 | 13 | Loukoum is a simple SQL Query Builder, only **PostgreSQL** is supported at the moment. 14 | 15 | If you have to generate complex queries, which rely on various contexts, **loukoum** is the right tool for you. 16 | 17 | Afraid to slip a tiny **SQL injection** manipulating `fmt` to append conditions? **Fear no more**, loukoum is here to protect you against yourself. 18 | 19 | Just a few examples when and where loukoum can become handy: 20 | 21 | - Remove user anonymity if user is an admin 22 | - Display news draft for an author 23 | - Add filters in query based on request parameters 24 | - Add a `ON CONFLICT` clause for resource's owner 25 | - And so on... 26 | 27 | ## Installation 28 | 29 | Using [Go Modules](https://github.com/golang/go/wiki/Modules) 30 | 31 | ```console 32 | go get github.com/ulule/loukoum/v3@v3.3.0 33 | ``` 34 | 35 | ## Usage 36 | 37 | Loukoum helps you generate SQL queries from composable parts. 38 | 39 | However, keep in mind it's not an ORM or a Mapper so you have to use a SQL connector 40 | ([database/sql][sql-url], [sqlx][sqlx-url], [makroud][makroud-url], etc.) to execute queries. 41 | 42 | ### INSERT 43 | 44 | Insert a new `Comment` and retrieve its `id`. 45 | 46 | ```go 47 | import lk "github.com/ulule/loukoum/v3" 48 | 49 | // Comment model 50 | type Comment struct { 51 | ID int64 52 | Email string `db:"email"` 53 | Status string `db:"status"` 54 | Message string `db:"message"` 55 | UserID int64 `db:"user_id"` 56 | CreatedAt pq.NullTime `db:"created_at"` 57 | DeletedAt pq.NullTime `db:"deleted_at"` 58 | } 59 | 60 | // CreateComment creates a comment. 61 | func CreateComment(db *sqlx.DB, comment Comment) (Comment, error) { 62 | builder := lk.Insert("comments"). 63 | Set( 64 | lk.Pair("email", comment.Email), 65 | lk.Pair("status", "waiting"), 66 | lk.Pair("message", comment.Message), 67 | lk.Pair("created_at", lk.Raw("NOW()")), 68 | ). 69 | Returning("id") 70 | 71 | // query: INSERT INTO comments (created_at, email, message, status, user_id) 72 | // VALUES (NOW(), :arg_1, :arg_2, :arg_3, :arg_4) RETURNING id 73 | // args: map[string]interface{}{ 74 | // "arg_1": string(comment.Email), 75 | // "arg_2": string(comment.Message), 76 | // "arg_3": string("waiting"), 77 | // "arg_4": string(comment.UserID), 78 | // } 79 | query, args := builder.NamedQuery() 80 | 81 | stmt, err := db.PrepareNamed(query) 82 | if err != nil { 83 | return comment, err 84 | } 85 | defer stmt.Close() 86 | 87 | err = stmt.Get(&comment, args) 88 | if err != nil { 89 | return comment, err 90 | } 91 | 92 | return comment, nil 93 | } 94 | ``` 95 | 96 | ### INSERT on conflict (UPSERT) 97 | 98 | ```go 99 | import lk "github.com/ulule/loukoum/v3" 100 | 101 | // UpsertComment inserts or updates a comment based on the email attribute. 102 | func UpsertComment(db *sqlx.DB, comment Comment) (Comment, error) { 103 | builder := lk.Insert("comments"). 104 | Set( 105 | lk.Pair("email", comment.Email), 106 | lk.Pair("status", "waiting"), 107 | lk.Pair("message", comment.Message), 108 | lk.Pair("user_id", comment.UserID), 109 | lk.Pair("created_at", lk.Raw("NOW()")), 110 | ). 111 | OnConflict("email", lk.DoUpdate( 112 | lk.Pair("message", comment.Message), 113 | lk.Pair("user_id", comment.UserID), 114 | lk.Pair("status", "waiting"), 115 | lk.Pair("created_at", lk.Raw("NOW()")), 116 | lk.Pair("deleted_at", nil), 117 | )). 118 | Returning("id, created_at") 119 | 120 | // query: INSERT INTO comments (created_at, email, message, status, user_id) 121 | // VALUES (NOW(), :arg_1, :arg_2, :arg_3, :arg_4) 122 | // ON CONFLICT (email) DO UPDATE SET created_at = NOW(), deleted_at = NULL, message = :arg_5, 123 | // status = :arg_6, user_id = :arg_7 RETURNING id, created_at 124 | // args: map[string]interface{}{ 125 | // "arg_1": string(comment.Email), 126 | // "arg_2": string(comment.Message), 127 | // "arg_3": string("waiting"), 128 | // "arg_4": string(comment.UserID), 129 | // "arg_5": string(comment.Message), 130 | // "arg_6": string("waiting"), 131 | // "arg_7": string(comment.UserID), 132 | // } 133 | query, args := builder.NamedQuery() 134 | 135 | stmt, err := db.PrepareNamed(query) 136 | if err != nil { 137 | return comment, err 138 | } 139 | defer stmt.Close() 140 | 141 | err = stmt.Get(&comment, args) 142 | if err != nil { 143 | return comment, err 144 | } 145 | 146 | return comment, nil 147 | } 148 | ``` 149 | 150 | ### UPDATE 151 | 152 | Publish a `News` by updating its status and publication date. 153 | 154 | ```go 155 | // News model 156 | type News struct { 157 | ID int64 158 | Status string `db:"status"` 159 | PublishedAt pq.NullTime `db:"published_at"` 160 | DeletedAt pq.NullTime `db:"deleted_at"` 161 | } 162 | 163 | // PublishNews publishes a news. 164 | func PublishNews(db *sqlx.DB, news News) (News, error) { 165 | builder := lk.Update("news"). 166 | Set( 167 | lk.Pair("published_at", lk.Raw("NOW()")), 168 | lk.Pair("status", "published"), 169 | ). 170 | Where(lk.Condition("id").Equal(news.ID)). 171 | And(lk.Condition("deleted_at").IsNull(true)). 172 | Returning("published_at") 173 | 174 | // query: UPDATE news SET published_at = NOW(), status = :arg_1 WHERE ((id = :arg_2) AND (deleted_at IS NULL)) 175 | // RETURNING published_at 176 | // args: map[string]interface{}{ 177 | // "arg_1": string("published"), 178 | // "arg_2": int64(news.ID), 179 | // } 180 | query, args := builder.NamedQuery() 181 | 182 | stmt, err := db.PrepareNamed(query) 183 | if err != nil { 184 | return news, err 185 | } 186 | defer stmt.Close() 187 | 188 | err = stmt.Get(&news, args) 189 | if err != nil { 190 | return news, err 191 | } 192 | 193 | return news, nil 194 | } 195 | ``` 196 | 197 | ### SELECT 198 | 199 | #### Basic SELECT with an unique condition 200 | 201 | Retrieve non-deleted users. 202 | 203 | ```go 204 | import lk "github.com/ulule/loukoum/v3" 205 | 206 | // User model 207 | type User struct { 208 | ID int64 209 | 210 | FirstName string `db:"first_name"` 211 | LastName string `db:"last_name"` 212 | Email string 213 | IsStaff bool `db:"is_staff"` 214 | DeletedAt pq.NullTime `db:"deleted_at"` 215 | } 216 | 217 | // FindUsers retrieves non-deleted users 218 | func FindUsers(db *sqlx.DB) ([]User, error) { 219 | builder := lk.Select("id", "first_name", "last_name", "email"). 220 | From("users"). 221 | Where(lk.Condition("deleted_at").IsNull(true)) 222 | 223 | // query: SELECT id, first_name, last_name, email FROM users WHERE (deleted_at IS NULL) 224 | // args: map[string]interface{}{ 225 | // 226 | // } 227 | query, args := builder.NamedQuery() 228 | 229 | stmt, err := db.PrepareNamed(query) 230 | if err != nil { 231 | return nil, err 232 | } 233 | defer stmt.Close() 234 | 235 | users := []User{} 236 | 237 | err = stmt.Select(&users, args) 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | return users, nil 243 | } 244 | ``` 245 | 246 | #### SELECT IN with subquery 247 | 248 | Retrieve comments only sent by staff users, the staff users query will be a subquery 249 | as we don't want to use any JOIN operations. 250 | 251 | ```go 252 | // FindStaffComments retrieves comments by staff users. 253 | func FindStaffComments(db *sqlx.DB, comment Comment) ([]Comment, error) { 254 | builder := lk.Select("id", "email", "status", "user_id", "message", "created_at"). 255 | From("comments"). 256 | Where(lk.Condition("deleted_at").IsNull(true)). 257 | Where( 258 | lk.Condition("user_id").In( 259 | lk.Select("id"). 260 | From("users"). 261 | Where(lk.Condition("is_staff").Equal(true)), 262 | ), 263 | ) 264 | 265 | // query: SELECT id, email, status, user_id, message, created_at 266 | // FROM comments WHERE ((deleted_at IS NULL) AND 267 | // (user_id IN (SELECT id FROM users WHERE (is_staff = :arg_1)))) 268 | // args: map[string]interface{}{ 269 | // "arg_1": bool(true), 270 | // } 271 | query, args := builder.NamedQuery() 272 | 273 | stmt, err := db.PrepareNamed(query) 274 | if err != nil { 275 | return nil, err 276 | } 277 | defer stmt.Close() 278 | 279 | comments := []Comment{} 280 | 281 | err = stmt.Select(&comments, args) 282 | if err != nil { 283 | return nil, err 284 | } 285 | 286 | return comments, nil 287 | } 288 | ``` 289 | 290 | #### SELECT with JOIN 291 | 292 | Retrieve non-deleted comments sent by a user with embedded user in results. 293 | 294 | First, we need to update the `Comment` struct to embed `User`. 295 | 296 | ```go 297 | // Comment model 298 | type Comment struct { 299 | ID int64 300 | Email string `db:"email"` 301 | Status string `db:"status"` 302 | Message string `db:"message"` 303 | UserID int64 `db:"user_id"` 304 | User *User `db:"users"` 305 | CreatedAt pq.NullTime `db:"created_at"` 306 | DeletedAt pq.NullTime `db:"deleted_at"` 307 | } 308 | ``` 309 | 310 | Let's create a `FindComments` method to retrieve these comments. 311 | 312 | In this scenario we will use an `INNER JOIN` but loukoum also supports `LEFT JOIN` and `RIGHT JOIN`. 313 | 314 | ```go 315 | // FindComments retrieves comments by users. 316 | func FindComments(db *sqlx.DB, comment Comment) ([]Comment, error) { 317 | builder := lk. 318 | Select( 319 | "comments.id", "comments.email", "comments.status", 320 | "comments.user_id", "comments.message", "comments.created_at", 321 | ). 322 | From("comments"). 323 | Join(lk.Table("users"), lk.On("comments.user_id", "users.id")). 324 | Where(lk.Condition("comments.deleted_at").IsNull(true)) 325 | 326 | // query: SELECT comments.id, comments.email, comments.status, comments.user_id, comments.message, 327 | // comments.created_at FROM comments INNER JOIN users ON comments.user_id = users.id 328 | // WHERE (comments.deleted_at IS NULL) 329 | // args: map[string]interface{}{ 330 | // 331 | // } 332 | query, args := builder.NamedQuery() 333 | 334 | stmt, err := db.PrepareNamed(query) 335 | if err != nil { 336 | return nil, err 337 | } 338 | defer stmt.Close() 339 | 340 | comments := []Comment{} 341 | 342 | err = stmt.Select(&comments, args) 343 | if err != nil { 344 | return nil, err 345 | } 346 | 347 | return comments, nil 348 | } 349 | ``` 350 | 351 | ### DELETE 352 | 353 | Delete a user based on ID. 354 | 355 | ```go 356 | // DeleteUser deletes a user. 357 | func DeleteUser(db *sqlx.DB, user User) error { 358 | builder := lk.Delete("users"). 359 | Where(lk.Condition("id").Equal(user.ID)) 360 | 361 | 362 | // query: DELETE FROM users WHERE (id = :arg_1) 363 | // args: map[string]interface{}{ 364 | // "arg_1": int64(user.ID), 365 | // } 366 | query, args := builder.NamedQuery() 367 | 368 | stmt, err := db.PrepareNamed(query) 369 | if err != nil { 370 | return err 371 | } 372 | defer stmt.Close() 373 | 374 | _, err = stmt.Exec(args) 375 | 376 | return err 377 | } 378 | ``` 379 | 380 | See [examples](examples/named) directory for more information. 381 | 382 | > **NOTE:** For `database/sql`, see [standard](examples/standard). 383 | 384 | ## Migration 385 | 386 | ### Migrating from v2.x.x 387 | 388 | - Migrate from [dep](https://github.com/golang/dep) to [go modules](https://github.com/golang/go/wiki/Modules) by 389 | replacing the import path `github.com/ulule/loukoum/...` by `github.com/ulule/loukoum/v3/...` 390 | 391 | ### Migrating from v1.x.x 392 | 393 | - Change `Prepare()` to `NamedQuery()` for [builder.Builder](https://github.com/ulule/loukoum/blob/d6ee7eac818ec74889870fa82dff411ea266463b/builder/builder.go#L19) interface. 394 | 395 | ## Inspiration 396 | 397 | - [squirrel](https://github.com/Masterminds/squirrel) 398 | - [goqu](https://github.com/doug-martin/goqu) 399 | - [sqlabble](https://github.com/minodisk/sqlabble) 400 | 401 | ## Thanks 402 | 403 | - [Ilia Choly](https://github.com/icholy) 404 | 405 | ## License 406 | 407 | This is Free Software, released under the [`MIT License`][software-license-url]. 408 | 409 | Loukoum artworks are released under the [`Creative Commons BY-SA License`][artwork-license-url]. 410 | 411 | ## Contributing 412 | 413 | - Ping us on twitter: 414 | - [@novln\_](https://twitter.com/novln_) 415 | - [@oibafsellig](https://twitter.com/oibafsellig) 416 | - [@thoas](https://twitter.com/thoas) 417 | - Fork the [project](https://github.com/ulule/loukoum) 418 | - Fix [bugs](https://github.com/ulule/loukoum/issues) 419 | 420 | **Don't hesitate ;)** 421 | 422 | [loukoum-url]: https://github.com/ulule/loukoum 423 | [loukoum-img]: docs/images/banner.png 424 | [godoc-url]: https://godoc.org/github.com/ulule/loukoum 425 | [godoc-img]: https://godoc.org/github.com/ulule/loukoum?status.svg 426 | [license-img]: https://img.shields.io/badge/license-MIT-blue.svg 427 | [software-license-url]: LICENSE 428 | [artwork-license-url]: docs/images/LICENSE 429 | [sql-url]: https://golang.org/pkg/database/sql/ 430 | [sqlx-url]: https://github.com/jmoiron/sqlx 431 | [makroud-url]: https://github.com/ulule/makroud/ 432 | [circle-url]: https://circleci.com/gh/ulule/loukoum/tree/master 433 | [circle-img]: https://circleci.com/gh/ulule/loukoum.svg?style=shield&circle-token=1de7bc4fd603b0df406ceef4bbba3fb3d6b5ed10 434 | -------------------------------------------------------------------------------- /builder/builder.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/ulule/loukoum/v3/stmt" 10 | "github.com/ulule/loukoum/v3/types" 11 | ) 12 | 13 | // Builder defines a generic methods available for Select, Insert, Update and Delete builders. 14 | type Builder interface { 15 | // String returns the underlying query as a raw statement. 16 | // This function should be used for debugging since it doesn't escape anything and is completely 17 | // vulnerable to SQL injection. 18 | // You should use either NamedQuery() or Query()... 19 | String() string 20 | // NamedQuery returns the underlying query as a named statement. 21 | NamedQuery() (string, map[string]interface{}) 22 | // Query returns the underlying query as a regular statement. 23 | Query() (string, []interface{}) 24 | // Statement returns underlying statement. 25 | Statement() stmt.Statement 26 | } 27 | 28 | // IsSelectBuilder returns true if given builder is of type "Select" 29 | func IsSelectBuilder(builder Builder) bool { 30 | _, ok := builder.(*Select) 31 | return ok 32 | } 33 | 34 | // IsInsertBuilder returns true if given builder is of type "Insert" 35 | func IsInsertBuilder(builder Builder) bool { 36 | _, ok := builder.(*Insert) 37 | return ok 38 | } 39 | 40 | // IsUpdateBuilder returns true if given builder is of type "Update" 41 | func IsUpdateBuilder(builder Builder) bool { 42 | _, ok := builder.(*Update) 43 | return ok 44 | } 45 | 46 | // IsDeleteBuilder returns true if given builder is of type "Delete" 47 | func IsDeleteBuilder(builder Builder) bool { 48 | _, ok := builder.(*Delete) 49 | return ok 50 | } 51 | 52 | // ToColumn takes an empty interfaces and returns a Column instance. 53 | func ToColumn(arg interface{}) stmt.Column { 54 | column := stmt.Column{} 55 | 56 | switch value := arg.(type) { 57 | case string: 58 | column = stmt.NewColumn(strings.TrimSpace(value)) 59 | case stmt.Column: 60 | column = value 61 | default: 62 | panic(fmt.Sprintf("loukoum: cannot use %T as column", arg)) 63 | } 64 | 65 | if column.IsEmpty() { 66 | panic("loukoum: given column is undefined") 67 | } 68 | 69 | return column 70 | } 71 | 72 | // ToColumns takes a list of empty interfaces and returns a slice of Column instance. 73 | func ToColumns(values []interface{}) []stmt.Column { // nolint: gocyclo 74 | // If values is a slice, we try to use recursion to obtain a slice of Column. 75 | if len(values) == 1 { 76 | switch array := values[0].(type) { 77 | case []stmt.Column: 78 | for i := range array { 79 | if array[i].IsEmpty() { 80 | panic("loukoum: given column is undefined") 81 | } 82 | } 83 | return array 84 | case []string: 85 | list := make([]interface{}, len(array)) 86 | for i := range array { 87 | list[i] = array[i] 88 | } 89 | return ToColumns(list) 90 | } 91 | } 92 | 93 | columns := make([]stmt.Column, 0, len(values)) 94 | 95 | for i := range values { 96 | switch value := values[i].(type) { 97 | case string: 98 | array := strings.Split(value, ",") 99 | for y := range array { 100 | column := stmt.NewColumn(strings.TrimSpace(array[y])) 101 | if column.IsEmpty() { 102 | panic("loukoum: given column is undefined") 103 | } 104 | columns = append(columns, column) 105 | } 106 | case stmt.Column: 107 | if value.IsEmpty() { 108 | panic("loukoum: given column is undefined") 109 | } 110 | columns = append(columns, value) 111 | default: 112 | panic(fmt.Sprintf("loukoum: cannot use %T as column", values[i])) 113 | } 114 | } 115 | 116 | return columns 117 | } 118 | 119 | // ToSelectExpressions takes a list of empty interfaces and returns a slice of SelectExpression instance. 120 | func ToSelectExpressions(values []interface{}) []stmt.SelectExpression { // nolint: gocyclo 121 | // If values is a slice, we try to use recursion to obtain a slice of Column. 122 | if len(values) == 1 { 123 | switch array := values[0].(type) { 124 | case []stmt.SelectExpression: 125 | return array 126 | case []stmt.Column: 127 | expressions := make([]stmt.SelectExpression, 0, len(values)) 128 | for i := range array { 129 | if array[i].IsEmpty() { 130 | panic("loukoum: given column is undefined") 131 | } 132 | expressions = append(expressions, array[i]) 133 | } 134 | return expressions 135 | case []string: 136 | expressions := make([]stmt.SelectExpression, 0, len(values)) 137 | for i := range array { 138 | expressions = append(expressions, stmt.NewColumn(array[i])) 139 | } 140 | return expressions 141 | } 142 | } 143 | 144 | columns := make([]stmt.SelectExpression, 0, len(values)) 145 | 146 | for i := range values { 147 | switch value := values[i].(type) { 148 | case stmt.SelectExpression: 149 | if value.IsEmpty() { 150 | panic("loukoum: given column is undefined") 151 | } 152 | columns = append(columns, value) 153 | case string: 154 | array := strings.Split(value, ",") 155 | for y := range array { 156 | column := stmt.NewColumn(strings.TrimSpace(array[y])) 157 | if column.IsEmpty() { 158 | panic("loukoum: given column is undefined") 159 | } 160 | columns = append(columns, column) 161 | } 162 | default: 163 | panic(fmt.Sprintf("loukoum: cannot use %T as column", values[i])) 164 | } 165 | } 166 | 167 | return columns 168 | } 169 | 170 | // ToTable takes an empty interfaces and returns a Table instance. 171 | func ToTable(arg interface{}) stmt.Table { 172 | table := stmt.Table{} 173 | 174 | switch value := arg.(type) { 175 | case string: 176 | table = stmt.NewTable(value) 177 | case stmt.Table: 178 | table = value 179 | default: 180 | panic(fmt.Sprintf("loukoum: cannot use %T as table", arg)) 181 | } 182 | 183 | if table.IsEmpty() { 184 | panic("loukoum: given table is undefined") 185 | } 186 | 187 | return table 188 | } 189 | 190 | // ToTables takes a list of empty interfaces and returns a slice of Table instance. 191 | func ToTables(values []interface{}) []stmt.Table { 192 | tables := make([]stmt.Table, 0, len(values)) 193 | 194 | for i := range values { 195 | tables = append(tables, ToTable(values[i])) 196 | } 197 | 198 | return tables 199 | } 200 | 201 | // ToFrom takes an empty interfaces and returns a From instance. 202 | func ToFrom(args ...interface{}) stmt.From { 203 | from := stmt.From{} 204 | 205 | tables := make([]stmt.Statement, len(args)) 206 | for i := range args { 207 | switch value := args[i].(type) { 208 | case string: 209 | tables[i] = stmt.NewTable(value) 210 | case stmt.From: 211 | from = value 212 | case stmt.Table: 213 | tables[i] = value 214 | case stmt.Raw: 215 | tables[i] = value 216 | default: 217 | panic(fmt.Sprintf("loukoum: cannot use %T as from clause", args[i])) 218 | } 219 | } 220 | 221 | from = stmt.NewFrom(tables) 222 | if from.IsEmpty() { 223 | panic("loukoum: given from clause is undefined") 224 | } 225 | 226 | return from 227 | } 228 | 229 | // ToInto takes an empty interfaces and returns a Into instance. 230 | func ToInto(arg interface{}) stmt.Into { 231 | into := stmt.Into{} 232 | 233 | switch value := arg.(type) { 234 | case string: 235 | into = stmt.NewInto(stmt.NewTable(value)) 236 | case stmt.Into: 237 | into = value 238 | case stmt.Table: 239 | into = stmt.NewInto(value) 240 | default: 241 | panic(fmt.Sprintf("loukoum: cannot use %T as into clause", arg)) 242 | } 243 | 244 | if into.IsEmpty() { 245 | panic("loukoum: given into clause is undefined") 246 | } 247 | 248 | return into 249 | } 250 | 251 | // ToSuffix takes an empty interfaces and returns a Suffix instance. 252 | func ToSuffix(arg interface{}) stmt.Suffix { 253 | suffix := stmt.Suffix{} 254 | 255 | switch value := arg.(type) { 256 | case string: 257 | suffix = stmt.NewSuffix(value) 258 | case stmt.Suffix: 259 | suffix = value 260 | default: 261 | panic(fmt.Sprintf("loukoum: cannot use %T as suffix", value)) 262 | } 263 | 264 | if suffix.IsEmpty() { 265 | panic("loukoum: given suffix is undefined") 266 | } 267 | 268 | return suffix 269 | } 270 | 271 | // ToPrefix takes an empty interfaces and returns a Prefix instance. 272 | func ToPrefix(arg interface{}) stmt.Prefix { 273 | prefix := stmt.Prefix{} 274 | 275 | switch value := arg.(type) { 276 | case string: 277 | prefix = stmt.NewPrefix(value) 278 | case stmt.Prefix: 279 | prefix = value 280 | default: 281 | panic(fmt.Sprintf("loukoum: cannot use %T as prefix", value)) 282 | } 283 | 284 | if prefix.IsEmpty() { 285 | panic("loukoum: given prefix is undefined") 286 | } 287 | 288 | return prefix 289 | } 290 | 291 | // ToInt64 takes an empty interfaces and returns a int64. 292 | func ToInt64(value interface{}) (int64, bool) { // nolint: gocyclo 293 | switch cast := value.(type) { 294 | case int64: 295 | return cast, true 296 | case int: 297 | return int64(cast), true 298 | case int8: 299 | return int64(cast), true 300 | case int16: 301 | return int64(cast), true 302 | case int32: 303 | return int64(cast), true 304 | case uint8: 305 | return int64(cast), true 306 | case uint16: 307 | return int64(cast), true 308 | case uint32: 309 | return int64(cast), true 310 | case uint64: 311 | if cast <= math.MaxInt64 { 312 | return int64(cast), true 313 | } 314 | case string: 315 | n, err := strconv.ParseInt(cast, 10, 64) 316 | if err == nil { 317 | return n, true 318 | } 319 | } 320 | return 0, false 321 | } 322 | 323 | // MergeSet merges new pairs into existing ones (last write wins). 324 | func MergeSet(set stmt.Set, args []interface{}) stmt.Set { 325 | for i := range args { 326 | switch value := args[i].(type) { 327 | case string, stmt.Column: 328 | columns := ToColumns([]interface{}{value}) 329 | for y := range columns { 330 | set.Pairs.Set(columns[y]) 331 | } 332 | case map[string]interface{}: 333 | for k, v := range value { 334 | set.Pairs.Add(ToColumn(k), stmt.NewWrapper(stmt.NewExpression(v))) 335 | } 336 | case types.Map: 337 | for k, v := range value { 338 | set.Pairs.Add(ToColumn(k), stmt.NewWrapper(stmt.NewExpression(v))) 339 | } 340 | case types.Pair: 341 | set.Pairs.Add(ToColumn(value.Key), stmt.NewWrapper(stmt.NewExpression(value.Value))) 342 | default: 343 | panic(fmt.Sprintf("loukoum: cannot use %T as pair", value)) 344 | } 345 | } 346 | return set 347 | } 348 | 349 | // ToSet takes either a types.Map or slice of types.Pair and returns a stmt.Set instance. 350 | func ToSet(args []interface{}) stmt.Set { 351 | set := stmt.NewSet() 352 | set.Pairs.Mode = stmt.PairAssociativeMode 353 | set = MergeSet(set, args) 354 | return set 355 | } 356 | 357 | // ToLimit takes an empty interfaces and returns a Limit instance. 358 | func ToLimit(arg interface{}) stmt.Limit { 359 | switch value := arg.(type) { 360 | case stmt.Limit: 361 | return value 362 | default: 363 | limit, ok := ToInt64(value) 364 | if !ok { 365 | panic(fmt.Sprintf("loukoum: cannot use %T as limit", value)) 366 | } 367 | if limit <= 0 { 368 | panic("loukoum: limit must be a positive integer") 369 | } 370 | return stmt.NewLimit(limit) 371 | } 372 | } 373 | 374 | // ToOffset takes an empty interfaces and returns a Offset instance. 375 | func ToOffset(arg interface{}) stmt.Offset { 376 | switch value := arg.(type) { 377 | case stmt.Offset: 378 | return value 379 | default: 380 | offset, ok := ToInt64(value) 381 | if !ok { 382 | panic(fmt.Sprintf("loukoum: cannot use %T as offset", value)) 383 | } 384 | if offset < 0 { 385 | panic("loukoum: offset must be a non-negative integer") 386 | } 387 | return stmt.NewOffset(offset) 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /builder/builder_test.go: -------------------------------------------------------------------------------- 1 | package builder_test 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/ulule/loukoum/v3/builder" 11 | "github.com/ulule/loukoum/v3/stmt" 12 | ) 13 | 14 | type BuilderTest struct { 15 | Name string 16 | Builder builder.Builder 17 | Builders []builder.Builder 18 | SameQuery string 19 | String string 20 | Query string 21 | NamedQuery string 22 | Args []interface{} 23 | Failure func() builder.Builder 24 | } 25 | 26 | func (b BuilderTest) builders() []builder.Builder { 27 | var builders []builder.Builder 28 | builders = append(builders, b.Builders...) 29 | if b.Builder != nil { 30 | builders = append(builders, b.Builder) 31 | } 32 | return builders 33 | } 34 | 35 | func toNamedArgs(args []interface{}) map[string]interface{} { 36 | if args == nil { 37 | return nil 38 | } 39 | named := make(map[string]interface{}) 40 | for i, arg := range args { 41 | name := fmt.Sprintf("arg_%d", i+1) 42 | named[name] = arg 43 | } 44 | return named 45 | } 46 | 47 | func RunBuilderTests(t *testing.T, tests []BuilderTest) { 48 | for _, tt := range tests { 49 | t.Run(tt.Name, func(t *testing.T) { 50 | if tt.Failure != nil { 51 | t.Run("Failure", func(t *testing.T) { 52 | require.Panics(t, func() { 53 | _ = tt.Failure().String() 54 | }) 55 | }) 56 | return 57 | } 58 | for i, builder := range tt.builders() { 59 | t.Run(strconv.Itoa(i), func(t *testing.T) { 60 | if tt.SameQuery != "" { 61 | tt.String = tt.SameQuery 62 | tt.Query = tt.SameQuery 63 | tt.NamedQuery = tt.SameQuery 64 | } 65 | t.Run("String", func(t *testing.T) { 66 | require.Equal(t, tt.String, builder.String()) 67 | }) 68 | t.Run("Query", func(t *testing.T) { 69 | query, args := builder.Query() 70 | require.Equal(t, tt.Query, query) 71 | require.Equal(t, tt.Args, args) 72 | }) 73 | t.Run("NamedQuery", func(t *testing.T) { 74 | query, args := builder.NamedQuery() 75 | require.Equal(t, tt.NamedQuery, query) 76 | require.Equal(t, toNamedArgs(tt.Args), args) 77 | }) 78 | }) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestToColumns(t *testing.T) { 85 | is := require.New(t) 86 | 87 | // Simple cases... 88 | { 89 | args := []interface{}{"foo", "bar"} 90 | columns := builder.ToColumns(args) 91 | is.Len(columns, 2) 92 | is.Equal(stmt.NewColumn("foo"), columns[0]) 93 | is.Equal(stmt.NewColumn("bar"), columns[1]) 94 | } 95 | { 96 | args := []interface{}{stmt.NewColumn("foo"), "bar"} 97 | columns := builder.ToColumns(args) 98 | is.Len(columns, 2) 99 | is.Equal(stmt.NewColumn("foo"), columns[0]) 100 | is.Equal(stmt.NewColumn("bar"), columns[1]) 101 | } 102 | { 103 | args := []interface{}{stmt.NewColumn("foo"), stmt.NewColumn("bar")} 104 | columns := builder.ToColumns(args) 105 | is.Len(columns, 2) 106 | is.Equal(stmt.NewColumn("foo"), columns[0]) 107 | is.Equal(stmt.NewColumn("bar"), columns[1]) 108 | } 109 | { 110 | args := []interface{}{[]string{"foo", "bar"}} 111 | columns := builder.ToColumns(args) 112 | is.Len(columns, 2) 113 | is.Equal(stmt.NewColumn("foo"), columns[0]) 114 | is.Equal(stmt.NewColumn("bar"), columns[1]) 115 | } 116 | { 117 | args := []interface{}{[]stmt.Column{stmt.NewColumn("foo"), stmt.NewColumn("bar")}} 118 | columns := builder.ToColumns(args) 119 | is.Len(columns, 2) 120 | is.Equal(stmt.NewColumn("foo"), columns[0]) 121 | is.Equal(stmt.NewColumn("bar"), columns[1]) 122 | } 123 | { 124 | args := []interface{}{"foo, bar"} 125 | columns := builder.ToColumns(args) 126 | is.Len(columns, 2) 127 | is.Equal(stmt.NewColumn("foo"), columns[0]) 128 | is.Equal(stmt.NewColumn("bar"), columns[1]) 129 | } 130 | 131 | // Corner cases... 132 | { 133 | is.Panics(func() { 134 | args := []interface{}{""} 135 | builder.ToColumns(args) 136 | }) 137 | } 138 | { 139 | is.Panics(func() { 140 | args := []interface{}{stmt.NewColumn("")} 141 | builder.ToColumns(args) 142 | }) 143 | } 144 | { 145 | is.Panics(func() { 146 | args := []interface{}{[]string{""}} 147 | builder.ToColumns(args) 148 | }) 149 | } 150 | { 151 | is.Panics(func() { 152 | args := []interface{}{[]stmt.Column{stmt.NewColumn("")}} 153 | builder.ToColumns(args) 154 | }) 155 | } 156 | { 157 | is.Panics(func() { 158 | args := []interface{}{","} 159 | builder.ToColumns(args) 160 | }) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /builder/delete.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/stmt" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Delete is a builder used for "SELECT" query. 9 | type Delete struct { 10 | query stmt.Delete 11 | } 12 | 13 | // NewDelete creates a new Delete. 14 | func NewDelete() Delete { 15 | return Delete{} 16 | } 17 | 18 | // From sets the FROM clause of the query. 19 | func (b Delete) From(arg ...interface{}) Delete { 20 | if !b.query.From.IsEmpty() { 21 | panic("loukoum: delete builder has from clause already defined") 22 | } 23 | 24 | b.query.From = ToFrom(arg...) 25 | 26 | return b 27 | } 28 | 29 | // Using adds a ONLY clause to the query. 30 | func (b Delete) Using(args ...interface{}) Delete { 31 | if !b.query.Using.IsEmpty() { 32 | panic("loukoum: delete builder has using clause already defined") 33 | } 34 | 35 | tables := ToTables(args) 36 | b.query.Using = stmt.NewUsing(tables) 37 | 38 | return b 39 | } 40 | 41 | // Where adds WHERE clauses. 42 | func (b Delete) Where(condition stmt.Expression) Delete { 43 | if b.query.Where.IsEmpty() { 44 | b.query.Where = stmt.NewWhere(condition) 45 | return b 46 | } 47 | 48 | return b.And(condition) 49 | } 50 | 51 | // And adds AND WHERE conditions. 52 | func (b Delete) And(condition stmt.Expression) Delete { 53 | b.query.Where = b.query.Where.And(condition) 54 | return b 55 | } 56 | 57 | // Or adds OR WHERE conditions. 58 | func (b Delete) Or(condition stmt.Expression) Delete { 59 | b.query.Where = b.query.Where.Or(condition) 60 | return b 61 | } 62 | 63 | // Returning adds a RETURNING clause. 64 | func (b Delete) Returning(values ...interface{}) Delete { 65 | if !b.query.Returning.IsEmpty() { 66 | panic("loukoum: delete builder has returning clause already defined") 67 | } 68 | 69 | b.query.Returning = stmt.NewReturning(ToSelectExpressions(values)) 70 | 71 | return b 72 | } 73 | 74 | // Comment adds comment to the query. 75 | func (b Delete) Comment(comment string) Delete { 76 | b.query.Comment = stmt.NewComment(comment) 77 | 78 | return b 79 | } 80 | 81 | // String returns the underlying query as a raw statement. 82 | // This function should be used for debugging since it doesn't escape anything and is completely 83 | // vulnerable to SQL injection. 84 | // You should use either NamedQuery() or Query()... 85 | func (b Delete) String() string { 86 | ctx := &types.RawContext{} 87 | b.query.Write(ctx) 88 | return ctx.Query() 89 | } 90 | 91 | // NamedQuery returns the underlying query as a named statement. 92 | func (b Delete) NamedQuery() (string, map[string]interface{}) { 93 | ctx := &types.NamedContext{} 94 | b.query.Write(ctx) 95 | return ctx.Query(), ctx.Values() 96 | } 97 | 98 | // Query returns the underlying query as a regular statement. 99 | func (b Delete) Query() (string, []interface{}) { 100 | ctx := &types.StdContext{} 101 | b.query.Write(ctx) 102 | return ctx.Query(), ctx.Values() 103 | } 104 | 105 | // Statement returns underlying statement. 106 | func (b Delete) Statement() stmt.Statement { 107 | return b.query 108 | } 109 | 110 | // Ensure that Delete is a Builder 111 | var _ Builder = Delete{} 112 | -------------------------------------------------------------------------------- /builder/delete_test.go: -------------------------------------------------------------------------------- 1 | package builder_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | loukoum "github.com/ulule/loukoum/v3" 8 | "github.com/ulule/loukoum/v3/builder" 9 | ) 10 | 11 | func TestDelete(t *testing.T) { 12 | RunBuilderTests(t, []BuilderTest{ 13 | { 14 | Name: "Simple", 15 | Builders: []builder.Builder{ 16 | loukoum.Delete("table"), 17 | loukoum.Delete(loukoum.Table("table")), 18 | }, 19 | SameQuery: "DELETE FROM \"table\"", 20 | }, 21 | { 22 | Name: "Only", 23 | Builders: []builder.Builder{ 24 | loukoum.Delete(loukoum.Table("table").Only()), 25 | }, 26 | SameQuery: "DELETE FROM ONLY \"table\"", 27 | }, 28 | { 29 | Name: "As", 30 | Builder: loukoum.Delete(loukoum.Table("table").As("foobar")), 31 | SameQuery: "DELETE FROM \"table\" AS \"foobar\"", 32 | }, 33 | { 34 | Name: "As only", 35 | Builder: loukoum.Delete(loukoum.Table("table").As("foobar").Only()), 36 | SameQuery: "DELETE FROM ONLY \"table\" AS \"foobar\"", 37 | }, 38 | }) 39 | } 40 | 41 | func TestDelete_Comment(t *testing.T) { 42 | RunBuilderTests(t, []BuilderTest{ 43 | { 44 | Name: "Simple", 45 | Builders: []builder.Builder{ 46 | loukoum.Delete("table").Comment("/foo"), 47 | loukoum.Delete(loukoum.Table("table")).Comment("/foo"), 48 | }, 49 | SameQuery: "DELETE FROM \"table\"; -- /foo", 50 | }, 51 | }) 52 | } 53 | 54 | func TestDelete_Using(t *testing.T) { 55 | RunBuilderTests(t, []BuilderTest{ 56 | { 57 | Name: "One table", 58 | Builders: []builder.Builder{ 59 | loukoum.Delete("table").Using("foobar"), 60 | loukoum.Delete("table").Using(loukoum.Table("foobar")), 61 | }, 62 | SameQuery: "DELETE FROM \"table\" USING \"foobar\"", 63 | }, 64 | { 65 | Name: "One table as", 66 | Builder: loukoum.Delete("table").Using(loukoum.Table("foobar").As("foo")), 67 | SameQuery: "DELETE FROM \"table\" USING \"foobar\" AS \"foo\"", 68 | }, 69 | { 70 | Name: "Two tables", 71 | Builders: []builder.Builder{ 72 | loukoum.Delete("table").Using("foobar", "example"), 73 | loukoum.Delete("table").Using(loukoum.Table("foobar"), "example"), 74 | }, 75 | SameQuery: "DELETE FROM \"table\" USING \"foobar\", \"example\"", 76 | }, 77 | { 78 | Name: "Two tables as", 79 | Builder: loukoum.Delete("table").Using(loukoum.Table("example"), loukoum.Table("foobar").As("foo")), 80 | SameQuery: "DELETE FROM \"table\" USING \"example\", \"foobar\" AS \"foo\"", 81 | }, 82 | }) 83 | } 84 | 85 | func TestDelete_Where(t *testing.T) { 86 | when, err := time.Parse(time.RFC3339, "2017-11-23T17:47:27+01:00") 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | RunBuilderTests(t, []BuilderTest{ 91 | { 92 | Name: "Simple", 93 | Builder: loukoum.Delete("table").Where(loukoum.Condition("id").Equal(1)), 94 | String: "DELETE FROM \"table\" WHERE (\"id\" = 1)", 95 | Query: "DELETE FROM \"table\" WHERE (\"id\" = $1)", 96 | NamedQuery: "DELETE FROM \"table\" WHERE (\"id\" = :arg_1)", 97 | Args: []interface{}{1}, 98 | }, 99 | { 100 | Name: "Complex", 101 | Builder: loukoum.Delete("table"). 102 | Where(loukoum.Condition("id").Equal(1)). 103 | And(loukoum.Condition("created_at").GreaterThan(when)), 104 | String: "DELETE FROM \"table\" WHERE ((\"id\" = 1) AND (\"created_at\" > '2017-11-23 16:47:27+00'))", 105 | Query: "DELETE FROM \"table\" WHERE ((\"id\" = $1) AND (\"created_at\" > $2))", 106 | NamedQuery: "DELETE FROM \"table\" WHERE ((\"id\" = :arg_1) AND (\"created_at\" > :arg_2))", 107 | Args: []interface{}{1, when}, 108 | }, 109 | }) 110 | } 111 | 112 | func TestDelete_Returning(t *testing.T) { 113 | RunBuilderTests(t, []BuilderTest{ 114 | { 115 | Name: "*", 116 | Builder: loukoum.Delete("table").Returning(loukoum.Raw("*")), 117 | SameQuery: "DELETE FROM \"table\" RETURNING *", 118 | }, 119 | { 120 | Name: "id", 121 | Builder: loukoum.Delete("table").Returning("id"), 122 | SameQuery: "DELETE FROM \"table\" RETURNING \"id\"", 123 | }, 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /builder/doc.go: -------------------------------------------------------------------------------- 1 | // Package builder receives user input and generates an AST using "stmt" package. 2 | // 3 | // There is four builder to manipulate an AST: Select, Insert, Update and Delete. 4 | // 5 | // When the AST is ready, you can use String(), NamedQuery() or Query() to generate the underlying query. 6 | // However, be vigilant with String(): it's mainly used for debugging because it's completely vulnerable 7 | // to SQL injection... 8 | package builder 9 | -------------------------------------------------------------------------------- /builder/insert.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ulule/loukoum/v3/stmt" 7 | "github.com/ulule/loukoum/v3/types" 8 | ) 9 | 10 | // Insert is a builder used for "INSERT" query. 11 | type Insert struct { 12 | query stmt.Insert 13 | } 14 | 15 | // NewInsert creates a new Insert. 16 | func NewInsert() Insert { 17 | return Insert{ 18 | query: stmt.NewInsert(), 19 | } 20 | } 21 | 22 | // Into sets the INTO clause of the query. 23 | func (b Insert) Into(into interface{}) Insert { 24 | if !b.query.Into.IsEmpty() { 25 | panic("loukoum: insert builder has into clause already defined") 26 | } 27 | 28 | b.query.Into = ToInto(into) 29 | 30 | return b 31 | } 32 | 33 | // Columns sets the query columns. 34 | func (b Insert) Columns(columns ...interface{}) Insert { 35 | if len(columns) == 0 { 36 | return b 37 | } 38 | if len(b.query.Columns) != 0 { 39 | panic("loukoum: insert builder has columns clause already defined") 40 | } 41 | 42 | b.query.Columns = ToColumns(columns) 43 | 44 | return b 45 | } 46 | 47 | // Values sets the query values. 48 | func (b Insert) Values(values ...interface{}) Insert { 49 | if !b.query.Values.IsEmpty() { 50 | panic("loukoum: insert builder has values clause already defined") 51 | } 52 | 53 | b.query.Values = stmt.NewValues(stmt.NewArrayListExpression(values...)) 54 | 55 | return b 56 | } 57 | 58 | // Returning builds the RETURNING clause. 59 | func (b Insert) Returning(values ...interface{}) Insert { 60 | if !b.query.Returning.IsEmpty() { 61 | panic("loukoum: insert builder has returning clause already defined") 62 | } 63 | 64 | b.query.Returning = stmt.NewReturning(ToSelectExpressions(values)) 65 | 66 | return b 67 | } 68 | 69 | // Comment adds comment to the query. 70 | func (b Insert) Comment(comment string) Insert { 71 | b.query.Comment = stmt.NewComment(comment) 72 | 73 | return b 74 | } 75 | 76 | // OnConflict builds the ON CONFLICT clause. 77 | func (b Insert) OnConflict(args ...interface{}) Insert { 78 | if !b.query.OnConflict.IsEmpty() { 79 | panic("loukoum: insert builder has on conflict clause already defined") 80 | } 81 | 82 | if len(args) == 0 { 83 | panic("loukoum: on conflict clause requires arguments") 84 | } 85 | 86 | for i := range args { 87 | switch value := args[i].(type) { 88 | case string, stmt.Column: 89 | b.query.OnConflict.Target.Columns = append(b.query.OnConflict.Target.Columns, ToColumn(value)) 90 | case stmt.ConflictNoAction: 91 | b.query.OnConflict.Action = value 92 | return b 93 | case stmt.ConflictUpdateAction: 94 | if b.query.OnConflict.Target.IsEmpty() { 95 | panic("loukoum: on conflict update clause requires at least one target") 96 | } 97 | b.query.OnConflict.Action = value 98 | return b 99 | default: 100 | panic(fmt.Sprintf("loukoum: cannot use %T as on conflict clause", args[i])) 101 | } 102 | } 103 | 104 | panic("loukoum: on conflict clause requires an action") 105 | } 106 | 107 | // Set is a wrapper that defines columns and values clauses using a pair. 108 | func (b Insert) Set(args ...interface{}) Insert { 109 | if len(b.query.Columns) != 0 { 110 | panic("loukoum: insert builder has columns clause already defined") 111 | } 112 | if !b.query.Values.IsEmpty() { 113 | panic("loukoum: insert builder has values clause already defined") 114 | } 115 | 116 | pairs := ToSet(args).Pairs 117 | columns, expressions := pairs.Values() 118 | 119 | array := stmt.NewArrayListExpression(expressions) 120 | values := stmt.NewValues(array) 121 | 122 | b.query.Columns = columns 123 | b.query.Values = values 124 | 125 | return b 126 | } 127 | 128 | // String returns the underlying query as a raw statement. 129 | // This function should be used for debugging since it doesn't escape anything and is completely 130 | // vulnerable to SQL injection. 131 | // You should use either NamedQuery() or Query()... 132 | func (b Insert) String() string { 133 | ctx := &types.RawContext{} 134 | b.query.Write(ctx) 135 | return ctx.Query() 136 | } 137 | 138 | // NamedQuery returns the underlying query as a named statement. 139 | func (b Insert) NamedQuery() (string, map[string]interface{}) { 140 | ctx := &types.NamedContext{} 141 | b.query.Write(ctx) 142 | return ctx.Query(), ctx.Values() 143 | } 144 | 145 | // Query returns the underlying query as a regular statement. 146 | func (b Insert) Query() (string, []interface{}) { 147 | ctx := &types.StdContext{} 148 | b.query.Write(ctx) 149 | return ctx.Query(), ctx.Values() 150 | } 151 | 152 | // Statement returns underlying statement. 153 | func (b Insert) Statement() stmt.Statement { 154 | return b.query 155 | } 156 | 157 | // Ensure that Insert is a Builder 158 | var _ Builder = Insert{} 159 | -------------------------------------------------------------------------------- /builder/select.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ulule/loukoum/v3/parser" 7 | "github.com/ulule/loukoum/v3/stmt" 8 | "github.com/ulule/loukoum/v3/types" 9 | ) 10 | 11 | // Select is a builder used for "SELECT" query. 12 | type Select struct { 13 | query stmt.Select 14 | } 15 | 16 | // NewSelect creates a new Select. 17 | func NewSelect() Select { 18 | return Select{} 19 | } 20 | 21 | // DistinctOn adds a DISTINCT ON clause to the query. 22 | func (b Select) DistinctOn(args ...interface{}) Select { 23 | if !b.query.GroupBy.IsEmpty() { 24 | panic("loukoum: select builder has distinct on clause already defined") 25 | } 26 | 27 | columns := ToColumns(args) 28 | distinctOn := stmt.NewDistinctOn(columns) 29 | if distinctOn.IsEmpty() { 30 | panic("loukoum: given distinct on clause is undefined") 31 | } 32 | 33 | b.query.DistinctOn = distinctOn 34 | 35 | return b 36 | } 37 | 38 | // Distinct adds a DISTINCT clause to the query. 39 | func (b Select) Distinct() Select { 40 | b.query.Distinct = true 41 | return b 42 | } 43 | 44 | // Columns adds result columns to the query. 45 | func (b Select) Columns(args ...interface{}) Select { 46 | if len(args) == 0 { 47 | args = []interface{}{"*"} 48 | } 49 | 50 | b.query.Expressions = ToSelectExpressions(args) 51 | 52 | return b 53 | } 54 | 55 | // From sets the FROM clause of the query. 56 | func (b Select) From(arg ...interface{}) Select { 57 | if !b.query.From.IsEmpty() { 58 | panic("loukoum: select builder has from clause already defined") 59 | } 60 | 61 | b.query.From = ToFrom(arg...) 62 | 63 | return b 64 | } 65 | 66 | // Join adds a JOIN clause to the query. 67 | func (b Select) Join(args ...interface{}) Select { 68 | switch len(args) { 69 | case 1: 70 | return b.join1(args) 71 | case 2: 72 | return b.join2(args) 73 | case 3: 74 | return b.join3(args) 75 | default: 76 | panic("loukoum: given join clause is invalid") 77 | } 78 | } 79 | 80 | func (b Select) join1(args []interface{}) Select { 81 | join := stmt.Join{} 82 | 83 | switch value := args[0].(type) { 84 | case string: 85 | join = parser.MustParseJoin(value) 86 | case stmt.Join: 87 | join = value 88 | default: 89 | panic(fmt.Sprintf("loukoum: cannot use %T as join clause", args[0])) 90 | } 91 | 92 | if join.IsEmpty() { 93 | panic("loukoum: given join clause is undefined") 94 | } 95 | 96 | b.query.Joins = append(b.query.Joins, join) 97 | 98 | return b 99 | } 100 | 101 | func (b Select) join2(args []interface{}) Select { 102 | join := handleSelectJoin(args) 103 | if join.IsEmpty() { 104 | panic("loukoum: given join clause is undefined") 105 | } 106 | 107 | b.query.Joins = append(b.query.Joins, join) 108 | 109 | return b 110 | } 111 | 112 | func (b Select) join3(args []interface{}) Select { 113 | join := handleSelectJoin(args) 114 | 115 | switch value := args[2].(type) { 116 | case types.JoinType: 117 | join.Type = value 118 | default: 119 | panic(fmt.Sprintf("loukoum: cannot use %T as join clause", args[1])) 120 | } 121 | 122 | if join.IsEmpty() { 123 | panic("loukoum: given join clause is undefined") 124 | } 125 | 126 | b.query.Joins = append(b.query.Joins, join) 127 | 128 | return b 129 | } 130 | 131 | // With adds WITH clauses. 132 | func (b Select) With(args ...stmt.WithQuery) Select { 133 | if b.query.With.IsEmpty() { 134 | b.query.With = stmt.NewWith(args) 135 | return b 136 | } 137 | 138 | b.query.With.Queries = append(b.query.With.Queries, args...) 139 | return b 140 | } 141 | 142 | // Where adds WHERE clauses. 143 | func (b Select) Where(condition stmt.Expression) Select { 144 | if condition == nil { 145 | panic("loukoum: condition must be not nil") 146 | } 147 | if b.query.Where.IsEmpty() { 148 | b.query.Where = stmt.NewWhere(condition) 149 | return b 150 | } 151 | 152 | return b.And(condition) 153 | } 154 | 155 | // And adds AND WHERE conditions. 156 | func (b Select) And(condition stmt.Expression) Select { 157 | if condition == nil { 158 | panic("loukoum: condition must be not nil") 159 | } 160 | b.query.Where = b.query.Where.And(condition) 161 | return b 162 | } 163 | 164 | // Or adds OR WHERE conditions. 165 | func (b Select) Or(condition stmt.Expression) Select { 166 | b.query.Where = b.query.Where.Or(condition) 167 | return b 168 | } 169 | 170 | // GroupBy adds GROUP BY clauses. 171 | func (b Select) GroupBy(args ...interface{}) Select { 172 | if !b.query.GroupBy.IsEmpty() { 173 | panic("loukoum: select builder has group by clause already defined") 174 | } 175 | 176 | columns := ToColumns(args) 177 | group := stmt.NewGroupBy(columns) 178 | if group.IsEmpty() { 179 | panic("loukoum: given join clause is undefined") 180 | } 181 | 182 | b.query.GroupBy = group 183 | 184 | return b 185 | } 186 | 187 | // Having adds HAVING clauses. 188 | func (b Select) Having(condition stmt.Expression) Select { 189 | if !b.query.Having.IsEmpty() { 190 | panic("loukoum: select builder has having clause already defined") 191 | } 192 | 193 | b.query.Having = stmt.NewHaving(condition) 194 | 195 | return b 196 | } 197 | 198 | // OrderBy adds ORDER BY clauses. 199 | func (b Select) OrderBy(orders ...stmt.Order) Select { 200 | b.query.OrderBy.Orders = append(b.query.OrderBy.Orders, orders...) 201 | return b 202 | } 203 | 204 | // Limit adds LIMIT clause. 205 | func (b Select) Limit(value interface{}) Select { 206 | if !b.query.Limit.IsEmpty() { 207 | panic("loukoum: select builder has limit clause already defined") 208 | } 209 | 210 | b.query.Limit = ToLimit(value) 211 | return b 212 | } 213 | 214 | // Offset adds OFFSET clause. 215 | func (b Select) Offset(value interface{}) Select { 216 | if !b.query.Offset.IsEmpty() { 217 | panic("loukoum: select builder has offset clause already defined") 218 | } 219 | 220 | b.query.Offset = ToOffset(value) 221 | return b 222 | } 223 | 224 | // Suffix adds given clauses as suffixes. 225 | func (b Select) Suffix(suffix interface{}) Select { 226 | if !b.query.Offset.IsEmpty() { 227 | panic("loukoum: select builder has suffixes clauses already defined") 228 | } 229 | 230 | b.query.Suffix = ToSuffix(suffix) 231 | 232 | return b 233 | } 234 | 235 | // Prefix adds given clauses as prefixes. 236 | func (b Select) Prefix(prefix interface{}) Select { 237 | if !b.query.Offset.IsEmpty() { 238 | panic("loukoum: select builder has prefixes clauses already defined") 239 | } 240 | 241 | b.query.Prefix = ToPrefix(prefix) 242 | 243 | return b 244 | } 245 | 246 | // Comment adds comment to the query. 247 | func (b Select) Comment(comment string) Select { 248 | b.query.Comment = stmt.NewComment(comment) 249 | 250 | return b 251 | } 252 | 253 | // String returns the underlying query as a raw statement. 254 | // This function should be used for debugging since it doesn't escape anything and is completely 255 | // vulnerable to SQL injection. 256 | // You should use either NamedQuery() or Query()... 257 | func (b Select) String() string { 258 | ctx := &types.RawContext{} 259 | b.query.Write(ctx) 260 | return ctx.Query() 261 | } 262 | 263 | // NamedQuery returns the underlying query as a named statement. 264 | func (b Select) NamedQuery() (string, map[string]interface{}) { 265 | ctx := &types.NamedContext{} 266 | b.query.Write(ctx) 267 | return ctx.Query(), ctx.Values() 268 | } 269 | 270 | // Query returns the underlying query as a regular statement. 271 | func (b Select) Query() (string, []interface{}) { 272 | ctx := &types.StdContext{} 273 | b.query.Write(ctx) 274 | return ctx.Query(), ctx.Values() 275 | } 276 | 277 | // Statement returns underlying statement. 278 | func (b Select) Statement() stmt.Statement { 279 | return b.query 280 | } 281 | 282 | func handleSelectJoin(args []interface{}) stmt.Join { 283 | join := stmt.Join{} 284 | table := stmt.Table{} 285 | 286 | switch value := args[0].(type) { 287 | case string: 288 | table = stmt.NewTable(value) 289 | case stmt.Table: 290 | table = value 291 | default: 292 | panic(fmt.Sprintf("loukoum: cannot use %T as table argument for join clause", args[0])) 293 | } 294 | 295 | switch value := args[1].(type) { 296 | case string: 297 | join = parser.MustParseJoin(value) 298 | case stmt.OnClause: 299 | join = stmt.NewInnerJoin(table, value) 300 | case stmt.InfixOnExpression: 301 | join = stmt.NewInnerJoin(table, value) 302 | default: 303 | panic(fmt.Sprintf("loukoum: cannot use %T as condition for join clause", args[1])) 304 | } 305 | 306 | join.Table = table 307 | 308 | return join 309 | } 310 | 311 | // Ensure that Select is a Builder 312 | var _ Builder = Select{} 313 | -------------------------------------------------------------------------------- /builder/update.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/stmt" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Update is a builder used for "UPDATE" query. 9 | type Update struct { 10 | query stmt.Update 11 | } 12 | 13 | // NewUpdate creates a new Update. 14 | func NewUpdate(arg interface{}) Update { 15 | return Update{ 16 | query: stmt.NewUpdate(ToTable(arg)), 17 | } 18 | } 19 | 20 | // Only sets the ONLY clause. 21 | func (b Update) Only() Update { 22 | b.query.Only = true 23 | return b 24 | } 25 | 26 | // Set adds a SET clause. 27 | func (b Update) Set(args ...interface{}) Update { 28 | if len(args) == 0 { 29 | panic("loukoum: update set clause requires at least one argument") 30 | } 31 | 32 | b.query.Set = MergeSet(b.query.Set, args) 33 | return b 34 | } 35 | 36 | // Using assigns the result of the given expression to 37 | // the columns defined in Set. 38 | func (b Update) Using(args ...interface{}) Update { 39 | if b.query.Set.Pairs.Mode != stmt.PairArrayMode { 40 | panic("loukoum: you can only use Using with column-list syntax") 41 | } 42 | 43 | if len(args) == 0 { 44 | panic("loukoum: using clause requires a column or an expression") 45 | } 46 | 47 | for i := range args { 48 | b.query.Set.Pairs.Use(stmt.NewExpression(args[i])) 49 | } 50 | 51 | return b 52 | } 53 | 54 | // With adds WITH clauses. 55 | func (b Update) With(args ...stmt.WithQuery) Update { 56 | if b.query.With.IsEmpty() { 57 | b.query.With = stmt.NewWith(args) 58 | return b 59 | } 60 | 61 | b.query.With.Queries = append(b.query.With.Queries, args...) 62 | return b 63 | } 64 | 65 | // Where adds WHERE clauses. 66 | func (b Update) Where(condition stmt.Expression) Update { 67 | if b.query.Where.IsEmpty() { 68 | b.query.Where = stmt.NewWhere(condition) 69 | return b 70 | } 71 | 72 | return b.And(condition) 73 | } 74 | 75 | // And adds AND WHERE conditions. 76 | func (b Update) And(condition stmt.Expression) Update { 77 | b.query.Where = b.query.Where.And(condition) 78 | return b 79 | } 80 | 81 | // Or adds OR WHERE conditions. 82 | func (b Update) Or(condition stmt.Expression) Update { 83 | b.query.Where = b.query.Where.Or(condition) 84 | return b 85 | } 86 | 87 | // From sets the FROM clause of the query. 88 | func (b Update) From(arg interface{}) Update { 89 | if !b.query.From.IsEmpty() { 90 | panic("loukoum: update builder has from clause already defined") 91 | } 92 | 93 | b.query.From = ToFrom(arg) 94 | 95 | return b 96 | } 97 | 98 | // Returning adds a RETURNING clause. 99 | func (b Update) Returning(values ...interface{}) Update { 100 | if !b.query.Returning.IsEmpty() { 101 | panic("loukoum: update builder has returning clause already defined") 102 | } 103 | 104 | b.query.Returning = stmt.NewReturning(ToSelectExpressions(values)) 105 | 106 | return b 107 | } 108 | 109 | // Comment adds comment to the query. 110 | func (b Update) Comment(comment string) Update { 111 | b.query.Comment = stmt.NewComment(comment) 112 | 113 | return b 114 | } 115 | 116 | // String returns the underlying query as a raw statement. 117 | // This function should be used for debugging since it doesn't escape anything and is completely 118 | // vulnerable to SQL injection. 119 | // You should use either NamedQuery() or Query()... 120 | func (b Update) String() string { 121 | ctx := &types.RawContext{} 122 | b.query.Write(ctx) 123 | return ctx.Query() 124 | } 125 | 126 | // NamedQuery returns the underlying query as a named statement. 127 | func (b Update) NamedQuery() (string, map[string]interface{}) { 128 | ctx := &types.NamedContext{} 129 | b.query.Write(ctx) 130 | return ctx.Query(), ctx.Values() 131 | } 132 | 133 | // Query returns the underlying query as a regular statement. 134 | func (b Update) Query() (string, []interface{}) { 135 | ctx := &types.StdContext{} 136 | b.query.Write(ctx) 137 | return ctx.Query(), ctx.Values() 138 | } 139 | 140 | // Statement returns underlying statement. 141 | func (b Update) Statement() stmt.Statement { 142 | return b.query 143 | } 144 | 145 | // Ensure that Update is a Builder 146 | var _ Builder = Update{} 147 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package loukoum provides a simple SQL Query Builder. 2 | // At the moment, only PostgreSQL is supported. 3 | // 4 | // If you have to generate complex queries, which rely on various contexts, loukoum is the right tool for you. 5 | // It helps you generate SQL queries from composable parts. 6 | // However, keep in mind it's not an ORM or a Mapper so you have to use a SQL connector 7 | // (like "database/sql" or "sqlx", for example) to execute queries. 8 | // 9 | // If you're afraid to slip a tiny SQL injection manipulating fmt (or a byte buffer...) when you append 10 | // conditions, loukoum is here to protect you against yourself. 11 | // 12 | // For further informations, you can read this documentation: 13 | // https://github.com/ulule/loukoum/blob/master/README.md 14 | // 15 | // Or you can discover loukoum with these examples. 16 | // An "insert" can be generated like that: 17 | // 18 | // builder := loukoum.Insert("comments"). 19 | // Set( 20 | // loukoum.Pair("email", comment.Email), 21 | // loukoum.Pair("status", "waiting"), 22 | // loukoum.Pair("message", comment.Message), 23 | // loukoum.Pair("created_at", loukoum.Raw("NOW()")), 24 | // ). 25 | // Returning("id") 26 | // 27 | // Also, if you need an upsert, you can define a "on conflict" clause: 28 | // 29 | // builder := loukoum.Insert("comments"). 30 | // Set( 31 | // loukoum.Pair("email", comment.Email), 32 | // loukoum.Pair("status", "waiting"), 33 | // loukoum.Pair("message", comment.Message), 34 | // loukoum.Pair("created_at", loukoum.Raw("NOW()")), 35 | // ). 36 | // OnConflict("email", loukoum.DoUpdate( 37 | // loukoum.Pair("message", comment.Message), 38 | // loukoum.Pair("status", "waiting"), 39 | // loukoum.Pair("created_at", loukoum.Raw("NOW()")), 40 | // loukoum.Pair("deleted_at", nil), 41 | // )). 42 | // Returning("id") 43 | // 44 | // Updating a news is also simple: 45 | // 46 | // builder := loukoum.Update("news"). 47 | // Set( 48 | // loukoum.Pair("published_at", loukoum.Raw("NOW()")), 49 | // loukoum.Pair("status", "published"), 50 | // ). 51 | // Where(loukoum.Condition("id").Equal(news.ID)). 52 | // And(loukoum.Condition("deleted_at").IsNull(true)). 53 | // Returning("published_at") 54 | // 55 | // You can remove a specific user: 56 | // 57 | // builder := loukoum.Delete("users"). 58 | // Where(loukoum.Condition("id").Equal(user.ID)) 59 | // 60 | // Or select a list of users... 61 | // 62 | // builder := loukoum.Select("id", "first_name", "last_name", "email"). 63 | // From("users"). 64 | // Where(loukoum.Condition("deleted_at").IsNull(true)) 65 | // 66 | package loukoum 67 | -------------------------------------------------------------------------------- /docs/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulule/loukoum/c9f4a70baef755693db09cc8fdfdc274817639ab/docs/images/banner.png -------------------------------------------------------------------------------- /docs/images/loukoum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulule/loukoum/c9f4a70baef755693db09cc8fdfdc274817639ab/docs/images/loukoum.png -------------------------------------------------------------------------------- /docs/images/square.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulule/loukoum/c9f4a70baef755693db09cc8fdfdc274817639ab/docs/images/square.jpg -------------------------------------------------------------------------------- /docs/images/square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulule/loukoum/c9f4a70baef755693db09cc8fdfdc274817639ab/docs/images/square.png -------------------------------------------------------------------------------- /examples/bootstrap.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id SERIAL NOT NULL PRIMARY KEY, 3 | deleted_at timestamp with time zone, 4 | first_name character varying(255) NOT NULL, 5 | last_name character varying(255) NOT NULL, 6 | email character varying(255) NOT NULL, 7 | is_staff boolean DEFAULT false NOT NULL 8 | ); 9 | 10 | INSERT INTO users (email, first_name, last_name, is_staff) 11 | VALUES ('tech@ulule.com', 'Ulule', 'Tech', true); 12 | 13 | INSERT INTO users (email, first_name, last_name, is_staff) 14 | VALUES ('thomas.leroux@ulule.com', 'Thomas', 'LE ROUX', true); 15 | 16 | CREATE TABLE news ( 17 | id SERIAL NOT NULL PRIMARY KEY, 18 | published_at timestamp with time zone, 19 | deleted_at timestamp with time zone, 20 | status character varying(255) NOT NULL 21 | ); 22 | 23 | INSERT INTO news (status) VALUES ('draft'); 24 | 25 | CREATE TABLE comments ( 26 | id SERIAL NOT NULL PRIMARY KEY, 27 | created_at timestamp with time zone, 28 | deleted_at timestamp with time zone, 29 | email character varying(255) NOT NULL, 30 | status character varying(255) NOT NULL, 31 | user_id integer NOT NULL, 32 | message character varying(2048) NOT NULL 33 | ); 34 | 35 | ALTER TABLE ONLY comments 36 | ADD CONSTRAINT comments_user_id_fkey FOREIGN KEY (user_id) 37 | REFERENCES users(id) ON DELETE CASCADE; 38 | 39 | ALTER TABLE ONLY comments 40 | ADD CONSTRAINT comments_email_idx UNIQUE (email); 41 | 42 | INSERT INTO comments (email, user_id, status, message, created_at) 43 | VALUES ('thomas.leroux@ulule.com', 2, 'waiting', 'Hello world', NOW()); 44 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ulule/loukoum/examples 2 | 3 | go 1.13 4 | 5 | replace github.com/ulule/loukoum/v3 => ../ 6 | 7 | require ( 8 | github.com/jmoiron/sqlx v1.2.0 9 | github.com/lib/pq v1.2.0 10 | github.com/ulule/loukoum/v3 v3.0.0-00010101000000-000000000000 11 | google.golang.org/appengine v1.6.4 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= 4 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 5 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 6 | github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= 7 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 8 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= 9 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 10 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 11 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 12 | github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= 13 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 14 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 15 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 20 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 21 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 22 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 23 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 24 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 25 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 26 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 31 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 32 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 33 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 34 | google.golang.org/appengine v1.6.4 h1:WiKh4+/eMB2HaY7QhCfW/R7MuRAoA8QMCSJA6jP5/fo= 35 | google.golang.org/appengine v1.6.4/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 39 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 40 | -------------------------------------------------------------------------------- /examples/named/comment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jmoiron/sqlx" 5 | "github.com/lib/pq" 6 | lk "github.com/ulule/loukoum/v3" 7 | ) 8 | 9 | // Comment model. 10 | type Comment struct { 11 | ID int64 `db:"id"` 12 | Email string `db:"email"` 13 | Status string `db:"status"` 14 | Message string `db:"message"` 15 | UserID int64 `db:"user_id"` 16 | CreatedAt pq.NullTime `db:"created_at"` 17 | DeletedAt pq.NullTime `db:"deleted_at"` 18 | } 19 | 20 | // FindComments retrieves comments by users. 21 | func FindComments(db *sqlx.DB) ([]Comment, error) { 22 | builder := lk. 23 | Select( 24 | "comments.id", "comments.email", "comments.status", 25 | "comments.user_id", "comments.message", "comments.created_at", 26 | ). 27 | From("comments"). 28 | Join(lk.Table("users"), lk.On("comments.user_id", "users.id")). 29 | Where(lk.Condition("comments.deleted_at").IsNull(true)) 30 | 31 | // query: SELECT comments.id, comments.email, comments.status, comments.user_id, comments.message, 32 | // comments.created_at FROM comments INNER JOIN users ON comments.user_id = users.id 33 | // WHERE (comments.deleted_at IS NULL) 34 | // args: map[string]interface{}{ 35 | // 36 | // } 37 | query, args := builder.NamedQuery() 38 | 39 | stmt, err := db.PrepareNamed(query) 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer stmt.Close() 44 | 45 | comments := []Comment{} 46 | 47 | err = stmt.Select(&comments, args) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return comments, nil 53 | } 54 | 55 | // FindStaffComments retrieves comments by staff users. 56 | func FindStaffComments(db *sqlx.DB) ([]Comment, error) { 57 | builder := lk.Select("id", "email", "status", "user_id", "message", "created_at"). 58 | From("comments"). 59 | Where(lk.Condition("deleted_at").IsNull(true)). 60 | Where( 61 | lk.Condition("user_id").In( 62 | lk.Select("id"). 63 | From("users"). 64 | Where(lk.Condition("is_staff").Equal(true)), 65 | ), 66 | ) 67 | 68 | // query: SELECT id, email, status, user_id, message, created_at 69 | // FROM comments WHERE ((deleted_at IS NULL) AND 70 | // (user_id IN (SELECT id FROM users WHERE (is_staff = :arg_1)))) 71 | // args: map[string]interface{}{ 72 | // "arg_1": bool(true), 73 | // } 74 | query, args := builder.NamedQuery() 75 | 76 | stmt, err := db.PrepareNamed(query) 77 | if err != nil { 78 | return nil, err 79 | } 80 | defer stmt.Close() 81 | 82 | comments := []Comment{} 83 | 84 | err = stmt.Select(&comments, args) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | return comments, nil 90 | } 91 | 92 | // CreateComment creates a comment. 93 | func CreateComment(db *sqlx.DB, comment Comment) (Comment, error) { 94 | builder := lk.Insert("comments"). 95 | Set( 96 | lk.Pair("email", comment.Email), 97 | lk.Pair("status", "waiting"), 98 | lk.Pair("message", comment.Message), 99 | lk.Pair("user_id", comment.UserID), 100 | lk.Pair("created_at", lk.Raw("NOW()")), 101 | ). 102 | Returning("id") 103 | 104 | // query: INSERT INTO comments (created_at, email, message, status, user_id) 105 | // VALUES (NOW(), :arg_1, :arg_2, :arg_3, :arg_4) RETURNING id 106 | // args: map[string]interface{}{ 107 | // "arg_1": string(comment.Email), 108 | // "arg_2": string(comment.Message), 109 | // "arg_3": string("waiting"), 110 | // "arg_4": string(comment.UserID), 111 | // } 112 | query, args := builder.NamedQuery() 113 | 114 | stmt, err := db.PrepareNamed(query) 115 | if err != nil { 116 | return comment, err 117 | } 118 | defer stmt.Close() 119 | 120 | err = stmt.Get(&comment, args) 121 | if err != nil { 122 | return comment, err 123 | } 124 | 125 | return comment, nil 126 | } 127 | 128 | // UpsertComment inserts or updates a comment based on the email attribute. 129 | func UpsertComment(db *sqlx.DB, comment Comment) (Comment, error) { 130 | builder := lk.Insert("comments"). 131 | Set( 132 | lk.Pair("email", comment.Email), 133 | lk.Pair("status", "waiting"), 134 | lk.Pair("message", comment.Message), 135 | lk.Pair("user_id", comment.UserID), 136 | lk.Pair("created_at", lk.Raw("NOW()")), 137 | ). 138 | OnConflict("email", lk.DoUpdate( 139 | lk.Pair("message", comment.Message), 140 | lk.Pair("user_id", comment.UserID), 141 | lk.Pair("status", "waiting"), 142 | lk.Pair("created_at", lk.Raw("NOW()")), 143 | lk.Pair("deleted_at", nil), 144 | )). 145 | Returning("id, created_at") 146 | 147 | // query: INSERT INTO comments (created_at, email, message, status, user_id) 148 | // VALUES (NOW(), :arg_1, :arg_2, :arg_3, :arg_4) 149 | // ON CONFLICT (email) DO UPDATE SET created_at = NOW(), deleted_at = NULL, message = :arg_5, 150 | // status = :arg_6, user_id = :arg_7 RETURNING id, created_at 151 | // args: map[string]interface{}{ 152 | // "arg_1": string(comment.Email), 153 | // "arg_2": string(comment.Message), 154 | // "arg_3": string("waiting"), 155 | // "arg_4": string(comment.UserID), 156 | // "arg_5": string(comment.Message), 157 | // "arg_6": string("waiting"), 158 | // "arg_7": string(comment.UserID), 159 | // } 160 | query, args := builder.NamedQuery() 161 | 162 | stmt, err := db.PrepareNamed(query) 163 | if err != nil { 164 | return comment, err 165 | } 166 | defer stmt.Close() 167 | 168 | err = stmt.Get(&comment, args) 169 | if err != nil { 170 | return comment, err 171 | } 172 | 173 | return comment, nil 174 | } 175 | -------------------------------------------------------------------------------- /examples/named/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/jmoiron/sqlx" 8 | ) 9 | 10 | func main() { 11 | db, err := sqlx.Connect("postgres", os.Getenv("DATABASE_DSN")) 12 | if err != nil { 13 | log.Fatalln(err) 14 | } 15 | 16 | _, err = FindUsers(db) 17 | if err != nil { 18 | log.Fatalln(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/named/news.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jmoiron/sqlx" 5 | "github.com/lib/pq" 6 | lk "github.com/ulule/loukoum/v3" 7 | ) 8 | 9 | // News model. 10 | type News struct { 11 | ID int64 `db:"id"` 12 | Status string `db:"status"` 13 | PublishedAt pq.NullTime `db:"deleted_at"` 14 | DeletedAt pq.NullTime `db:"deleted_at"` 15 | } 16 | 17 | // PublishNews publishes a news. 18 | func PublishNews(db *sqlx.DB, news News) (News, error) { 19 | builder := lk.Update("news"). 20 | Set( 21 | lk.Pair("published_at", lk.Raw("NOW()")), 22 | lk.Pair("status", "published"), 23 | ). 24 | Where(lk.Condition("id").Equal(news.ID)). 25 | And(lk.Condition("deleted_at").IsNull(true)). 26 | Returning("published_at") 27 | 28 | // query: UPDATE news SET published_at = NOW(), status = :arg_1 WHERE ((id = :arg_2) AND (deleted_at IS NULL)) 29 | // RETURNING published_at 30 | // args: map[string]interface{}{ 31 | // "arg_1": string("published"), 32 | // "arg_2": int64(news.ID), 33 | // } 34 | query, args := builder.NamedQuery() 35 | 36 | stmt, err := db.PrepareNamed(query) 37 | if err != nil { 38 | return news, err 39 | } 40 | defer stmt.Close() 41 | 42 | err = stmt.Get(&news, args) 43 | if err != nil { 44 | return news, err 45 | } 46 | 47 | return news, nil 48 | } 49 | -------------------------------------------------------------------------------- /examples/named/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jmoiron/sqlx" 5 | "github.com/lib/pq" 6 | lk "github.com/ulule/loukoum/v3" 7 | ) 8 | 9 | // User model. 10 | type User struct { 11 | ID int64 `db:"id"` 12 | FirstName string `db:"first_name"` 13 | LastName string `db:"last_name"` 14 | Email string `db:"email"` 15 | IsStaff bool `db:"is_staff"` 16 | DeletedAt pq.NullTime `db:"deleted_at"` 17 | } 18 | 19 | // FindUsers retrieves non-deleted users 20 | func FindUsers(db *sqlx.DB) ([]User, error) { 21 | builder := lk.Select("id", "first_name", "last_name", "email"). 22 | From("users"). 23 | Where(lk.Condition("deleted_at").IsNull(true)) 24 | 25 | // query: SELECT id, first_name, last_name, email FROM users WHERE (deleted_at IS NULL) 26 | // args: map[string]interface{}{ 27 | // 28 | // } 29 | query, args := builder.NamedQuery() 30 | 31 | stmt, err := db.PrepareNamed(query) 32 | if err != nil { 33 | return nil, err 34 | } 35 | defer stmt.Close() 36 | 37 | users := []User{} 38 | 39 | err = stmt.Select(&users, args) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return users, nil 45 | } 46 | 47 | // DeleteUser deletes a user. 48 | func DeleteUser(db *sqlx.DB, user User) error { 49 | builder := lk.Delete("users"). 50 | Where(lk.Condition("id").Equal(user.ID)) 51 | 52 | // query: DELETE FROM users WHERE (id = :arg_1) 53 | // args: map[string]interface{}{ 54 | // "arg_1": int64(user.ID), 55 | // } 56 | query, args := builder.NamedQuery() 57 | 58 | stmt, err := db.PrepareNamed(query) 59 | if err != nil { 60 | return err 61 | } 62 | defer stmt.Close() 63 | 64 | _, err = stmt.Exec(args) 65 | 66 | return err 67 | } 68 | -------------------------------------------------------------------------------- /examples/standard/comment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/lib/pq" 7 | lk "github.com/ulule/loukoum/v3" 8 | ) 9 | 10 | // Comment model. 11 | type Comment struct { 12 | ID int64 `db:"id"` 13 | Email string `db:"email"` 14 | Status string `db:"status"` 15 | Message string `db:"message"` 16 | UserID int64 `db:"user_id"` 17 | CreatedAt pq.NullTime `db:"created_at"` 18 | DeletedAt pq.NullTime `db:"deleted_at"` 19 | } 20 | 21 | // FindComments retrieves comments by users. 22 | func FindComments(db *sql.DB) ([]Comment, error) { 23 | builder := lk. 24 | Select( 25 | "comments.id", "comments.email", "comments.status", 26 | "comments.user_id", "comments.message", "comments.created_at", 27 | ). 28 | From("comments"). 29 | Join(lk.Table("users"), lk.On("comments.user_id", "users.id")). 30 | Where(lk.Condition("comments.deleted_at").IsNull(true)) 31 | 32 | // query: SELECT comments.id, comments.email, comments.status, comments.user_id, comments.message, 33 | // comments.created_at FROM comments INNER JOIN users ON comments.user_id = users.id 34 | // WHERE (comments.deleted_at IS NULL) 35 | // args: []interface{}{ 36 | // 37 | // } 38 | query, args := builder.Query() 39 | 40 | stmt, err := db.Query(query, args...) 41 | if err != nil { 42 | return nil, err 43 | } 44 | defer stmt.Close() 45 | 46 | comments := []Comment{} 47 | 48 | for stmt.Next() { 49 | comment := Comment{} 50 | err = stmt.Scan( 51 | &comment.ID, &comment.Email, &comment.Status, 52 | &comment.UserID, &comment.Message, &comment.CreatedAt, 53 | ) 54 | if err != nil { 55 | return nil, err 56 | } 57 | comments = append(comments, comment) 58 | } 59 | 60 | err = stmt.Err() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return comments, nil 66 | } 67 | 68 | // FindStaffComments retrieves comments by staff users. 69 | func FindStaffComments(db *sql.DB) ([]Comment, error) { 70 | builder := lk.Select("id", "email", "status", "user_id", "message", "created_at"). 71 | From("comments"). 72 | Where(lk.Condition("deleted_at").IsNull(true)). 73 | Where( 74 | lk.Condition("user_id").In( 75 | lk.Select("id"). 76 | From("users"). 77 | Where(lk.Condition("is_staff").Equal(true)), 78 | ), 79 | ) 80 | 81 | // query: SELECT id, email, status, user_id, message, created_at 82 | // FROM comments WHERE ((deleted_at IS NULL) AND 83 | // (user_id IN (SELECT id FROM users WHERE (is_staff = $1)))) 84 | // args: []interface{}{ 85 | // bool(true), 86 | // } 87 | query, args := builder.Query() 88 | 89 | stmt, err := db.Query(query, args...) 90 | if err != nil { 91 | return nil, err 92 | } 93 | defer stmt.Close() 94 | 95 | comments := []Comment{} 96 | 97 | for stmt.Next() { 98 | comment := Comment{} 99 | err = stmt.Scan( 100 | &comment.ID, &comment.Email, &comment.Status, 101 | &comment.UserID, &comment.Message, &comment.CreatedAt, 102 | ) 103 | if err != nil { 104 | return nil, err 105 | } 106 | comments = append(comments, comment) 107 | } 108 | 109 | err = stmt.Err() 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | return comments, nil 115 | } 116 | 117 | // CreateComment creates a comment. 118 | func CreateComment(db *sql.DB, comment Comment) (Comment, error) { 119 | builder := lk.Insert("comments"). 120 | Set( 121 | lk.Pair("email", comment.Email), 122 | lk.Pair("status", "waiting"), 123 | lk.Pair("message", comment.Message), 124 | lk.Pair("user_id", comment.UserID), 125 | lk.Pair("created_at", lk.Raw("NOW()")), 126 | ). 127 | Returning("id") 128 | 129 | // query: INSERT INTO comments (created_at, email, message, status, user_id) 130 | // VALUES (NOW(), $1, $2, $3, $4) RETURNING id 131 | // args: []interface{}{ 132 | // string(comment.Email), 133 | // string(comment.Message), 134 | // string("waiting"), 135 | // int64(comment.UserID), 136 | // } 137 | query, args := builder.Query() 138 | 139 | stmt, err := db.Query(query, args...) 140 | if err != nil { 141 | return comment, err 142 | } 143 | defer stmt.Close() 144 | 145 | for stmt.Next() { 146 | err = stmt.Scan(&comment.ID) 147 | if err != nil { 148 | return comment, err 149 | } 150 | } 151 | 152 | err = stmt.Err() 153 | return comment, err 154 | } 155 | 156 | // UpsertComment inserts or updates a comment based on the email attribute. 157 | func UpsertComment(db *sql.DB, comment Comment) (Comment, error) { 158 | builder := lk.Insert("comments"). 159 | Set( 160 | lk.Pair("email", comment.Email), 161 | lk.Pair("status", "waiting"), 162 | lk.Pair("message", comment.Message), 163 | lk.Pair("user_id", comment.UserID), 164 | lk.Pair("created_at", lk.Raw("NOW()")), 165 | ). 166 | OnConflict("email", lk.DoUpdate( 167 | lk.Pair("message", comment.Message), 168 | lk.Pair("user_id", comment.UserID), 169 | lk.Pair("status", "waiting"), 170 | lk.Pair("created_at", lk.Raw("NOW()")), 171 | lk.Pair("deleted_at", nil), 172 | )). 173 | Returning("id, created_at") 174 | 175 | // query: INSERT INTO comments (created_at, email, message, status, user_id) VALUES (NOW(), $1, $2, $3, $4) 176 | // ON CONFLICT (email) DO UPDATE SET created_at = NOW(), deleted_at = NULL, message = $5, status = $6, 177 | // user_id = $7 RETURNING id, created_at 178 | // args: []interface{}{ 179 | // string(comments.Email), 180 | // string(comments.Message), 181 | // string("waiting"), 182 | // int64(comment.UserID), 183 | // string(comments.Message), 184 | // string("waiting"), 185 | // int64(comment.UserID), 186 | // } 187 | query, args := builder.Query() 188 | 189 | stmt, err := db.Query(query, args...) 190 | if err != nil { 191 | return comment, err 192 | } 193 | defer stmt.Close() 194 | 195 | for stmt.Next() { 196 | err = stmt.Scan(&comment.ID, &comment.CreatedAt) 197 | if err != nil { 198 | return comment, err 199 | } 200 | } 201 | 202 | err = stmt.Err() 203 | return comment, err 204 | } 205 | -------------------------------------------------------------------------------- /examples/standard/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "os" 7 | 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | func main() { 12 | db, err := sql.Open("postgres", os.Getenv("DATABASE_DSN")) 13 | if err != nil { 14 | log.Fatalln(err) 15 | } 16 | 17 | _, err = FindUsers(db) 18 | if err != nil { 19 | log.Fatalln(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/standard/news.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/lib/pq" 7 | lk "github.com/ulule/loukoum/v3" 8 | ) 9 | 10 | // News model. 11 | type News struct { 12 | ID int64 `db:"id"` 13 | Status string `db:"status"` 14 | PublishedAt pq.NullTime `db:"deleted_at"` 15 | DeletedAt pq.NullTime `db:"deleted_at"` 16 | } 17 | 18 | // PublishNews publishes a news. 19 | func PublishNews(db *sql.DB, news News) (News, error) { 20 | builder := lk.Update("news"). 21 | Set( 22 | lk.Pair("published_at", lk.Raw("NOW()")), 23 | lk.Pair("status", "published"), 24 | ). 25 | Where(lk.Condition("id").Equal(news.ID)). 26 | And(lk.Condition("deleted_at").IsNull(true)). 27 | Returning("published_at") 28 | 29 | // query: UPDATE news SET published_at = NOW(), status = $1 WHERE ((id = $2) AND (deleted_at IS NULL)) 30 | // RETURNING published_at 31 | // args: []interface{}{ 32 | // string("published"), 33 | // int64(news.ID), 34 | // } 35 | query, args := builder.Query() 36 | 37 | stmt, err := db.Query(query, args...) 38 | if err != nil { 39 | return news, err 40 | } 41 | defer stmt.Close() 42 | 43 | if stmt.Next() { 44 | stmt.Scan(&news.PublishedAt) 45 | } 46 | 47 | err = stmt.Err() 48 | return news, err 49 | } 50 | -------------------------------------------------------------------------------- /examples/standard/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/lib/pq" 7 | lk "github.com/ulule/loukoum/v3" 8 | ) 9 | 10 | // User model. 11 | type User struct { 12 | ID int64 `db:"id"` 13 | FirstName string `db:"first_name"` 14 | LastName string `db:"last_name"` 15 | Email string `db:"email"` 16 | IsStaff bool `db:"is_staff"` 17 | DeletedAt pq.NullTime `db:"deleted_at"` 18 | } 19 | 20 | // FindUsers retrieves non-deleted users. 21 | func FindUsers(db *sql.DB) ([]User, error) { 22 | builder := lk.Select("id", "first_name", "last_name", "email"). 23 | From("users"). 24 | Where(lk.Condition("deleted_at").IsNull(true)) 25 | 26 | // query: SELECT id, first_name, last_name, email FROM users WHERE (deleted_at IS NULL) 27 | // args: []interface{}{ 28 | // 29 | // } 30 | query, args := builder.Query() 31 | 32 | stmt, err := db.Query(query, args...) 33 | if err != nil { 34 | return nil, err 35 | } 36 | defer stmt.Close() 37 | 38 | users := []User{} 39 | 40 | for stmt.Next() { 41 | user := User{} 42 | err = stmt.Scan(&user.ID, &user.FirstName, &user.LastName, &user.Email) 43 | if err != nil { 44 | return nil, err 45 | } 46 | users = append(users, user) 47 | } 48 | 49 | err = stmt.Err() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return users, nil 55 | } 56 | 57 | // DeleteUser deletes a user. 58 | func DeleteUser(db *sql.DB, user User) error { 59 | builder := lk.Delete("users"). 60 | Where(lk.Condition("id").Equal(user.ID)) 61 | 62 | // query: DELETE FROM users WHERE (id = $1) 63 | // args: []interface{}{ 64 | // int64(user.ID), 65 | // } 66 | query, args := builder.Query() 67 | 68 | _, err := db.Exec(query, args...) 69 | return err 70 | } 71 | -------------------------------------------------------------------------------- /format/doc.go: -------------------------------------------------------------------------------- 1 | // Package format escape various types for types.RawContext. 2 | package format 3 | -------------------------------------------------------------------------------- /format/format.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "bytes" 5 | "database/sql/driver" 6 | "encoding/hex" 7 | "fmt" 8 | "reflect" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // Value formats the given value. 14 | func Value(arg interface{}) string { // nolint: gocyclo 15 | if arg == nil { 16 | return "NULL" 17 | } 18 | 19 | switch value := arg.(type) { 20 | case string: 21 | return String(value) 22 | case []byte: 23 | return Bytes(value) 24 | case time.Time: 25 | return Time(value) 26 | case driver.Valuer: 27 | reflectvalue := reflect.ValueOf(value) 28 | if reflectvalue.Kind() == reflect.Ptr && 29 | reflectvalue.IsNil() { 30 | return "NULL" 31 | } 32 | 33 | v, err := value.Value() 34 | if err != nil { 35 | panic("loukoum: was not able to retrieve valuer value") 36 | } 37 | return Value(v) 38 | case int: 39 | return Int(int64(value)) 40 | case int8: 41 | return Int(int64(value)) 42 | case int16: 43 | return Int(int64(value)) 44 | case int32: 45 | return Int(int64(value)) 46 | case int64: 47 | return Int(value) 48 | case uint: 49 | return Uint(uint64(value)) 50 | case uint8: 51 | return Uint(uint64(value)) 52 | case uint16: 53 | return Uint(uint64(value)) 54 | case uint32: 55 | return Uint(uint64(value)) 56 | case uint64: 57 | return Uint(value) 58 | case bool: 59 | return Bool(value) 60 | case float32: 61 | return Float(float64(value)) 62 | case float64: 63 | return Float(value) 64 | default: 65 | return fmt.Sprint(value) 66 | } 67 | } 68 | 69 | // String formats the given string. 70 | func String(value string) string { 71 | buffer := &bytes.Buffer{} 72 | writeRune(buffer, '\'') 73 | for _, char := range value { 74 | switch char { 75 | case '\'': 76 | writeString(buffer, `\'`) 77 | case '\\': 78 | writeString(buffer, `\\`) 79 | case '\n': 80 | writeString(buffer, `\n`) 81 | case '\r': 82 | writeString(buffer, `\r`) 83 | case '\t': 84 | writeString(buffer, `\t`) 85 | default: 86 | writeRune(buffer, char) 87 | } 88 | } 89 | writeRune(buffer, '\'') 90 | return buffer.String() 91 | } 92 | 93 | // Bytes formats the give bytes. 94 | func Bytes(value []byte) string { 95 | encoded := hex.EncodeToString(value) 96 | return fmt.Sprintf("decode('%s', 'hex')", encoded) 97 | } 98 | 99 | // Int formats the given number. 100 | func Int(value int64) string { 101 | return strconv.FormatInt(value, 10) 102 | } 103 | 104 | // Uint formats the given number. 105 | func Uint(value uint64) string { 106 | return strconv.FormatUint(value, 10) 107 | } 108 | 109 | // Bool formats the given boolean. 110 | func Bool(value bool) string { 111 | return strconv.FormatBool(value) 112 | } 113 | 114 | // Float formats the given number. 115 | func Float(value float64) string { 116 | return strconv.FormatFloat(value, 'f', -1, 64) 117 | } 118 | 119 | // Time formats the given time. 120 | func Time(value time.Time) string { 121 | return fmt.Sprint("'", value.UTC().Format("2006-01-02 15:04:05.999999"), "+00'") 122 | } 123 | 124 | // nolint: interfacer 125 | func writeRune(buffer *bytes.Buffer, chunk rune) { 126 | _, err := buffer.WriteRune(chunk) 127 | if err != nil { 128 | panic("loukoum: cannot write on bytes buffer") 129 | } 130 | } 131 | 132 | // nolint: interfacer 133 | func writeString(buffer *bytes.Buffer, chunk string) { 134 | _, err := buffer.WriteString(chunk) 135 | if err != nil { 136 | panic("loukoum: cannot write on bytes buffer") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ulule/loukoum/v3 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/jackc/pgx/v5 v5.1.1 7 | github.com/lib/pq v1.10.4 8 | github.com/pkg/errors v0.9.1 9 | github.com/stretchr/testify v1.8.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/kr/text v0.2.0 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | github.com/rogpeppe/go-internal v1.9.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 6 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 7 | github.com/jackc/pgx/v5 v5.1.1 h1:pZD79K1SYv8wc2HmCQA6VdmRQi7/OtCfv9bM3WAXUYA= 8 | github.com/jackc/pgx/v5 v5.1.1/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk= 9 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= 13 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 14 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 15 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 19 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 24 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 25 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= 26 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 29 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | -------------------------------------------------------------------------------- /lexer/iteratee.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | ) 6 | 7 | // An Iteratee contains a collection of token from a Lexer. 8 | type Iteratee struct { 9 | cursor int 10 | list []token.Token 11 | } 12 | 13 | // HasNext defines if a token is available. 14 | func (it Iteratee) HasNext() bool { 15 | return it.cursor < len(it.list) 16 | } 17 | 18 | // Is defines if next token has given type. 19 | func (it Iteratee) Is(next token.Type) bool { 20 | if !it.HasNext() { 21 | return false 22 | } 23 | return it.list[it.cursor].Type == next 24 | } 25 | 26 | // Next returns the next token. 27 | func (it *Iteratee) Next() token.Token { 28 | element := it.list[it.cursor] 29 | it.cursor++ 30 | return element 31 | } 32 | -------------------------------------------------------------------------------- /lexer/lexer.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | 7 | "github.com/ulule/loukoum/v3/token" 8 | ) 9 | 10 | const ( 11 | // EOF sentinel 12 | eof = 0 13 | // Newline sentinel 14 | newline = '\n' 15 | ) 16 | 17 | // A Lexer will return a list of Token from a reader. 18 | type Lexer struct { 19 | input *bufio.Reader 20 | e0, en rune // current/next rune in reader 21 | } 22 | 23 | // New return a new Lexer from given source. 24 | func New(input io.Reader) Lexer { 25 | 26 | buffer := bufio.NewReader(input) 27 | 28 | l := Lexer{} 29 | l.input = buffer 30 | 31 | // Read two runes so current and next items are both available. 32 | l.read() 33 | l.read() 34 | 35 | return l 36 | } 37 | 38 | func (l *Lexer) read() rune { 39 | e, _, err := l.input.ReadRune() 40 | 41 | if err != nil && err != io.EOF { 42 | //l.error(err.Error()) 43 | // TODO FIX ME 44 | panic(err) 45 | } 46 | 47 | if err == io.EOF { 48 | e = eof 49 | } 50 | 51 | l.e0 = l.en 52 | 53 | l.en = e 54 | return l.e0 55 | } 56 | 57 | // current return the current rune in reader. 58 | func (l *Lexer) current() rune { 59 | return l.e0 60 | } 61 | 62 | // next return the next rune in reader. 63 | // nolint: deadcode, megacheck 64 | func (l *Lexer) next() rune { 65 | return l.en 66 | } 67 | 68 | // Iterator returns an Iteratee from reader. 69 | func (l *Lexer) Iterator() *Iteratee { 70 | list := []token.Token{} 71 | for { 72 | current := l.Next() 73 | if current.Type == token.EOF { 74 | break 75 | } 76 | list = append(list, current) 77 | } 78 | return &Iteratee{list: list} 79 | } 80 | 81 | // Next will return the next token on reader. 82 | func (l *Lexer) Next() token.Token { 83 | 84 | l.skipWhitespace() 85 | 86 | if l.current() == eof { 87 | return l.getToken(token.EOF) 88 | } 89 | 90 | if l.current() == newline { 91 | return l.getToken(token.Semicolon) 92 | } 93 | 94 | t, ok := l.getOperatorToken() 95 | if ok { 96 | return t 97 | } 98 | 99 | t, ok = l.getDelimiterToken() 100 | if ok { 101 | return t 102 | } 103 | 104 | return l.getDefaultToken() 105 | } 106 | 107 | func (l *Lexer) getToken(t token.Type) token.Token { 108 | defer l.read() 109 | switch t { 110 | case token.EOF: 111 | return token.New(t, "") 112 | case token.Semicolon: 113 | return token.New(t, ";") 114 | default: 115 | return token.New(t, string(l.current())) 116 | } 117 | } 118 | 119 | func (l *Lexer) getOperatorToken() (token.Token, bool) { 120 | switch l.current() { 121 | case '*': 122 | return l.getToken(token.Asterisk), true 123 | case '=': 124 | return l.getToken(token.Equals), true 125 | default: 126 | return token.Token{}, false 127 | } 128 | } 129 | 130 | func (l *Lexer) getDelimiterToken() (token.Token, bool) { 131 | switch l.current() { 132 | case ';': 133 | return l.getToken(token.Semicolon), true 134 | case ',': 135 | return l.getToken(token.Comma), true 136 | case ':': 137 | return l.getToken(token.Colon), true 138 | case '(': 139 | return l.getToken(token.LParen), true 140 | case ')': 141 | return l.getToken(token.RParen), true 142 | default: 143 | return token.Token{}, false 144 | } 145 | } 146 | 147 | func (l *Lexer) getDefaultToken() token.Token { 148 | 149 | if isLetter(l.current()) || isDigit(l.current()) { 150 | return l.getIdentifier() 151 | } 152 | 153 | return l.getToken(token.Illegal) 154 | } 155 | 156 | func (l *Lexer) getIdentifier() token.Token { 157 | 158 | t := token.Token{} 159 | v, ok := l.readIdentifier() 160 | 161 | t.Value = v 162 | t.Type = token.Lookup(t.Value) 163 | 164 | if !ok { 165 | t.Type = token.Illegal 166 | } 167 | 168 | return t 169 | } 170 | 171 | func (l *Lexer) skipWhitespace() { 172 | for isWhitespace(l.current()) { 173 | l.read() 174 | } 175 | } 176 | 177 | func (l *Lexer) readIdentifier() (string, bool) { 178 | 179 | buffer := []rune{} 180 | 181 | for isLetter(l.current()) || isDigit(l.current()) { 182 | buffer = append(buffer, l.current()) 183 | l.read() 184 | } 185 | 186 | return string(buffer), true 187 | } 188 | 189 | func isLetter(e rune) bool { 190 | return 'a' <= e && e <= 'z' || 'A' <= e && e <= 'Z' || e == '_' || e == '.' 191 | } 192 | 193 | func isWhitespace(e rune) bool { 194 | return e == ' ' || e == '\t' || e == '\n' || e == '\r' 195 | } 196 | 197 | func isDigit(e rune) bool { 198 | return '0' <= e && e <= '9' 199 | } 200 | 201 | // nolint: deadcode, megacheck 202 | func isHex(e rune) bool { 203 | return isDigit(e) || 'a' <= e && e <= 'f' || 'A' <= e && e <= 'F' 204 | } 205 | -------------------------------------------------------------------------------- /lexer/lexer_test.go: -------------------------------------------------------------------------------- 1 | package lexer_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/ulule/loukoum/v3/lexer" 11 | "github.com/ulule/loukoum/v3/token" 12 | ) 13 | 14 | type LexScenario struct { 15 | Input string 16 | Tokens []token.Token 17 | } 18 | 19 | func execute(t *testing.T, scenarios []LexScenario) { 20 | is := require.New(t) 21 | 22 | for i, scenario := range scenarios { 23 | 24 | l := lexer.New(strings.NewReader(scenario.Input)) 25 | 26 | for y, expected := range scenario.Tokens { 27 | message := fmt.Sprintf("Scenario #%d / Token #%d", (i + 1), (y + 1)) 28 | 29 | actual := l.Next() 30 | 31 | is.Equal(expected.Type, actual.Type, message) 32 | is.Equal(expected.Value, actual.Value, message) 33 | } 34 | 35 | actual := l.Next() 36 | is.Equal(token.EOF, actual.Type, "EOF was expected") 37 | } 38 | } 39 | 40 | func TestNextToken(t *testing.T) { 41 | 42 | tests := []LexScenario{} 43 | 44 | // Scenario #1: Check EOF on empty source 45 | tests = append(tests, LexScenario{ 46 | Input: ``, 47 | Tokens: []token.Token{ 48 | token.New(token.EOF, ""), 49 | token.New(token.EOF, ""), 50 | token.New(token.EOF, ""), 51 | token.New(token.EOF, ""), 52 | token.New(token.EOF, ""), 53 | token.New(token.EOF, ""), 54 | token.New(token.EOF, ""), 55 | token.New(token.EOF, ""), 56 | token.New(token.EOF, ""), 57 | token.New(token.EOF, ""), 58 | token.New(token.EOF, ""), 59 | token.New(token.EOF, ""), 60 | }, 61 | }) 62 | 63 | // Scenario #2: A simple SELECT query 64 | tests = append(tests, LexScenario{ 65 | Input: ` 66 | SELECT * FROM foobar WHERE id = 2; 67 | `, 68 | Tokens: []token.Token{ 69 | token.New(token.Select, "SELECT"), 70 | token.New(token.Asterisk, "*"), 71 | token.New(token.From, "FROM"), 72 | token.New(token.Literal, "foobar"), 73 | token.New(token.Where, "WHERE"), 74 | token.New(token.Literal, "id"), 75 | token.New(token.Equals, "="), 76 | token.New(token.Literal, "2"), 77 | token.New(token.Semicolon, ";"), 78 | }, 79 | }) 80 | 81 | // Scenario #3: Another simple SELECT query with multiples columns 82 | tests = append(tests, LexScenario{ 83 | Input: ` 84 | SELECT a,b,c FROM foobar; 85 | `, 86 | Tokens: []token.Token{ 87 | token.New(token.Select, "SELECT"), 88 | token.New(token.Literal, "a"), 89 | token.New(token.Comma, ","), 90 | token.New(token.Literal, "b"), 91 | token.New(token.Comma, ","), 92 | token.New(token.Literal, "c"), 93 | token.New(token.From, "FROM"), 94 | token.New(token.Literal, "foobar"), 95 | token.New(token.Semicolon, ";"), 96 | }, 97 | }) 98 | 99 | // Scenario #4: Detect if newline is ignored 100 | tests = append(tests, LexScenario{ 101 | Input: ` 102 | SELECT a, b, c FROM foobar 103 | `, 104 | Tokens: []token.Token{ 105 | token.New(token.Select, "SELECT"), 106 | token.New(token.Literal, "a"), 107 | token.New(token.Comma, ","), 108 | token.New(token.Literal, "b"), 109 | token.New(token.Comma, ","), 110 | token.New(token.Literal, "c"), 111 | token.New(token.From, "FROM"), 112 | token.New(token.Literal, "foobar"), 113 | }, 114 | }) 115 | 116 | // Scenario #5: Detect if EOF is ignored 117 | tests = append(tests, LexScenario{ 118 | Input: `SELECT a, b, c FROM foobar`, 119 | Tokens: []token.Token{ 120 | token.New(token.Select, "SELECT"), 121 | token.New(token.Literal, "a"), 122 | token.New(token.Comma, ","), 123 | token.New(token.Literal, "b"), 124 | token.New(token.Comma, ","), 125 | token.New(token.Literal, "c"), 126 | token.New(token.From, "FROM"), 127 | token.New(token.Literal, "foobar"), 128 | }, 129 | }) 130 | 131 | // Scenario #6: A subquery using an INNER JOIN 132 | tests = append(tests, LexScenario{ 133 | Input: `INNER JOIN test2 ON test2.id = test.fk_id`, 134 | Tokens: []token.Token{ 135 | token.New(token.Inner, "INNER"), 136 | token.New(token.Join, "JOIN"), 137 | token.New(token.Literal, "test2"), 138 | token.New(token.On, "ON"), 139 | token.New(token.Literal, "test2.id"), 140 | token.New(token.Equals, "="), 141 | token.New(token.Literal, "test.fk_id"), 142 | }, 143 | }) 144 | 145 | // Scenario #7: A simple delete query 146 | tests = append(tests, LexScenario{ 147 | Input: `DELETE FROM test2 WHERE id = 5`, 148 | Tokens: []token.Token{ 149 | token.New(token.Delete, "DELETE"), 150 | token.New(token.From, "FROM"), 151 | token.New(token.Literal, "test2"), 152 | token.New(token.Where, "WHERE"), 153 | token.New(token.Literal, "id"), 154 | token.New(token.Equals, "="), 155 | token.New(token.Literal, "5"), 156 | }, 157 | }) 158 | 159 | execute(t, tests) 160 | } 161 | -------------------------------------------------------------------------------- /loukoum.go: -------------------------------------------------------------------------------- 1 | package loukoum 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/builder" 5 | "github.com/ulule/loukoum/v3/stmt" 6 | "github.com/ulule/loukoum/v3/types" 7 | ) 8 | 9 | const ( 10 | // InnerJoin is used for "INNER JOIN" in join statement. 11 | InnerJoin = types.InnerJoin 12 | // LeftJoin is used for "LEFT JOIN" in join statement. 13 | LeftJoin = types.LeftJoin 14 | // RightJoin is used for "RIGHT JOIN" in join statement. 15 | RightJoin = types.RightJoin 16 | // LeftOuterJoin is used for "LEFT OUTER JOIN" in join statement. 17 | LeftOuterJoin = types.LeftOuterJoin 18 | // RightOuterJoin is used for "RIGHT OUTER JOIN" in join statement. 19 | RightOuterJoin = types.RightOuterJoin 20 | // Asc is used for "ORDER BY" statement. 21 | Asc = types.Asc 22 | // Desc is used for "ORDER BY" statement. 23 | Desc = types.Desc 24 | ) 25 | 26 | // Map is a key/value map. 27 | type Map = types.Map 28 | 29 | // Pair takes a key and its related value and returns a Pair. 30 | func Pair(key, value interface{}) types.Pair { 31 | return types.Pair{Key: key, Value: value} 32 | } 33 | 34 | // Value is a wrapper to create a new Value expression. 35 | func Value(value interface{}) stmt.Value { 36 | return stmt.NewValue(value) 37 | } 38 | 39 | // Select starts a SelectBuilder using the given columns. 40 | func Select(columns ...interface{}) builder.Select { 41 | return builder.NewSelect().Columns(columns...) 42 | } 43 | 44 | // Column is a wrapper to create a new Column statement. 45 | func Column(name string) stmt.Column { 46 | return stmt.NewColumn(name) 47 | } 48 | 49 | // Table is a wrapper to create a new Table statement. 50 | func Table(name string) stmt.Table { 51 | return stmt.NewTable(name) 52 | } 53 | 54 | // On is a wrapper to create a new On statement. 55 | func On(left string, right string) stmt.OnClause { 56 | return stmt.NewOnClause(stmt.NewColumn(left), stmt.NewColumn(right)) 57 | } 58 | 59 | // AndOn is a wrapper to create a new On statement using an infix expression. 60 | func AndOn(left stmt.OnExpression, right stmt.OnExpression) stmt.OnExpression { 61 | return stmt.NewInfixOnExpression(left, stmt.NewLogicalOperator(types.And), right) 62 | } 63 | 64 | // OrOn is a wrapper to create a new On statement using an infix expression. 65 | func OrOn(left stmt.OnExpression, right stmt.OnExpression) stmt.OnExpression { 66 | return stmt.NewInfixOnExpression(left, stmt.NewLogicalOperator(types.Or), right) 67 | } 68 | 69 | // Condition is a wrapper to create a new Identifier statement. 70 | func Condition(column interface{}) stmt.Identifier { 71 | return stmt.NewIdentifier(column) 72 | } 73 | 74 | // Order is a wrapper to create a new Order statement. 75 | func Order(column string, option ...types.OrderType) stmt.Order { 76 | order := types.Asc 77 | if len(option) > 0 { 78 | order = option[0] 79 | } 80 | return stmt.NewOrder(column, order) 81 | } 82 | 83 | // Offset is a wrapper to create a new Offset statement. 84 | func Offset(start int64) stmt.Offset { 85 | return stmt.NewOffset(start) 86 | } 87 | 88 | // Limit is a wrapper to create a new Limit statement. 89 | func Limit(count int64) stmt.Limit { 90 | return stmt.NewLimit(count) 91 | } 92 | 93 | // And is a wrapper to create a new InfixExpression statement. 94 | func And(left stmt.Expression, right stmt.Expression) stmt.InfixExpression { 95 | return stmt.NewInfixExpression(left, stmt.NewLogicalOperator(types.And), right) 96 | } 97 | 98 | // Or is a wrapper to create a new InfixExpression statement. 99 | func Or(left stmt.Expression, right stmt.Expression) stmt.InfixExpression { 100 | return stmt.NewInfixExpression(left, stmt.NewLogicalOperator(types.Or), right) 101 | } 102 | 103 | // Raw is a wrapper to create a new Raw expression. 104 | func Raw(value string) stmt.Raw { 105 | return stmt.NewRaw(value) 106 | } 107 | 108 | // Exists is a wrapper to create a new Exists expression. 109 | func Exists(value interface{}) stmt.Exists { 110 | return stmt.NewExists(value) 111 | } 112 | 113 | // NotExists is a wrapper to create a new NotExists expression. 114 | func NotExists(value interface{}) stmt.NotExists { 115 | return stmt.NewNotExists(value) 116 | } 117 | 118 | // Count is a wrapper to create a new Count expression. 119 | func Count(value string) stmt.Count { 120 | return stmt.NewCount(value) 121 | } 122 | 123 | // Max is a wrapper to create a new Max expression. 124 | func Max(value string) stmt.Max { 125 | return stmt.NewMax(value) 126 | } 127 | 128 | // Min is a wrapper to create a new Min expression. 129 | func Min(value string) stmt.Min { 130 | return stmt.NewMin(value) 131 | } 132 | 133 | // Sum is a wrapper to create a new Sum expression. 134 | func Sum(value string) stmt.Sum { 135 | return stmt.NewSum(value) 136 | } 137 | 138 | // With is a wrapper to create a new WithQuery statement. 139 | func With(name string, value interface{}) stmt.WithQuery { 140 | return stmt.NewWithQuery(name, value) 141 | } 142 | 143 | // Insert starts an InsertBuilder using the given table as into clause. 144 | func Insert(into interface{}) builder.Insert { 145 | return builder.NewInsert().Into(into) 146 | } 147 | 148 | // Delete starts a DeleteBuilder using the given table as from clause. 149 | func Delete(from interface{}) builder.Delete { 150 | return builder.NewDelete().From(from) 151 | } 152 | 153 | // Update starts an Update builder using the given table. 154 | func Update(table interface{}) builder.Update { 155 | return builder.NewUpdate(table) 156 | } 157 | 158 | // DoNothing is a wrapper to create a new ConflictNoAction statement. 159 | func DoNothing() stmt.ConflictNoAction { 160 | return stmt.NewConflictNoAction() 161 | } 162 | 163 | // DoUpdate is a wrapper to create a new ConflictUpdateAction statement. 164 | func DoUpdate(args ...interface{}) stmt.ConflictUpdateAction { 165 | return stmt.NewConflictUpdateAction(builder.ToSet(args)) 166 | } 167 | -------------------------------------------------------------------------------- /parser/join.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/ulule/loukoum/v3/lexer" 10 | "github.com/ulule/loukoum/v3/stmt" 11 | "github.com/ulule/loukoum/v3/token" 12 | "github.com/ulule/loukoum/v3/types" 13 | ) 14 | 15 | // ErrJoinInvalidCondition is returned when join condition cannot be parsed. 16 | var ErrJoinInvalidCondition = fmt.Errorf("join condition is invalid") 17 | 18 | // ParseJoin will try to parse given subquery as a join statement. 19 | func ParseJoin(subquery string) (stmt.Join, error) { // nolint: gocyclo 20 | lexer := lexer.New(strings.NewReader(subquery)) 21 | it := lexer.Iterator() 22 | 23 | join := stmt.Join{ 24 | Type: types.InnerJoin, 25 | } 26 | 27 | for it.HasNext() { 28 | e := it.Next() 29 | 30 | switch e.Type { 31 | // Parse join type 32 | case token.Join: 33 | continue 34 | case token.Inner: 35 | if it.Is(token.Join) { 36 | it.Next() 37 | join.Type = types.InnerJoin 38 | continue 39 | } 40 | case token.Left: 41 | if it.Is(token.Join) { 42 | it.Next() 43 | join.Type = types.LeftJoin 44 | continue 45 | } 46 | case token.Right: 47 | if it.Is(token.Join) { 48 | it.Next() 49 | join.Type = types.RightJoin 50 | continue 51 | } 52 | case token.Literal: 53 | // Parse join table 54 | if it.Is(token.On) { 55 | it.Next() 56 | join.Table = stmt.NewTable(e.Value) 57 | continue 58 | } 59 | 60 | // Parse join condition 61 | if it.Is(token.Equals) { 62 | 63 | // Left condition 64 | left := stmt.NewColumn(e.Value) 65 | 66 | // Check that we have a right condition 67 | e = it.Next() 68 | if e.Type != token.Equals || !it.Is(token.Literal) { 69 | err := errors.Wrapf(ErrJoinInvalidCondition, "given query cannot be parsed: %s", subquery) 70 | return stmt.Join{}, err 71 | } 72 | 73 | // Right condition 74 | e = it.Next() 75 | right := stmt.NewColumn(e.Value) 76 | 77 | join.Condition = stmt.NewOnClause(left, right) 78 | 79 | for it.Is(token.And) || it.Is(token.Or) { 80 | // We have an AND operator 81 | if it.Is(token.And) { 82 | e := it.Next() 83 | if e.Type != token.And || !it.Is(token.Literal) { 84 | err := errors.Wrapf(ErrJoinInvalidCondition, "given query cannot be parsed: %s", subquery) 85 | return stmt.Join{}, err 86 | } 87 | 88 | // Left condition 89 | e = it.Next() 90 | left := stmt.NewColumn(e.Value) 91 | 92 | // Check that we have a right condition 93 | e = it.Next() 94 | if e.Type != token.Equals || !it.Is(token.Literal) { 95 | err := errors.Wrapf(ErrJoinInvalidCondition, "given query cannot be parsed: %s", subquery) 96 | return stmt.Join{}, err 97 | } 98 | 99 | // Right condition 100 | e = it.Next() 101 | right := stmt.NewColumn(e.Value) 102 | 103 | join.Condition = stmt.NewInfixExpression(join.Condition, stmt.NewLogicalOperator(types.And), stmt.NewOnClause(left, right)) //nolint:lll 104 | } 105 | // We have an OR operator 106 | if it.Is(token.Or) { 107 | e := it.Next() 108 | if e.Type != token.Or || !it.Is(token.Literal) { 109 | err := errors.Wrapf(ErrJoinInvalidCondition, "given query cannot be parsed: %s", subquery) 110 | return stmt.Join{}, err 111 | } 112 | 113 | // Left condition 114 | e = it.Next() 115 | left := stmt.NewColumn(e.Value) 116 | 117 | // Check that we have a right condition 118 | e = it.Next() 119 | if e.Type != token.Equals || !it.Is(token.Literal) { 120 | err := errors.Wrapf(ErrJoinInvalidCondition, "given query cannot be parsed: %s", subquery) 121 | return stmt.Join{}, err 122 | } 123 | 124 | // Right condition 125 | e = it.Next() 126 | right := stmt.NewColumn(e.Value) 127 | 128 | join.Condition = stmt.NewInfixExpression(join.Condition, stmt.NewLogicalOperator(types.Or), stmt.NewOnClause(left, right)) //nolint:lll 129 | } 130 | } 131 | 132 | continue 133 | } 134 | 135 | case token.On, token.LParen, token.RParen: 136 | continue 137 | } 138 | 139 | // Ignore invalid token and stop iterating. 140 | break 141 | } 142 | 143 | return join, nil 144 | } 145 | 146 | // MustParseJoin will execute ParseJoin and panic on error. 147 | func MustParseJoin(subquery string) stmt.Join { 148 | join, err := ParseJoin(subquery) 149 | if err != nil { 150 | panic(fmt.Sprintf("loukoum: %s", err)) 151 | } 152 | return join 153 | } 154 | -------------------------------------------------------------------------------- /parser/join_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/ulule/loukoum/v3/parser" 10 | "github.com/ulule/loukoum/v3/stmt" 11 | "github.com/ulule/loukoum/v3/types" 12 | ) 13 | 14 | func TestParseJoin(t *testing.T) { 15 | is := require.New(t) 16 | 17 | // Expressions 18 | { 19 | query, err := parser.ParseJoin("LEFT JOIN project ON user.id = project.user_id") 20 | is.NoError(err) 21 | is.Equal(types.LeftJoin, query.Type) 22 | is.Equal("project", query.Table.Name) 23 | is.Empty(query.Table.Alias) 24 | on, ok := query.Condition.(stmt.OnClause) 25 | is.True(ok) 26 | is.NotEmpty(on) 27 | is.Equal("user.id", on.Left.Name) 28 | is.Empty(on.Left.Alias) 29 | is.Equal("project.user_id", on.Right.Name) 30 | is.Empty(on.Right.Alias) 31 | } 32 | { 33 | query, err := parser.ParseJoin("INNER JOIN account ON (project.account_id = account.id)") 34 | is.NoError(err) 35 | is.Equal(types.InnerJoin, query.Type) 36 | is.Equal("account", query.Table.Name) 37 | is.Empty(query.Table.Alias) 38 | on, ok := query.Condition.(stmt.OnClause) 39 | is.True(ok) 40 | is.NotEmpty(on) 41 | is.Equal("project.account_id", on.Left.Name) 42 | is.Empty(on.Left.Alias) 43 | is.Equal("account.id", on.Right.Name) 44 | is.Empty(on.Right.Alias) 45 | } 46 | { 47 | query, err := parser.ParseJoin("RIGHT JOIN foobar ON foobar.group_id = test.group_id;") 48 | is.NoError(err) 49 | is.Equal(types.RightJoin, query.Type) 50 | is.Equal("foobar", query.Table.Name) 51 | is.Empty(query.Table.Alias) 52 | on, ok := query.Condition.(stmt.OnClause) 53 | is.True(ok) 54 | is.NotEmpty(on) 55 | is.Equal("foobar.group_id", on.Left.Name) 56 | is.Empty(on.Left.Alias) 57 | is.Equal("test.group_id", on.Right.Name) 58 | is.Empty(on.Right.Alias) 59 | } 60 | 61 | // Partials 62 | { 63 | query, err := parser.ParseJoin("ON user.id = project.user_id") 64 | is.NoError(err) 65 | is.Equal(types.InnerJoin, query.Type) 66 | is.Empty(query.Table.Name) 67 | is.Empty(query.Table.Alias) 68 | on, ok := query.Condition.(stmt.OnClause) 69 | is.True(ok) 70 | is.NotEmpty(on) 71 | is.Equal("user.id", on.Left.Name) 72 | is.Empty(on.Left.Alias) 73 | is.Equal("project.user_id", on.Right.Name) 74 | is.Empty(on.Right.Alias) 75 | } 76 | { 77 | query, err := parser.ParseJoin("user.id = project.user_id") 78 | is.NoError(err) 79 | is.Equal(types.InnerJoin, query.Type) 80 | is.Empty(query.Table.Name) 81 | is.Empty(query.Table.Alias) 82 | on, ok := query.Condition.(stmt.OnClause) 83 | is.True(ok) 84 | is.NotEmpty(on) 85 | is.Equal("user.id", on.Left.Name) 86 | is.Empty(on.Left.Alias) 87 | is.Equal("project.user_id", on.Right.Name) 88 | is.Empty(on.Right.Alias) 89 | } 90 | 91 | // With logical operator 92 | { 93 | query, err := parser.ParseJoin("ON user.id = project.user_id AND user.hash = project.hash") 94 | is.NoError(err) 95 | is.Equal(types.InnerJoin, query.Type) 96 | is.Empty(query.Table.Name) 97 | is.Empty(query.Table.Alias) 98 | infix, ok := query.Condition.(stmt.InfixExpression) 99 | is.True(ok) 100 | is.NotEmpty(infix) 101 | on, ok := infix.Left.(stmt.OnClause) 102 | is.True(ok) 103 | is.NotEmpty(on) 104 | is.Equal("user.id", on.Left.Name) 105 | is.Empty(on.Left.Alias) 106 | is.Equal("project.user_id", on.Right.Name) 107 | is.Empty(on.Right.Alias) 108 | is.Equal(stmt.LogicalOperator{Operator: types.And}, infix.Operator) 109 | on, ok = infix.Right.(stmt.OnClause) 110 | is.True(ok) 111 | is.NotEmpty(on) 112 | is.Equal("user.hash", on.Left.Name) 113 | is.Empty(on.Left.Alias) 114 | is.Equal("project.hash", on.Right.Name) 115 | is.Empty(on.Right.Alias) 116 | } 117 | { 118 | query, err := parser.ParseJoin("ON user.id = project.user_id OR user.hash = project.hash") 119 | is.NoError(err) 120 | is.Equal(types.InnerJoin, query.Type) 121 | is.Empty(query.Table.Name) 122 | is.Empty(query.Table.Alias) 123 | infix, ok := query.Condition.(stmt.InfixExpression) 124 | is.True(ok) 125 | is.NotEmpty(infix) 126 | on, ok := infix.Left.(stmt.OnClause) 127 | is.True(ok) 128 | is.NotEmpty(on) 129 | is.Equal("user.id", on.Left.Name) 130 | is.Empty(on.Left.Alias) 131 | is.Equal("project.user_id", on.Right.Name) 132 | is.Empty(on.Right.Alias) 133 | is.Equal(stmt.LogicalOperator{Operator: types.Or}, infix.Operator) 134 | on, ok = infix.Right.(stmt.OnClause) 135 | is.True(ok) 136 | is.NotEmpty(on) 137 | is.Equal("user.hash", on.Left.Name) 138 | is.Empty(on.Left.Alias) 139 | is.Equal("project.hash", on.Right.Name) 140 | is.Empty(on.Right.Alias) 141 | } 142 | { 143 | query, err := parser.ParseJoin( 144 | "ON user.id = project.user_id AND user.hash = project.hash OR user.group_id = project.group_id", 145 | ) 146 | is.NoError(err) 147 | is.Equal(types.InnerJoin, query.Type) 148 | is.Empty(query.Table.Name) 149 | is.Empty(query.Table.Alias) 150 | infix1, ok := query.Condition.(stmt.InfixExpression) 151 | is.True(ok) 152 | is.NotEmpty(infix1) 153 | infix2, ok := infix1.Left.(stmt.InfixExpression) 154 | is.True(ok) 155 | is.NotEmpty(infix2) 156 | on, ok := infix2.Left.(stmt.OnClause) 157 | is.True(ok) 158 | is.NotEmpty(on) 159 | is.Equal("user.id", on.Left.Name) 160 | is.Empty(on.Left.Alias) 161 | is.Equal("project.user_id", on.Right.Name) 162 | is.Empty(on.Right.Alias) 163 | is.Equal(stmt.LogicalOperator{Operator: types.And}, infix2.Operator) 164 | on, ok = infix2.Right.(stmt.OnClause) 165 | is.True(ok) 166 | is.NotEmpty(on) 167 | is.Equal("user.hash", on.Left.Name) 168 | is.Empty(on.Left.Alias) 169 | is.Equal("project.hash", on.Right.Name) 170 | is.Empty(on.Right.Alias) 171 | is.Equal(stmt.LogicalOperator{Operator: types.Or}, infix1.Operator) 172 | on, ok = infix1.Right.(stmt.OnClause) 173 | is.True(ok) 174 | is.NotEmpty(on) 175 | is.Equal("user.group_id", on.Left.Name) 176 | is.Empty(on.Left.Alias) 177 | is.Equal("project.group_id", on.Right.Name) 178 | is.Empty(on.Right.Alias) 179 | } 180 | 181 | // Invalid 182 | { 183 | query, err := parser.ParseJoin("INNER JOIN account ON (project.account_id = *)") 184 | is.Error(err) 185 | is.Equal(parser.ErrJoinInvalidCondition, errors.Cause(err)) 186 | is.Zero(query) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /scripts/conf/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1-buster 2 | 3 | MAINTAINER thomas@leroux.io 4 | 5 | ENV DEBIAN_FRONTEND noninteractive 6 | ENV LANG C.UTF-8 7 | ENV LC_ALL C.UTF-8 8 | 9 | RUN apt-get -y update \ 10 | && apt-get upgrade -y \ 11 | && apt-get -y install git \ 12 | && apt-get clean \ 13 | && rm -rf /var/lib/apt/lists/* \ 14 | && useradd -ms /bin/bash gopher 15 | 16 | COPY go.mod go.sum /media/ulule/loukoum/ 17 | RUN chown -R gopher:gopher /media/ulule/loukoum 18 | ENV GOPATH /home/gopher/go 19 | ENV PATH $GOPATH/bin:$PATH 20 | USER gopher 21 | 22 | RUN GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.27.0 23 | 24 | WORKDIR /media/ulule/loukoum 25 | RUN go mod download 26 | COPY --chown=gopher:gopher . /media/ulule/loukoum 27 | 28 | CMD [ "/bin/bash" ] 29 | -------------------------------------------------------------------------------- /scripts/go-wrapper: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | SOURCE_DIRECTORY=$(dirname "${BASH_SOURCE[0]}") 6 | cd "${SOURCE_DIRECTORY}/.." 7 | 8 | ROOT_DIRECTORY=`pwd` 9 | IMAGE_NAME="loukoum-go" 10 | DOCKERFILE="scripts/conf/go/Dockerfile" 11 | CONTAINER_IMAGE="golang:1-buster" 12 | 13 | create_docker_image() { 14 | declare tag="$1" dockerfile="$2" path="$3" 15 | 16 | echo "[go-wrapper] update golang image" 17 | docker pull ${CONTAINER_IMAGE} || true 18 | 19 | echo "[go-wrapper] build docker image" 20 | docker build -f "${dockerfile}" -t "${tag}" "${path}" 21 | } 22 | 23 | do_command() { 24 | declare command="$@" 25 | 26 | echo "[go-wrapper] run '${command}' in docker container" 27 | docker run --rm --net=host \ 28 | "${IMAGE_NAME}" ${command} 29 | } 30 | 31 | do_usage() { 32 | 33 | echo >&2 "Usage: $0 command" 34 | exit 255 35 | 36 | } 37 | 38 | if [ -z "$1" ]; then 39 | do_usage 40 | fi 41 | 42 | create_docker_image "${IMAGE_NAME}" "${DOCKERFILE}" "${ROOT_DIRECTORY}" 43 | do_command "$@" 44 | 45 | exit 0 46 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | golinter_path="${GOPATH}/bin/golangci-lint" 6 | 7 | if [[ ! -x "${golinter_path}" ]]; then 8 | go get -u github.com/golangci/golangci-lint/cmd/golangci-lint 9 | fi 10 | 11 | SOURCE_DIRECTORY=$(dirname "${BASH_SOURCE[0]}") 12 | cd "${SOURCE_DIRECTORY}/.." 13 | 14 | if [[ -n $1 ]]; then 15 | golangci-lint run "$1" 16 | else 17 | golangci-lint run ./... 18 | fi 19 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SOURCE_DIRECTORY=$(dirname "${BASH_SOURCE[0]}") 4 | cd "${SOURCE_DIRECTORY}/.." 5 | 6 | go test -count=1 -v $(go list ./... | grep -v -E '\/(vendor|examples)\/') 7 | -------------------------------------------------------------------------------- /stmt/aggregate.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Count is a aggregate expression. 9 | type Count struct { 10 | Value Raw 11 | IsDistinct bool 12 | Alias string 13 | } 14 | 15 | // NewCount returns a new Count instance. 16 | func NewCount(value string) Count { 17 | return Count{ 18 | Value: NewRaw(value), 19 | } 20 | } 21 | 22 | // As is used to give an alias name to the COUNT function. 23 | func (count Count) As(alias string) Count { 24 | count.Alias = alias 25 | return count 26 | } 27 | 28 | // Distinct is used to define if count has a distinct clause. 29 | func (count Count) Distinct(value bool) Count { 30 | count.IsDistinct = value 31 | return count 32 | } 33 | 34 | // Write exposes statement as a SQL query. 35 | func (count Count) Write(ctx types.Context) { 36 | ctx.Write(token.Count.String()) 37 | ctx.Write("(") 38 | if count.IsDistinct { 39 | ctx.Write(token.Distinct.String()) 40 | ctx.Write(" ") 41 | } 42 | count.Value.Write(ctx) 43 | ctx.Write(")") 44 | if count.Alias != "" { 45 | ctx.Write(" ") 46 | ctx.Write(token.As.String()) 47 | ctx.Write(" ") 48 | ctx.Write(count.Alias) 49 | } 50 | } 51 | 52 | // IsEmpty returns true if statement is undefined. 53 | func (count Count) IsEmpty() bool { 54 | return count.Value.IsEmpty() 55 | } 56 | 57 | func (Count) selectExpression() {} 58 | 59 | // Ensure that Count is an SelectExpression 60 | var _ SelectExpression = Count{} 61 | 62 | // Max is a aggregate expression. 63 | type Max struct { 64 | Value Raw 65 | Alias string 66 | } 67 | 68 | // NewMax returns a new Max instance. 69 | func NewMax(value string) Max { 70 | return Max{ 71 | Value: NewRaw(value), 72 | } 73 | } 74 | 75 | // As is used to give an alias name to the MAX function. 76 | func (max Max) As(alias string) Max { 77 | max.Alias = alias 78 | return max 79 | } 80 | 81 | // Write exposes statement as a SQL query. 82 | func (max Max) Write(ctx types.Context) { 83 | ctx.Write(token.Max.String()) 84 | ctx.Write("(") 85 | max.Value.Write(ctx) 86 | ctx.Write(")") 87 | if max.Alias != "" { 88 | ctx.Write(" ") 89 | ctx.Write(token.As.String()) 90 | ctx.Write(" ") 91 | ctx.Write(max.Alias) 92 | } 93 | } 94 | 95 | // IsEmpty returns true if statement is undefined. 96 | func (max Max) IsEmpty() bool { 97 | return max.Value.IsEmpty() 98 | } 99 | 100 | func (Max) selectExpression() {} 101 | 102 | // Ensure that Max is an SelectExpression 103 | var _ SelectExpression = Max{} 104 | 105 | // Min is a aggregate expression. 106 | type Min struct { 107 | Value Raw 108 | Alias string 109 | } 110 | 111 | // NewMin returns a new Min instance. 112 | func NewMin(value string) Min { 113 | return Min{ 114 | Value: NewRaw(value), 115 | } 116 | } 117 | 118 | // As is used to give an alias name to the MIN function. 119 | func (min Min) As(alias string) Min { 120 | min.Alias = alias 121 | return min 122 | } 123 | 124 | // Write exposes statement as a SQL query. 125 | func (min Min) Write(ctx types.Context) { 126 | ctx.Write(token.Min.String()) 127 | ctx.Write("(") 128 | min.Value.Write(ctx) 129 | ctx.Write(")") 130 | if min.Alias != "" { 131 | ctx.Write(" ") 132 | ctx.Write(token.As.String()) 133 | ctx.Write(" ") 134 | ctx.Write(min.Alias) 135 | } 136 | } 137 | 138 | // IsEmpty returns true if statement is undefined. 139 | func (min Min) IsEmpty() bool { 140 | return min.Value.IsEmpty() 141 | } 142 | 143 | func (Min) selectExpression() {} 144 | 145 | // Ensure that Min is an SelectExpression 146 | var _ SelectExpression = Min{} 147 | 148 | // Sum is a aggregate expression. 149 | type Sum struct { 150 | Value Raw 151 | Alias string 152 | } 153 | 154 | // NewSum returns a new Sum instance. 155 | func NewSum(value string) Sum { 156 | return Sum{ 157 | Value: NewRaw(value), 158 | } 159 | } 160 | 161 | // As is used to give an alias name to the SUM function. 162 | func (sum Sum) As(alias string) Sum { 163 | sum.Alias = alias 164 | return sum 165 | } 166 | 167 | // Write exposes statement as a SQL query. 168 | func (sum Sum) Write(ctx types.Context) { 169 | ctx.Write(token.Sum.String()) 170 | ctx.Write("(") 171 | sum.Value.Write(ctx) 172 | ctx.Write(")") 173 | if sum.Alias != "" { 174 | ctx.Write(" ") 175 | ctx.Write(token.As.String()) 176 | ctx.Write(" ") 177 | ctx.Write(sum.Alias) 178 | } 179 | } 180 | 181 | // IsEmpty returns true if statement is undefined. 182 | func (sum Sum) IsEmpty() bool { 183 | return sum.Value.IsEmpty() 184 | } 185 | 186 | func (Sum) selectExpression() {} 187 | 188 | // Ensure that Sum is an SelectExpression 189 | var _ SelectExpression = Sum{} 190 | -------------------------------------------------------------------------------- /stmt/between.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/types" 5 | ) 6 | 7 | // Between is a BETWEEN expression. 8 | type Between struct { 9 | Identifier Identifier 10 | Operator ComparisonOperator 11 | From Expression 12 | And LogicalOperator 13 | To Expression 14 | } 15 | 16 | // NewBetween returns a new Between instance using an inclusive operator. 17 | func NewBetween(identifier Identifier, from, to Expression) Between { 18 | return Between{ 19 | Identifier: identifier, 20 | Operator: NewComparisonOperator(types.Between), 21 | From: from, 22 | And: NewAndOperator(), 23 | To: to, 24 | } 25 | } 26 | 27 | // NewNotBetween returns a new Between instance using an exclusive operator. 28 | func NewNotBetween(identifier Identifier, from, to Expression) Between { 29 | return Between{ 30 | Identifier: identifier, 31 | Operator: NewComparisonOperator(types.NotBetween), 32 | From: from, 33 | And: NewAndOperator(), 34 | To: to, 35 | } 36 | } 37 | 38 | func (Between) expression() {} 39 | 40 | // Write exposes statement as a SQL query. 41 | func (between Between) Write(ctx types.Context) { 42 | if between.IsEmpty() { 43 | panic("loukoum: expression is undefined") 44 | } 45 | 46 | ctx.Write("(") 47 | between.Identifier.Write(ctx) 48 | ctx.Write(" ") 49 | between.Operator.Write(ctx) 50 | ctx.Write(" ") 51 | between.From.Write(ctx) 52 | ctx.Write(" ") 53 | between.And.Write(ctx) 54 | ctx.Write(" ") 55 | between.To.Write(ctx) 56 | ctx.Write(")") 57 | } 58 | 59 | // IsEmpty returns true if statement is undefined. 60 | func (between Between) IsEmpty() bool { 61 | return between.Identifier.IsEmpty() || between.Operator.IsEmpty() || between.And.IsEmpty() || 62 | between.From == nil || between.To == nil || between.From.IsEmpty() || between.To.IsEmpty() 63 | } 64 | 65 | // Ensure that Between is an Expression 66 | var _ Expression = Between{} 67 | -------------------------------------------------------------------------------- /stmt/column.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Column is a column identifier. 9 | type Column struct { 10 | Name string 11 | Alias string 12 | } 13 | 14 | // NewColumn returns a new Column instance. 15 | func NewColumn(name string) Column { 16 | return NewColumnAlias(name, "") 17 | } 18 | 19 | // NewColumnAlias returns a new Column instance with an alias. 20 | func NewColumnAlias(name, alias string) Column { 21 | return Column{ 22 | Name: name, 23 | Alias: alias, 24 | } 25 | } 26 | 27 | // As is used to give an alias name to the column. 28 | func (column Column) As(alias string) Column { 29 | column.Alias = alias 30 | return column 31 | } 32 | 33 | // Asc is used to transform a column to an order expression. 34 | func (column Column) Asc() Order { 35 | expression := column.Name 36 | if column.Alias != "" { 37 | expression = column.Alias 38 | } 39 | 40 | return NewOrder(expression, types.Asc) 41 | } 42 | 43 | // Desc is used to transform a column to an order expression. 44 | func (column Column) Desc() Order { 45 | expression := column.Name 46 | if column.Alias != "" { 47 | expression = column.Alias 48 | } 49 | 50 | return NewOrder(expression, types.Desc) 51 | } 52 | 53 | // Write exposes statement as a SQL query. 54 | func (column Column) Write(ctx types.Context) { 55 | ctx.Write(quote(column.Name)) 56 | if column.Alias != "" { 57 | ctx.Write(" ") 58 | ctx.Write(token.As.String()) 59 | ctx.Write(" ") 60 | ctx.Write(quote(column.Alias)) 61 | } 62 | } 63 | 64 | // IsEmpty returns true if statement is undefined. 65 | func (column Column) IsEmpty() bool { 66 | return column.Name == "" 67 | } 68 | 69 | func (Column) selectExpression() {} 70 | 71 | // Ensure that Column is a SelectExpression. 72 | var _ SelectExpression = Column{} 73 | -------------------------------------------------------------------------------- /stmt/comment.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Comment is a comment expression. 9 | type Comment struct { 10 | Comment string 11 | } 12 | 13 | // NewComment returns a new Comment instance. 14 | func NewComment(comment string) Comment { 15 | return Comment{ 16 | Comment: comment, 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (comment Comment) Write(ctx types.Context) { 22 | if comment.IsEmpty() { 23 | return 24 | } 25 | ctx.Write(token.Comment.String()) 26 | ctx.Write(" ") 27 | ctx.Write(comment.Comment) 28 | } 29 | 30 | // IsEmpty returns true if statement is undefined. 31 | func (comment Comment) IsEmpty() bool { 32 | return comment.Comment == "" 33 | } 34 | 35 | // Ensure that Comment is a Statement 36 | var _ Statement = Comment{} 37 | -------------------------------------------------------------------------------- /stmt/conflict.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // OnConflict is a ON CONFLICT expression. 9 | type OnConflict struct { 10 | Target ConflictTarget 11 | Action ConflictAction 12 | } 13 | 14 | // NewOnConflict returns a new OnConflict instance. 15 | func NewOnConflict(target ConflictTarget, action ConflictAction) OnConflict { 16 | return OnConflict{ 17 | Target: target, 18 | Action: action, 19 | } 20 | } 21 | 22 | // Write exposes statement as a SQL query. 23 | func (conflict OnConflict) Write(ctx types.Context) { 24 | if conflict.IsEmpty() { 25 | return 26 | } 27 | 28 | ctx.Write(token.On.String()) 29 | ctx.Write(" ") 30 | ctx.Write(token.Conflict.String()) 31 | ctx.Write(" ") 32 | 33 | if !conflict.Target.IsEmpty() { 34 | conflict.Target.Write(ctx) 35 | ctx.Write(" ") 36 | } 37 | 38 | conflict.Action.Write(ctx) 39 | } 40 | 41 | // IsEmpty returns true if statement is undefined. 42 | func (conflict OnConflict) IsEmpty() bool { 43 | return conflict.Action == nil || conflict.Action.IsEmpty() 44 | } 45 | 46 | // ConflictTarget is a column identifier. 47 | type ConflictTarget struct { 48 | Columns []Column 49 | } 50 | 51 | // NewConflictTarget returns a new ConflictTarget instance. 52 | func NewConflictTarget(columns []Column) ConflictTarget { 53 | return ConflictTarget{ 54 | Columns: columns, 55 | } 56 | } 57 | 58 | // Write exposes statement as a SQL query. 59 | func (target ConflictTarget) Write(ctx types.Context) { 60 | if target.IsEmpty() { 61 | return 62 | } 63 | 64 | ctx.Write("(") 65 | for i := range target.Columns { 66 | if i != 0 { 67 | ctx.Write(", ") 68 | } 69 | target.Columns[i].Write(ctx) 70 | } 71 | ctx.Write(")") 72 | } 73 | 74 | // IsEmpty returns true if statement is undefined. 75 | func (target ConflictTarget) IsEmpty() bool { 76 | return len(target.Columns) == 0 77 | } 78 | 79 | // ConflictAction is a action used by ON CONFLICT expression. 80 | // It can be either DO NOTHING, or a DO UPDATE clause. 81 | type ConflictAction interface { 82 | Statement 83 | conflictAction() 84 | } 85 | 86 | // ConflictUpdateAction is a DO UPDATE clause on ON CONFLICT expression. 87 | type ConflictUpdateAction struct { 88 | Set Set 89 | } 90 | 91 | // NewConflictUpdateAction returns a new ConflictUpdateAction instance. 92 | func NewConflictUpdateAction(set Set) ConflictUpdateAction { 93 | return ConflictUpdateAction{ 94 | Set: set, 95 | } 96 | } 97 | 98 | // Write exposes statement as a SQL query. 99 | func (action ConflictUpdateAction) Write(ctx types.Context) { 100 | ctx.Write(token.Do.String()) 101 | ctx.Write(" ") 102 | ctx.Write(token.Update.String()) 103 | ctx.Write(" ") 104 | action.Set.Write(ctx) 105 | } 106 | 107 | // IsEmpty returns true if statement is undefined. 108 | func (action ConflictUpdateAction) IsEmpty() bool { 109 | return action.Set.IsEmpty() 110 | } 111 | 112 | func (ConflictUpdateAction) conflictAction() {} 113 | 114 | // ConflictNoAction is a DO NOTHING clause on ON CONFLICT expression. 115 | type ConflictNoAction struct{} 116 | 117 | // NewConflictNoAction returns a new ConflictNoAction instance. 118 | func NewConflictNoAction() ConflictNoAction { 119 | return ConflictNoAction{} 120 | } 121 | 122 | // Write exposes statement as a SQL query. 123 | func (ConflictNoAction) Write(ctx types.Context) { 124 | ctx.Write(token.Do.String()) 125 | ctx.Write(" ") 126 | ctx.Write(token.Nothing.String()) 127 | } 128 | 129 | // IsEmpty returns true if statement is undefined. 130 | func (ConflictNoAction) IsEmpty() bool { 131 | return false 132 | } 133 | 134 | func (ConflictNoAction) conflictAction() {} 135 | 136 | // Ensure that OnConflict is a Statement 137 | var _ Statement = OnConflict{} 138 | 139 | // Ensure that ConflictTarget is a Statement 140 | var _ Statement = ConflictTarget{} 141 | 142 | // Ensure that ConflictUpdateAction is a ConflictAction 143 | var _ ConflictAction = ConflictUpdateAction{} 144 | 145 | // Ensure that ConflictNoAction is a ConflictAction 146 | var _ ConflictAction = ConflictNoAction{} 147 | -------------------------------------------------------------------------------- /stmt/delete.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Delete is a DELETE statement. 9 | type Delete struct { 10 | From From 11 | Using Using 12 | Where Where 13 | Returning Returning 14 | Comment Comment 15 | } 16 | 17 | // NewDelete returns a new Delete instance. 18 | func NewDelete() Delete { 19 | return Delete{} 20 | } 21 | 22 | // Write exposes statement as a SQL query. 23 | func (delete Delete) Write(ctx types.Context) { 24 | if delete.IsEmpty() { 25 | panic("loukoum: a delete statement must have a table") 26 | } 27 | 28 | ctx.Write(token.Delete.String()) 29 | ctx.Write(" ") 30 | delete.From.Write(ctx) 31 | 32 | if !delete.Using.IsEmpty() { 33 | ctx.Write(" ") 34 | delete.Using.Write(ctx) 35 | } 36 | 37 | if !delete.Where.IsEmpty() { 38 | ctx.Write(" ") 39 | delete.Where.Write(ctx) 40 | } 41 | 42 | if !delete.Returning.IsEmpty() { 43 | ctx.Write(" ") 44 | delete.Returning.Write(ctx) 45 | } 46 | 47 | if !delete.Comment.IsEmpty() { 48 | ctx.Write(token.Semicolon.String()) 49 | ctx.Write(" ") 50 | delete.Comment.Write(ctx) 51 | } 52 | } 53 | 54 | // IsEmpty returns true if statement is undefined. 55 | func (delete Delete) IsEmpty() bool { 56 | return delete.From.IsEmpty() 57 | } 58 | 59 | // Ensure that Delete is a Statement 60 | var _ Statement = Delete{} 61 | -------------------------------------------------------------------------------- /stmt/distinct_on.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // DistinctOn is a distinct on expression. 9 | type DistinctOn struct { 10 | Columns []Column 11 | } 12 | 13 | // NewDistinctOn returns a new DistinctOn instance. 14 | func NewDistinctOn(columns []Column) DistinctOn { 15 | return DistinctOn{ 16 | Columns: columns, 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (distinctOn DistinctOn) Write(ctx types.Context) { 22 | if distinctOn.IsEmpty() { 23 | return 24 | } 25 | ctx.Write(token.DistinctOn.String()) 26 | ctx.Write(" (") 27 | for i := range distinctOn.Columns { 28 | if i != 0 { 29 | ctx.Write(", ") 30 | } 31 | distinctOn.Columns[i].Write(ctx) 32 | } 33 | ctx.Write(")") 34 | } 35 | 36 | // IsEmpty returns true if statement is undefined. 37 | func (distinctOn DistinctOn) IsEmpty() bool { 38 | return len(distinctOn.Columns) == 0 39 | } 40 | 41 | // Ensure that DistinctOn is a Statement 42 | var _ Statement = DistinctOn{} 43 | -------------------------------------------------------------------------------- /stmt/doc.go: -------------------------------------------------------------------------------- 1 | // Package stmt defines various statements and clauses that are used to generate queries. 2 | // 3 | // Unless you have to develop complex queries, you should'nt have to use this package. 4 | package stmt 5 | -------------------------------------------------------------------------------- /stmt/encoder.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // StatementEncoder can encode a value as a statement to creates a Expression instance. 8 | type StatementEncoder interface { 9 | Statement() Statement 10 | } 11 | 12 | // StringEncoder can encode a value as a string to creates a Value instance. 13 | type StringEncoder interface { 14 | String() string 15 | } 16 | 17 | // Int64Encoder can encode a value as a int64 to creates a Value instance. 18 | type Int64Encoder interface { 19 | Int64() int64 20 | } 21 | 22 | // BoolEncoder can encode a value as a bool to creates a Value instance. 23 | type BoolEncoder interface { 24 | Bool() bool 25 | } 26 | 27 | // TimeEncoder can encode a value as a time.Time to creates a Value instance. 28 | type TimeEncoder interface { 29 | Time() time.Time 30 | } 31 | -------------------------------------------------------------------------------- /stmt/expression.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/jackc/pgx/v5/pgtype" 9 | "github.com/ulule/loukoum/v3/types" 10 | ) 11 | 12 | // Expression is a SQL expression. 13 | type Expression interface { 14 | Statement 15 | expression() 16 | } 17 | 18 | // NewExpression returns a new Expression instance from arg. 19 | func NewExpression(arg interface{}) Expression { // nolint: gocyclo 20 | if arg == nil { 21 | return NewValue(nil) 22 | } 23 | switch value := arg.(type) { 24 | case Expression: 25 | return value 26 | case string, bool, int, int8, int16, int32, int64, 27 | uint, uint8, uint16, uint32, uint64, float32, float64, 28 | []byte, driver.Valuer, []int64, []string, pgtype.RangeValuer, 29 | pgtype.FlatArray[string], pgtype.FlatArray[int64], 30 | map[string]string, map[string]interface{}: 31 | return NewValue(value) 32 | case time.Time: 33 | return NewValue(value) 34 | case *time.Time: 35 | return NewValue(*value) 36 | case StatementEncoder: 37 | stmt := value.Statement() 38 | expression, ok := stmt.(Expression) 39 | if !ok { 40 | panic(fmt.Sprintf("cannot use {%+v}[%T] as loukoum Expression", value, value)) 41 | } 42 | return expression 43 | case Int64Encoder: 44 | return NewValue(value.Int64()) 45 | case BoolEncoder: 46 | return NewValue(value.Bool()) 47 | case TimeEncoder: 48 | return NewValue(value.Time()) 49 | case StringEncoder: 50 | return NewValue(value.String()) 51 | default: 52 | panic(fmt.Sprintf("cannot use {%+v}[%T] as loukoum Expression", arg, arg)) 53 | } 54 | } 55 | 56 | // ---------------------------------------------------------------------------- 57 | // Identifier 58 | // ---------------------------------------------------------------------------- 59 | 60 | // Identifier is an identifier. 61 | type Identifier struct { 62 | Identifier interface{} 63 | } 64 | 65 | // NewIdentifier returns a new Identifier. 66 | func NewIdentifier(identifier interface{}) Identifier { 67 | return Identifier{ 68 | Identifier: identifier, 69 | } 70 | } 71 | 72 | func (Identifier) expression() {} 73 | 74 | // Write exposes statement as a SQL query. 75 | func (identifier Identifier) Write(ctx types.Context) { 76 | switch t := identifier.Identifier.(type) { 77 | case string: 78 | ctx.Write(quote(t)) 79 | case Raw: 80 | t.Write(ctx) 81 | } 82 | } 83 | 84 | // IsEmpty returns true if statement is undefined. 85 | func (identifier Identifier) IsEmpty() bool { 86 | return identifier.Identifier == "" 87 | } 88 | 89 | // Contains performs a "contains" comparison. 90 | func (identifier Identifier) Contains(value interface{}) InfixExpression { 91 | operator := NewComparisonOperator(types.Contains) 92 | return NewInfixExpression(identifier, operator, NewWrapper(NewExpression(value))) 93 | } 94 | 95 | // IsContainedBy performs a "is contained by" comparison. 96 | func (identifier Identifier) IsContainedBy(value interface{}) InfixExpression { 97 | operator := NewComparisonOperator(types.IsContainedBy) 98 | return NewInfixExpression(identifier, operator, NewWrapper(NewExpression(value))) 99 | } 100 | 101 | // Overlap performs an "overlap" comparison. 102 | func (identifier Identifier) Overlap(value interface{}) InfixExpression { 103 | operator := NewComparisonOperator(types.Overlap) 104 | return NewInfixExpression(identifier, operator, NewWrapper(NewExpression(value))) 105 | } 106 | 107 | // Equal performs an "equal" comparison. 108 | func (identifier Identifier) Equal(value interface{}) InfixExpression { 109 | operator := NewComparisonOperator(types.Equal) 110 | return NewInfixExpression(identifier, operator, NewWrapper(NewExpression(value))) 111 | } 112 | 113 | // NotEqual performs a "not equal" comparison. 114 | func (identifier Identifier) NotEqual(value interface{}) InfixExpression { 115 | operator := NewComparisonOperator(types.NotEqual) 116 | return NewInfixExpression(identifier, operator, NewWrapper(NewExpression(value))) 117 | } 118 | 119 | // Is performs a "is" comparison. 120 | func (identifier Identifier) Is(value interface{}) InfixExpression { 121 | operator := NewComparisonOperator(types.Is) 122 | return NewInfixExpression(identifier, operator, NewExpression(value)) 123 | } 124 | 125 | // IsNot performs a "is not" comparison. 126 | func (identifier Identifier) IsNot(value interface{}) InfixExpression { 127 | operator := NewComparisonOperator(types.IsNot) 128 | return NewInfixExpression(identifier, operator, NewExpression(value)) 129 | } 130 | 131 | // IsNull performs a "is null" comparison. 132 | func (identifier Identifier) IsNull(value bool) InfixExpression { 133 | if value { 134 | return identifier.Is(nil) 135 | } 136 | return identifier.IsNot(nil) 137 | } 138 | 139 | // GreaterThan performs a "greater than" comparison. 140 | func (identifier Identifier) GreaterThan(value interface{}) InfixExpression { 141 | operator := NewComparisonOperator(types.GreaterThan) 142 | return NewInfixExpression(identifier, operator, NewWrapper(NewExpression(value))) 143 | } 144 | 145 | // GreaterThanOrEqual performs a "greater than or equal to" comparison. 146 | func (identifier Identifier) GreaterThanOrEqual(value interface{}) InfixExpression { 147 | operator := NewComparisonOperator(types.GreaterThanOrEqual) 148 | return NewInfixExpression(identifier, operator, NewWrapper(NewExpression(value))) 149 | } 150 | 151 | // LessThan performs a "less than" comparison. 152 | func (identifier Identifier) LessThan(value interface{}) InfixExpression { 153 | operator := NewComparisonOperator(types.LessThan) 154 | return NewInfixExpression(identifier, operator, NewWrapper(NewExpression(value))) 155 | } 156 | 157 | // LessThanOrEqual performs a "less than or equal to" comparison. 158 | func (identifier Identifier) LessThanOrEqual(value interface{}) InfixExpression { 159 | operator := NewComparisonOperator(types.LessThanOrEqual) 160 | return NewInfixExpression(identifier, operator, NewWrapper(NewExpression(value))) 161 | } 162 | 163 | // In performs a "in" condition. 164 | func (identifier Identifier) In(value ...interface{}) In { 165 | return NewIn(identifier, NewArrayExpression(value...)) 166 | } 167 | 168 | // NotIn performs a "not in" condition. 169 | func (identifier Identifier) NotIn(value ...interface{}) In { 170 | return NewNotIn(identifier, NewArrayExpression(value...)) 171 | } 172 | 173 | // Like performs a "like" condition. 174 | func (identifier Identifier) Like(value interface{}) InfixExpression { 175 | operator := NewComparisonOperator(types.Like) 176 | return NewInfixExpression(identifier, operator, NewExpression(value)) 177 | } 178 | 179 | // NotLike performs a "not like" condition. 180 | func (identifier Identifier) NotLike(value interface{}) InfixExpression { 181 | operator := NewComparisonOperator(types.NotLike) 182 | return NewInfixExpression(identifier, operator, NewExpression(value)) 183 | } 184 | 185 | // ILike performs a "ilike" condition. 186 | func (identifier Identifier) ILike(value interface{}) InfixExpression { 187 | operator := NewComparisonOperator(types.ILike) 188 | return NewInfixExpression(identifier, operator, NewExpression(value)) 189 | } 190 | 191 | // NotILike performs a "not ilike" condition. 192 | func (identifier Identifier) NotILike(value interface{}) InfixExpression { 193 | operator := NewComparisonOperator(types.NotILike) 194 | return NewInfixExpression(identifier, operator, NewExpression(value)) 195 | } 196 | 197 | // Between performs a "between" condition. 198 | func (identifier Identifier) Between(from, to interface{}) Between { 199 | return NewBetween(identifier, NewExpression(from), NewExpression(to)) 200 | } 201 | 202 | // NotBetween performs a "not between" condition. 203 | func (identifier Identifier) NotBetween(from, to interface{}) Between { 204 | return NewNotBetween(identifier, NewExpression(from), NewExpression(to)) 205 | } 206 | 207 | // IsDistinctFrom performs an "is distinct from" comparison. 208 | func (identifier Identifier) IsDistinctFrom(value interface{}) InfixExpression { 209 | operator := NewComparisonOperator(types.IsDistinctFrom) 210 | return NewInfixExpression(identifier, operator, NewExpression(value)) 211 | } 212 | 213 | // IsNotDistinctFrom performs an "is not distinct from" comparison. 214 | func (identifier Identifier) IsNotDistinctFrom(value interface{}) InfixExpression { 215 | operator := NewComparisonOperator(types.IsNotDistinctFrom) 216 | return NewInfixExpression(identifier, operator, NewExpression(value)) 217 | } 218 | 219 | // Ensure that Identifier is an Expression 220 | var _ Expression = Identifier{} 221 | 222 | // ---------------------------------------------------------------------------- 223 | // Value 224 | // ---------------------------------------------------------------------------- 225 | 226 | // Value is an expression value. 227 | type Value struct { 228 | Value interface{} 229 | } 230 | 231 | // NewValue returns an expression value. 232 | func NewValue(value interface{}) Value { 233 | return Value{ 234 | Value: value, 235 | } 236 | } 237 | 238 | func (Value) expression() {} 239 | 240 | // Write exposes statement as a SQL query. 241 | func (value Value) Write(ctx types.Context) { 242 | if value.Value == nil { 243 | ctx.Write("NULL") 244 | } else { 245 | ctx.Bind(value.Value) 246 | } 247 | } 248 | 249 | // Overlap performs an "overlap" comparison. 250 | func (value Value) Overlap(what interface{}) InfixExpression { 251 | operator := NewComparisonOperator(types.Overlap) 252 | return NewInfixExpression(value, operator, NewWrapper(NewExpression(what))) 253 | } 254 | 255 | // Equal performs an "equal" comparison. 256 | func (value Value) Equal(what interface{}) InfixExpression { 257 | operator := NewComparisonOperator(types.Equal) 258 | return NewInfixExpression(value, operator, NewWrapper(NewExpression(what))) 259 | } 260 | 261 | // NotEqual performs a "not equal" comparison. 262 | func (value Value) NotEqual(what interface{}) InfixExpression { 263 | operator := NewComparisonOperator(types.NotEqual) 264 | return NewInfixExpression(value, operator, NewWrapper(NewExpression(what))) 265 | } 266 | 267 | // Contains performs a "contains" comparison. 268 | func (value Value) Contains(what interface{}) InfixExpression { 269 | operator := NewComparisonOperator(types.Contains) 270 | return NewInfixExpression(value, operator, NewWrapper(NewExpression(what))) 271 | } 272 | 273 | // IsContainedBy performs a "is contained by" comparison. 274 | func (value Value) IsContainedBy(what interface{}) InfixExpression { 275 | operator := NewComparisonOperator(types.IsContainedBy) 276 | return NewInfixExpression(value, operator, NewWrapper(NewExpression(what))) 277 | } 278 | 279 | // IsEmpty returns true if statement is undefined. 280 | func (value Value) IsEmpty() bool { 281 | return false 282 | } 283 | 284 | // Ensure that Value is an Expression 285 | var _ Expression = Value{} 286 | 287 | // ---------------------------------------------------------------------------- 288 | // Array 289 | // ---------------------------------------------------------------------------- 290 | 291 | // ArrayList contains a list of array expression values. 292 | type ArrayList struct { 293 | Values []Expression 294 | } 295 | 296 | // NewArrayListExpression creates a new Expression using a list of values. 297 | func NewArrayListExpression(values ...interface{}) Expression { // nolint: gocyclo 298 | // We pass only one argument and it's a slice or an expression. 299 | arraylist := toArrayList(values[0]) 300 | if len(arraylist.Values) == 0 { 301 | arraylist.Values = []Expression{NewArrayExpression(values...)} 302 | 303 | } 304 | return arraylist 305 | } 306 | 307 | func toArrayList(value interface{}) ArrayList { // nolint: gocyclo 308 | arraylist := ArrayList{} 309 | switch values := value.(type) { 310 | case [][]interface{}: 311 | for i := range values { 312 | arraylist.Values = append(arraylist.Values, NewArrayExpression(values[i]...)) 313 | } 314 | case [][]string: 315 | for i := range values { 316 | raws := make([]interface{}, len(values[i])) 317 | for j := range values[i] { 318 | raws[j] = values[i][j] 319 | } 320 | 321 | arraylist.Values = append(arraylist.Values, NewArrayExpression(raws...)) 322 | } 323 | case [][]int: 324 | for i := range values { 325 | raws := make([]interface{}, len(values[i])) 326 | for j := range values[i] { 327 | raws[j] = values[i][j] 328 | } 329 | arraylist.Values = append(arraylist.Values, NewArrayExpression(raws...)) 330 | } 331 | case [][]int64: 332 | for i := range values { 333 | raws := make([]interface{}, len(values[i])) 334 | for j := range values[i] { 335 | raws[j] = values[i][j] 336 | } 337 | arraylist.Values = append(arraylist.Values, NewArrayExpression(raws...)) 338 | } 339 | case [][]bool: 340 | for i := range values { 341 | raws := make([]interface{}, len(values[i])) 342 | for j := range values[i] { 343 | raws[j] = values[i][j] 344 | } 345 | arraylist.Values = append(arraylist.Values, NewArrayExpression(raws...)) 346 | } 347 | case [][]Expression: 348 | for i := range values { 349 | raws := make([]interface{}, len(values[i])) 350 | for j := range values[i] { 351 | raws[j] = values[i][j] 352 | } 353 | arraylist.Values = append(arraylist.Values, NewArrayExpression(raws...)) 354 | } 355 | } 356 | 357 | return arraylist 358 | } 359 | 360 | func (ArrayList) expression() {} 361 | 362 | // Write exposes statement as a SQL query. 363 | func (array ArrayList) Write(ctx types.Context) { 364 | for i, value := range array.Values { 365 | if i > 0 { 366 | ctx.Write(", ") 367 | } 368 | ctx.Write("(") 369 | value.Write(ctx) 370 | ctx.Write(")") 371 | } 372 | } 373 | 374 | // IsEmpty returns true if statement is undefined. 375 | func (array ArrayList) IsEmpty() bool { 376 | return len(array.Values) == 0 377 | } 378 | 379 | // Ensure that Array is an Expression 380 | var _ Expression = ArrayList{} 381 | 382 | // Array contains a list of expression values. 383 | type Array struct { 384 | Values []Expression 385 | } 386 | 387 | // NewArrayExpression creates a new Expression using a list of values. 388 | func NewArrayExpression(values ...interface{}) Expression { // nolint: gocyclo 389 | // We pass only one argument and it's a slice or an expression. 390 | if len(values) == 1 { 391 | return toArray(values[0]) 392 | } 393 | array := Array{} 394 | for _, value := range values { 395 | array.Append(value) 396 | } 397 | return array 398 | } 399 | 400 | // toArray tries to cast the value to a slice. 401 | // It returns a single element Array otherwise. 402 | func toArray(value interface{}) Array { // nolint: gocyclo 403 | array := Array{} 404 | if value == nil { 405 | return array 406 | } 407 | 408 | switch values := value.(type) { 409 | case []string: 410 | for _, v := range values { 411 | array.Append(v) 412 | } 413 | case []int: 414 | for _, v := range values { 415 | array.Append(v) 416 | } 417 | case []uint: 418 | for _, v := range values { 419 | array.Append(v) 420 | } 421 | case []int8: 422 | for _, v := range values { 423 | array.Append(v) 424 | } 425 | case []int16: 426 | for _, v := range values { 427 | array.Append(v) 428 | } 429 | case []uint16: 430 | for _, v := range values { 431 | array.Append(v) 432 | } 433 | case []int32: 434 | for _, v := range values { 435 | array.Append(v) 436 | } 437 | case []uint32: 438 | for _, v := range values { 439 | array.Append(v) 440 | } 441 | case []int64: 442 | for _, v := range values { 443 | array.Append(v) 444 | } 445 | case []uint64: 446 | for _, v := range values { 447 | array.Append(v) 448 | } 449 | case []bool: 450 | for _, v := range values { 451 | array.Append(v) 452 | } 453 | case []float32: 454 | for _, v := range values { 455 | array.Append(v) 456 | } 457 | case []float64: 458 | for _, v := range values { 459 | array.Append(v) 460 | } 461 | case [][]byte: 462 | for _, v := range values { 463 | array.Append(v) 464 | } 465 | case []Expression: 466 | for _, v := range values { 467 | array.Append(v) 468 | } 469 | case []interface{}: 470 | for _, v := range values { 471 | array.Append(v) 472 | } 473 | default: 474 | array.Append(value) 475 | } 476 | return array 477 | } 478 | 479 | func (Array) expression() {} 480 | 481 | // Write exposes statement as a SQL query. 482 | func (array Array) Write(ctx types.Context) { 483 | for i, value := range array.Values { 484 | if i > 0 { 485 | ctx.Write(", ") 486 | } 487 | value.Write(ctx) 488 | } 489 | } 490 | 491 | // IsEmpty returns true if statement is undefined. 492 | func (array Array) IsEmpty() bool { 493 | return len(array.Values) == 0 494 | } 495 | 496 | // Append an expression to the given array. 497 | func (array *Array) Append(value interface{}) { 498 | array.Values = append(array.Values, NewExpression(value)) 499 | } 500 | 501 | // Ensure that Array is an Expression 502 | var _ Expression = Array{} 503 | 504 | // ---------------------------------------------------------------------------- 505 | // Raw 506 | // ---------------------------------------------------------------------------- 507 | 508 | // Raw is a raw expression value. 509 | type Raw struct { 510 | Value string 511 | } 512 | 513 | // NewRaw returns a raw expression value. 514 | func NewRaw(value string) Raw { 515 | return Raw{ 516 | Value: value, 517 | } 518 | } 519 | 520 | func (Raw) expression() {} 521 | func (Raw) selectExpression() {} 522 | 523 | // Write exposes statement as a SQL query. 524 | func (raw Raw) Write(ctx types.Context) { 525 | ctx.Write(raw.Value) 526 | } 527 | 528 | // IsEmpty returns true if statement is undefined. 529 | func (raw Raw) IsEmpty() bool { 530 | return raw.Value == "" 531 | } 532 | 533 | // Ensure that Raw is an Expression 534 | var _ Expression = Raw{} 535 | 536 | // Ensure that Raw is a SelectExpression 537 | var _ SelectExpression = Raw{} 538 | 539 | // ---------------------------------------------------------------------------- 540 | // Wrapper 541 | // ---------------------------------------------------------------------------- 542 | 543 | // Wrapper encapsulates an expression between parenthesis. 544 | type Wrapper struct { 545 | Value Expression 546 | } 547 | 548 | // NewWrapper returns a new Wrapper expression when it's required. 549 | func NewWrapper(arg Expression) Expression { 550 | switch value := arg.(type) { 551 | case Select: 552 | return &Wrapper{ 553 | Value: value, 554 | } 555 | case Exists: 556 | return &Wrapper{ 557 | Value: value, 558 | } 559 | case NotExists: 560 | return &Wrapper{ 561 | Value: value, 562 | } 563 | default: 564 | return arg 565 | } 566 | } 567 | 568 | func (Wrapper) expression() {} 569 | 570 | // Write exposes statement as a SQL query. 571 | func (wrapper Wrapper) Write(ctx types.Context) { 572 | ctx.Write("(") 573 | wrapper.Value.Write(ctx) 574 | ctx.Write(")") 575 | } 576 | 577 | // IsEmpty returns true if statement is undefined. 578 | func (wrapper Wrapper) IsEmpty() bool { 579 | return wrapper.Value.IsEmpty() 580 | } 581 | 582 | // Ensure that Wrapper is an Expression 583 | var _ Expression = Wrapper{} 584 | 585 | // ---------------------------------------------------------------------------- 586 | // Call 587 | // ---------------------------------------------------------------------------- 588 | 589 | // Call is a call expression. 590 | type Call struct { 591 | function string 592 | args []Expression 593 | } 594 | 595 | // NewCall returns a new Call. 596 | func NewCall(function string, args ...Expression) Call { 597 | return Call{ 598 | function: function, 599 | args: args, 600 | } 601 | } 602 | 603 | func (Call) expression() {} 604 | 605 | // Write writes call to ctx. 606 | func (call Call) Write(ctx types.Context) { 607 | ctx.Write(call.function) 608 | ctx.Write("(") 609 | for i, arg := range call.args { 610 | if i > 0 { 611 | ctx.Write(", ") 612 | } 613 | arg.Write(ctx) 614 | } 615 | ctx.Write(")") 616 | } 617 | 618 | // Equal adds an = expression. 619 | func (call Call) Equal(what interface{}) InfixExpression { 620 | operator := NewComparisonOperator(types.Equal) 621 | return NewInfixExpression(call, operator, NewWrapper(NewExpression(what))) 622 | } 623 | 624 | // GreaterThan performs a "greater than" comparison. 625 | func (call Call) GreaterThan(value interface{}) InfixExpression { 626 | operator := NewComparisonOperator(types.GreaterThan) 627 | return NewInfixExpression(call, operator, NewWrapper(NewExpression(value))) 628 | } 629 | 630 | // LessThan performs a "less than" comparison. 631 | func (call Call) LessThan(value interface{}) InfixExpression { 632 | operator := NewComparisonOperator(types.LessThan) 633 | return NewInfixExpression(call, operator, NewWrapper(NewExpression(value))) 634 | } 635 | 636 | // Like performs a "like" condition. 637 | func (call Call) Like(value interface{}) InfixExpression { 638 | operator := NewComparisonOperator(types.Like) 639 | return NewInfixExpression(call, operator, NewExpression(value)) 640 | } 641 | 642 | // ILike performs a "ilike" condition. 643 | func (call Call) ILike(value interface{}) InfixExpression { 644 | operator := NewComparisonOperator(types.ILike) 645 | return NewInfixExpression(call, operator, NewExpression(value)) 646 | } 647 | 648 | // In adds a IN expression. 649 | func (call Call) In(value ...interface{}) In { 650 | return NewIn(call, NewArrayExpression(value...)) 651 | } 652 | 653 | // Overlap performs an "overlap" comparison. 654 | func (call Call) Overlap(what interface{}) InfixExpression { 655 | operator := NewComparisonOperator(types.Overlap) 656 | return NewInfixExpression(call, operator, NewWrapper(NewExpression(what))) 657 | } 658 | 659 | // IsEmpty reports whether call is empty. 660 | func (call Call) IsEmpty() bool { 661 | return false 662 | } 663 | 664 | // Ensure that Value is an Expression 665 | var _ Expression = Call{} 666 | -------------------------------------------------------------------------------- /stmt/expression_test.go: -------------------------------------------------------------------------------- 1 | package stmt_test 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | "time" 7 | 8 | "github.com/lib/pq" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/ulule/loukoum/v3/stmt" 12 | "github.com/ulule/loukoum/v3/types" 13 | ) 14 | 15 | func TestExpression_Valuer(t *testing.T) { 16 | is := require.New(t) 17 | 18 | // pq.NullTime 19 | { 20 | ctx := &types.NamedContext{} 21 | 22 | source := pq.NullTime{Valid: true, Time: time.Now()} 23 | expression := stmt.NewExpression(source) 24 | value := expression.(stmt.Value) 25 | 26 | is.Equal(source, value.Value) 27 | 28 | value.Write(ctx) 29 | query := ctx.Query() 30 | args := ctx.Values() 31 | 32 | is.Equal(":arg_1", query) 33 | is.Equal(source, args["arg_1"]) 34 | } 35 | { 36 | ctx := &types.NamedContext{} 37 | 38 | source := pq.NullTime{} 39 | expression := stmt.NewExpression(source) 40 | value := expression.(stmt.Value) 41 | 42 | is.Equal(source, value.Value) 43 | 44 | value.Write(ctx) 45 | query := ctx.Query() 46 | args := ctx.Values() 47 | 48 | is.Equal(":arg_1", query) 49 | is.Equal(source, args["arg_1"]) 50 | } 51 | 52 | // sql.NullString 53 | { 54 | ctx := &types.NamedContext{} 55 | 56 | source := sql.NullString{Valid: true, String: "ok"} 57 | expression := stmt.NewExpression(source) 58 | value := expression.(stmt.Value) 59 | 60 | is.Equal(source, value.Value) 61 | 62 | value.Write(ctx) 63 | query := ctx.Query() 64 | args := ctx.Values() 65 | 66 | is.Equal(":arg_1", query) 67 | is.Equal(source, args["arg_1"]) 68 | } 69 | { 70 | ctx := &types.NamedContext{} 71 | 72 | source := sql.NullString{} 73 | expression := stmt.NewExpression(source) 74 | value := expression.(stmt.Value) 75 | 76 | is.Equal(source, value.Value) 77 | 78 | value.Write(ctx) 79 | query := ctx.Query() 80 | args := ctx.Values() 81 | 82 | is.Equal(":arg_1", query) 83 | is.Equal(source, args["arg_1"]) 84 | } 85 | 86 | // sql.NullInt64 87 | { 88 | ctx := &types.NamedContext{} 89 | 90 | source := sql.NullInt64{Valid: true, Int64: 32} 91 | expression := stmt.NewExpression(source) 92 | value := expression.(stmt.Value) 93 | 94 | is.Equal(source, value.Value) 95 | 96 | value.Write(ctx) 97 | query := ctx.Query() 98 | args := ctx.Values() 99 | 100 | is.Equal(":arg_1", query) 101 | is.Equal(source, args["arg_1"]) 102 | } 103 | { 104 | ctx := &types.NamedContext{} 105 | 106 | source := sql.NullInt64{} 107 | expression := stmt.NewExpression(source) 108 | value := expression.(stmt.Value) 109 | 110 | is.Equal(source, value.Value) 111 | 112 | value.Write(ctx) 113 | query := ctx.Query() 114 | args := ctx.Values() 115 | 116 | is.Equal(":arg_1", query) 117 | is.Equal(source, args["arg_1"]) 118 | } 119 | } 120 | 121 | func TestExpression_Encoder(t *testing.T) { 122 | is := require.New(t) 123 | 124 | // StringEncoder 125 | { 126 | ctx := &types.NamedContext{} 127 | 128 | source := tsencoder{value: "foobar"} 129 | expression := stmt.NewExpression(source) 130 | value := expression.(stmt.Value) 131 | 132 | is.Equal(source.value, value.Value) 133 | 134 | value.Write(ctx) 135 | query := ctx.Query() 136 | args := ctx.Values() 137 | 138 | is.Equal(":arg_1", query) 139 | is.Equal(source.value, args["arg_1"]) 140 | } 141 | 142 | // Int64Encoder 143 | { 144 | ctx := &types.NamedContext{} 145 | 146 | source := tiencoder{value: 32} 147 | expression := stmt.NewExpression(source) 148 | value := expression.(stmt.Value) 149 | 150 | is.Equal(source.value, value.Value) 151 | 152 | value.Write(ctx) 153 | query := ctx.Query() 154 | args := ctx.Values() 155 | 156 | is.Equal(":arg_1", query) 157 | is.Equal(source.value, args["arg_1"]) 158 | } 159 | 160 | // BoolEncoder 161 | { 162 | ctx := &types.NamedContext{} 163 | 164 | source := tbencoder{value: true} 165 | expression := stmt.NewExpression(source) 166 | value := expression.(stmt.Value) 167 | 168 | is.Equal(source.value, value.Value) 169 | 170 | value.Write(ctx) 171 | query := ctx.Query() 172 | args := ctx.Values() 173 | 174 | is.Equal(":arg_1", query) 175 | is.Equal(source.value, args["arg_1"]) 176 | } 177 | 178 | // TimeEncoder 179 | { 180 | ctx := &types.NamedContext{} 181 | 182 | source := ttencoder{value: time.Now()} 183 | expression := stmt.NewExpression(source) 184 | value := expression.(stmt.Value) 185 | 186 | is.Equal(source.value, value.Value) 187 | 188 | value.Write(ctx) 189 | query := ctx.Query() 190 | args := ctx.Values() 191 | 192 | is.Equal(":arg_1", query) 193 | is.Equal(source.value, args["arg_1"]) 194 | } 195 | 196 | } 197 | 198 | type tsencoder struct { 199 | value string 200 | } 201 | 202 | func (e tsencoder) String() string { 203 | return e.value 204 | } 205 | 206 | type tiencoder struct { 207 | value int64 208 | } 209 | 210 | func (e tiencoder) Int64() int64 { 211 | return e.value 212 | } 213 | 214 | type tbencoder struct { 215 | value bool 216 | } 217 | 218 | func (e tbencoder) Bool() bool { 219 | return e.value 220 | } 221 | 222 | type ttencoder struct { 223 | value time.Time 224 | } 225 | 226 | func (e ttencoder) Time() time.Time { 227 | return e.value 228 | } 229 | -------------------------------------------------------------------------------- /stmt/from.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // From is a FROM clause. 9 | type From struct { 10 | Tables []Statement 11 | } 12 | 13 | // NewFrom returns a new From instance. 14 | func NewFrom(tables []Statement) From { 15 | return From{ 16 | Tables: tables, 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (from From) Write(ctx types.Context) { 22 | ctx.Write(token.From.String()) 23 | ctx.Write(" ") 24 | 25 | for i := range from.Tables { 26 | from.Tables[i].Write(ctx) 27 | 28 | if i != len(from.Tables)-1 { 29 | ctx.Write(", ") 30 | } 31 | } 32 | } 33 | 34 | // IsEmpty returns true if statement is undefined. 35 | func (from From) IsEmpty() bool { 36 | return len(from.Tables) == 0 37 | } 38 | 39 | // Ensure that From is a Statement 40 | var _ Statement = From{} 41 | -------------------------------------------------------------------------------- /stmt/groupby.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // GroupBy is a GROUP BY clause. 9 | type GroupBy struct { 10 | Columns []Column 11 | } 12 | 13 | // NewGroupBy returns a new GroupBy instance. 14 | func NewGroupBy(columns []Column) GroupBy { 15 | return GroupBy{ 16 | Columns: columns, 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (group GroupBy) Write(ctx types.Context) { 22 | ctx.Write(token.Group.String()) 23 | ctx.Write(" ") 24 | ctx.Write(token.By.String()) 25 | ctx.Write(" ") 26 | for i := range group.Columns { 27 | if i != 0 { 28 | ctx.Write(", ") 29 | } 30 | group.Columns[i].Write(ctx) 31 | } 32 | } 33 | 34 | // IsEmpty returns true if statement is undefined. 35 | func (group GroupBy) IsEmpty() bool { 36 | return len(group.Columns) == 0 37 | } 38 | 39 | // Ensure that GroupBy is a Statement 40 | var _ Statement = GroupBy{} 41 | -------------------------------------------------------------------------------- /stmt/having.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Having is a HAVING clause. 9 | type Having struct { 10 | Statement 11 | Condition Expression 12 | } 13 | 14 | // NewHaving returns a new Having instance. 15 | func NewHaving(expression Expression) Having { 16 | return Having{ 17 | Condition: expression, 18 | } 19 | } 20 | 21 | // Write exposes statement as a SQL query. 22 | func (having Having) Write(ctx types.Context) { 23 | if having.IsEmpty() { 24 | panic("loukoum: a having clause expects at least one condition") 25 | } 26 | 27 | ctx.Write(token.Having.String()) 28 | ctx.Write(" ") 29 | having.Condition.Write(ctx) 30 | } 31 | 32 | // IsEmpty returns true if statement is undefined. 33 | func (having Having) IsEmpty() bool { 34 | return having.Condition == nil || having.Condition.IsEmpty() 35 | } 36 | 37 | // And appends given Expression using AND as logical operator. 38 | func (having Having) And(right Expression) Having { 39 | if having.IsEmpty() { 40 | panic("loukoum: two conditions are required for AND statement") 41 | } 42 | 43 | left := having.Condition 44 | operator := NewAndOperator() 45 | having.Condition = NewInfixExpression(left, operator, right) 46 | return having 47 | } 48 | 49 | // Or appends given Expression using OR as logical operator. 50 | func (having Having) Or(right Expression) Having { 51 | if having.IsEmpty() { 52 | panic("loukoum: two conditions are required for OR statement") 53 | } 54 | 55 | left := having.Condition 56 | operator := NewOrOperator() 57 | having.Condition = NewInfixExpression(left, operator, right) 58 | return having 59 | } 60 | 61 | // Ensure that Having is a Statement 62 | var _ Statement = Having{} 63 | -------------------------------------------------------------------------------- /stmt/in.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/types" 5 | ) 6 | 7 | // In is a IN expression. 8 | type In struct { 9 | Expression Expression 10 | Operator ComparisonOperator 11 | Value Expression 12 | } 13 | 14 | // NewIn returns a new In instance using an inclusive operator. 15 | func NewIn(expression Expression, value Expression) In { 16 | return In{ 17 | Expression: expression, 18 | Operator: NewComparisonOperator(types.In), 19 | Value: value, 20 | } 21 | } 22 | 23 | // NewNotIn returns a new In instance using an exclusive operator. 24 | func NewNotIn(expression Expression, value Expression) In { 25 | return In{ 26 | Expression: expression, 27 | Operator: NewComparisonOperator(types.NotIn), 28 | Value: value, 29 | } 30 | } 31 | 32 | func (In) expression() {} 33 | 34 | // Write exposes statement as a SQL query. 35 | func (in In) Write(ctx types.Context) { 36 | if in.IsEmpty() { 37 | panic("loukoum: expression is undefined") 38 | } 39 | 40 | ctx.Write("(") 41 | in.Expression.Write(ctx) 42 | ctx.Write(" ") 43 | in.Operator.Write(ctx) 44 | ctx.Write(" (") 45 | if !in.Value.IsEmpty() { 46 | in.Value.Write(ctx) 47 | } 48 | ctx.Write("))") 49 | } 50 | 51 | // IsEmpty returns true if statement is undefined. 52 | func (in In) IsEmpty() bool { 53 | return in.Expression.IsEmpty() || in.Operator.IsEmpty() || in.Value == nil 54 | } 55 | 56 | // And creates a new InfixExpression using given Expression. 57 | func (in In) And(value Expression) InfixExpression { 58 | operator := NewAndOperator() 59 | return NewInfixExpression(in, operator, value) 60 | } 61 | 62 | // Or creates a new InfixExpression using given Expression. 63 | func (in In) Or(value Expression) InfixExpression { 64 | operator := NewOrOperator() 65 | return NewInfixExpression(in, operator, value) 66 | } 67 | 68 | // Ensure that In is an Expression 69 | var _ Expression = In{} 70 | -------------------------------------------------------------------------------- /stmt/infix.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/types" 5 | ) 6 | 7 | // InfixExpression is an Expression that has a left and right operand with an operator. 8 | // For example, the expression 'id >= 30' is an infix expression. 9 | type InfixExpression struct { 10 | Left Expression 11 | Operator Operator 12 | Right Expression 13 | } 14 | 15 | // NewInfixExpression returns a new InfixExpression instance. 16 | func NewInfixExpression(left Expression, operator Operator, right Expression) InfixExpression { 17 | return InfixExpression{ 18 | Left: left, 19 | Operator: operator, 20 | Right: right, 21 | } 22 | } 23 | 24 | func (InfixExpression) expression() {} 25 | 26 | // Write exposes statement as a SQL query. 27 | func (expression InfixExpression) Write(ctx types.Context) { 28 | if expression.IsEmpty() { 29 | panic("loukoum: expression is undefined") 30 | } 31 | 32 | ctx.Write("(") 33 | expression.Left.Write(ctx) 34 | ctx.Write(" ") 35 | expression.Operator.Write(ctx) 36 | ctx.Write(" ") 37 | expression.Right.Write(ctx) 38 | ctx.Write(")") 39 | } 40 | 41 | // IsEmpty returns true if statement is undefined. 42 | func (expression InfixExpression) IsEmpty() bool { 43 | return expression.Left == nil || expression.Operator == nil || expression.Right == nil || 44 | expression.Left.IsEmpty() || expression.Operator.IsEmpty() || expression.Right.IsEmpty() 45 | } 46 | 47 | // And creates a new InfixExpression using given Expression. 48 | func (expression InfixExpression) And(value Expression) InfixExpression { 49 | operator := NewAndOperator() 50 | return NewInfixExpression(expression, operator, NewWrapper(value)) 51 | } 52 | 53 | // Or creates a new InfixExpression using given Expression. 54 | func (expression InfixExpression) Or(value Expression) InfixExpression { 55 | operator := NewOrOperator() 56 | return NewInfixExpression(expression, operator, NewWrapper(value)) 57 | } 58 | 59 | // Ensure that InfixExpression is an Expression 60 | var _ Expression = InfixExpression{} 61 | -------------------------------------------------------------------------------- /stmt/insert.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Insert is a INSERT statement. 9 | type Insert struct { 10 | Into Into 11 | Columns []Column 12 | Values Values 13 | OnConflict OnConflict 14 | Returning Returning 15 | Comment Comment 16 | } 17 | 18 | // NewInsert returns a new Insert instance. 19 | func NewInsert() Insert { 20 | return Insert{} 21 | } 22 | 23 | // Write exposes statement as a SQL query. 24 | func (insert Insert) Write(ctx types.Context) { 25 | if insert.IsEmpty() { 26 | panic("loukoum: an insert statement must have at least one column") 27 | } 28 | 29 | ctx.Write(token.Insert.String()) 30 | ctx.Write(" ") 31 | insert.Into.Write(ctx) 32 | 33 | if len(insert.Columns) > 0 { 34 | ctx.Write(" (") 35 | for i := range insert.Columns { 36 | if i != 0 { 37 | ctx.Write(", ") 38 | } 39 | insert.Columns[i].Write(ctx) 40 | } 41 | ctx.Write(")") 42 | } 43 | 44 | if !insert.Values.IsEmpty() { 45 | ctx.Write(" ") 46 | insert.Values.Write(ctx) 47 | } 48 | 49 | if !insert.OnConflict.IsEmpty() { 50 | ctx.Write(" ") 51 | insert.OnConflict.Write(ctx) 52 | } 53 | 54 | if !insert.Returning.IsEmpty() { 55 | ctx.Write(" ") 56 | insert.Returning.Write(ctx) 57 | } 58 | 59 | if !insert.Comment.IsEmpty() { 60 | ctx.Write(token.Semicolon.String()) 61 | ctx.Write(" ") 62 | insert.Comment.Write(ctx) 63 | } 64 | } 65 | 66 | // IsEmpty returns true if statement is undefined. 67 | func (insert Insert) IsEmpty() bool { 68 | return insert.Into.IsEmpty() 69 | } 70 | 71 | // Ensure that Insert is a Statement 72 | var _ Statement = Insert{} 73 | -------------------------------------------------------------------------------- /stmt/into.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Into is a INTO clause. 9 | type Into struct { 10 | Table Table 11 | } 12 | 13 | // NewInto returns a new Into instance. 14 | func NewInto(table Table) Into { 15 | return Into{ 16 | Table: table, 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (into Into) Write(ctx types.Context) { 22 | ctx.Write(token.Into.String()) 23 | ctx.Write(" ") 24 | into.Table.Write(ctx) 25 | } 26 | 27 | // IsEmpty returns true if statement is undefined. 28 | func (into Into) IsEmpty() bool { 29 | return into.Table.IsEmpty() 30 | } 31 | 32 | // Ensure that Into is a Statement 33 | var _ Statement = Into{} 34 | -------------------------------------------------------------------------------- /stmt/join.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Join is a JOIN clause. 9 | type Join struct { 10 | Type types.JoinType 11 | Table Table 12 | Condition Expression 13 | } 14 | 15 | // NewJoin returns a new Join instance. 16 | func NewJoin(kind types.JoinType, table Table, condition Expression) Join { 17 | return Join{ 18 | Type: kind, 19 | Table: table, 20 | Condition: condition, 21 | } 22 | } 23 | 24 | // NewInnerJoin returns a new Join instance using an INNER JOIN. 25 | func NewInnerJoin(table Table, condition Expression) Join { 26 | return NewJoin(types.InnerJoin, table, condition) 27 | } 28 | 29 | // NewLeftJoin returns a new Join instance using a LEFT JOIN. 30 | func NewLeftJoin(table Table, condition Expression) Join { 31 | return NewJoin(types.LeftJoin, table, condition) 32 | } 33 | 34 | // NewRightJoin returns a new Join instance using a RIGHT JOIN. 35 | func NewRightJoin(table Table, condition Expression) Join { 36 | return NewJoin(types.RightJoin, table, condition) 37 | } 38 | 39 | // Write exposes statement as a SQL query. 40 | func (join Join) Write(ctx types.Context) { 41 | ctx.Write(join.Type.String()) 42 | ctx.Write(" ") 43 | join.Table.Write(ctx) 44 | ctx.Write(" ") 45 | ctx.Write(token.On.String()) 46 | ctx.Write(" ") 47 | join.Condition.Write(ctx) 48 | } 49 | 50 | // IsEmpty returns true if statement is undefined. 51 | func (join Join) IsEmpty() bool { 52 | return join.Type == "" || join.Table.IsEmpty() || join.Condition.IsEmpty() 53 | } 54 | 55 | // Ensure that Join is a Statement 56 | var _ Statement = Join{} 57 | -------------------------------------------------------------------------------- /stmt/limit.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/ulule/loukoum/v3/token" 7 | "github.com/ulule/loukoum/v3/types" 8 | ) 9 | 10 | // Limit is a LIMIT clause. 11 | type Limit struct { 12 | Count int64 13 | } 14 | 15 | // NewLimit returns a new Limit instance. 16 | func NewLimit(count int64) Limit { 17 | return Limit{ 18 | Count: count, 19 | } 20 | } 21 | 22 | // Write exposes statement as a SQL query. 23 | func (limit Limit) Write(ctx types.Context) { 24 | if limit.IsEmpty() { 25 | return 26 | } 27 | ctx.Write(token.Limit.String()) 28 | ctx.Write(" ") 29 | ctx.Write(strconv.FormatInt(limit.Count, 10)) 30 | } 31 | 32 | // IsEmpty returns true if statement is undefined. 33 | func (limit Limit) IsEmpty() bool { 34 | return limit.Count == 0 35 | } 36 | 37 | // Ensure that Limit is a Statement 38 | var _ Statement = Limit{} 39 | -------------------------------------------------------------------------------- /stmt/offset.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/ulule/loukoum/v3/token" 7 | "github.com/ulule/loukoum/v3/types" 8 | ) 9 | 10 | // Offset is a OFFSET clause. 11 | type Offset struct { 12 | Start int64 13 | } 14 | 15 | // NewOffset returns a new Offset instance. 16 | func NewOffset(start int64) Offset { 17 | return Offset{ 18 | Start: start, 19 | } 20 | } 21 | 22 | // Write exposes statement as a SQL query. 23 | func (offset Offset) Write(ctx types.Context) { 24 | if offset.IsEmpty() { 25 | return 26 | } 27 | ctx.Write(token.Offset.String()) 28 | ctx.Write(" ") 29 | ctx.Write(strconv.FormatInt(offset.Start, 10)) 30 | } 31 | 32 | // IsEmpty returns true if statement is undefined. 33 | func (offset Offset) IsEmpty() bool { 34 | return offset.Start == 0 35 | } 36 | 37 | // Ensure that Offset is a Statement 38 | var _ Statement = Offset{} 39 | -------------------------------------------------------------------------------- /stmt/on.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // OnExpression is a SQL expression for a ON statement. 9 | type OnExpression interface { 10 | Statement 11 | And(value OnExpression) OnExpression 12 | Or(value OnExpression) OnExpression 13 | onExpression() 14 | } 15 | 16 | // OnClause is a ON clause. 17 | type OnClause struct { 18 | Left Column 19 | Right Column 20 | } 21 | 22 | // NewOnClause returns a new On instance. 23 | func NewOnClause(left, right Column) OnClause { 24 | return OnClause{ 25 | Left: left, 26 | Right: right, 27 | } 28 | } 29 | 30 | func (OnClause) expression() {} 31 | func (OnClause) onExpression() {} 32 | 33 | // And creates a new InfixOnExpression using given OnExpression. 34 | func (on OnClause) And(value OnExpression) OnExpression { 35 | operator := NewAndOperator() 36 | return NewInfixOnExpression(on, operator, value) 37 | } 38 | 39 | // Or creates a new InfixOnExpression using given OnExpression. 40 | func (on OnClause) Or(value OnExpression) OnExpression { 41 | operator := NewOrOperator() 42 | return NewInfixOnExpression(on, operator, value) 43 | } 44 | 45 | // Write exposes statement as a SQL query. 46 | func (on OnClause) Write(ctx types.Context) { 47 | on.Left.Write(ctx) 48 | ctx.Write(" ") 49 | ctx.Write(token.Equals.String()) 50 | ctx.Write(" ") 51 | on.Right.Write(ctx) 52 | } 53 | 54 | // IsEmpty returns true if statement is undefined. 55 | func (on OnClause) IsEmpty() bool { 56 | return on.Left.IsEmpty() || on.Right.IsEmpty() 57 | } 58 | 59 | // Ensure that OnClause is an OnExpression 60 | var _ OnExpression = OnClause{} 61 | 62 | // InfixOnExpression is an OnExpression that has a left and right clauses with a logical operator 63 | // for an ON statement. 64 | type InfixOnExpression struct { 65 | Left OnExpression 66 | Operator LogicalOperator 67 | Right OnExpression 68 | } 69 | 70 | // NewInfixOnExpression returns a new InfixOnExpression instance. 71 | func NewInfixOnExpression(left OnExpression, operator LogicalOperator, right OnExpression) InfixOnExpression { 72 | return InfixOnExpression{ 73 | Left: left, 74 | Operator: operator, 75 | Right: right, 76 | } 77 | } 78 | 79 | func (InfixOnExpression) expression() {} 80 | func (InfixOnExpression) onExpression() {} 81 | 82 | // Write exposes statement as a SQL query. 83 | func (expression InfixOnExpression) Write(ctx types.Context) { 84 | if expression.IsEmpty() { 85 | panic("loukoum: expression is undefined") 86 | } 87 | 88 | ctx.Write("(") 89 | expression.Left.Write(ctx) 90 | ctx.Write(" ") 91 | expression.Operator.Write(ctx) 92 | ctx.Write(" ") 93 | expression.Right.Write(ctx) 94 | ctx.Write(")") 95 | } 96 | 97 | // IsEmpty returns true if statement is undefined. 98 | func (expression InfixOnExpression) IsEmpty() bool { 99 | return expression.Left == nil || expression.Right == nil || 100 | expression.Left.IsEmpty() || expression.Operator.IsEmpty() || expression.Right.IsEmpty() 101 | } 102 | 103 | // And creates a new InfixOnExpression using given OnExpression. 104 | func (expression InfixOnExpression) And(value OnExpression) OnExpression { 105 | operator := NewAndOperator() 106 | return NewInfixOnExpression(expression, operator, value) 107 | } 108 | 109 | // Or creates a new InfixOnExpression using given OnExpression. 110 | func (expression InfixOnExpression) Or(value OnExpression) OnExpression { 111 | operator := NewOrOperator() 112 | return NewInfixOnExpression(expression, operator, value) 113 | } 114 | 115 | // Ensure that InfixOnExpression is an OnExpression 116 | var _ OnExpression = InfixOnExpression{} 117 | -------------------------------------------------------------------------------- /stmt/operator.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/types" 5 | ) 6 | 7 | // Operator are used to compose expressions. 8 | type Operator interface { 9 | Statement 10 | operator() 11 | } 12 | 13 | // LogicalOperator are used to evaluate two expressions using a logical operator. 14 | type LogicalOperator struct { 15 | Operator types.LogicalOperator 16 | } 17 | 18 | // NewAndOperator returns a new AND LogicalOperator instance. 19 | func NewAndOperator() LogicalOperator { 20 | return NewLogicalOperator(types.And) 21 | } 22 | 23 | // NewOrOperator returns a new OR LogicalOperator instance. 24 | func NewOrOperator() LogicalOperator { 25 | return NewLogicalOperator(types.Or) 26 | } 27 | 28 | // NewLogicalOperator returns a new LogicalOperator instance. 29 | func NewLogicalOperator(operator types.LogicalOperator) LogicalOperator { 30 | return LogicalOperator{ 31 | Operator: operator, 32 | } 33 | } 34 | 35 | func (LogicalOperator) operator() {} 36 | 37 | // Write exposes statement as a SQL query. 38 | func (operator LogicalOperator) Write(ctx types.Context) { 39 | ctx.Write(operator.Operator.String()) 40 | } 41 | 42 | // IsEmpty returns true if statement is undefined. 43 | func (operator LogicalOperator) IsEmpty() bool { 44 | return operator.Operator == "" 45 | } 46 | 47 | // Ensure that LogicalOperator is an Operator 48 | var _ Operator = LogicalOperator{} 49 | 50 | // ComparisonOperator are used to evaluate two expressions using a comparison operator. 51 | type ComparisonOperator struct { 52 | Operator types.ComparisonOperator 53 | } 54 | 55 | // NewComparisonOperator returns a new ComparisonOperator instance. 56 | func NewComparisonOperator(operator types.ComparisonOperator) ComparisonOperator { 57 | return ComparisonOperator{ 58 | Operator: operator, 59 | } 60 | } 61 | 62 | func (ComparisonOperator) operator() {} 63 | 64 | // Write exposes statement as a SQL query. 65 | func (operator ComparisonOperator) Write(ctx types.Context) { 66 | ctx.Write(operator.Operator.String()) 67 | } 68 | 69 | // IsEmpty returns true if statement is undefined. 70 | func (operator ComparisonOperator) IsEmpty() bool { 71 | return operator.Operator == "" 72 | } 73 | 74 | // Ensure that ComparisonOperator is an Operator 75 | var _ Operator = ComparisonOperator{} 76 | -------------------------------------------------------------------------------- /stmt/order.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/types" 5 | ) 6 | 7 | // Order is an expression of a ORDER BY clause. 8 | type Order struct { 9 | Expression string 10 | Type types.OrderType 11 | } 12 | 13 | // NewOrder returns a new Order instance. 14 | func NewOrder(expression string, kind types.OrderType) Order { 15 | return Order{ 16 | Expression: expression, 17 | Type: kind, 18 | } 19 | } 20 | 21 | // Write exposes statement as a SQL query. 22 | func (order Order) Write(ctx types.Context) { 23 | if order.IsEmpty() { 24 | return 25 | } 26 | ctx.Write(order.Expression) 27 | ctx.Write(" ") 28 | ctx.Write(order.Type.String()) 29 | } 30 | 31 | // IsEmpty returns true if statement is undefined. 32 | func (order Order) IsEmpty() bool { 33 | return order.Expression == "" 34 | } 35 | 36 | // Ensure that Order is a Statement 37 | var _ Statement = Order{} 38 | -------------------------------------------------------------------------------- /stmt/orderby.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // OrderBy is a ORDER BY clause. 9 | type OrderBy struct { 10 | Orders []Order 11 | } 12 | 13 | // NewOrderBy returns a new OrderBy instance. 14 | func NewOrderBy(orders []Order) OrderBy { 15 | return OrderBy{ 16 | Orders: orders, 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (order OrderBy) Write(ctx types.Context) { 22 | if order.IsEmpty() { 23 | return 24 | } 25 | ctx.Write(token.Order.String()) 26 | ctx.Write(" ") 27 | ctx.Write(token.By.String()) 28 | ctx.Write(" ") 29 | for i := range order.Orders { 30 | if i != 0 { 31 | ctx.Write(", ") 32 | } 33 | order.Orders[i].Write(ctx) 34 | } 35 | } 36 | 37 | // IsEmpty returns true if statement is undefined. 38 | func (order OrderBy) IsEmpty() bool { 39 | return len(order.Orders) == 0 40 | } 41 | 42 | // Ensure that OrderBy is a Statement 43 | var _ Statement = OrderBy{} 44 | -------------------------------------------------------------------------------- /stmt/prefix.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/types" 5 | ) 6 | 7 | // Prefix is a prefix expression. 8 | type Prefix struct { 9 | Prefix string 10 | } 11 | 12 | // NewPrefix returns a new Prefix instance. 13 | func NewPrefix(prefix string) Prefix { 14 | return Prefix{ 15 | Prefix: prefix, 16 | } 17 | } 18 | 19 | // Write exposes statement as a SQL query. 20 | func (prefix Prefix) Write(ctx types.Context) { 21 | if prefix.IsEmpty() { 22 | return 23 | } 24 | ctx.Write(prefix.Prefix) 25 | } 26 | 27 | // IsEmpty returns true if statement is undefined. 28 | func (prefix Prefix) IsEmpty() bool { 29 | return prefix.Prefix == "" 30 | } 31 | 32 | // Ensure that Prefix is a Statement 33 | var _ Statement = Prefix{} 34 | -------------------------------------------------------------------------------- /stmt/returning.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Returning is a RETURNING clause. 9 | type Returning struct { 10 | Columns []SelectExpression 11 | } 12 | 13 | // NewReturning returns a new Returning instance. 14 | func NewReturning(exprs []SelectExpression) Returning { 15 | return Returning{ 16 | Columns: exprs, 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (returning Returning) Write(ctx types.Context) { 22 | ctx.Write(token.Returning.String()) 23 | ctx.Write(" ") 24 | 25 | for i := range returning.Columns { 26 | if i > 0 { 27 | ctx.Write(", ") 28 | } 29 | returning.Columns[i].Write(ctx) 30 | } 31 | } 32 | 33 | // IsEmpty returns true if statement is undefined. 34 | func (returning Returning) IsEmpty() bool { 35 | return len(returning.Columns) == 0 36 | } 37 | 38 | // Ensure that Returning is a Statement 39 | var _ Statement = Returning{} 40 | -------------------------------------------------------------------------------- /stmt/select.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // SelectExpression is a SQL expression for a SELECT statement. 9 | type SelectExpression interface { 10 | Statement 11 | selectExpression() 12 | } 13 | 14 | // Select is a SELECT statement. 15 | type Select struct { 16 | Prefix Prefix 17 | With With 18 | Distinct bool 19 | DistinctOn DistinctOn 20 | Expressions []SelectExpression 21 | From From 22 | Joins []Join 23 | Where Where 24 | GroupBy GroupBy 25 | Having Having 26 | OrderBy OrderBy 27 | Limit Limit 28 | Offset Offset 29 | Suffix Suffix 30 | Comment Comment 31 | } 32 | 33 | // NewSelect returns a new Select instance. 34 | func NewSelect() Select { 35 | return Select{} 36 | } 37 | 38 | // Write exposes statement as a SQL query. 39 | func (selekt Select) Write(ctx types.Context) { 40 | if selekt.IsEmpty() { 41 | panic("loukoum: select statements must have at least one column") 42 | } 43 | 44 | selekt.writeHead(ctx) 45 | selekt.writeMiddle(ctx) 46 | selekt.writeTail(ctx) 47 | } 48 | 49 | func (selekt Select) writeHead(ctx types.Context) { 50 | if !selekt.Prefix.IsEmpty() { 51 | selekt.Prefix.Write(ctx) 52 | ctx.Write(" ") 53 | } 54 | 55 | if !selekt.With.IsEmpty() { 56 | selekt.With.Write(ctx) 57 | ctx.Write(" ") 58 | } 59 | 60 | ctx.Write(token.Select.String()) 61 | 62 | if !selekt.DistinctOn.IsEmpty() { 63 | ctx.Write(" ") 64 | selekt.DistinctOn.Write(ctx) 65 | } 66 | 67 | if selekt.Distinct { 68 | ctx.Write(" ") 69 | ctx.Write(token.Distinct.String()) 70 | } 71 | 72 | for i := range selekt.Expressions { 73 | if i == 0 { 74 | ctx.Write(" ") 75 | } else { 76 | ctx.Write(", ") 77 | } 78 | selekt.Expressions[i].Write(ctx) 79 | } 80 | 81 | if !selekt.From.IsEmpty() { 82 | ctx.Write(" ") 83 | selekt.From.Write(ctx) 84 | } 85 | } 86 | 87 | func (selekt Select) writeMiddle(ctx types.Context) { 88 | for i := range selekt.Joins { 89 | ctx.Write(" ") 90 | selekt.Joins[i].Write(ctx) 91 | } 92 | 93 | if !selekt.Where.IsEmpty() { 94 | ctx.Write(" ") 95 | selekt.Where.Write(ctx) 96 | } 97 | 98 | if !selekt.GroupBy.IsEmpty() { 99 | ctx.Write(" ") 100 | selekt.GroupBy.Write(ctx) 101 | } 102 | 103 | if !selekt.Having.IsEmpty() { 104 | ctx.Write(" ") 105 | selekt.Having.Write(ctx) 106 | } 107 | } 108 | 109 | func (selekt Select) writeTail(ctx types.Context) { 110 | if !selekt.OrderBy.IsEmpty() { 111 | ctx.Write(" ") 112 | selekt.OrderBy.Write(ctx) 113 | } 114 | 115 | if !selekt.Limit.IsEmpty() { 116 | ctx.Write(" ") 117 | selekt.Limit.Write(ctx) 118 | } 119 | 120 | if !selekt.Offset.IsEmpty() { 121 | ctx.Write(" ") 122 | selekt.Offset.Write(ctx) 123 | } 124 | 125 | if !selekt.Suffix.IsEmpty() { 126 | ctx.Write(" ") 127 | selekt.Suffix.Write(ctx) 128 | } 129 | 130 | if !selekt.Comment.IsEmpty() { 131 | ctx.Write(token.Semicolon.String()) 132 | ctx.Write(" ") 133 | selekt.Comment.Write(ctx) 134 | } 135 | } 136 | 137 | // IsEmpty returns true if statement is undefined. 138 | func (selekt Select) IsEmpty() bool { 139 | return len(selekt.Expressions) == 0 140 | } 141 | 142 | func (Select) expression() {} 143 | 144 | // Ensure that Select is an Expression 145 | var _ Expression = Select{} 146 | -------------------------------------------------------------------------------- /stmt/set.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/ulule/loukoum/v3/token" 7 | "github.com/ulule/loukoum/v3/types" 8 | ) 9 | 10 | // Set is a SET clause. 11 | type Set struct { 12 | Pairs PairContainer 13 | } 14 | 15 | // NewSet returns a new Set instance. 16 | func NewSet() Set { 17 | return Set{ 18 | Pairs: NewPairContainer(), 19 | } 20 | } 21 | 22 | // Write exposes statement as a SQL query. 23 | func (set Set) Write(ctx types.Context) { 24 | ctx.Write(token.Set.String()) 25 | ctx.Write(" ") 26 | set.Pairs.Write(ctx) 27 | } 28 | 29 | // IsEmpty returns true if statement is undefined. 30 | func (set Set) IsEmpty() bool { 31 | return set.Pairs.IsEmpty() 32 | } 33 | 34 | // Ensure that Set is a Statement. 35 | var _ Statement = Set{} 36 | 37 | // PairMode define the mode of PairContainer. 38 | type PairMode uint8 39 | 40 | const ( 41 | // PairUnknownMode define an unknown mode. 42 | PairUnknownMode = PairMode(iota) 43 | // PairAssociativeMode define a key-value mode for PairContainer. 44 | PairAssociativeMode 45 | // PairArrayMode define a column-list mode for PairContainer. 46 | PairArrayMode 47 | ) 48 | 49 | // PairContainer is a composite collection that store a list of values for SET clause. 50 | type PairContainer struct { 51 | Mode PairMode 52 | Map map[Column]Expression 53 | Columns []Column 54 | Expressions []Expression 55 | } 56 | 57 | // NewPairContainer creates a new PairContainer. 58 | func NewPairContainer() PairContainer { 59 | return PairContainer{ 60 | Mode: PairUnknownMode, 61 | Map: map[Column]Expression{}, 62 | Columns: []Column{}, 63 | Expressions: []Expression{}, 64 | } 65 | } 66 | 67 | // Add appends given column and expression. 68 | // It will configure Set's syntax to key-value (a.k.a "standard", "default" or "associative"). 69 | // 70 | // Example: 71 | // 72 | // * SET foo = 1, bar = 2, baz = 3 73 | // 74 | func (pairs *PairContainer) Add(column Column, expression Expression) { 75 | if pairs.Mode == PairUnknownMode { 76 | pairs.Mode = PairAssociativeMode 77 | } 78 | if pairs.Mode != PairAssociativeMode { 79 | panic("loukoum: you can only use pairs in key-value or column-list syntax") 80 | } 81 | 82 | _, ok := pairs.Map[column] 83 | if !ok { 84 | pairs.Columns = append(pairs.Columns, column) 85 | } 86 | 87 | pairs.Map[column] = expression 88 | } 89 | 90 | // Set appends given column. It will configure Set's syntax to column-list. 91 | // You may use Use(...) function to provide required expressions. 92 | // 93 | // Example: 94 | // 95 | // * SET (foo, bar, baz) = (1, 2, 3) 96 | // * SET (foo, bar, baz) = (sub-select) 97 | // 98 | func (pairs *PairContainer) Set(column Column) { 99 | if pairs.Mode == PairUnknownMode { 100 | pairs.Mode = PairArrayMode 101 | } 102 | if pairs.Mode != PairArrayMode { 103 | panic("loukoum: you can only use pairs in key-value or column-list syntax") 104 | } 105 | 106 | pairs.Columns = append(pairs.Columns, column) 107 | } 108 | 109 | // Use appends given expression if Set's syntax is defined column-list. 110 | // You have to use Set(...) function to provide required columns. 111 | // 112 | // Example: 113 | // 114 | // * SET (foo, bar, baz) = (1, 2, 3) 115 | // * SET (foo, bar, baz) = (sub-select) 116 | // 117 | func (pairs *PairContainer) Use(expression Expression) { 118 | if pairs.Mode != PairArrayMode { 119 | panic("loukoum: you have to define pairs columns first") 120 | } 121 | 122 | pairs.Expressions = append(pairs.Expressions, expression) 123 | } 124 | 125 | // Values returns columns and expressions of current instance. 126 | func (pairs PairContainer) Values() ([]Column, []Expression) { 127 | if pairs.Mode != PairAssociativeMode { 128 | return pairs.Columns, pairs.Expressions 129 | } 130 | 131 | sort.Slice(pairs.Columns, func(i, j int) bool { 132 | return pairs.Columns[i].Name < pairs.Columns[j].Name || 133 | pairs.Columns[i].Alias < pairs.Columns[j].Alias 134 | }) 135 | 136 | columns := make([]Column, 0, len(pairs.Columns)) 137 | expressions := make([]Expression, 0, len(pairs.Columns)) 138 | 139 | for i := range pairs.Columns { 140 | column := pairs.Columns[i] 141 | expression, ok := pairs.Map[column] 142 | if !ok { 143 | panic("loukoum: invalid state for stmt.PairContainer") 144 | } 145 | columns = append(columns, column) 146 | expressions = append(expressions, expression) 147 | } 148 | 149 | return columns, expressions 150 | } 151 | 152 | // Write exposes statement as a SQL query. 153 | func (pairs PairContainer) Write(ctx types.Context) { 154 | if pairs.IsEmpty() { 155 | panic("loukoum: values for SET clause are required") 156 | } 157 | if pairs.Mode == PairAssociativeMode { 158 | pairs.WriteAssociative(ctx) 159 | } 160 | if pairs.Mode == PairArrayMode { 161 | pairs.WriteArray(ctx) 162 | } 163 | } 164 | 165 | // WriteAssociative exposes statement as a SQL query using a key-value syntax. 166 | func (pairs PairContainer) WriteAssociative(ctx types.Context) { 167 | columns, expressions := pairs.Values() 168 | 169 | for i := range columns { 170 | if i != 0 { 171 | ctx.Write(", ") 172 | } 173 | 174 | columns[i].Write(ctx) 175 | ctx.Write(" = ") 176 | expressions[i].Write(ctx) 177 | } 178 | } 179 | 180 | // WriteArray exposes statement as a SQL query using a column-list syntax. 181 | func (pairs PairContainer) WriteArray(ctx types.Context) { 182 | ctx.Write("(") 183 | for i := range pairs.Columns { 184 | if i != 0 { 185 | ctx.Write(", ") 186 | } 187 | pairs.Columns[i].Write(ctx) 188 | } 189 | ctx.Write(")") 190 | 191 | ctx.Write(" = ") 192 | 193 | ctx.Write("(") 194 | for i := range pairs.Expressions { 195 | if i != 0 { 196 | ctx.Write(", ") 197 | } 198 | pairs.Expressions[i].Write(ctx) 199 | } 200 | ctx.Write(")") 201 | } 202 | 203 | // IsEmpty returns true if statement is undefined. 204 | func (pairs PairContainer) IsEmpty() bool { 205 | return pairs.Mode == PairUnknownMode || (len(pairs.Map) == 0 && len(pairs.Expressions) == 0) 206 | } 207 | 208 | // Ensure that PairContainer is a Statement. 209 | var _ Statement = PairContainer{} 210 | -------------------------------------------------------------------------------- /stmt/stmt.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/ulule/loukoum/v3/types" 8 | ) 9 | 10 | // Statement is the interface of the component which is the minimum unit constituting SQL. 11 | // All types that implement this interface can be built as SQL. 12 | type Statement interface { 13 | // IsEmpty returns true if statement is undefined. 14 | IsEmpty() bool 15 | // Write exposes statement as a SQL query. 16 | Write(ctx types.Context) 17 | } 18 | 19 | func quote(ident string) string { 20 | split := strings.Split(ident, ".") 21 | quoted := make([]string, 0, len(split)) 22 | for i := range split { 23 | quoted = append(quoted, strconv.Quote(split[i])) 24 | } 25 | return strings.Join(quoted, ".") 26 | } 27 | -------------------------------------------------------------------------------- /stmt/subquery.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Exists is a subquery expression. 9 | type Exists struct { 10 | Subquery Expression 11 | } 12 | 13 | // NewExists returns a new Exists instance. 14 | func NewExists(value interface{}) Exists { 15 | return Exists{ 16 | Subquery: NewExpression(value), 17 | } 18 | } 19 | 20 | func (Exists) expression() {} 21 | 22 | // Write exposes statement as a SQL query. 23 | func (exists Exists) Write(ctx types.Context) { 24 | ctx.Write(token.Exists.String()) 25 | ctx.Write(" (") 26 | exists.Subquery.Write(ctx) 27 | ctx.Write(")") 28 | } 29 | 30 | // IsEmpty returns true if statement is undefined. 31 | func (Exists) IsEmpty() bool { 32 | return false 33 | } 34 | 35 | func (Exists) selectExpression() {} 36 | 37 | // Ensure that Exists is an Expression 38 | var _ Expression = Exists{} 39 | 40 | // NotExists is a subquery expression. 41 | type NotExists struct { 42 | Subquery Expression 43 | } 44 | 45 | // NewNotExists returns a new NotExists instance. 46 | func NewNotExists(value interface{}) NotExists { 47 | return NotExists{ 48 | Subquery: NewExpression(value), 49 | } 50 | } 51 | 52 | func (NotExists) expression() {} 53 | 54 | // Write exposes statement as a SQL query. 55 | func (nexists NotExists) Write(ctx types.Context) { 56 | ctx.Write(token.Not.String()) 57 | ctx.Write(" ") 58 | ctx.Write(token.Exists.String()) 59 | ctx.Write(" (") 60 | nexists.Subquery.Write(ctx) 61 | ctx.Write(")") 62 | } 63 | 64 | // IsEmpty returns true if statement is undefined. 65 | func (NotExists) IsEmpty() bool { 66 | return false 67 | } 68 | 69 | func (NotExists) selectExpression() {} 70 | 71 | // Ensure that NotExists is an Expression 72 | var _ Expression = NotExists{} 73 | -------------------------------------------------------------------------------- /stmt/suffix.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/types" 5 | ) 6 | 7 | // Suffix is a suffix expression. 8 | type Suffix struct { 9 | Suffix string 10 | } 11 | 12 | // NewSuffix returns a new Suffix instance. 13 | func NewSuffix(suffix string) Suffix { 14 | return Suffix{ 15 | Suffix: suffix, 16 | } 17 | } 18 | 19 | // Write exposes statement as a SQL query. 20 | func (suffix Suffix) Write(ctx types.Context) { 21 | if suffix.IsEmpty() { 22 | return 23 | } 24 | ctx.Write(suffix.Suffix) 25 | } 26 | 27 | // IsEmpty returns true if statement is undefined. 28 | func (suffix Suffix) IsEmpty() bool { 29 | return suffix.Suffix == "" 30 | } 31 | 32 | // Ensure that Suffix is a Statement 33 | var _ Statement = Suffix{} 34 | -------------------------------------------------------------------------------- /stmt/table.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Table is a table identifier. 9 | type Table struct { 10 | Alias string 11 | Name string 12 | only bool 13 | } 14 | 15 | // NewTable returns a new Table instance. 16 | func NewTable(name string) Table { 17 | return NewTableAlias(name, "") 18 | } 19 | 20 | // Only sets ONLY clause to the table. 21 | func (table Table) Only() Table { 22 | table.only = true 23 | return table 24 | } 25 | 26 | // NewTableAlias returns a new Table instance with an alias. 27 | func NewTableAlias(name, alias string) Table { 28 | return Table{ 29 | Name: name, 30 | Alias: alias, 31 | } 32 | } 33 | 34 | // As is used to give an alias name to the column. 35 | func (table Table) As(alias string) Table { 36 | table.Alias = alias 37 | return table 38 | } 39 | 40 | // Write exposes statement as a SQL query. 41 | func (table Table) Write(ctx types.Context) { 42 | if table.only { 43 | ctx.Write(token.Only.String()) 44 | ctx.Write(" ") 45 | } 46 | ctx.Write(quote(table.Name)) 47 | if table.Alias != "" { 48 | ctx.Write(" ") 49 | ctx.Write(token.As.String()) 50 | ctx.Write(" ") 51 | ctx.Write(quote(table.Alias)) 52 | } 53 | } 54 | 55 | // IsEmpty returns true if statement is undefined. 56 | func (table Table) IsEmpty() bool { 57 | return table.Name == "" 58 | } 59 | 60 | // Ensure that Table is a Statement 61 | var _ Statement = Table{} 62 | -------------------------------------------------------------------------------- /stmt/update.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Update is the UPDATE statement. 9 | type Update struct { 10 | With With 11 | Table Table 12 | Only bool 13 | From From 14 | Set Set 15 | Where Where 16 | Returning Returning 17 | Comment Comment 18 | } 19 | 20 | // NewUpdate returns a new Update instance. 21 | func NewUpdate(table Table) Update { 22 | return Update{ 23 | Table: table, 24 | Set: NewSet(), 25 | } 26 | } 27 | 28 | // Write exposes statement as a SQL query. 29 | func (update Update) Write(ctx types.Context) { 30 | if update.IsEmpty() { 31 | panic("loukoum: an update statement must have a table and/or values") 32 | } 33 | 34 | if !update.With.IsEmpty() { 35 | update.With.Write(ctx) 36 | ctx.Write(" ") 37 | } 38 | 39 | ctx.Write(token.Update.String()) 40 | 41 | if update.Only { 42 | ctx.Write(" ") 43 | ctx.Write(token.Only.String()) 44 | } 45 | 46 | ctx.Write(" ") 47 | update.Table.Write(ctx) 48 | 49 | ctx.Write(" ") 50 | update.Set.Write(ctx) 51 | 52 | if !update.From.IsEmpty() { 53 | ctx.Write(" ") 54 | update.From.Write(ctx) 55 | } 56 | 57 | if !update.Where.IsEmpty() { 58 | ctx.Write(" ") 59 | update.Where.Write(ctx) 60 | } 61 | 62 | if !update.Returning.IsEmpty() { 63 | ctx.Write(" ") 64 | update.Returning.Write(ctx) 65 | } 66 | 67 | if !update.Comment.IsEmpty() { 68 | ctx.Write(token.Semicolon.String()) 69 | ctx.Write(" ") 70 | update.Comment.Write(ctx) 71 | } 72 | } 73 | 74 | // IsEmpty returns true if statement is undefined. 75 | func (update Update) IsEmpty() bool { 76 | return update.Table.IsEmpty() || update.Set.IsEmpty() 77 | } 78 | 79 | // Ensure that Update is a Statement 80 | var _ Statement = Update{} 81 | -------------------------------------------------------------------------------- /stmt/using.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Using is a USING clause. 9 | type Using struct { 10 | Tables []Table 11 | } 12 | 13 | // NewUsing returns a new Using instance. 14 | func NewUsing(tables []Table) Using { 15 | return Using{ 16 | Tables: tables, 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (using Using) Write(ctx types.Context) { 22 | if using.IsEmpty() { 23 | return 24 | } 25 | 26 | ctx.Write(token.Using.String()) 27 | ctx.Write(" ") 28 | 29 | for i := range using.Tables { 30 | if i != 0 { 31 | ctx.Write(", ") 32 | } 33 | using.Tables[i].Write(ctx) 34 | } 35 | } 36 | 37 | // IsEmpty returns true if statement is undefined. 38 | func (using Using) IsEmpty() bool { 39 | return len(using.Tables) == 0 40 | } 41 | 42 | // Ensure that Using is a Statement 43 | var _ Statement = Using{} 44 | -------------------------------------------------------------------------------- /stmt/values.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Values is a VALUES clause. 9 | type Values struct { 10 | Values Expression 11 | } 12 | 13 | // NewValues returns a new Values instance. 14 | func NewValues(values Expression) Values { 15 | return Values{ 16 | Values: values, 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (values Values) Write(ctx types.Context) { 22 | if values.IsEmpty() { 23 | return 24 | } 25 | 26 | ctx.Write(token.Values.String()) 27 | ctx.Write(" ") 28 | values.Values.Write(ctx) 29 | } 30 | 31 | // IsEmpty returns true if statement is undefined. 32 | func (values Values) IsEmpty() bool { 33 | return values.Values == nil || (values.Values != nil && values.Values.IsEmpty()) 34 | } 35 | 36 | // Ensure that Values is a Statement 37 | var _ Statement = Values{} 38 | -------------------------------------------------------------------------------- /stmt/where.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // Where is a WHERE clause. 9 | type Where struct { 10 | Condition Expression 11 | } 12 | 13 | // NewWhere returns a new Where instance. 14 | func NewWhere(expression Expression) Where { 15 | return Where{ 16 | Condition: NewWrapper(expression), 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (where Where) Write(ctx types.Context) { 22 | if where.IsEmpty() { 23 | panic("loukoum: a where clause expects at least one condition") 24 | } 25 | 26 | ctx.Write(token.Where.String()) 27 | ctx.Write(" ") 28 | where.Condition.Write(ctx) 29 | } 30 | 31 | // IsEmpty returns true if statement is undefined. 32 | func (where Where) IsEmpty() bool { 33 | return where.Condition == nil || where.Condition.IsEmpty() 34 | } 35 | 36 | // And appends given Expression using AND as logical operator. 37 | func (where Where) And(right Expression) Where { 38 | if where.IsEmpty() { 39 | panic("loukoum: two conditions are required for AND statement") 40 | } 41 | 42 | left := where.Condition 43 | operator := NewAndOperator() 44 | where.Condition = NewInfixExpression(left, operator, NewWrapper(right)) 45 | return where 46 | } 47 | 48 | // Or appends given Expression using OR as logical operator. 49 | func (where Where) Or(right Expression) Where { 50 | if where.IsEmpty() { 51 | panic("loukoum: two conditions are required for OR statement") 52 | } 53 | 54 | left := where.Condition 55 | operator := NewOrOperator() 56 | where.Condition = NewInfixExpression(left, operator, NewWrapper(right)) 57 | return where 58 | } 59 | 60 | // Ensure that Where is a Statement 61 | var _ Statement = Where{} 62 | -------------------------------------------------------------------------------- /stmt/with.go: -------------------------------------------------------------------------------- 1 | package stmt 2 | 3 | import ( 4 | "github.com/ulule/loukoum/v3/token" 5 | "github.com/ulule/loukoum/v3/types" 6 | ) 7 | 8 | // With is a WITH clause. 9 | type With struct { 10 | Queries []WithQuery 11 | } 12 | 13 | // NewWith returns a new With instance. 14 | func NewWith(queries []WithQuery) With { 15 | return With{ 16 | Queries: queries, 17 | } 18 | } 19 | 20 | // Write exposes statement as a SQL query. 21 | func (with With) Write(ctx types.Context) { 22 | if with.IsEmpty() { 23 | return 24 | } 25 | ctx.Write(token.With.String()) 26 | ctx.Write(" ") 27 | for i := range with.Queries { 28 | if i != 0 { 29 | ctx.Write(", ") 30 | } 31 | with.Queries[i].Write(ctx) 32 | } 33 | } 34 | 35 | // IsEmpty returns true if statement is undefined. 36 | func (with With) IsEmpty() bool { 37 | return len(with.Queries) == 0 38 | } 39 | 40 | // WithQuery is a statement in a With clause. 41 | type WithQuery struct { 42 | Name string 43 | Subquery Expression 44 | } 45 | 46 | // Write exposes statement as a SQL query. 47 | func (with WithQuery) Write(ctx types.Context) { 48 | if with.IsEmpty() { 49 | return 50 | } 51 | ctx.Write(with.Name) 52 | ctx.Write(" ") 53 | ctx.Write(token.As.String()) 54 | ctx.Write(" (") 55 | with.Subquery.Write(ctx) 56 | ctx.Write(")") 57 | } 58 | 59 | // IsEmpty returns true if statement is undefined. 60 | func (with WithQuery) IsEmpty() bool { 61 | return with.Name == "" || with.Subquery == nil || (with.Subquery != nil && with.Subquery.IsEmpty()) 62 | } 63 | 64 | // NewWithQuery returns a new WithQuery instance. 65 | func NewWithQuery(name string, value interface{}) WithQuery { 66 | return WithQuery{ 67 | Name: name, 68 | Subquery: NewExpression(value), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Type defines a token type identified by the lexer. 9 | type Type string 10 | 11 | func (t Type) String() string { 12 | return string(t) 13 | } 14 | 15 | const ( 16 | // Illegal is an unknown token type. 17 | Illegal = Type("Illegal") 18 | 19 | // EOF indicates the End-Of-File for the lexer. 20 | EOF = Type("EOF") 21 | 22 | // Literal defines entities such as columns, tables, etc... 23 | Literal = Type("Literal") 24 | ) 25 | 26 | // Symbols token types. 27 | const ( 28 | Comment = Type("--") 29 | Comma = Type(",") 30 | Semicolon = Type(";") 31 | Colon = Type(":") 32 | LParen = Type("(") 33 | RParen = Type(")") 34 | Equals = Type("=") 35 | Asterisk = Type("*") 36 | ) 37 | 38 | // Keywords token types. 39 | const ( 40 | Select = Type("SELECT") 41 | Update = Type("UPDATE") 42 | Insert = Type("INSERT") 43 | Delete = Type("DELETE") 44 | From = Type("FROM") 45 | Where = Type("WHERE") 46 | And = Type("AND") 47 | Or = Type("OR") 48 | Limit = Type("LIMIT") 49 | Offset = Type("OFFSET") 50 | Set = Type("SET") 51 | As = Type("AS") 52 | Inner = Type("INNER") 53 | Cross = Type("CROSS") 54 | Left = Type("LEFT") 55 | Right = Type("RIGHT") 56 | Join = Type("JOIN") 57 | On = Type("ON") 58 | Group = Type("GROUP") 59 | By = Type("BY") 60 | Having = Type("HAVING") 61 | Order = Type("ORDER") 62 | Distinct = Type("DISTINCT") 63 | DistinctOn = Type("DISTINCT ON") 64 | Only = Type("ONLY") 65 | Using = Type("USING") 66 | Returning = Type("RETURNING") 67 | Values = Type("VALUES") 68 | Into = Type("INTO") 69 | Conflict = Type("CONFLICT") 70 | Do = Type("DO") 71 | Nothing = Type("NOTHING") 72 | With = Type("WITH") 73 | Not = Type("NOT") 74 | Exists = Type("EXISTS") 75 | Count = Type("COUNT") 76 | Max = Type("MAX") 77 | Min = Type("MIN") 78 | Sum = Type("SUM") 79 | ) 80 | 81 | // A Token is defined by its type and a value. 82 | type Token struct { 83 | Type Type 84 | Value string 85 | } 86 | 87 | func (t *Token) String() string { 88 | return fmt.Sprintf(`{"%s": "%s"}`, t.Type, t.Value) 89 | } 90 | 91 | var keywords = map[string]Type{ 92 | "SELECT": Select, 93 | "UPDATE": Update, 94 | "INSERT": Insert, 95 | "DELETE": Delete, 96 | "FROM": From, 97 | "WHERE": Where, 98 | "AND": And, 99 | "OR": Or, 100 | "LIMIT": Limit, 101 | "OFFSET": Offset, 102 | "SET": Set, 103 | "AS": As, 104 | "INNER": Inner, 105 | "CROSS": Cross, 106 | "LEFT": Left, 107 | "RIGHT": Right, 108 | "JOIN": Join, 109 | "ON": On, 110 | "GROUP": Group, 111 | "BY": By, 112 | "HAVING": Having, 113 | "ORDER": Order, 114 | "DISTINCT": Distinct, 115 | "DISTINCT ON": DistinctOn, 116 | "ONLY": Only, 117 | "USING": Using, 118 | "RETURNING": Returning, 119 | "VALUES": Values, 120 | "INTO": Into, 121 | "CONFLICT": Conflict, 122 | "DO": Do, 123 | "NOTHING": Nothing, 124 | "WITH": With, 125 | "NOT": Not, 126 | "EXISTS": Exists, 127 | "COUNT": Count, 128 | "MAX": Max, 129 | "MIN": Min, 130 | "SUM": Sum, 131 | } 132 | 133 | // Lookup will try to map a statement to a keyword. 134 | func Lookup(e string) Type { 135 | n := strings.ToUpper(e) 136 | t, ok := keywords[n] 137 | if ok { 138 | return t 139 | } 140 | return Literal 141 | } 142 | 143 | // New creates a new Token using given type and a value. 144 | func New(t Type, v string) Token { 145 | return Token{ 146 | Type: t, 147 | Value: v, 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /types/context.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/ulule/loukoum/v3/format" 8 | ) 9 | 10 | // A Context is passed to a root stmt.Statement to generate a query. 11 | type Context interface { 12 | Write(query string) 13 | Bind(value interface{}) 14 | } 15 | 16 | // RawContext embeds values directly in the query. 17 | type RawContext struct { 18 | buffer strings.Builder 19 | } 20 | 21 | // Write appends given subquery in context's buffer. 22 | func (ctx *RawContext) Write(query string) { 23 | _, err := ctx.buffer.WriteString(query) 24 | if err != nil { 25 | panic("loukoum: cannot write on buffer") 26 | } 27 | } 28 | 29 | // Bind adds given value in context's values. 30 | func (ctx *RawContext) Bind(value interface{}) { 31 | ctx.Write(format.Value(value)) 32 | } 33 | 34 | // Query returns the underlaying query. 35 | func (ctx *RawContext) Query() string { 36 | return ctx.buffer.String() 37 | } 38 | 39 | // NamedContext uses named query placeholders. 40 | type NamedContext struct { 41 | RawContext 42 | values map[string]interface{} 43 | } 44 | 45 | // Bind adds given value in context's values. 46 | func (ctx *NamedContext) Bind(value interface{}) { 47 | if ctx.values == nil { 48 | ctx.values = make(map[string]interface{}) 49 | } 50 | idx := len(ctx.values) + 1 51 | name := fmt.Sprintf("arg_%d", idx) 52 | ctx.values[name] = value 53 | ctx.Write(":" + name) 54 | } 55 | 56 | // Values returns the named argument values. 57 | func (ctx *NamedContext) Values() map[string]interface{} { 58 | return ctx.values 59 | } 60 | 61 | // StdContext uses positional query placeholders. 62 | type StdContext struct { 63 | RawContext 64 | values []interface{} 65 | } 66 | 67 | // Bind adds given value in context's values. 68 | func (ctx *StdContext) Bind(value interface{}) { 69 | idx := len(ctx.values) + 1 70 | ctx.values = append(ctx.values, value) 71 | ctx.Write(fmt.Sprintf("$%d", idx)) 72 | } 73 | 74 | // Values returns the positional argument values. 75 | func (ctx *StdContext) Values() []interface{} { 76 | return ctx.values 77 | } 78 | -------------------------------------------------------------------------------- /types/doc.go: -------------------------------------------------------------------------------- 1 | // Package types defines some internal types that are handled by the "builder" and "stmt" package. 2 | // 3 | // RawContext, StdContext and NamedContext are used to generate queries. 4 | package types 5 | -------------------------------------------------------------------------------- /types/join.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // JoinType represents a join type. 4 | type JoinType string 5 | 6 | func (e JoinType) String() string { 7 | return string(e) 8 | } 9 | 10 | // Join types. 11 | const ( 12 | // InnerJoin has a "INNER JOIN" type. 13 | InnerJoin = JoinType("INNER JOIN") 14 | // LeftJoin has a "LEFT JOIN" type. 15 | LeftJoin = JoinType("LEFT JOIN") 16 | // RightJoin has a "RIGHT JOIN" type. 17 | RightJoin = JoinType("RIGHT JOIN") 18 | // LeftOuterJoin has a "LEFT OUTER JOIN" type. 19 | LeftOuterJoin = JoinType("LEFT OUTER JOIN") 20 | // RightOuterJoin has a "RIGHT OUTER JOIN" type. 21 | RightOuterJoin = JoinType("RIGHT OUTER JOIN") 22 | ) 23 | -------------------------------------------------------------------------------- /types/kv.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Map is a key/value map. 4 | type Map map[interface{}]interface{} 5 | 6 | // Pair is a key/value pair. 7 | type Pair struct { 8 | Key interface{} 9 | Value interface{} 10 | } 11 | -------------------------------------------------------------------------------- /types/operator.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // LogicalOperator represents a logical operator. 4 | type LogicalOperator string 5 | 6 | func (e LogicalOperator) String() string { 7 | return string(e) 8 | } 9 | 10 | // Logical operators. 11 | const ( 12 | And = LogicalOperator("AND") 13 | Or = LogicalOperator("OR") 14 | Not = LogicalOperator("NOT") 15 | ) 16 | 17 | // ComparisonOperator represents a comparison operator. 18 | type ComparisonOperator string 19 | 20 | func (e ComparisonOperator) String() string { 21 | return string(e) 22 | } 23 | 24 | // Comparison operators. 25 | const ( 26 | Equal = ComparisonOperator("=") 27 | NotEqual = ComparisonOperator("!=") 28 | Is = ComparisonOperator("IS") 29 | IsNot = ComparisonOperator("IS NOT") 30 | GreaterThan = ComparisonOperator(">") 31 | GreaterThanOrEqual = ComparisonOperator(">=") 32 | LessThan = ComparisonOperator("<") 33 | LessThanOrEqual = ComparisonOperator("<=") 34 | In = ComparisonOperator("IN") 35 | NotIn = ComparisonOperator("NOT IN") 36 | Like = ComparisonOperator("LIKE") 37 | NotLike = ComparisonOperator("NOT LIKE") 38 | ILike = ComparisonOperator("ILIKE") 39 | NotILike = ComparisonOperator("NOT ILIKE") 40 | Between = ComparisonOperator("BETWEEN") 41 | NotBetween = ComparisonOperator("NOT BETWEEN") 42 | IsDistinctFrom = ComparisonOperator("IS DISTINCT FROM") 43 | IsNotDistinctFrom = ComparisonOperator("IS NOT DISTINCT FROM") 44 | Contains = ComparisonOperator("@>") 45 | IsContainedBy = ComparisonOperator("<@") 46 | Overlap = ComparisonOperator("&&") 47 | ) 48 | -------------------------------------------------------------------------------- /types/order.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // OrderType represents an order type. 4 | type OrderType string 5 | 6 | func (e OrderType) String() string { 7 | return string(e) 8 | } 9 | 10 | // Order types. 11 | const ( 12 | // Asc indicates forward order. 13 | Asc = OrderType("ASC") 14 | // Desc indicates reverse order. 15 | Desc = OrderType("DESC") 16 | ) 17 | --------------------------------------------------------------------------------