├── .gitignore ├── .golangci.yml ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd └── goqueryset │ ├── goqueryset │ └── goqueryset.go ├── examples └── comparison │ ├── gorm1 │ └── gorm1.go │ ├── gorm2 │ └── gorm2.go │ ├── gorm3 │ └── gorm3.go │ └── gorm4 │ ├── autogenerated_gorm4.go │ └── gorm4.go ├── go.mod ├── go.sum └── internal ├── parser ├── parser.go ├── parser_test.go └── test │ └── test.go └── queryset ├── field ├── field.go └── field_test.go ├── generator ├── generator.go ├── methodsbuilder.go ├── queryset.go ├── queryset_test.go ├── template.go ├── test │ ├── autogenerated_models.go │ ├── models.go │ └── pkgimport │ │ ├── autogenerated_models.go │ │ ├── forex │ │ └── v1 │ │ │ └── types.go │ │ └── models.go └── tmp │ └── tmp.go └── methods ├── base.go ├── context.go ├── field_utils.go ├── field_utils_test.go ├── fields.go ├── gorm.go ├── queryset.go ├── struct.go └── updater.go /.gitignore: -------------------------------------------------------------------------------- 1 | /go-queryset 2 | /vendor 3 | /internal/parser/test/tmptestdir*/ 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - ^internal/parser/test/tmptestdir.* 4 | 5 | linters-settings: 6 | govet: 7 | check-shadowing: true 8 | golint: 9 | min-confidence: 0 10 | gocyclo: 11 | min-complexity: 15 12 | goconst: 13 | min-len: 2 14 | min-occurrences: 2 15 | lll: 16 | line-length: 140 17 | 18 | linters: 19 | enable-all: true 20 | disable: 21 | - scopelint 22 | - gochecknoglobals 23 | - gosec 24 | 25 | issues: 26 | exclude-rules: 27 | - linters: 28 | - unparam 29 | text: always receives 30 | - linters: 31 | - staticcheck 32 | text: "SA9003:" 33 | 34 | # golangci.com configuration 35 | # https://github.com/golangci/golangci/wiki/Configuration 36 | # service: 37 | # golangci-lint-version: 1.14.0 # use fixed version to not introduce new linters unexpectedly 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.11.x 4 | - 1.12.x 5 | before_install: 6 | - go get github.com/mattn/goveralls 7 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.15.0 8 | script: 9 | - env GO111MODULE=on make test 10 | - env GO111MODULE=on $HOME/gopath/bin/goveralls -ignore "internal/queryset/generator/test/autogenerated_models.go,examples/comparison/*/*.go,internal/queryset/generator/test/pkgimport/*.go,internal/queryset/generator/test/pkgimport/*/*/*.go" -v -service=travis-ci 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Isaev Denis 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_static: 2 | golangci-lint run 3 | 4 | test_unit: test_gen 5 | mkdir -p test 6 | go test -v ./... 7 | 8 | AUTOGEN_FILES = \ 9 | ./internal/queryset/generator/test/autogenerated_models.go \ 10 | ./examples/comparison/gorm4/autogenerated_gorm4.go \ 11 | ./internal/queryset/generator/test/pkgimport/autogenerated_models.go 12 | 13 | test_gen: gen 14 | @- $(foreach F,$(AUTOGEN_FILES), \ 15 | go build $$(dirname $F)/*.go; \ 16 | ) 17 | 18 | test: test_unit bench test_static 19 | 20 | bench: 21 | go test -bench=. -benchtime=1s -v -run=^$$ ./internal/queryset/generator/ 22 | 23 | gen: 24 | @- $(foreach F,$(AUTOGEN_FILES), \ 25 | go generate $$(dirname $F); \ 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-queryset [![GoDoc](https://godoc.org/github.com/jirfag/go-queryset?status.png)](http://godoc.org/github.com/jirfag/go-queryset) [![Go Report Card](https://goreportcard.com/badge/github.com/jirfag/go-queryset)](https://goreportcard.com/report/github.com/jirfag/go-queryset) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/04f432950cbc4e8cb0ec4ef36f4e90bb)](https://www.codacy.com/app/jirfag/go-queryset?utm_source=github.com&utm_medium=referral&utm_content=jirfag/go-queryset&utm_campaign=Badge_Grade) [![Maintainability](https://api.codeclimate.com/v1/badges/8a726af72f338e8998bd/maintainability)](https://codeclimate.com/github/jirfag/go-queryset/maintainability) [![Build Status](https://travis-ci.org/jirfag/go-queryset.svg?branch=master)](https://travis-ci.org/jirfag/go-queryset) [![Coverage Status](https://coveralls.io/repos/github/jirfag/go-queryset/badge.svg?branch=master)](https://coveralls.io/github/jirfag/go-queryset?branch=master) 2 | 3 | 100% type-safe ORM for Go (Golang) with code generation and MySQL, PostgreSQL, Sqlite3, SQL Server support. GORM under the hood. 4 | 5 | 6 | ## Contents 7 | 8 | * [Installation](#installation) 9 | * [Usage](#usage) 10 | * [Define models](#define-models) 11 | * [Relation with GORM](#relation-with-gorm) 12 | * [Create models](#create) 13 | * [Select models](#select) 14 | * [Update models](#update) 15 | * [Delete models](#delete) 16 | * [Full list of generated methods](#full-list-of-generated-methods) 17 | * [Golang version](#golang-version) 18 | * [Why](#why) 19 | * [Why not just use GORM?](#why-not-just-use-gorm) 20 | * [Why not another ORM?](#why-not-another-orm) 21 | * [Why not any ORM?](#why-not-any-orm) 22 | * [Why not raw SQL queries?](#why-not-raw-sql-queries) 23 | * [Why not go-kallax?](#why-not-go-kallax) 24 | * [How it relates to another languages ORMs](#how-it-relates-to-another-languages-orms) 25 | * [Features](#features) 26 | * [Limitations](#limitations) 27 | * [Performance](#performance) 28 | 29 | 30 | # Installation 31 | ```bash 32 | go get -u github.com/jirfag/go-queryset/cmd/goqueryset 33 | ``` 34 | 35 | # Usage 36 | ## Define models 37 | Imagine you have model `User` in your [`models.go`](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm1/gorm1.go#L16) file: 38 | 39 | ```go 40 | type User struct { 41 | gorm.Model 42 | Rating int 43 | RatingMarks int 44 | } 45 | ``` 46 | 47 | Now transform it by [adding comments for query set generation](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm4/gorm4.go#L15): 48 | ```go 49 | //go:generate goqueryset -in models.go 50 | 51 | // User struct represent user model. Next line (gen:qs) is needed to autogenerate UserQuerySet. 52 | // gen:qs 53 | type User struct { 54 | gorm.Model 55 | Rating int 56 | RatingMarks int 57 | } 58 | ``` 59 | 60 | Take a look at line `// gen:qs`. It's a necessary line to enable querysets for this struct. You can put it at any line in struct's doc-comment. 61 | 62 | Then execute next shell command: 63 | ```bash 64 | go generate ./... 65 | ``` 66 | 67 | And you will get file [`autogenerated_models.go`](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm4/autogenerated_gorm4.go) in the same directory (and package) as `models.go`. 68 | 69 | In this autogenerated file you will find a lot of autogenerated typesafe methods like these: 70 | ```go 71 | func (qs UserQuerySet) CreatedAtGte(createdAt time.Time) UserQuerySet { 72 | return qs.w(qs.db.Where("created_at >= ?", createdAt)) 73 | } 74 | 75 | func (qs UserQuerySet) RatingGt(rating int) UserQuerySet { 76 | return qs.w(qs.db.Where("rating > ?", rating)) 77 | } 78 | 79 | func (qs UserQuerySet) IDEq(ID uint) UserQuerySet { 80 | return qs.w(qs.db.Where("id = ?", ID)) 81 | } 82 | 83 | func (qs UserQuerySet) DeletedAtIsNull() UserQuerySet { 84 | return qs.w(qs.db.Where("deleted_at IS NULL")) 85 | } 86 | 87 | func (o *User) Delete(db *gorm.DB) error { 88 | return db.Delete(o).Error 89 | } 90 | 91 | func (qs UserQuerySet) OrderAscByCreatedAt() UserQuerySet { 92 | return qs.w(qs.db.Order("created_at ASC")) 93 | } 94 | ``` 95 | 96 | See full autogenerated file [here](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm4/autogenerated_gorm4.go). 97 | 98 | Now you can use this queryset for creating/reading/updating/deleting. Let's take a look at these operations. 99 | 100 | ## Relation with GORM 101 | You can embed and not embed `gorm.Model` into your model (e.g. if you don't need `DeletedAt` field), but you must use `*gorm.DB` 102 | to properly work. Don't worry if you don't use GORM yet, it's [easy to create `*gorm.DB`](http://jinzhu.me/gorm/database.html#connecting-to-a-database): 103 | ```go 104 | import ( 105 | "github.com/jinzhu/gorm" 106 | _ "github.com/jinzhu/gorm/dialects/mysql" 107 | ) 108 | 109 | func getGormDB() *gorm.DB { 110 | db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") 111 | // ... 112 | } 113 | ``` 114 | 115 | If you already use another ORM or raw `sql.DB`, you can reuse your `sql.DB` object (to reuse connections pool): 116 | ```go 117 | var sqlDB *sql.DB = getSQLDBFromAnotherORM() 118 | var gormDB *gorm.DB 119 | gormDB, err = gorm.Open("mysql", sqlDB) 120 | ``` 121 | 122 | ## Create 123 | ```go 124 | u := User{ 125 | Rating: 5, 126 | RatingMarks: 0, 127 | } 128 | err := u.Create(getGormDB()) 129 | ``` 130 | Under the hood `Create` method [just calls `db.Create(&u)`](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm4/autogenerated_gorm4.go#L38). 131 | 132 | ## Select 133 | It's the most powerful feature of query set. Let's execute some queries: 134 | ### Select all users 135 | ```go 136 | var users []User 137 | err := NewUserQuerySet(getGormDB()).All(&users) 138 | if err == gorm.ErrRecordNotFound { 139 | // no records were found 140 | } 141 | ``` 142 | 143 | It generates this SQL request for MySQL: 144 | ```sql 145 | SELECT * FROM `users` WHERE `users`.deleted_at IS NULL 146 | ``` 147 | 148 | `deleted_at` filtering is added by GORM (soft-delete), to disable it use [`Unscoped`](http://jinzhu.me/gorm/crud.html#delete). 149 | 150 | ### Select one user 151 | ```go 152 | var user User 153 | err := NewUserQuerySet(getGormDB()).One(&user) 154 | ``` 155 | 156 | ### Select N users with highest rating 157 | ```go 158 | var users []User 159 | err := NewUserQuerySet(getGormDB()). 160 | RatingMarksGte(minMarks). 161 | OrderDescByRating(). 162 | Limit(N). 163 | All(&users) 164 | ``` 165 | 166 | ### Select users registered today 167 | In this example we will define custom method on generated `UserQuerySet` for later reuse in multiple functions: 168 | ```go 169 | func (qs UserQuerySet) RegisteredToday() UserQuerySet { 170 | // autogenerated typesafe method CreatedAtGte(time.Time) 171 | return qs.CreatedAtGte(getTodayBegin()) 172 | } 173 | 174 | ... 175 | var users []User 176 | err := NewUserQuerySet(getGormDB()). 177 | RegisteredToday(). 178 | OrderDescByCreatedAt(). 179 | Limit(N). 180 | All(&users) 181 | ``` 182 | 183 | ### Select specific fields 184 | By default all fields are fetched using the `*` field selector. 185 | using the `select` methd it is possible to limit the SQL statement to fetch specific fields: 186 | ```go 187 | var users []User 188 | err := NewUserQuerySet(getGormDB()).Select(UserDBSchema.ID, UserDBSchema.Rating).All(&users) 189 | if err == gorm.ErrRecordNotFound { 190 | // no records were found 191 | } 192 | ``` 193 | 194 | It generates this SQL request for MySQL: 195 | ```sql 196 | SELECT id, rating FROM `users` WHERE `users`.deleted_at IS NULL 197 | ``` 198 | 199 | 200 | ## Update 201 | 202 | ### Update one record by primary key 203 | ```go 204 | u := User{ 205 | Model: gorm.Model{ 206 | ID: uint(7), 207 | }, 208 | Rating: 1, 209 | } 210 | err := u.Update(getGormDB(), UserDBSchema.Rating) 211 | ``` 212 | 213 | Goqueryset generates DB names for struct fields into [`UserDBSchema`](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm4/autogenerated_gorm4.go#L414) variable. 214 | In this example we used [`UserDBSchema.Rating`](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm4/autogenerated_gorm4.go#L419). 215 | 216 | And this code generates next SQL: 217 | ```sql 218 | UPDATE `users` SET `rating` = ? WHERE `users`.deleted_at IS NULL AND `users`.`id` = ? 219 | ``` 220 | 221 | ### Update multiple record or without model object 222 | Sometimes we don't have model object or we are updating multiple rows in DB. 223 | For these cases there is another typesafe interface: 224 | 225 | ```go 226 | err := NewUserQuerySet(getGormDB()). 227 | RatingLt(1). 228 | GetUpdater(). 229 | SetRatingMarks(0). 230 | Update() 231 | ``` 232 | ```sql 233 | UPDATE `users` SET `rating_marks` = ? WHERE `users`.deleted_at IS NULL AND ((rating < ?)) 234 | ``` 235 | 236 | ### UpdateNum 237 | This method makes the same sql queries as Update() method, except return values: it returns 238 | number of affected rows and error 239 | 240 | ```go 241 | num, err := NewUserQuerySet(getGormDB()). 242 | RatingLt(1). 243 | GetUpdater(). 244 | SetRatingMarks(0). 245 | UpdateNum() 246 | ``` 247 | ```sql 248 | UPDATE `users` SET `rating_marks` = ? WHERE `users`.deleted_at IS NULL AND ((rating < ?)) 249 | ``` 250 | 251 | ## Delete 252 | ### Delete one record by primary key 253 | ```go 254 | u := User{ 255 | Model: gorm.Model{ 256 | ID: uint(7), 257 | }, 258 | } 259 | err := u.Delete(getGormDB()) 260 | ``` 261 | 262 | ### Delete multiple records 263 | ```go 264 | err := NewUserQuerySet(getGormDB()). 265 | RatingMarksEq(0). 266 | Delete() 267 | ``` 268 | 269 | ## Full list of generated methods 270 | ### QuerySet methods - `func (qs {StructName}QuerySet)` 271 | * create new queryset: `New{StructName}QuerySet(db *gorm.DB)` 272 | ```go 273 | func NewUserQuerySet(db *gorm.DB) UserQuerySet 274 | ``` 275 | * filter by field (`where`) 276 | * all field types 277 | * Equals: `{FieldName}(Eq|Ne)(arg {FieldType})` 278 | ```go 279 | func (qs UserQuerySet) RatingEq(rating int) UserQuerySet 280 | ``` 281 | * In: `{FieldName}(Not)In(arg {FieldType}, argsRest ...{FieldType})` 282 | ```go 283 | func (qs UserQuerySet) NameIn(name string, nameRest ...string) UserQuerySet {} 284 | func (qs UserQuerySet) NameNotIn(name string, nameRest ...string) UserQuerySet {} 285 | ``` 286 | * `Order(Asc|Desc)By{FieldName}()` 287 | ```go 288 | func (qs UserQuerySet) OrderDescByRating() UserQuerySet 289 | ``` 290 | * numeric types (`int`, `int64`, `uint` etc + `time.Time`): 291 | * `{FieldName}(Lt|Lte|Gt|Gte)(arg {FieldType)` 292 | ```go 293 | func (qs UserQuerySet) RatingGt(rating int) UserQuerySet 294 | ``` 295 | * string types (`string`): 296 | * `{FieldName}(Like/Notlike)(arg {FieldType)` 297 | ```go 298 | func (qs UserQuerySet) NameLike(name string) UserQuerySet 299 | ``` 300 | * pointer fields: `{FieldName}IsNull()`, `{FieldName}IsNotNull()` 301 | ```go 302 | func (qs UserQuerySet) ProfileIsNull() UserQuerySet {} 303 | func (qs UserQuerySet) ProfileIsNotNull() UserQuerySet {} 304 | ``` 305 | * preload related object (for structs fields or pointers to structs fields): `Preload{FieldName}()` 306 | For struct 307 | ```go 308 | type User struct { 309 | profile *Profile 310 | } 311 | ``` 312 | will be generated: 313 | ```go 314 | func (qs UserQuerySet) PreloadProfile() UserQuerySet 315 | ``` 316 | `Preload` functions call `gorm.Preload` to preload related object. 317 | 318 | * selectors 319 | * Select all objects, return `gorm.ErrRecordNotFound` if no records 320 | ```go 321 | func (qs UserQuerySet) All(users *[]User) error 322 | ``` 323 | * Select one object, return `gorm.ErrRecordNotFound` if no records 324 | ```go 325 | func (qs UserQuerySet) One(user *User) error 326 | ``` 327 | * Limit 328 | ```go 329 | func (qs UserQuerySet) Limit(limit int) UserQuerySet 330 | ``` 331 | * [get updater](#update-multiple-record-or-without-model-object) (for update + where, based on current queryset): 332 | ```go 333 | func (qs UserQuerySet) GetUpdater() UserUpdater 334 | ``` 335 | * delete with conditions from current queryset: `Delete()` 336 | ```go 337 | func (qs UserQuerySet) Delete() error 338 | ``` 339 | * Aggregations 340 | * Count 341 | ```go 342 | func (qs UserQuerySet) Count() (int, error) 343 | ``` 344 | 345 | ### Object methods - `func (u *User)` 346 | * create object 347 | ```go 348 | func (o *User) Create(db *gorm.DB) error 349 | ``` 350 | * delete object by PK 351 | ```go 352 | func (o *User) Delete(db *gorm.DB) error 353 | ``` 354 | * update object by PK 355 | ```go 356 | func (o *User) Update(db *gorm.DB, fields ...userDBSchemaField) error 357 | ``` 358 | Pay attention that field names are automatically generated into variable 359 | ```go 360 | type userDBSchemaField string 361 | 362 | // UserDBSchema stores db field names of User 363 | var UserDBSchema = struct { 364 | ID userDBSchemaField 365 | CreatedAt userDBSchemaField 366 | UpdatedAt userDBSchemaField 367 | DeletedAt userDBSchemaField 368 | Rating userDBSchemaField 369 | RatingMarks userDBSchemaField 370 | }{ 371 | 372 | ID: userDBSchemaField("id"), 373 | CreatedAt: userDBSchemaField("created_at"), 374 | UpdatedAt: userDBSchemaField("updated_at"), 375 | DeletedAt: userDBSchemaField("deleted_at"), 376 | Rating: userDBSchemaField("rating"), 377 | RatingMarks: userDBSchemaField("rating_marks"), 378 | } 379 | ``` 380 | 381 | And they are typed, so you won't have string-misprint error. 382 | 383 | 384 | ### Updater methods - `func (u UserUpdater)` 385 | * set field: `Set{FieldName}` 386 | ```go 387 | func (u UserUpdater) SetCreatedAt(createdAt time.Time) UserUpdater 388 | ``` 389 | * execute update: `Update()` 390 | ```go 391 | func (u UserUpdater) Update() error 392 | ``` 393 | 394 | # Golang version 395 | Golang >= 1.8 is required. Tested on go 1.8, 1.9 versions by [Travis CI](https://travis-ci.org/jirfag/go-queryset) 396 | 397 | # Why? 398 | ## Why not just use GORM? 399 | I like GORM: it's the best ORM for golang, it has fantastic documentation, but as a Golang developers team lead I can point out some troubles with it: 400 | 1. GORM isn't typesafe: it's so easy to spend 1 hour trying to execute simple Update. GORM gets all arguments as `interface{}` 401 | and in the case of invalid GORM usage you won't get error: you will get invalid SQL, no SQL (!) and `error == nil` etc. 402 | It's easy to get `SELECT * FROM t WHERE string_field == 1` SQL in production without type safety. 403 | 2. GORM is difficult for beginners because of unclear `interface{}` interfaces: one can't easily find which arguments to pass to GORM methods. 404 | 405 | ## Why not another ORM? 406 | Type-safety, like with GORM. 407 | 408 | ## Why not any ORM? 409 | I didn't see any ORM that properly handles code duplication. GORM is the best with `Scopes` support, but even it's far from ideal. E.g. we have GORM and [next typical code](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm1/gorm1.go#L16): 410 | ```go 411 | type User struct { 412 | gorm.Model 413 | Rating int 414 | RatingMarks int 415 | } 416 | 417 | func GetUsersWithMaxRating(limit int) ([]User, error) { 418 | var users []User 419 | if err := getGormDB().Order("rating DESC").Limit(limit).Find(&users).Error; err != nil { 420 | return nil, err 421 | } 422 | return users, nil 423 | } 424 | 425 | func GetUsersRegisteredToday(limit int) ([]User, error) { 426 | var users []User 427 | today := getTodayBegin() 428 | err := getGormDB().Where("created_at >= ?", today).Limit(limit).Find(&users).Error 429 | if err != nil { 430 | return nil, err 431 | } 432 | return users, nil 433 | } 434 | ``` 435 | 436 | At one moment PM asks us to implement new function, returning list of users registered today AND sorted by rating. Copy-paste way is to add `Order("rating DESC")` to `GetUsersRegisteredToday`. But it leads to typical copy-paste troubles: when we change rating calculation logics (e.g. to `.Where("rating_marks >= ?", 10).Order("rating DESC")`) we must change it in two places. 437 | 438 | How to solve it? First idea is to [make reusable functions](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm2/gorm2.go#L27): 439 | ```go 440 | func queryUsersWithMaxRating(db *gorm.DB, limit int) *gorm.DB { 441 | return db.Order("rating DESC").Limit(limit) 442 | } 443 | 444 | func queryUsersRegisteredToday(db *gorm.DB, limit int) *gorm.DB { 445 | today := getTodayBegin() 446 | return db.Where("created_at >= ?", today).Limit(limit) 447 | } 448 | 449 | func GetUsersWithMaxRating(limit int) ([]User, error) { 450 | var users []User 451 | if err := queryUsersWithMaxRating(getGormDB(), limit).Find(&users).Error; err != nil { 452 | return nil, err 453 | } 454 | return users, nil 455 | } 456 | 457 | func GetUsersRegisteredToday(limit int) ([]User, error) { 458 | var users []User 459 | if err := queryUsersRegisteredToday(getGormDB(), limit).Find(&users).Error; err != nil { 460 | return nil, err 461 | } 462 | return users, nil 463 | } 464 | 465 | func GetUsersRegisteredTodayWithMaxRating(limit int) ([]User, error) { 466 | var users []User 467 | err := queryUsersWithMaxRating(queryUsersRegisteredToday(getGormDB(), limit), limit). 468 | Find(&users).Error 469 | if err != nil { 470 | return nil, err 471 | } 472 | return users, nil 473 | } 474 | ``` 475 | 476 | We can use GORM [Scopes](http://jinzhu.me/gorm/crud.html#scopes) to improve [how it looks](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm3/gorm3.go#L64): 477 | ```go 478 | func queryUsersWithMaxRating(db *gorm.DB) *gorm.DB { 479 | return db.Order("rating DESC") 480 | } 481 | 482 | func queryUsersRegisteredToday(db *gorm.DB) *gorm.DB { 483 | return db.Where("created_at >= ?", getTodayBegin()) 484 | } 485 | 486 | func GetUsersRegisteredTodayWithMaxRating(limit int) ([]User, error) { 487 | var users []User 488 | err := getGormDB(). 489 | Scopes(queryUsersWithMaxRating, queryUsersRegisteredToday). 490 | Limit(limit). 491 | Find(&users).Error 492 | if err != nil { 493 | return nil, err 494 | } 495 | return users, nil 496 | } 497 | ``` 498 | 499 | Looks nice, but we loosed ability to parametrize our reusable GORM queries (scopes): they must have only one argument of type `*gorm.DB`. It means that we must move out `Limit` from them (let's say we get it from user). If we need to implement query `QueryUsersRegisteredAfter(db *gorm.DB, t time.Time)` - we can't do it. 500 | 501 | Now compare it with [go-queryset solution](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm4/gorm4.go#L30): 502 | ```go 503 | // UserQuerySet is an autogenerated struct with a lot of typesafe methods. 504 | // We can define any methods on it because it's in the same package 505 | func (qs UserQuerySet) WithMaxRating(minMarks int) UserQuerySet { 506 | return qs.RatingMarksGte(minMarks).OrderDescByRating() 507 | } 508 | 509 | func (qs UserQuerySet) RegisteredToday() UserQuerySet { 510 | // autogenerated typesafe method CreatedAtGte(time.Time) 511 | return qs.CreatedAtGte(getTodayBegin()) 512 | } 513 | 514 | // now we can parametrize it 515 | const minRatingMarks = 10 516 | 517 | func GetUsersWithMaxRating(limit int) ([]User, error) { 518 | var users []User 519 | err := NewUserQuerySet(getGormDB()). 520 | WithMaxRating(minRatingMarks). // reuse our method 521 | Limit(limit). // autogenerated typesafe method Limit(int) 522 | All(&users) // autogenerated typesafe method All(*[]User) 523 | if err != nil { 524 | return nil, err 525 | } 526 | return users, nil 527 | } 528 | 529 | func GetUsersRegisteredToday(limit int) ([]User, error) { 530 | var users []User 531 | err := NewUserQuerySet(getGormDB()). 532 | RegisteredToday(). // reuse our method 533 | Limit(limit). // autogenerated typesafe method Limit(int) 534 | All(&users) // autogenerated typesafe method All(*[]User) 535 | if err != nil { 536 | return nil, err 537 | } 538 | return users, nil 539 | } 540 | 541 | func GetUsersRegisteredTodayWithMaxRating(limit int) ([]User, error) { 542 | var users []User 543 | err := NewUserQuerySet(getGormDB()). 544 | RegisteredToday(). // reuse our method 545 | WithMaxRating(minRatingMarks). // reuse our method 546 | Limit(limit). 547 | All(&users) // autogenerated typesafe method All(*[]User) 548 | if err != nil { 549 | return nil, err 550 | } 551 | return users, nil 552 | } 553 | ``` 554 | ## Why not raw SQL queries? 555 | No type-safety, a lot of boilerplate code. 556 | 557 | ## Why not [go-kallax](https://github.com/src-d/go-kallax)? 558 | 1. It works only with PostgreSQL. Go-queryset supports mysql, postgresql, sqlite, mssql etc (all that gorm supports). 559 | 2. Lacks simplier model updating interface 560 | 561 | ## How it relates to another languages ORMs 562 | QuerySet pattern is similar to: 563 | * [Django QuerySet](https://docs.djangoproject.com/en/dev/ref/models/querysets/), but better than it because of type-safety (Python) 564 | * [Rails Active Record and it's scopes](http://guides.rubyonrails.org/active_record_querying.html#scopes), but better than it because of type-safety (Ruby) 565 | 566 | # Features 567 | * 100% typesafe: there is no one method with `interface{}` arguments. 568 | * QuerySet pattern allows to reuse queries by defining [custom methods](https://github.com/jirfag/go-queryset/blob/master/examples/comparison/gorm4/gorm4.go#L30) on it. 569 | * Supports all DBMS that GORM supports: MySQL, PostgreSQL, Sqlite3, SQL Server. 570 | * Supports creating, selecting, updating, deleting of objects. 571 | 572 | # Limitations 573 | * Joins aren't supported 574 | * Struct tags aren't supported 575 | 576 | # Performance 577 | ## Runtime 578 | Performance is similar to GORM performance. GORM uses reflection and it may be slow, so why don't we generate raw SQL code? 579 | 1. Despite the fact GORM uses reflection, it's the most popular ORM for golang. There are really few tasks where you are CPU-bound while working with DB, usually you are CPU-bound in machine with DB and network/disk bound on machine with golang server. 580 | 2. Premature optimization is the root of all evil. 581 | 3. Go-queryset is fully compatible with GORM. 582 | 4. Code generation is used here not to speedup things, but to create nice interfaces. 583 | 5. The main purpose of go-queryset isn't speed, but usage convenience. 584 | 585 | ## Code generation 586 | Code generation is fast: 587 | 1. We parse AST of needed file and find needed structs. 588 | 2. We load package and parse it by `go/types` 589 | 3. We don't use `reflect` module for parsing, because it's slow 590 | -------------------------------------------------------------------------------- /cmd/goqueryset/goqueryset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jirfag/go-queryset/36aee76bfd779da9e7f00b7591c255a20137f1a2/cmd/goqueryset/goqueryset -------------------------------------------------------------------------------- /cmd/goqueryset/goqueryset.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/jirfag/go-queryset/internal/parser" 11 | "github.com/jirfag/go-queryset/internal/queryset/generator" 12 | ) 13 | 14 | func main() { 15 | const defaultOutPath = "autogenerated_{in}" 16 | 17 | inFile := flag.String("in", "models.go", "path to input file") 18 | outFile := flag.String("out", defaultOutPath, "path to output file") 19 | timeout := flag.Duration("timeout", time.Minute, "timeout for generation") 20 | flag.Parse() 21 | 22 | if *outFile == defaultOutPath { 23 | *outFile = filepath.Join(filepath.Dir(*inFile), "autogenerated_"+filepath.Base(*inFile)) 24 | } 25 | 26 | g := generator.Generator{ 27 | StructsParser: &parser.Structs{}, 28 | } 29 | 30 | ctx, finish := context.WithTimeout(context.Background(), *timeout) 31 | defer finish() 32 | 33 | if err := g.Generate(ctx, *inFile, *outFile); err != nil { 34 | log.Fatalf("can't generate query sets: %s", err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/comparison/gorm1/gorm1.go: -------------------------------------------------------------------------------- 1 | package gorm1 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | func getGormDB() *gorm.DB { 10 | db, _ := gorm.Open("mysql", 11 | "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") 12 | return db 13 | } 14 | 15 | // User struct represents user model. 16 | type User struct { 17 | gorm.Model 18 | Rating int 19 | RatingMarks int 20 | } 21 | 22 | func getTodayBegin() time.Time { 23 | year, month, day := time.Now().Date() 24 | return time.Date(year, month, day, 0, 0, 0, 0, time.Now().Location()) 25 | } 26 | 27 | // GetUsersWithMaxRating returns limit users with maximal rating 28 | func GetUsersWithMaxRating(limit int) ([]User, error) { 29 | var users []User 30 | if err := getGormDB().Order("rating DESC").Limit(limit).Find(&users).Error; err != nil { 31 | return nil, err 32 | } 33 | return users, nil 34 | } 35 | 36 | // GetUsersRegisteredToday returns all users registered today 37 | func GetUsersRegisteredToday(limit int) ([]User, error) { 38 | var users []User 39 | today := getTodayBegin() 40 | err := getGormDB().Where("created_at >= ?", today).Limit(limit).Find(&users).Error 41 | if err != nil { 42 | return nil, err 43 | } 44 | return users, nil 45 | } 46 | -------------------------------------------------------------------------------- /examples/comparison/gorm2/gorm2.go: -------------------------------------------------------------------------------- 1 | package gorm2 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | func getGormDB() *gorm.DB { 10 | db, _ := gorm.Open("mysql", 11 | "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") 12 | return db 13 | } 14 | 15 | // User struct represents user model. 16 | type User struct { 17 | gorm.Model 18 | Rating int 19 | RatingMarks int 20 | } 21 | 22 | func getTodayBegin() time.Time { 23 | year, month, day := time.Now().Date() 24 | return time.Date(year, month, day, 0, 0, 0, 0, time.Now().Location()) 25 | } 26 | 27 | func queryUsersWithMaxRating(db *gorm.DB, limit int) *gorm.DB { 28 | return db.Order("rating DESC").Limit(limit) 29 | } 30 | 31 | func queryUsersRegisteredToday(db *gorm.DB, limit int) *gorm.DB { 32 | today := getTodayBegin() 33 | return db.Where("created_at >= ?", today).Limit(limit) 34 | } 35 | 36 | // GetUsersWithMaxRating returns limit users with maximal rating 37 | func GetUsersWithMaxRating(limit int) ([]User, error) { 38 | var users []User 39 | if err := queryUsersWithMaxRating(getGormDB(), limit).Find(&users).Error; err != nil { 40 | return nil, err 41 | } 42 | return users, nil 43 | } 44 | 45 | // GetUsersRegisteredToday returns all users registered today 46 | func GetUsersRegisteredToday(limit int) ([]User, error) { 47 | var users []User 48 | if err := queryUsersRegisteredToday(getGormDB(), limit).Find(&users).Error; err != nil { 49 | return nil, err 50 | } 51 | return users, nil 52 | } 53 | 54 | // GetUsersRegisteredTodayWithMaxRating returns all users 55 | // registered today with max rating 56 | func GetUsersRegisteredTodayWithMaxRating(limit int) ([]User, error) { 57 | var users []User 58 | err := queryUsersWithMaxRating(queryUsersRegisteredToday(getGormDB(), limit), limit). 59 | Find(&users).Error 60 | if err != nil { 61 | return nil, err 62 | } 63 | return users, nil 64 | } 65 | -------------------------------------------------------------------------------- /examples/comparison/gorm3/gorm3.go: -------------------------------------------------------------------------------- 1 | package gorm3 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | func getGormDB() *gorm.DB { 10 | db, _ := gorm.Open("mysql", 11 | "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") 12 | return db 13 | } 14 | 15 | // User struct represents user model. 16 | type User struct { 17 | gorm.Model 18 | Rating int 19 | RatingMarks int 20 | } 21 | 22 | func getTodayBegin() time.Time { 23 | year, month, day := time.Now().Date() 24 | return time.Date(year, month, day, 0, 0, 0, 0, time.Now().Location()) 25 | } 26 | 27 | func queryUsersWithMaxRating(db *gorm.DB) *gorm.DB { 28 | return db.Order("rating DESC") 29 | } 30 | 31 | func queryUsersRegisteredToday(db *gorm.DB) *gorm.DB { 32 | return db.Where("created_at >= ?", getTodayBegin()) 33 | } 34 | 35 | // GetUsersWithMaxRating returns limit users with maximal rating 36 | func GetUsersWithMaxRating(limit int) ([]User, error) { 37 | var users []User 38 | err := queryUsersWithMaxRating(getGormDB()). 39 | Limit(limit). 40 | Find(&users).Error 41 | if err != nil { 42 | return nil, err 43 | } 44 | return users, nil 45 | } 46 | 47 | // GetUsersRegisteredToday returns all users registered today 48 | func GetUsersRegisteredToday(limit int) ([]User, error) { 49 | var users []User 50 | err := queryUsersRegisteredToday(getGormDB()). 51 | Limit(limit). 52 | Find(&users).Error 53 | if err != nil { 54 | return nil, err 55 | } 56 | return users, nil 57 | } 58 | 59 | // GetUsersRegisteredTodayWithMaxRating returns all users 60 | // registered today with max rating 61 | func GetUsersRegisteredTodayWithMaxRating(limit int) ([]User, error) { 62 | var users []User 63 | err := getGormDB(). 64 | Scopes(queryUsersWithMaxRating, queryUsersRegisteredToday). 65 | Limit(limit). 66 | Find(&users).Error 67 | if err != nil { 68 | return nil, err 69 | } 70 | return users, nil 71 | } 72 | -------------------------------------------------------------------------------- /examples/comparison/gorm4/autogenerated_gorm4.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-queryset. DO NOT EDIT. 2 | package gorm4 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/jinzhu/gorm" 11 | ) 12 | 13 | // ===== BEGIN of all query sets 14 | 15 | // ===== BEGIN of query set UserQuerySet 16 | 17 | // UserQuerySet is an queryset type for User 18 | type UserQuerySet struct { 19 | db *gorm.DB 20 | } 21 | 22 | // NewUserQuerySet constructs new UserQuerySet 23 | func NewUserQuerySet(db *gorm.DB) UserQuerySet { 24 | return UserQuerySet{ 25 | db: db.Model(&User{}), 26 | } 27 | } 28 | 29 | func (qs UserQuerySet) w(db *gorm.DB) UserQuerySet { 30 | return NewUserQuerySet(db) 31 | } 32 | 33 | func (qs UserQuerySet) Select(fields ...UserDBSchemaField) UserQuerySet { 34 | names := []string{} 35 | for _, f := range fields { 36 | names = append(names, f.String()) 37 | } 38 | 39 | return qs.w(qs.db.Select(strings.Join(names, ","))) 40 | } 41 | 42 | // Create is an autogenerated method 43 | // nolint: dupl 44 | func (o *User) Create(db *gorm.DB) error { 45 | return db.Create(o).Error 46 | } 47 | 48 | // Delete is an autogenerated method 49 | // nolint: dupl 50 | func (o *User) Delete(db *gorm.DB) error { 51 | return db.Delete(o).Error 52 | } 53 | 54 | // All is an autogenerated method 55 | // nolint: dupl 56 | func (qs UserQuerySet) All(ret *[]User) error { 57 | return qs.db.Find(ret).Error 58 | } 59 | 60 | // Count is an autogenerated method 61 | // nolint: dupl 62 | func (qs UserQuerySet) Count() (int, error) { 63 | var count int 64 | err := qs.db.Count(&count).Error 65 | return count, err 66 | } 67 | 68 | // CreatedAtEq is an autogenerated method 69 | // nolint: dupl 70 | func (qs UserQuerySet) CreatedAtEq(createdAt time.Time) UserQuerySet { 71 | return qs.w(qs.db.Where("created_at = ?", createdAt)) 72 | } 73 | 74 | // CreatedAtGt is an autogenerated method 75 | // nolint: dupl 76 | func (qs UserQuerySet) CreatedAtGt(createdAt time.Time) UserQuerySet { 77 | return qs.w(qs.db.Where("created_at > ?", createdAt)) 78 | } 79 | 80 | // CreatedAtGte is an autogenerated method 81 | // nolint: dupl 82 | func (qs UserQuerySet) CreatedAtGte(createdAt time.Time) UserQuerySet { 83 | return qs.w(qs.db.Where("created_at >= ?", createdAt)) 84 | } 85 | 86 | // CreatedAtLt is an autogenerated method 87 | // nolint: dupl 88 | func (qs UserQuerySet) CreatedAtLt(createdAt time.Time) UserQuerySet { 89 | return qs.w(qs.db.Where("created_at < ?", createdAt)) 90 | } 91 | 92 | // CreatedAtLte is an autogenerated method 93 | // nolint: dupl 94 | func (qs UserQuerySet) CreatedAtLte(createdAt time.Time) UserQuerySet { 95 | return qs.w(qs.db.Where("created_at <= ?", createdAt)) 96 | } 97 | 98 | // CreatedAtNe is an autogenerated method 99 | // nolint: dupl 100 | func (qs UserQuerySet) CreatedAtNe(createdAt time.Time) UserQuerySet { 101 | return qs.w(qs.db.Where("created_at != ?", createdAt)) 102 | } 103 | 104 | // Delete is an autogenerated method 105 | // nolint: dupl 106 | func (qs UserQuerySet) Delete() error { 107 | return qs.db.Delete(User{}).Error 108 | } 109 | 110 | // DeleteNum is an autogenerated method 111 | // nolint: dupl 112 | func (qs UserQuerySet) DeleteNum() (int64, error) { 113 | db := qs.db.Delete(User{}) 114 | return db.RowsAffected, db.Error 115 | } 116 | 117 | // DeleteNumUnscoped is an autogenerated method 118 | // nolint: dupl 119 | func (qs UserQuerySet) DeleteNumUnscoped() (int64, error) { 120 | db := qs.db.Unscoped().Delete(User{}) 121 | return db.RowsAffected, db.Error 122 | } 123 | 124 | // DeletedAtEq is an autogenerated method 125 | // nolint: dupl 126 | func (qs UserQuerySet) DeletedAtEq(deletedAt time.Time) UserQuerySet { 127 | return qs.w(qs.db.Where("deleted_at = ?", deletedAt)) 128 | } 129 | 130 | // DeletedAtGt is an autogenerated method 131 | // nolint: dupl 132 | func (qs UserQuerySet) DeletedAtGt(deletedAt time.Time) UserQuerySet { 133 | return qs.w(qs.db.Where("deleted_at > ?", deletedAt)) 134 | } 135 | 136 | // DeletedAtGte is an autogenerated method 137 | // nolint: dupl 138 | func (qs UserQuerySet) DeletedAtGte(deletedAt time.Time) UserQuerySet { 139 | return qs.w(qs.db.Where("deleted_at >= ?", deletedAt)) 140 | } 141 | 142 | // DeletedAtIsNotNull is an autogenerated method 143 | // nolint: dupl 144 | func (qs UserQuerySet) DeletedAtIsNotNull() UserQuerySet { 145 | return qs.w(qs.db.Where("deleted_at IS NOT NULL")) 146 | } 147 | 148 | // DeletedAtIsNull is an autogenerated method 149 | // nolint: dupl 150 | func (qs UserQuerySet) DeletedAtIsNull() UserQuerySet { 151 | return qs.w(qs.db.Where("deleted_at IS NULL")) 152 | } 153 | 154 | // DeletedAtLt is an autogenerated method 155 | // nolint: dupl 156 | func (qs UserQuerySet) DeletedAtLt(deletedAt time.Time) UserQuerySet { 157 | return qs.w(qs.db.Where("deleted_at < ?", deletedAt)) 158 | } 159 | 160 | // DeletedAtLte is an autogenerated method 161 | // nolint: dupl 162 | func (qs UserQuerySet) DeletedAtLte(deletedAt time.Time) UserQuerySet { 163 | return qs.w(qs.db.Where("deleted_at <= ?", deletedAt)) 164 | } 165 | 166 | // DeletedAtNe is an autogenerated method 167 | // nolint: dupl 168 | func (qs UserQuerySet) DeletedAtNe(deletedAt time.Time) UserQuerySet { 169 | return qs.w(qs.db.Where("deleted_at != ?", deletedAt)) 170 | } 171 | 172 | // GetDB is an autogenerated method 173 | // nolint: dupl 174 | func (qs UserQuerySet) GetDB() *gorm.DB { 175 | return qs.db 176 | } 177 | 178 | // GetUpdater is an autogenerated method 179 | // nolint: dupl 180 | func (qs UserQuerySet) GetUpdater() UserUpdater { 181 | return NewUserUpdater(qs.db) 182 | } 183 | 184 | // IDEq is an autogenerated method 185 | // nolint: dupl 186 | func (qs UserQuerySet) IDEq(ID uint) UserQuerySet { 187 | return qs.w(qs.db.Where("id = ?", ID)) 188 | } 189 | 190 | // IDGt is an autogenerated method 191 | // nolint: dupl 192 | func (qs UserQuerySet) IDGt(ID uint) UserQuerySet { 193 | return qs.w(qs.db.Where("id > ?", ID)) 194 | } 195 | 196 | // IDGte is an autogenerated method 197 | // nolint: dupl 198 | func (qs UserQuerySet) IDGte(ID uint) UserQuerySet { 199 | return qs.w(qs.db.Where("id >= ?", ID)) 200 | } 201 | 202 | // IDIn is an autogenerated method 203 | // nolint: dupl 204 | func (qs UserQuerySet) IDIn(ID ...uint) UserQuerySet { 205 | if len(ID) == 0 { 206 | qs.db.AddError(errors.New("must at least pass one ID in IDIn")) 207 | return qs.w(qs.db) 208 | } 209 | return qs.w(qs.db.Where("id IN (?)", ID)) 210 | } 211 | 212 | // IDLt is an autogenerated method 213 | // nolint: dupl 214 | func (qs UserQuerySet) IDLt(ID uint) UserQuerySet { 215 | return qs.w(qs.db.Where("id < ?", ID)) 216 | } 217 | 218 | // IDLte is an autogenerated method 219 | // nolint: dupl 220 | func (qs UserQuerySet) IDLte(ID uint) UserQuerySet { 221 | return qs.w(qs.db.Where("id <= ?", ID)) 222 | } 223 | 224 | // IDNe is an autogenerated method 225 | // nolint: dupl 226 | func (qs UserQuerySet) IDNe(ID uint) UserQuerySet { 227 | return qs.w(qs.db.Where("id != ?", ID)) 228 | } 229 | 230 | // IDNotIn is an autogenerated method 231 | // nolint: dupl 232 | func (qs UserQuerySet) IDNotIn(ID ...uint) UserQuerySet { 233 | if len(ID) == 0 { 234 | qs.db.AddError(errors.New("must at least pass one ID in IDNotIn")) 235 | return qs.w(qs.db) 236 | } 237 | return qs.w(qs.db.Where("id NOT IN (?)", ID)) 238 | } 239 | 240 | // Limit is an autogenerated method 241 | // nolint: dupl 242 | func (qs UserQuerySet) Limit(limit int) UserQuerySet { 243 | return qs.w(qs.db.Limit(limit)) 244 | } 245 | 246 | // Offset is an autogenerated method 247 | // nolint: dupl 248 | func (qs UserQuerySet) Offset(offset int) UserQuerySet { 249 | return qs.w(qs.db.Offset(offset)) 250 | } 251 | 252 | // One is used to retrieve one result. It returns gorm.ErrRecordNotFound 253 | // if nothing was fetched 254 | func (qs UserQuerySet) One(ret *User) error { 255 | return qs.db.First(ret).Error 256 | } 257 | 258 | // OrderAscByCreatedAt is an autogenerated method 259 | // nolint: dupl 260 | func (qs UserQuerySet) OrderAscByCreatedAt() UserQuerySet { 261 | return qs.w(qs.db.Order("created_at ASC")) 262 | } 263 | 264 | // OrderAscByDeletedAt is an autogenerated method 265 | // nolint: dupl 266 | func (qs UserQuerySet) OrderAscByDeletedAt() UserQuerySet { 267 | return qs.w(qs.db.Order("deleted_at ASC")) 268 | } 269 | 270 | // OrderAscByID is an autogenerated method 271 | // nolint: dupl 272 | func (qs UserQuerySet) OrderAscByID() UserQuerySet { 273 | return qs.w(qs.db.Order("id ASC")) 274 | } 275 | 276 | // OrderAscByRating is an autogenerated method 277 | // nolint: dupl 278 | func (qs UserQuerySet) OrderAscByRating() UserQuerySet { 279 | return qs.w(qs.db.Order("rating ASC")) 280 | } 281 | 282 | // OrderAscByRatingMarks is an autogenerated method 283 | // nolint: dupl 284 | func (qs UserQuerySet) OrderAscByRatingMarks() UserQuerySet { 285 | return qs.w(qs.db.Order("rating_marks ASC")) 286 | } 287 | 288 | // OrderAscByUpdatedAt is an autogenerated method 289 | // nolint: dupl 290 | func (qs UserQuerySet) OrderAscByUpdatedAt() UserQuerySet { 291 | return qs.w(qs.db.Order("updated_at ASC")) 292 | } 293 | 294 | // OrderDescByCreatedAt is an autogenerated method 295 | // nolint: dupl 296 | func (qs UserQuerySet) OrderDescByCreatedAt() UserQuerySet { 297 | return qs.w(qs.db.Order("created_at DESC")) 298 | } 299 | 300 | // OrderDescByDeletedAt is an autogenerated method 301 | // nolint: dupl 302 | func (qs UserQuerySet) OrderDescByDeletedAt() UserQuerySet { 303 | return qs.w(qs.db.Order("deleted_at DESC")) 304 | } 305 | 306 | // OrderDescByID is an autogenerated method 307 | // nolint: dupl 308 | func (qs UserQuerySet) OrderDescByID() UserQuerySet { 309 | return qs.w(qs.db.Order("id DESC")) 310 | } 311 | 312 | // OrderDescByRating is an autogenerated method 313 | // nolint: dupl 314 | func (qs UserQuerySet) OrderDescByRating() UserQuerySet { 315 | return qs.w(qs.db.Order("rating DESC")) 316 | } 317 | 318 | // OrderDescByRatingMarks is an autogenerated method 319 | // nolint: dupl 320 | func (qs UserQuerySet) OrderDescByRatingMarks() UserQuerySet { 321 | return qs.w(qs.db.Order("rating_marks DESC")) 322 | } 323 | 324 | // OrderDescByUpdatedAt is an autogenerated method 325 | // nolint: dupl 326 | func (qs UserQuerySet) OrderDescByUpdatedAt() UserQuerySet { 327 | return qs.w(qs.db.Order("updated_at DESC")) 328 | } 329 | 330 | // RatingEq is an autogenerated method 331 | // nolint: dupl 332 | func (qs UserQuerySet) RatingEq(rating int) UserQuerySet { 333 | return qs.w(qs.db.Where("rating = ?", rating)) 334 | } 335 | 336 | // RatingGt is an autogenerated method 337 | // nolint: dupl 338 | func (qs UserQuerySet) RatingGt(rating int) UserQuerySet { 339 | return qs.w(qs.db.Where("rating > ?", rating)) 340 | } 341 | 342 | // RatingGte is an autogenerated method 343 | // nolint: dupl 344 | func (qs UserQuerySet) RatingGte(rating int) UserQuerySet { 345 | return qs.w(qs.db.Where("rating >= ?", rating)) 346 | } 347 | 348 | // RatingIn is an autogenerated method 349 | // nolint: dupl 350 | func (qs UserQuerySet) RatingIn(rating ...int) UserQuerySet { 351 | if len(rating) == 0 { 352 | qs.db.AddError(errors.New("must at least pass one rating in RatingIn")) 353 | return qs.w(qs.db) 354 | } 355 | return qs.w(qs.db.Where("rating IN (?)", rating)) 356 | } 357 | 358 | // RatingLt is an autogenerated method 359 | // nolint: dupl 360 | func (qs UserQuerySet) RatingLt(rating int) UserQuerySet { 361 | return qs.w(qs.db.Where("rating < ?", rating)) 362 | } 363 | 364 | // RatingLte is an autogenerated method 365 | // nolint: dupl 366 | func (qs UserQuerySet) RatingLte(rating int) UserQuerySet { 367 | return qs.w(qs.db.Where("rating <= ?", rating)) 368 | } 369 | 370 | // RatingMarksEq is an autogenerated method 371 | // nolint: dupl 372 | func (qs UserQuerySet) RatingMarksEq(ratingMarks int) UserQuerySet { 373 | return qs.w(qs.db.Where("rating_marks = ?", ratingMarks)) 374 | } 375 | 376 | // RatingMarksGt is an autogenerated method 377 | // nolint: dupl 378 | func (qs UserQuerySet) RatingMarksGt(ratingMarks int) UserQuerySet { 379 | return qs.w(qs.db.Where("rating_marks > ?", ratingMarks)) 380 | } 381 | 382 | // RatingMarksGte is an autogenerated method 383 | // nolint: dupl 384 | func (qs UserQuerySet) RatingMarksGte(ratingMarks int) UserQuerySet { 385 | return qs.w(qs.db.Where("rating_marks >= ?", ratingMarks)) 386 | } 387 | 388 | // RatingMarksIn is an autogenerated method 389 | // nolint: dupl 390 | func (qs UserQuerySet) RatingMarksIn(ratingMarks ...int) UserQuerySet { 391 | if len(ratingMarks) == 0 { 392 | qs.db.AddError(errors.New("must at least pass one ratingMarks in RatingMarksIn")) 393 | return qs.w(qs.db) 394 | } 395 | return qs.w(qs.db.Where("rating_marks IN (?)", ratingMarks)) 396 | } 397 | 398 | // RatingMarksLt is an autogenerated method 399 | // nolint: dupl 400 | func (qs UserQuerySet) RatingMarksLt(ratingMarks int) UserQuerySet { 401 | return qs.w(qs.db.Where("rating_marks < ?", ratingMarks)) 402 | } 403 | 404 | // RatingMarksLte is an autogenerated method 405 | // nolint: dupl 406 | func (qs UserQuerySet) RatingMarksLte(ratingMarks int) UserQuerySet { 407 | return qs.w(qs.db.Where("rating_marks <= ?", ratingMarks)) 408 | } 409 | 410 | // RatingMarksNe is an autogenerated method 411 | // nolint: dupl 412 | func (qs UserQuerySet) RatingMarksNe(ratingMarks int) UserQuerySet { 413 | return qs.w(qs.db.Where("rating_marks != ?", ratingMarks)) 414 | } 415 | 416 | // RatingMarksNotIn is an autogenerated method 417 | // nolint: dupl 418 | func (qs UserQuerySet) RatingMarksNotIn(ratingMarks ...int) UserQuerySet { 419 | if len(ratingMarks) == 0 { 420 | qs.db.AddError(errors.New("must at least pass one ratingMarks in RatingMarksNotIn")) 421 | return qs.w(qs.db) 422 | } 423 | return qs.w(qs.db.Where("rating_marks NOT IN (?)", ratingMarks)) 424 | } 425 | 426 | // RatingNe is an autogenerated method 427 | // nolint: dupl 428 | func (qs UserQuerySet) RatingNe(rating int) UserQuerySet { 429 | return qs.w(qs.db.Where("rating != ?", rating)) 430 | } 431 | 432 | // RatingNotIn is an autogenerated method 433 | // nolint: dupl 434 | func (qs UserQuerySet) RatingNotIn(rating ...int) UserQuerySet { 435 | if len(rating) == 0 { 436 | qs.db.AddError(errors.New("must at least pass one rating in RatingNotIn")) 437 | return qs.w(qs.db) 438 | } 439 | return qs.w(qs.db.Where("rating NOT IN (?)", rating)) 440 | } 441 | 442 | // UpdatedAtEq is an autogenerated method 443 | // nolint: dupl 444 | func (qs UserQuerySet) UpdatedAtEq(updatedAt time.Time) UserQuerySet { 445 | return qs.w(qs.db.Where("updated_at = ?", updatedAt)) 446 | } 447 | 448 | // UpdatedAtGt is an autogenerated method 449 | // nolint: dupl 450 | func (qs UserQuerySet) UpdatedAtGt(updatedAt time.Time) UserQuerySet { 451 | return qs.w(qs.db.Where("updated_at > ?", updatedAt)) 452 | } 453 | 454 | // UpdatedAtGte is an autogenerated method 455 | // nolint: dupl 456 | func (qs UserQuerySet) UpdatedAtGte(updatedAt time.Time) UserQuerySet { 457 | return qs.w(qs.db.Where("updated_at >= ?", updatedAt)) 458 | } 459 | 460 | // UpdatedAtLt is an autogenerated method 461 | // nolint: dupl 462 | func (qs UserQuerySet) UpdatedAtLt(updatedAt time.Time) UserQuerySet { 463 | return qs.w(qs.db.Where("updated_at < ?", updatedAt)) 464 | } 465 | 466 | // UpdatedAtLte is an autogenerated method 467 | // nolint: dupl 468 | func (qs UserQuerySet) UpdatedAtLte(updatedAt time.Time) UserQuerySet { 469 | return qs.w(qs.db.Where("updated_at <= ?", updatedAt)) 470 | } 471 | 472 | // UpdatedAtNe is an autogenerated method 473 | // nolint: dupl 474 | func (qs UserQuerySet) UpdatedAtNe(updatedAt time.Time) UserQuerySet { 475 | return qs.w(qs.db.Where("updated_at != ?", updatedAt)) 476 | } 477 | 478 | // SetCreatedAt is an autogenerated method 479 | // nolint: dupl 480 | func (u UserUpdater) SetCreatedAt(createdAt time.Time) UserUpdater { 481 | u.fields[string(UserDBSchema.CreatedAt)] = createdAt 482 | return u 483 | } 484 | 485 | // SetDeletedAt is an autogenerated method 486 | // nolint: dupl 487 | func (u UserUpdater) SetDeletedAt(deletedAt *time.Time) UserUpdater { 488 | u.fields[string(UserDBSchema.DeletedAt)] = deletedAt 489 | return u 490 | } 491 | 492 | // SetID is an autogenerated method 493 | // nolint: dupl 494 | func (u UserUpdater) SetID(ID uint) UserUpdater { 495 | u.fields[string(UserDBSchema.ID)] = ID 496 | return u 497 | } 498 | 499 | // SetRating is an autogenerated method 500 | // nolint: dupl 501 | func (u UserUpdater) SetRating(rating int) UserUpdater { 502 | u.fields[string(UserDBSchema.Rating)] = rating 503 | return u 504 | } 505 | 506 | // SetRatingMarks is an autogenerated method 507 | // nolint: dupl 508 | func (u UserUpdater) SetRatingMarks(ratingMarks int) UserUpdater { 509 | u.fields[string(UserDBSchema.RatingMarks)] = ratingMarks 510 | return u 511 | } 512 | 513 | // SetUpdatedAt is an autogenerated method 514 | // nolint: dupl 515 | func (u UserUpdater) SetUpdatedAt(updatedAt time.Time) UserUpdater { 516 | u.fields[string(UserDBSchema.UpdatedAt)] = updatedAt 517 | return u 518 | } 519 | 520 | // Update is an autogenerated method 521 | // nolint: dupl 522 | func (u UserUpdater) Update() error { 523 | return u.db.Updates(u.fields).Error 524 | } 525 | 526 | // UpdateNum is an autogenerated method 527 | // nolint: dupl 528 | func (u UserUpdater) UpdateNum() (int64, error) { 529 | db := u.db.Updates(u.fields) 530 | return db.RowsAffected, db.Error 531 | } 532 | 533 | // ===== END of query set UserQuerySet 534 | 535 | // ===== BEGIN of User modifiers 536 | 537 | // UserDBSchemaField describes database schema field. It requires for method 'Update' 538 | type UserDBSchemaField string 539 | 540 | // String method returns string representation of field. 541 | // nolint: dupl 542 | func (f UserDBSchemaField) String() string { 543 | return string(f) 544 | } 545 | 546 | // UserDBSchema stores db field names of User 547 | var UserDBSchema = struct { 548 | ID UserDBSchemaField 549 | CreatedAt UserDBSchemaField 550 | UpdatedAt UserDBSchemaField 551 | DeletedAt UserDBSchemaField 552 | Rating UserDBSchemaField 553 | RatingMarks UserDBSchemaField 554 | }{ 555 | 556 | ID: UserDBSchemaField("id"), 557 | CreatedAt: UserDBSchemaField("created_at"), 558 | UpdatedAt: UserDBSchemaField("updated_at"), 559 | DeletedAt: UserDBSchemaField("deleted_at"), 560 | Rating: UserDBSchemaField("rating"), 561 | RatingMarks: UserDBSchemaField("rating_marks"), 562 | } 563 | 564 | // Update updates User fields by primary key 565 | // nolint: dupl 566 | func (o *User) Update(db *gorm.DB, fields ...UserDBSchemaField) error { 567 | dbNameToFieldName := map[string]interface{}{ 568 | "id": o.ID, 569 | "created_at": o.CreatedAt, 570 | "updated_at": o.UpdatedAt, 571 | "deleted_at": o.DeletedAt, 572 | "rating": o.Rating, 573 | "rating_marks": o.RatingMarks, 574 | } 575 | u := map[string]interface{}{} 576 | for _, f := range fields { 577 | fs := f.String() 578 | u[fs] = dbNameToFieldName[fs] 579 | } 580 | if err := db.Model(o).Updates(u).Error; err != nil { 581 | if err == gorm.ErrRecordNotFound { 582 | return err 583 | } 584 | 585 | return fmt.Errorf("can't update User %v fields %v: %s", 586 | o, fields, err) 587 | } 588 | 589 | return nil 590 | } 591 | 592 | // UserUpdater is an User updates manager 593 | type UserUpdater struct { 594 | fields map[string]interface{} 595 | db *gorm.DB 596 | } 597 | 598 | // NewUserUpdater creates new User updater 599 | // nolint: dupl 600 | func NewUserUpdater(db *gorm.DB) UserUpdater { 601 | return UserUpdater{ 602 | fields: map[string]interface{}{}, 603 | db: db.Model(&User{}), 604 | } 605 | } 606 | 607 | // ===== END of User modifiers 608 | 609 | // ===== END of all query sets 610 | -------------------------------------------------------------------------------- /examples/comparison/gorm4/gorm4.go: -------------------------------------------------------------------------------- 1 | package gorm4 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jinzhu/gorm" 7 | ) 8 | 9 | func getGormDB() *gorm.DB { 10 | db, _ := gorm.Open("mysql", 11 | "user:password@/dbname?charset=utf8&parseTime=True&loc=Local") 12 | return db 13 | } 14 | 15 | //go:generate goqueryset -in gorm4.go 16 | 17 | // User struct represents user model. 18 | // gen:qs 19 | type User struct { 20 | gorm.Model 21 | Rating int 22 | RatingMarks int 23 | } 24 | 25 | func getTodayBegin() time.Time { 26 | year, month, day := time.Now().Date() 27 | return time.Date(year, month, day, 0, 0, 0, 0, time.Now().Location()) 28 | } 29 | 30 | // WithMaxRating is our defined on UserQuerySet method, that selects 31 | // users with max rating. 32 | // UserQuerySet is an autogenerated struct with a lot of typesafe methods. 33 | // We can define any methods on it because it's in the same package 34 | func (qs UserQuerySet) WithMaxRating(minMarks int) UserQuerySet { 35 | return qs.RatingMarksGte(minMarks).OrderDescByRating() 36 | } 37 | 38 | // RegisteredToday returns only users registered today 39 | func (qs UserQuerySet) RegisteredToday() UserQuerySet { 40 | // autogenerated typesafe method CreatedAtGte(time.Time) 41 | return qs.CreatedAtGte(getTodayBegin()) 42 | } 43 | 44 | // now we can parametrize it 45 | const minRatingMarks = 10 46 | 47 | // GetUsersWithMaxRating returns limit users with max rating 48 | func GetUsersWithMaxRating(limit int) ([]User, error) { 49 | var users []User 50 | err := NewUserQuerySet(getGormDB()). 51 | WithMaxRating(minRatingMarks). // reuse our method 52 | Limit(limit). // autogenerated typesafe method Limit(int) 53 | All(&users) // autogenerated typesafe method All(*[]User) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return users, nil 58 | } 59 | 60 | // GetUsersRegisteredToday returns limit users registered today 61 | func GetUsersRegisteredToday(limit int) ([]User, error) { 62 | var users []User 63 | err := NewUserQuerySet(getGormDB()). 64 | RegisteredToday(). // reuse our method 65 | Limit(limit). // autogenerated typesafe method Limit(int) 66 | All(&users) // autogenerated typesafe method All(*[]User) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return users, nil 71 | } 72 | 73 | // GetUsersRegisteredTodayWithMaxRating returns limit users 74 | // registered today and with max rating 75 | func GetUsersRegisteredTodayWithMaxRating(limit int) ([]User, error) { 76 | var users []User 77 | err := NewUserQuerySet(getGormDB()). 78 | RegisteredToday(). // reuse our method 79 | WithMaxRating(minRatingMarks). // reuse our method 80 | Limit(limit). 81 | All(&users) // autogenerated typesafe method All(*[]User) 82 | if err != nil { 83 | return nil, err 84 | } 85 | return users, nil 86 | } 87 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jirfag/go-queryset 2 | 3 | require ( 4 | cloud.google.com/go v0.37.0 // indirect 5 | github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853 // indirect 6 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect 7 | github.com/go-sql-driver/mysql v0.0.0-20170822214809-26471af196a1 // indirect 8 | github.com/gofrs/uuid v3.2.0+incompatible // indirect 9 | github.com/jinzhu/gorm v1.9.2 10 | github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d // indirect 11 | github.com/jinzhu/now v1.0.0 // indirect 12 | github.com/lib/pq v1.0.0 // indirect 13 | github.com/mattn/go-sqlite3 v1.10.0 // indirect 14 | github.com/pkg/errors v0.8.1 15 | github.com/stretchr/testify v1.3.0 16 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 // indirect 17 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c 18 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.2.0 19 | ) 20 | 21 | go 1.13 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.37.0 h1:69FNAINiZfsEuwH3fKq8QrAAnHz+2m4XL4kVYi5BX0Q= 4 | cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= 5 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 6 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 7 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 8 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 9 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 10 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 11 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 12 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 13 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 14 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 15 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 16 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853 h1:tTngnoO/B6HQnJ+pK8tN7kEAhmhIfaJOutqq/A4/JTM= 21 | github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= 22 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 23 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 24 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 25 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 26 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 27 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 28 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 29 | github.com/go-sql-driver/mysql v0.0.0-20170822214809-26471af196a1 h1:9i8K0Xu0fdkZtOGv83cgBNQEBrrMRCrjV7Ln/uXso+s= 30 | github.com/go-sql-driver/mysql v0.0.0-20170822214809-26471af196a1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 31 | github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= 32 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 33 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 34 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 35 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 36 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 37 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 38 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 40 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 41 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 42 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 43 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 44 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 45 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 46 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 47 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 48 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 49 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 50 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 51 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 52 | github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw= 53 | github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= 54 | github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= 55 | github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 56 | github.com/jinzhu/now v1.0.0 h1:6WV8LvwPpDhKjo5U9O6b4+xdG/jTXNPwlDme/MTo8Ns= 57 | github.com/jinzhu/now v1.0.0/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= 58 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 59 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 60 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 61 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 62 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 63 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 64 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= 65 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 66 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= 67 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 68 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 69 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 70 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 71 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 72 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 73 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 74 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 75 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 76 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 77 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 78 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 79 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 80 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 81 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 82 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 83 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 84 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 85 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 86 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 87 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 88 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 89 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 90 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 91 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 92 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 93 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 94 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 95 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 96 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 97 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 98 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 99 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 100 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 101 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 102 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 103 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 104 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 105 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 106 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 107 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 108 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 109 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 110 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 111 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 112 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 113 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 114 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 115 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 116 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 117 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 118 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 119 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 120 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 121 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 122 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 123 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 124 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 125 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 126 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 127 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 128 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 129 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 130 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 131 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 132 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 136 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 137 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 138 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 139 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 140 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 141 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 142 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 143 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 144 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 145 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 146 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c h1:vamGzbGri8IKo20MQncCuljcQ5uAO6kaCeawQPVblAI= 147 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 148 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 149 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 150 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 151 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 152 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 153 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 154 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 155 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 156 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 157 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 158 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 159 | google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 160 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 161 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 162 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 163 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 164 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.2.0 h1:8Zgzp+2CH8op65cc0isUmmqwlwO3t9b1Nc/BG74JiBw= 165 | gopkg.in/DATA-DOG/go-sqlmock.v1 v1.2.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= 166 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 167 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 168 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 169 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 170 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 171 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 172 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 173 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 174 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 175 | -------------------------------------------------------------------------------- /internal/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "go/types" 10 | "path/filepath" 11 | "reflect" 12 | "strings" 13 | 14 | "github.com/pkg/errors" 15 | 16 | "golang.org/x/tools/go/packages" 17 | ) 18 | 19 | // StructField represents one field in struct 20 | type StructField struct { 21 | name string // 22 | typ types.Type // field/method/parameter type 23 | tag reflect.StructTag // field tag; or nil 24 | } 25 | 26 | func (sf StructField) Name() string { 27 | return sf.name 28 | } 29 | 30 | func (sf StructField) Type() types.Type { 31 | return sf.typ 32 | } 33 | 34 | func (sf StructField) Tag() reflect.StructTag { 35 | return sf.tag 36 | } 37 | 38 | // ParsedStruct represents struct info 39 | type ParsedStruct struct { 40 | TypeName string 41 | Fields []StructField 42 | Doc *ast.CommentGroup // line comments; or nil 43 | } 44 | 45 | type Result struct { 46 | Structs map[string]ParsedStruct 47 | PackageName string 48 | Types *types.Package 49 | } 50 | 51 | type Structs struct{} 52 | 53 | func (p Structs) ParseFile(ctx context.Context, filePath string) (*Result, error) { 54 | absFilePath, err := filepath.Abs(filePath) 55 | if err != nil { 56 | return nil, errors.Wrapf(err, "can't get abs path for %s", filePath) 57 | } 58 | 59 | neededStructs, err := p.getStructNamesInFile(absFilePath) 60 | if err != nil { 61 | return nil, errors.Wrap(err, "can't get struct names") 62 | } 63 | 64 | // need load the full package type info because 65 | // some deps can be in other files 66 | inPkgName := filepath.Dir(filePath) 67 | if !filepath.IsAbs(inPkgName) && !strings.HasPrefix(inPkgName, ".") { 68 | // to make this dir name a local package name 69 | // can't use filepath.Join because it calls Clean and removes "."+sep 70 | inPkgName = fmt.Sprintf(".%c%s", filepath.Separator, inPkgName) 71 | } 72 | 73 | pkgs, err := packages.Load(&packages.Config{ 74 | Mode: packages.LoadAllSyntax, 75 | Context: ctx, 76 | Tests: false, 77 | }, inPkgName) 78 | if err != nil { 79 | return nil, errors.Wrapf(err, "failed to load package for file %s", filePath) 80 | } 81 | 82 | if len(pkgs) != 1 { 83 | return nil, fmt.Errorf("got too many (%d) packages: %#v", len(pkgs), pkgs) 84 | } 85 | 86 | structs := p.buildParsedStructs(pkgs[0], neededStructs) 87 | return &Result{ 88 | Structs: structs, 89 | PackageName: pkgs[0].Name, 90 | Types: pkgs[0].Types, 91 | }, nil 92 | } 93 | 94 | func (p Structs) buildParsedStructs(pkg *packages.Package, neededStructs structNamesInfo) map[string]ParsedStruct { 95 | ret := map[string]ParsedStruct{} 96 | 97 | scope := pkg.Types.Scope() 98 | for _, name := range scope.Names() { 99 | obj := scope.Lookup(name) 100 | 101 | if neededStructs[name] == nil { 102 | continue 103 | } 104 | 105 | t := obj.Type().(*types.Named) 106 | s := t.Underlying().(*types.Struct) 107 | 108 | parsedStruct := parseStruct(s, neededStructs[name]) 109 | if parsedStruct != nil { 110 | parsedStruct.TypeName = name 111 | ret[name] = *parsedStruct 112 | } else { 113 | // TODO 114 | } 115 | } 116 | 117 | return ret 118 | } 119 | 120 | type structNamesInfo map[string]*ast.GenDecl 121 | 122 | type structNamesVisitor struct { 123 | names structNamesInfo 124 | curGenDecl *ast.GenDecl 125 | } 126 | 127 | func (v *structNamesVisitor) Visit(n ast.Node) (w ast.Visitor) { 128 | switch n := n.(type) { 129 | case *ast.GenDecl: 130 | v.curGenDecl = n 131 | case *ast.TypeSpec: 132 | if _, ok := n.Type.(*ast.StructType); ok { 133 | v.names[n.Name.Name] = v.curGenDecl 134 | } 135 | } 136 | 137 | return v 138 | } 139 | 140 | func (p Structs) getStructNamesInFile(fname string) (structNamesInfo, error) { 141 | fset := token.NewFileSet() 142 | f, err := parser.ParseFile(fset, fname, nil, parser.ParseComments) 143 | if err != nil { 144 | return nil, fmt.Errorf("can't parse file %q: %s", fname, err) 145 | } 146 | 147 | v := structNamesVisitor{ 148 | names: structNamesInfo{}, 149 | } 150 | ast.Walk(&v, f) 151 | return v.names, nil 152 | } 153 | 154 | func newStructField(f *types.Var, tag string) *StructField { 155 | return &StructField{ 156 | name: f.Name(), 157 | typ: f.Type(), 158 | tag: reflect.StructTag(tag), 159 | } 160 | } 161 | 162 | func parseStructFields(s *types.Struct) []StructField { 163 | var fields []StructField 164 | for i := 0; i < s.NumFields(); i++ { 165 | f := s.Field(i) 166 | if _, ok := f.Type().Underlying().(*types.Interface); ok { 167 | // skip interfaces 168 | continue 169 | } 170 | 171 | if f.Anonymous() { 172 | e, ok := f.Type().Underlying().(*types.Struct) 173 | if !ok { 174 | continue 175 | } 176 | 177 | pf := parseStructFields(e) 178 | if len(pf) == 0 { 179 | continue 180 | } 181 | 182 | fields = append(fields, pf...) 183 | continue 184 | } 185 | 186 | if !f.Exported() { 187 | continue 188 | } 189 | 190 | sf := newStructField(f, s.Tag(i)) 191 | fields = append(fields, *sf) 192 | } 193 | 194 | return fields 195 | } 196 | 197 | func parseStruct(s *types.Struct, decl *ast.GenDecl) *ParsedStruct { 198 | fields := parseStructFields(s) 199 | if len(fields) == 0 { 200 | // e.g. no exported fields in struct 201 | return nil 202 | } 203 | 204 | var doc *ast.CommentGroup 205 | if decl != nil { // decl can be nil for embedded structs 206 | doc = decl.Doc // can obtain doc only from AST 207 | } 208 | 209 | return &ParsedStruct{ 210 | Fields: fields, 211 | Doc: doc, 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /internal/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/hex" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "testing" 14 | 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | func getRepoRoot() string { 19 | _, selfFilePath, _, ok := runtime.Caller(0) 20 | if !ok { 21 | log.Fatalf("can't get caller") 22 | } 23 | 24 | root, err := filepath.Abs(filepath.Join(filepath.Dir(selfFilePath), "..")) 25 | if err != nil { 26 | log.Fatalf("can't get repo root: %s", err) 27 | } 28 | 29 | return root 30 | } 31 | 32 | func getTempDirRoot() string { 33 | return filepath.Join(getRepoRoot(), "parser", "test") 34 | } 35 | 36 | func getTempFileName(rootDir, prefix, suffix string) (*os.File, error) { 37 | randBytes := make([]byte, 16) 38 | _, err := rand.Read(randBytes) 39 | if err != nil { 40 | return nil, fmt.Errorf("can't generate random bytes: %s", err) 41 | } 42 | 43 | p := filepath.Join(rootDir, prefix+hex.EncodeToString(randBytes)+suffix) 44 | return os.Create(p) 45 | } 46 | 47 | func getTmpFileForCode(code string) *os.File { 48 | tmpDir, err := ioutil.TempDir(getTempDirRoot(), "tmptestdir") 49 | if err != nil { 50 | log.Fatalf("can't create temp dir: %s", err) 51 | } 52 | 53 | f, err := getTempFileName(tmpDir, "go-queryset-test", ".go") 54 | if err != nil { 55 | log.Fatalf("can't create temp file: %s", err) 56 | } 57 | 58 | _, err = f.Write([]byte(code)) 59 | if err != nil { 60 | log.Fatalf("can't write to temp file %q: %s", f.Name(), err) 61 | } 62 | 63 | return f 64 | } 65 | 66 | func removeTempFileAndDir(f *os.File) { 67 | root := filepath.Dir(f.Name()) 68 | if err := os.RemoveAll(root); err != nil { 69 | log.Fatalf("can't remove files from root %s: %s", root, err) 70 | } 71 | } 72 | 73 | func TestGetStructNamesInFile(t *testing.T) { 74 | cases := []struct { 75 | code string 76 | expectedStructNames []string 77 | errorIsExpected bool 78 | }{ 79 | { 80 | code: "", 81 | errorIsExpected: true, 82 | }, 83 | { 84 | code: `package p 85 | type T struct {}`, 86 | expectedStructNames: []string{"T"}, 87 | }, 88 | { 89 | code: `package p 90 | type T1 struct {} 91 | type T2 struct {}`, 92 | expectedStructNames: []string{"T1", "T2"}, 93 | }, 94 | { 95 | code: `package p 96 | type T1 int 97 | type T2 struct {}`, 98 | expectedStructNames: []string{"T2"}, 99 | }, 100 | { 101 | code: `package p 102 | var v struct {F int}`, 103 | }, 104 | { 105 | code: `package p 106 | const c = 1`, 107 | }, 108 | } 109 | 110 | p := Structs{} 111 | for i, tc := range cases { 112 | tc := tc // capture range variable 113 | t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { 114 | t.Parallel() 115 | f := getTmpFileForCode(tc.code) 116 | defer removeTempFileAndDir(f) 117 | 118 | res, err := p.getStructNamesInFile(f.Name()) 119 | if tc.errorIsExpected { 120 | assert.NotNil(t, err) 121 | return 122 | } 123 | 124 | assert.Nil(t, err) 125 | if tc.expectedStructNames == nil { 126 | tc.expectedStructNames = []string{} 127 | } 128 | for _, expStructName := range tc.expectedStructNames { 129 | decl, ok := res[expStructName] 130 | assert.True(t, ok, "no struct %s", expStructName) 131 | assert.NotNil(t, decl) 132 | } 133 | assert.Len(t, res, len(tc.expectedStructNames)) 134 | }) 135 | } 136 | } 137 | 138 | type structFieldsCase struct { 139 | code string 140 | expectedStructFields []string 141 | errorIsExpected bool 142 | expectedDoc []string 143 | expectedStructsCount int 144 | } 145 | 146 | func (tc structFieldsCase) getExpectedtructsCount() int { 147 | expectedStructsCount := 1 148 | if tc.expectedStructFields == nil { 149 | expectedStructsCount = 0 150 | } 151 | if tc.expectedStructsCount != 0 { 152 | expectedStructsCount = tc.expectedStructsCount 153 | } 154 | 155 | return expectedStructsCount 156 | } 157 | 158 | func TestGetStructsInFile(t *testing.T) { 159 | cases := []structFieldsCase{ 160 | { 161 | code: "", 162 | errorIsExpected: true, 163 | }, 164 | { 165 | code: `package p 166 | type T struct {}`, 167 | }, 168 | { 169 | code: `package p 170 | type T struct { 171 | F int 172 | }`, 173 | expectedStructFields: []string{"F"}, 174 | }, 175 | { 176 | code: `package p 177 | type T struct { 178 | f int 179 | }`, 180 | }, 181 | { 182 | code: `package p 183 | // doc line 1 184 | // doc line 2 185 | type T struct { 186 | F int 187 | }`, 188 | expectedStructFields: []string{"F"}, 189 | expectedDoc: []string{"// doc line 1", "// doc line 2"}, 190 | }, 191 | { 192 | code: `package p 193 | type m struct { 194 | ID int 195 | } 196 | 197 | type T struct { 198 | m 199 | F int 200 | }`, 201 | expectedStructFields: []string{"F", "ID"}, 202 | expectedStructsCount: 2, 203 | }, 204 | { // test local reordered embedding 205 | code: `package p 206 | type T struct { 207 | m 208 | F int 209 | } 210 | type m struct { 211 | ID int 212 | }`, 213 | expectedStructFields: []string{"F", "ID"}, 214 | expectedStructsCount: 2, 215 | }, 216 | { // test another package imported embedding 217 | code: `package p 218 | import "github.com/jinzhu/gorm" 219 | type T struct { 220 | gorm.Model 221 | F int 222 | }`, 223 | expectedStructFields: []string{"ID", "CreatedAt", "UpdatedAt", "DeletedAt", "F"}, 224 | }, 225 | { 226 | code: `package p 227 | type MyType int`, 228 | }, 229 | { 230 | code: `package p 231 | type m struct { 232 | } 233 | 234 | type T struct { 235 | m 236 | F int 237 | }`, 238 | expectedStructFields: []string{"F"}, 239 | }, 240 | } 241 | 242 | for i, tc := range cases { 243 | tc := tc // capture range variable 244 | t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { 245 | t.Parallel() 246 | testStructFields(t, tc) 247 | }) 248 | } 249 | } 250 | 251 | func testStructFields(t *testing.T, tc structFieldsCase) { 252 | f := getTmpFileForCode(tc.code) 253 | defer removeTempFileAndDir(f) 254 | 255 | p := Structs{} 256 | ret, err := p.ParseFile(context.Background(), f.Name()) 257 | if tc.errorIsExpected { 258 | assert.NotNil(t, err) 259 | return 260 | } 261 | 262 | assert.Nil(t, err) 263 | assert.NotNil(t, ret) 264 | 265 | assert.Len(t, ret.Structs, tc.getExpectedtructsCount()) 266 | if tc.getExpectedtructsCount() == 0 { 267 | return 268 | } 269 | 270 | var typeName string 271 | 272 | for structTypeName := range ret.Structs { 273 | if structTypeName == "T" { 274 | typeName = structTypeName 275 | break 276 | } 277 | } 278 | assert.NotNil(t, typeName) 279 | 280 | s := ret.Structs[typeName] 281 | fieldNames := []string{} 282 | for _, field := range s.Fields { 283 | assert.NotEmpty(t, field.Name) 284 | fieldNames = append(fieldNames, field.name) 285 | } 286 | assert.Len(t, fieldNames, len(tc.expectedStructFields)) 287 | 288 | if tc.expectedDoc != nil { 289 | docLines := []string{} 290 | assert.NotNil(t, s.Doc) 291 | for _, docLine := range s.Doc.List { 292 | docLines = append(docLines, docLine.Text) 293 | } 294 | assert.Equal(t, tc.expectedDoc, docLines) 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /internal/parser/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | // Model is a simple struct for testing package parser 4 | type Model struct { 5 | ID int 6 | } 7 | -------------------------------------------------------------------------------- /internal/queryset/field/field.go: -------------------------------------------------------------------------------- 1 | package field 2 | 3 | import ( 4 | "fmt" 5 | "go/types" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/jinzhu/gorm" 10 | ) 11 | 12 | type BaseInfo struct { 13 | Name string // name of field 14 | DBName string // name of field in DB 15 | TypeName string // name of type of field 16 | IsStruct bool 17 | IsNumeric bool 18 | IsTime bool 19 | IsString bool 20 | } 21 | 22 | type Info struct { 23 | pointed *BaseInfo 24 | BaseInfo 25 | IsPointer bool 26 | } 27 | 28 | func (fi Info) GetPointed() Info { 29 | return Info{ 30 | BaseInfo: *fi.pointed, 31 | } 32 | } 33 | 34 | type InfoGenerator struct { 35 | pkg *types.Package 36 | } 37 | 38 | type Field interface { 39 | Name() string 40 | Type() types.Type 41 | Tag() reflect.StructTag 42 | } 43 | 44 | type field struct { 45 | name string 46 | typ types.Type 47 | tag reflect.StructTag 48 | } 49 | 50 | func (f field) Name() string { 51 | return f.name 52 | } 53 | 54 | func (f field) Type() types.Type { 55 | return f.typ 56 | } 57 | 58 | func (f field) Tag() reflect.StructTag { 59 | return f.tag 60 | } 61 | 62 | func NewInfoGenerator(pkg *types.Package) *InfoGenerator { 63 | return &InfoGenerator{ 64 | pkg: pkg, 65 | } 66 | } 67 | 68 | func (g InfoGenerator) getOriginalTypeName(t *types.Named) string { 69 | if t.Obj().Pkg() == g.pkg { 70 | // t is from the same package as a struct 71 | return t.Obj().Name() 72 | } 73 | 74 | // t is an imported from another package type 75 | return fmt.Sprintf("%s.%s", t.Obj().Pkg().Name(), t.Obj().Name()) 76 | } 77 | 78 | // parseTagSetting is copy-pasted from gorm source code. 79 | func parseTagSetting(tags reflect.StructTag) map[string]string { 80 | setting := map[string]string{} 81 | for _, str := range []string{tags.Get("sql"), tags.Get("gorm")} { 82 | tags := strings.Split(str, ";") 83 | for _, value := range tags { 84 | v := strings.Split(value, ":") 85 | k := strings.TrimSpace(strings.ToUpper(v[0])) 86 | if len(v) >= 2 { 87 | setting[k] = strings.Join(v[1:], ":") 88 | } else { 89 | setting[k] = k 90 | } 91 | } 92 | } 93 | return setting 94 | } 95 | 96 | func (g InfoGenerator) GenFieldInfo(f Field) *Info { 97 | tagSetting := parseTagSetting(f.Tag()) 98 | if tagSetting["-"] != "" { // skipped by tag field 99 | return nil 100 | } 101 | 102 | dbName := gorm.ToDBName(f.Name()) 103 | if dbColName := tagSetting["COLUMN"]; dbColName != "" { 104 | dbName = dbColName 105 | } 106 | bi := BaseInfo{ 107 | Name: f.Name(), 108 | TypeName: f.Type().String(), 109 | DBName: dbName, 110 | } 111 | 112 | if bi.TypeName == "time.Time" { 113 | bi.IsTime = true 114 | bi.IsNumeric = true 115 | return &Info{ 116 | BaseInfo: bi, 117 | } 118 | } 119 | 120 | switch t := f.Type().(type) { 121 | case *types.Basic: 122 | bi.IsString = t.Info()&types.IsString != 0 123 | bi.IsNumeric = t.Info()&types.IsNumeric != 0 124 | return &Info{ 125 | BaseInfo: bi, 126 | } 127 | case *types.Slice: 128 | if t.Elem().String() == "byte" { 129 | return &Info{ 130 | BaseInfo: bi, 131 | } 132 | } 133 | return nil 134 | case *types.Named: 135 | r := g.GenFieldInfo(field{ 136 | name: f.Name(), 137 | typ: t.Underlying(), 138 | tag: f.Tag(), 139 | }) 140 | if r != nil { 141 | r.TypeName = g.getOriginalTypeName(t) 142 | } 143 | return r 144 | case *types.Struct: 145 | bi.IsStruct = true 146 | return &Info{ 147 | BaseInfo: bi, 148 | } 149 | case *types.Pointer: 150 | pf := g.GenFieldInfo(field{ 151 | name: f.Name(), 152 | typ: t.Elem(), 153 | tag: f.Tag(), 154 | }) 155 | return &Info{ 156 | BaseInfo: bi, 157 | IsPointer: true, 158 | pointed: &pf.BaseInfo, 159 | } 160 | default: 161 | // no filtering is needed 162 | return nil 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /internal/queryset/field/field_test.go: -------------------------------------------------------------------------------- 1 | package field 2 | 3 | import ( 4 | "go/token" 5 | "go/types" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type tf struct { 13 | name string 14 | typ types.Type 15 | tag reflect.StructTag 16 | } 17 | 18 | func (f tf) Name() string { return f.name } 19 | func (f tf) Type() types.Type { return f.typ } 20 | func (f tf) Tag() reflect.StructTag { return f.tag } 21 | 22 | func newTf(name string, typ types.Type, tag string) tf { 23 | return tf{ 24 | name: name, 25 | typ: typ, 26 | tag: reflect.StructTag(tag), 27 | } 28 | } 29 | 30 | func newG() *InfoGenerator { 31 | return NewInfoGenerator(nil) 32 | } 33 | 34 | func genFieldInfo(f Field) *Info { 35 | return newG().GenFieldInfo(f) 36 | } 37 | 38 | var typeString = types.Typ[types.String] 39 | var typeStringPtr = types.NewPointer(typeString) 40 | var typeNamedString = types.NewNamed( 41 | types.NewTypeName(token.Pos(0), nil, "myNamedType", typeString), 42 | typeString, 43 | nil) 44 | 45 | const fName = "F" 46 | 47 | func TestIgnoredByTagColumn(t *testing.T) { 48 | const ( 49 | gormIgnore = `gorm:"-"` 50 | sqlIgnore = `sql:"-"` 51 | ) 52 | 53 | assert.Nil(t, genFieldInfo(newTf(fName, typeString, gormIgnore))) 54 | assert.Nil(t, genFieldInfo(newTf(fName, typeStringPtr, gormIgnore))) 55 | assert.Nil(t, genFieldInfo(newTf(fName, typeString, sqlIgnore))) 56 | } 57 | 58 | func TestColumnNameSetInTag(t *testing.T) { 59 | const colNameZ = `gorm:"column:z"` 60 | 61 | info := genFieldInfo(newTf(fName, typeString, colNameZ)) 62 | assert.Equal(t, "z", info.DBName) 63 | assert.Equal(t, fName, info.Name) 64 | assert.Equal(t, typeString.String(), info.TypeName) 65 | 66 | info = genFieldInfo(newTf(fName, typeStringPtr, colNameZ)) 67 | assert.Equal(t, "z", info.DBName) 68 | assert.Equal(t, fName, info.Name) 69 | assert.Equal(t, typeStringPtr.String(), info.TypeName) 70 | 71 | info = genFieldInfo(newTf(fName, typeNamedString, colNameZ)) 72 | assert.Equal(t, "z", info.DBName) 73 | assert.Equal(t, fName, info.Name) 74 | assert.Equal(t, typeNamedString.String(), info.TypeName) 75 | } 76 | -------------------------------------------------------------------------------- /internal/queryset/generator/generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/jirfag/go-queryset/internal/parser" 13 | "github.com/pkg/errors" 14 | "golang.org/x/tools/imports" 15 | ) 16 | 17 | type Generator struct { 18 | StructsParser *parser.Structs 19 | } 20 | 21 | // Generate generates output file with querysets 22 | func (g Generator) Generate(ctx context.Context, inFilePath, outFilePath string) error { 23 | parsedFile, err := g.StructsParser.ParseFile(ctx, inFilePath) 24 | if err != nil { 25 | return errors.Wrapf(err, "can't parse file %s to get structs", inFilePath) 26 | } 27 | 28 | var r io.Reader 29 | r, err = GenerateQuerySetsForStructs(parsedFile.Types, parsedFile.Structs) 30 | if err != nil { 31 | return errors.Wrap(err, "can't generate query sets") 32 | } 33 | 34 | if r == nil { 35 | return fmt.Errorf("no structs to generate query set in %s", inFilePath) 36 | } 37 | 38 | if err = g.writeQuerySetsToOutput(r, parsedFile.PackageName, outFilePath); err != nil { 39 | return errors.Wrapf(err, "can't save query sets to out file %s", outFilePath) 40 | } 41 | 42 | var absOutPath string 43 | absOutPath, err = filepath.Abs(outFilePath) 44 | if err != nil { 45 | absOutPath = outFilePath 46 | } 47 | 48 | log.Printf("successfully wrote querysets to %s", absOutPath) 49 | return nil 50 | } 51 | 52 | func (g Generator) writeQuerySetsToOutput(r io.Reader, packageName, outFile string) error { 53 | const hdrTmpl = `%s 54 | package %s 55 | 56 | import ( 57 | "errors" 58 | "fmt" 59 | "strings" 60 | "time" 61 | 62 | "github.com/jinzhu/gorm" 63 | ) 64 | ` 65 | 66 | // https://golang.org/s/generatedcode 67 | const genHdr = `// Code generated by go-queryset. DO NOT EDIT.` 68 | 69 | var buf bytes.Buffer 70 | pkgName := fmt.Sprintf(hdrTmpl, genHdr, packageName) 71 | if _, err := buf.WriteString(pkgName); err != nil { 72 | return errors.Wrap(err, "can't write hdr string into buf") 73 | } 74 | if _, err := io.Copy(&buf, r); err != nil { 75 | return errors.Wrap(err, "can't write to buf") 76 | } 77 | 78 | formattedRes, err := imports.Process(outFile, buf.Bytes(), nil) 79 | if err != nil { 80 | if os.Getenv("GOQUERYSET_DEBUG_IMPORTS_ERRORS") == "1" { 81 | log.Printf("Can't format generated file: %s", err) 82 | formattedRes = buf.Bytes() 83 | } else { 84 | return errors.Wrap(err, "can't format generated file") 85 | } 86 | } 87 | 88 | var outF *os.File 89 | outF, err = os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0640) // nolint: gas 90 | if err != nil { 91 | return fmt.Errorf("can't open out file: %s", err) 92 | } 93 | defer func() { 94 | if e := outF.Close(); e != nil { 95 | log.Printf("can't close file: %s", e) 96 | } 97 | }() 98 | 99 | if _, err = outF.Write(formattedRes); err != nil { 100 | return errors.Wrap(err, "can't write to out file") 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/queryset/generator/methodsbuilder.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "github.com/jirfag/go-queryset/internal/parser" 5 | "github.com/jirfag/go-queryset/internal/queryset/field" 6 | "github.com/jirfag/go-queryset/internal/queryset/methods" 7 | ) 8 | 9 | type methodsBuilder struct { 10 | fields []field.Info 11 | s parser.ParsedStruct 12 | ret []methods.Method 13 | sctx methods.QsStructContext 14 | } 15 | 16 | func (b *methodsBuilder) qsTypeName() string { 17 | return b.s.TypeName + "QuerySet" 18 | } 19 | 20 | func newMethodsBuilder(s parser.ParsedStruct, fields []field.Info) *methodsBuilder { 21 | return &methodsBuilder{ 22 | s: s, 23 | sctx: methods.NewQsStructContext(s), 24 | fields: fields, 25 | } 26 | } 27 | 28 | func (b *methodsBuilder) getQuerySetMethodsForField(f field.Info) []methods.Method { 29 | fctx := b.sctx.FieldCtx(f) 30 | basicTypeMethods := []methods.Method{ 31 | methods.NewBinaryFilterMethod(fctx.WithOperationName("eq")), 32 | methods.NewBinaryFilterMethod(fctx.WithOperationName("ne")), 33 | methods.NewOrderAscByMethod(fctx), 34 | methods.NewOrderDescByMethod(fctx), 35 | } 36 | 37 | if !f.IsTime { 38 | inMethod := methods.NewInFilterMethod(fctx) 39 | notInMethod := methods.NewNotInFilterMethod(fctx) 40 | basicTypeMethods = append(basicTypeMethods, inMethod, notInMethod) 41 | } 42 | 43 | numericMethods := []methods.Method{ 44 | methods.NewBinaryFilterMethod(fctx.WithOperationName("lt")), 45 | methods.NewBinaryFilterMethod(fctx.WithOperationName("gt")), 46 | methods.NewBinaryFilterMethod(fctx.WithOperationName("lte")), 47 | methods.NewBinaryFilterMethod(fctx.WithOperationName("gte")), 48 | } 49 | 50 | if f.IsString { 51 | likeMethod := methods.NewBinaryFilterMethod(fctx.WithOperationName("like")) 52 | notLikeMethod := methods.NewBinaryFilterMethod(fctx.WithOperationName("notlike")) 53 | 54 | methods := append(basicTypeMethods, likeMethod, notLikeMethod) 55 | return append(methods, numericMethods...) 56 | } 57 | 58 | if f.IsNumeric { 59 | return append(basicTypeMethods, numericMethods...) 60 | } 61 | 62 | if f.IsStruct { 63 | // Association was found (any struct or struct pointer) 64 | return []methods.Method{methods.NewPreloadMethod(fctx)} 65 | } 66 | 67 | if f.IsPointer { 68 | ptrMethods := b.getQuerySetMethodsForField(f.GetPointed()) 69 | return append(ptrMethods, 70 | methods.NewIsNullMethod(fctx), 71 | methods.NewIsNotNullMethod(fctx)) 72 | } 73 | 74 | // it's a string 75 | return basicTypeMethods 76 | } 77 | 78 | func (b *methodsBuilder) buildQuerySetFieldMethods(f field.Info) *methodsBuilder { 79 | methods := b.getQuerySetMethodsForField(f) 80 | b.ret = append(b.ret, methods...) 81 | return b 82 | } 83 | 84 | func getUpdaterTypeName(structTypeName string) string { 85 | return structTypeName + "Updater" 86 | } 87 | 88 | func (b *methodsBuilder) buildUpdaterStructMethods() { 89 | updaterTypeName := getUpdaterTypeName(b.s.TypeName) 90 | b.ret = append(b.ret, 91 | methods.NewUpdaterUpdateMethod(updaterTypeName), 92 | methods.NewUpdaterUpdateNumMethod(updaterTypeName), 93 | ) 94 | } 95 | 96 | func (b *methodsBuilder) buildUpdaterFieldMethods(f field.Info) { 97 | if f.IsPointer { 98 | p := f.GetPointed() 99 | if p.IsStruct { 100 | // TODO 101 | return 102 | } 103 | 104 | // It's a pointer to simple field (string, int). 105 | // Developer used pointer to distinguish between NULL and not NULL values. 106 | } 107 | 108 | dbSchemaTypeName := b.s.TypeName + "DBSchema" 109 | updaterTypeName := getUpdaterTypeName(b.s.TypeName) 110 | b.ret = append(b.ret, 111 | methods.NewUpdaterSetMethod(f.Name, f.TypeName, updaterTypeName, 112 | dbSchemaTypeName)) 113 | } 114 | 115 | func (b *methodsBuilder) buildStructSelectMethods() *methodsBuilder { 116 | b.ret = append(b.ret, 117 | methods.NewAllMethod(b.s.TypeName, b.qsTypeName()), 118 | methods.NewOneMethod(b.s.TypeName, b.qsTypeName()), 119 | methods.NewLimitMethod(b.qsTypeName()), 120 | methods.NewOffsetMethod(b.qsTypeName())) 121 | return b 122 | } 123 | 124 | func (b *methodsBuilder) buildAggrMethods() *methodsBuilder { 125 | b.ret = append(b.ret, 126 | methods.NewCountMethod(b.qsTypeName())) 127 | return b 128 | } 129 | 130 | func (b *methodsBuilder) buildCRUDMethods() *methodsBuilder { 131 | b.ret = append(b.ret, 132 | methods.NewGetUpdaterMethod(b.qsTypeName(), getUpdaterTypeName(b.s.TypeName)), 133 | methods.NewDeleteMethod(b.qsTypeName(), b.s.TypeName), 134 | methods.NewStructModifierMethod("Create", b.s.TypeName), 135 | methods.NewStructModifierMethod("Delete", b.s.TypeName), 136 | methods.NewDeleteNumMethod(b.qsTypeName(), b.s.TypeName), 137 | methods.NewDeleteNumUnscopedMethod(b.qsTypeName(), b.s.TypeName), 138 | methods.NewGetDBMethod(b.qsTypeName()), 139 | ) 140 | 141 | return b 142 | } 143 | 144 | func (b methodsBuilder) Build() []methods.Method { 145 | b.buildStructSelectMethods(). 146 | buildAggrMethods(). 147 | buildCRUDMethods(). 148 | buildUpdaterStructMethods() 149 | 150 | for _, f := range b.fields { 151 | b.buildQuerySetFieldMethods(f).buildUpdaterFieldMethods(f) 152 | } 153 | 154 | return b.ret 155 | } 156 | -------------------------------------------------------------------------------- /internal/queryset/generator/queryset.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/ast" 7 | "go/types" 8 | "io" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/jirfag/go-queryset/internal/parser" 13 | "github.com/jirfag/go-queryset/internal/queryset/field" 14 | "github.com/jirfag/go-queryset/internal/queryset/methods" 15 | ) 16 | 17 | type querySetStructConfig struct { 18 | StructName string 19 | Name string 20 | Methods methodsSlice 21 | Fields []field.Info 22 | } 23 | 24 | type methodsSlice []methods.Method 25 | 26 | func (s methodsSlice) Len() int { return len(s) } 27 | func (s methodsSlice) Less(i, j int) bool { 28 | // first, group by receiver 29 | receiverCmp := strings.Compare(s[i].GetReceiverDeclaration(), s[j].GetReceiverDeclaration()) 30 | if receiverCmp != 0 { 31 | return receiverCmp < 0 32 | } 33 | 34 | // second, sort by method name inside a receiver group 35 | return strings.Compare(s[i].GetMethodName(), s[j].GetMethodName()) < 0 36 | } 37 | func (s methodsSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 38 | 39 | type querySetStructConfigSlice []querySetStructConfig 40 | 41 | func (s querySetStructConfigSlice) Len() int { return len(s) } 42 | func (s querySetStructConfigSlice) Less(i, j int) bool { 43 | return strings.Compare(s[i].Name, s[j].Name) < 0 44 | } 45 | func (s querySetStructConfigSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 46 | 47 | func doesNeedToGenerateQuerySet(doc *ast.CommentGroup) bool { 48 | if doc == nil { 49 | return false 50 | } 51 | 52 | for _, c := range doc.List { 53 | parts := strings.Split(strings.TrimSpace(c.Text), ":") 54 | ok := len(parts) == 2 && 55 | strings.TrimSpace(strings.TrimPrefix(parts[0], "//")) == "gen" && 56 | strings.TrimSpace(parts[1]) == "qs" 57 | if ok { 58 | return true 59 | } 60 | } 61 | 62 | return false 63 | } 64 | 65 | func genStructFieldInfos(s parser.ParsedStruct, types *types.Package) (ret []field.Info) { 66 | g := field.NewInfoGenerator(types) 67 | for _, f := range s.Fields { 68 | fi := g.GenFieldInfo(f) 69 | if fi == nil { 70 | continue 71 | } 72 | ret = append(ret, *fi) 73 | } 74 | return ret 75 | } 76 | 77 | func generateQuerySetConfigs(types *types.Package, 78 | structs map[string]parser.ParsedStruct) querySetStructConfigSlice { 79 | 80 | querySetStructConfigs := querySetStructConfigSlice{} 81 | 82 | for _, s := range structs { 83 | if !doesNeedToGenerateQuerySet(s.Doc) { 84 | continue 85 | } 86 | 87 | fields := genStructFieldInfos(s, types) 88 | b := newMethodsBuilder(s, fields) 89 | methods := b.Build() 90 | 91 | qsConfig := querySetStructConfig{ 92 | StructName: s.TypeName, 93 | Name: s.TypeName + "QuerySet", 94 | Methods: methods, 95 | Fields: fields, 96 | } 97 | sort.Sort(qsConfig.Methods) // make output queryset stable 98 | querySetStructConfigs = append(querySetStructConfigs, qsConfig) 99 | } 100 | 101 | return querySetStructConfigs 102 | } 103 | 104 | // GenerateQuerySetsForStructs is an internal method to retrieve querysets 105 | // generated code from parsed structs 106 | func GenerateQuerySetsForStructs(types *types.Package, structs map[string]parser.ParsedStruct) (io.Reader, error) { 107 | querySetStructConfigs := generateQuerySetConfigs(types, structs) 108 | if len(querySetStructConfigs) == 0 { 109 | return nil, nil 110 | } 111 | 112 | sort.Sort(querySetStructConfigs) 113 | 114 | var b bytes.Buffer 115 | err := qsTmpl.Execute(&b, struct { 116 | Configs querySetStructConfigSlice 117 | }{ 118 | Configs: querySetStructConfigs, 119 | }) 120 | 121 | if err != nil { 122 | return nil, fmt.Errorf("can't generate structs query sets: %s", err) 123 | } 124 | 125 | return &b, nil 126 | } 127 | -------------------------------------------------------------------------------- /internal/queryset/generator/queryset_test.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "database/sql/driver" 7 | "fmt" 8 | "log" 9 | "math/rand" 10 | "os" 11 | "path/filepath" 12 | "reflect" 13 | "regexp" 14 | "runtime" 15 | "strings" 16 | "testing" 17 | "time" 18 | 19 | "github.com/jirfag/go-queryset/internal/parser" 20 | 21 | "github.com/jinzhu/gorm" 22 | _ "github.com/jinzhu/gorm/dialects/mysql" 23 | "github.com/jirfag/go-queryset/internal/queryset/generator/test" 24 | assert "github.com/stretchr/testify/require" 25 | 26 | sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" 27 | ) 28 | 29 | const testSurname = "Ivanov" 30 | 31 | func fixedFullRe(s string) string { 32 | return fmt.Sprintf("^%s$", regexp.QuoteMeta(s)) 33 | } 34 | 35 | func newDB() (sqlmock.Sqlmock, *gorm.DB) { 36 | db, mock, err := sqlmock.New() 37 | if err != nil { 38 | log.Fatalf("can't create sqlmock: %s", err) 39 | } 40 | 41 | gormDB, gerr := gorm.Open("mysql", db) 42 | if gerr != nil { 43 | log.Fatalf("can't open gorm connection: %s", err) 44 | } 45 | gormDB.LogMode(true) 46 | 47 | return mock, gormDB.Set("gorm:update_column", true) 48 | } 49 | 50 | func getRowsForUsers(users []test.User) *sqlmock.Rows { 51 | var userFieldNames = []string{"id", "name", "user_surname", "email", "created_at", "updated_at", "deleted_at"} 52 | rows := sqlmock.NewRows(userFieldNames) 53 | 54 | for _, u := range users { 55 | rows = rows.AddRow(u.ID, u.Name, u.Surname, u.Email, u.CreatedAt, u.UpdatedAt, u.DeletedAt) 56 | } 57 | return rows 58 | } 59 | 60 | func getRowWithFields(fields []driver.Value) *sqlmock.Rows { 61 | fieldNames := []string{} 62 | for i := range fields { 63 | fieldNames = append(fieldNames, fmt.Sprintf("f%d", i)) 64 | } 65 | 66 | return sqlmock.NewRows(fieldNames).AddRow(fields...) 67 | } 68 | 69 | func getTestUsers(n int) []test.User { 70 | ret := []test.User{} 71 | 72 | for i := 0; i < n; i++ { 73 | u := test.User{ 74 | Model: gorm.Model{ 75 | ID: uint(i), 76 | CreatedAt: time.Now(), 77 | UpdatedAt: time.Now(), 78 | }, 79 | Email: fmt.Sprintf("u%d@mail.ru", i), 80 | Name: fmt.Sprintf("name_%d", i), 81 | } 82 | ret = append(ret, u) 83 | } 84 | 85 | return ret 86 | } 87 | 88 | func getUserNoID() test.User { 89 | randInt := rand.Int() 90 | return test.User{ 91 | Model: gorm.Model{ 92 | CreatedAt: time.Now(), 93 | UpdatedAt: time.Now(), 94 | }, 95 | Email: fmt.Sprintf("qs_%d@mail.ru", randInt), 96 | Name: fmt.Sprintf("name_rand_%d", randInt), 97 | } 98 | } 99 | 100 | func getUser() test.User { 101 | u := getUserNoID() 102 | u.ID = uint(rand.Int()) 103 | return u 104 | } 105 | 106 | func checkMock(t *testing.T, mock sqlmock.Sqlmock) { 107 | if err := mock.ExpectationsWereMet(); err != nil { 108 | t.Errorf("there were unfulfilled expections: %s", err) 109 | } 110 | } 111 | 112 | type testQueryFunc func(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) 113 | 114 | func TestQueries(t *testing.T) { 115 | funcs := []testQueryFunc{ 116 | testUserSelectAll, 117 | testUserSelectAllSingleField, 118 | testUserSelectAllMultipleFields, 119 | testUserSelectWithLimitAndOffset, 120 | testUserSelectAllNoRecords, 121 | testUserSelectOne, 122 | testUserSelectWithSurnameFilter, 123 | testUserCreateOne, 124 | testUserCreateOneWithSurname, 125 | testUserUpdateFieldsByPK, 126 | testUserUpdateByEmail, 127 | testUserDeleteByEmail, 128 | testUserDeleteByPK, 129 | testUserQueryFilters, 130 | testUsersCount, 131 | testUsersUpdateNum, 132 | testUsersDeleteNum, 133 | testUsersDeleteNumUnscoped, 134 | } 135 | for _, f := range funcs { 136 | f := f // save range var 137 | funcName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() 138 | funcName = filepath.Ext(funcName) 139 | funcName = strings.TrimPrefix(funcName, ".") 140 | t.Run(funcName, func(t *testing.T) { 141 | t.Parallel() 142 | m, db := newDB() 143 | defer checkMock(t, m) 144 | f(t, m, db) 145 | }) 146 | } 147 | } 148 | 149 | func testUserSelectAll(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 150 | expUsers := getTestUsers(2) 151 | m.ExpectQuery(fixedFullRe("SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL")). 152 | WillReturnRows(getRowsForUsers(expUsers)) 153 | 154 | var users []test.User 155 | 156 | assert.Nil(t, test.NewUserQuerySet(db).All(&users)) 157 | assert.Equal(t, expUsers, users) 158 | } 159 | 160 | func testUserSelectAllSingleField(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 161 | expUsers := getTestUsers(2) 162 | m.ExpectQuery(fixedFullRe("SELECT name FROM `users` WHERE `users`.`deleted_at` IS NULL")). 163 | WillReturnRows(getRowsForUsers(expUsers)) 164 | 165 | var users []test.User 166 | 167 | assert.Nil(t, test.NewUserQuerySet(db).Select(test.UserDBSchema.Name).All(&users)) 168 | assert.Equal(t, expUsers, users) 169 | } 170 | 171 | func testUserSelectAllMultipleFields(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 172 | expUsers := getTestUsers(2) 173 | m.ExpectQuery(fixedFullRe("SELECT name,email FROM `users` WHERE `users`.`deleted_at` IS NULL")). 174 | WillReturnRows(getRowsForUsers(expUsers)) 175 | 176 | var users []test.User 177 | 178 | assert.Nil(t, test.NewUserQuerySet(db).Select(test.UserDBSchema.Name, test.UserDBSchema.Email).All(&users)) 179 | assert.Equal(t, expUsers, users) 180 | } 181 | 182 | func testUserSelectWithLimitAndOffset(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 183 | expUsers := getTestUsers(2) 184 | req := "SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL LIMIT 1 OFFSET 1" 185 | m.ExpectQuery(fixedFullRe(req)). 186 | WillReturnRows(getRowsForUsers(expUsers)) 187 | 188 | var users []test.User 189 | 190 | assert.Nil(t, test.NewUserQuerySet(db).Limit(1).Offset(1).All(&users)) 191 | assert.Equal(t, expUsers[0], users[0]) 192 | } 193 | 194 | func testUserSelectAllNoRecords(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 195 | m.ExpectQuery(fixedFullRe("SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL")). 196 | WillReturnError(sql.ErrNoRows) 197 | 198 | var users []test.User 199 | 200 | assert.Error(t, gorm.ErrRecordNotFound, test.NewUserQuerySet(db).All(&users)) 201 | assert.Len(t, users, 0) 202 | } 203 | 204 | func testUserSelectOne(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 205 | expUsers := getTestUsers(1) 206 | req := "SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL ORDER BY `users`.`id` ASC LIMIT 1" 207 | m.ExpectQuery(fixedFullRe(req)). 208 | WillReturnRows(getRowsForUsers(expUsers)) 209 | 210 | var user test.User 211 | 212 | assert.Nil(t, test.NewUserQuerySet(db).One(&user)) 213 | assert.Equal(t, expUsers[0], user) 214 | } 215 | 216 | func testUserSelectWithSurnameFilter(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 217 | expUsers := getTestUsers(1) 218 | 219 | surname := testSurname 220 | expUsers[0].Surname = &surname 221 | 222 | req := "SELECT * FROM `users` " + 223 | "WHERE `users`.`deleted_at` IS NULL AND ((user_surname = ?)) ORDER BY `users`.`id` ASC LIMIT 1" 224 | m.ExpectQuery(fixedFullRe(req)). 225 | WillReturnRows(getRowsForUsers(expUsers)) 226 | 227 | var user test.User 228 | 229 | assert.Nil(t, test.NewUserQuerySet(db).SurnameEq(surname).One(&user)) 230 | assert.Equal(t, expUsers[0], user) 231 | } 232 | 233 | type qsQuerier func(qs test.UserQuerySet) test.UserQuerySet 234 | 235 | type userQueryTestCase struct { 236 | q string 237 | args []driver.Value 238 | qs qsQuerier 239 | } 240 | 241 | func testUserQueryFilters(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 242 | cases := []userQueryTestCase{ 243 | { 244 | q: "((name IN (?)))", 245 | args: []driver.Value{"a"}, 246 | qs: func(qs test.UserQuerySet) test.UserQuerySet { 247 | return qs.NameIn("a") 248 | }, 249 | }, 250 | { 251 | q: "((name IN (?,?)))", 252 | args: []driver.Value{"a", "b"}, 253 | qs: func(qs test.UserQuerySet) test.UserQuerySet { 254 | return qs.NameIn("a", "b") 255 | }, 256 | }, 257 | { 258 | q: "((name NOT IN (?)))", 259 | args: []driver.Value{"a"}, 260 | qs: func(qs test.UserQuerySet) test.UserQuerySet { 261 | return qs.NameNotIn("a") 262 | }, 263 | }, 264 | { 265 | q: "((name NOT IN (?,?)))", 266 | args: []driver.Value{"a", "b"}, 267 | qs: func(qs test.UserQuerySet) test.UserQuerySet { 268 | return qs.NameNotIn("a", "b") 269 | }, 270 | }, 271 | } 272 | for _, c := range cases { 273 | t.Run(c.q, func(t *testing.T) { 274 | runUserQueryFilterSubTest(t, c, m, db) 275 | }) 276 | } 277 | } 278 | 279 | func runUserQueryFilterSubTest(t *testing.T, c userQueryTestCase, m sqlmock.Sqlmock, db *gorm.DB) { 280 | expUsers := getTestUsers(5) 281 | req := "SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND " + c.q 282 | m.ExpectQuery(fixedFullRe(req)).WithArgs(c.args...). 283 | WillReturnRows(getRowsForUsers(expUsers)) 284 | 285 | var users []test.User 286 | 287 | assert.Nil(t, c.qs(test.NewUserQuerySet(db)).All(&users)) 288 | assert.Equal(t, expUsers, users) 289 | } 290 | 291 | func testUserCreateOne(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 292 | u := getUserNoID() 293 | req := "INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`user_surname`,`email`) " + 294 | "VALUES (?,?,?,?,?,?)" 295 | 296 | args := []driver.Value{sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 297 | u.Name, nil, u.Email} 298 | m.ExpectExec(fixedFullRe(req)). 299 | WithArgs(args...). 300 | WillReturnResult(sqlmock.NewResult(2, 1)) 301 | assert.Nil(t, u.Create(db)) 302 | assert.Equal(t, uint(2), u.ID) 303 | } 304 | 305 | func testUserCreateOneWithSurname(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 306 | u := getUserNoID() 307 | req := "INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`user_surname`,`email`) " + 308 | "VALUES (?,?,?,?,?,?)" 309 | 310 | surname := testSurname 311 | u.Surname = &surname 312 | 313 | args := []driver.Value{sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 314 | u.Name, &surname, u.Email} 315 | m.ExpectExec(fixedFullRe(req)). 316 | WithArgs(args...). 317 | WillReturnResult(sqlmock.NewResult(2, 1)) 318 | assert.Nil(t, u.Create(db)) 319 | assert.Equal(t, uint(2), u.ID) 320 | } 321 | 322 | func testUserUpdateByEmail(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 323 | u := getUser() 324 | req := "UPDATE `users` SET `name` = ? WHERE `users`.`deleted_at` IS NULL AND ((email = ?))" 325 | m.ExpectExec(fixedFullRe(req)). 326 | WithArgs(u.Name, u.Email). 327 | WillReturnResult(sqlmock.NewResult(0, 1)) 328 | 329 | err := test.NewUserQuerySet(db). 330 | EmailEq(u.Email). 331 | GetUpdater(). 332 | SetName(u.Name). 333 | Update() 334 | assert.Nil(t, err) 335 | } 336 | 337 | func testUserUpdateFieldsByPK(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 338 | u := getUser() 339 | req := "UPDATE `users` SET `name` = ? WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = ?" 340 | m.ExpectExec(fixedFullRe(req)). 341 | WithArgs(u.Name, u.ID). 342 | WillReturnResult(sqlmock.NewResult(0, 1)) 343 | 344 | assert.Nil(t, u.Update(db, test.UserDBSchema.Name)) 345 | } 346 | 347 | func testUserDeleteByEmail(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 348 | u := getUser() 349 | req := "UPDATE `users` SET `deleted_at`=? WHERE `users`.`deleted_at` IS NULL AND ((email = ?))" 350 | m.ExpectExec(fixedFullRe(req)). 351 | WithArgs(sqlmock.AnyArg(), u.Email). 352 | WillReturnResult(sqlmock.NewResult(0, 1)) 353 | 354 | err := test.NewUserQuerySet(db). 355 | EmailEq(u.Email). 356 | Delete() 357 | assert.Nil(t, err) 358 | } 359 | 360 | func testUserDeleteByPK(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 361 | u := getUser() 362 | req := "UPDATE `users` SET `deleted_at`=? WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = ?" 363 | m.ExpectExec(fixedFullRe(req)). 364 | WithArgs(sqlmock.AnyArg(), u.ID). 365 | WillReturnResult(sqlmock.NewResult(0, 1)) 366 | 367 | assert.Nil(t, u.Delete(db)) 368 | } 369 | 370 | func testUsersDeleteNum(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 371 | usersNum := 2 372 | users := getTestUsers(usersNum) 373 | req := "UPDATE `users` SET `deleted_at`=? WHERE `users`.`deleted_at` IS NULL AND ((email IN (?,?)))" 374 | m.ExpectExec(fixedFullRe(req)). 375 | WithArgs(sqlmock.AnyArg(), users[0].Email, users[1].Email). 376 | WillReturnResult(sqlmock.NewResult(0, int64(usersNum))) 377 | 378 | num, err := test.NewUserQuerySet(db). 379 | EmailIn(users[0].Email, users[1].Email). 380 | DeleteNum() 381 | assert.Nil(t, err) 382 | assert.Equal(t, int64(usersNum), num) 383 | } 384 | 385 | func testUsersDeleteNumUnscoped(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 386 | usersNum := 2 387 | users := getTestUsers(usersNum) 388 | req := "DELETE FROM `users` WHERE (email IN (?,?))" 389 | m.ExpectExec(fixedFullRe(req)). 390 | WithArgs(users[0].Email, users[1].Email). 391 | WillReturnResult(sqlmock.NewResult(0, int64(usersNum))) 392 | 393 | num, err := test.NewUserQuerySet(db). 394 | EmailIn(users[0].Email, users[1].Email). 395 | DeleteNumUnscoped() 396 | assert.Nil(t, err) 397 | assert.Equal(t, int64(usersNum), num) 398 | } 399 | 400 | func testUsersUpdateNum(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 401 | usersNum := 2 402 | users := getTestUsers(usersNum) 403 | req := "UPDATE `users` SET `name` = ? WHERE `users`.`deleted_at` IS NULL AND ((email IN (?,?)))" 404 | m.ExpectExec(fixedFullRe(req)). 405 | WithArgs(sqlmock.AnyArg(), users[0].Email, users[1].Email). 406 | WillReturnResult(sqlmock.NewResult(0, int64(usersNum))) 407 | 408 | num, err := test.NewUserQuerySet(db). 409 | EmailIn(users[0].Email, users[1].Email). 410 | GetUpdater(). 411 | SetName("some name"). 412 | UpdateNum() 413 | assert.Nil(t, err) 414 | assert.Equal(t, int64(usersNum), num) 415 | } 416 | 417 | func testUsersCount(t *testing.T, m sqlmock.Sqlmock, db *gorm.DB) { 418 | expCount := 5 419 | req := "SELECT count(*) FROM `users` WHERE `users`.`deleted_at` IS NULL AND ((name != ?))" 420 | m.ExpectQuery(fixedFullRe(req)).WithArgs(driver.Value("")). 421 | WillReturnRows(getRowWithFields([]driver.Value{expCount})) 422 | 423 | cnt, err := test.NewUserQuerySet(db).NameNe("").Count() 424 | assert.Nil(t, err) 425 | assert.Equal(t, expCount, cnt) 426 | } 427 | 428 | func TestMain(m *testing.M) { 429 | g := Generator{ 430 | StructsParser: &parser.Structs{}, 431 | } 432 | err := g.Generate(context.Background(), "test/models.go", "test/autogenerated_models.go") 433 | if err != nil { 434 | panic(err) 435 | } 436 | 437 | os.Exit(m.Run()) 438 | } 439 | 440 | func BenchmarkHello(b *testing.B) { 441 | g := Generator{ 442 | StructsParser: &parser.Structs{}, 443 | } 444 | 445 | for i := 0; i < b.N; i++ { 446 | err := g.Generate(context.Background(), "test/models.go", "test/autogenerated_models.go") 447 | if err != nil { 448 | b.Fatalf("can't generate querysets: %s", err) 449 | } 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /internal/queryset/generator/template.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "text/template" 5 | ) 6 | 7 | var qsTmpl = template.Must( 8 | template.New("generator"). 9 | Parse(qsCode), 10 | ) 11 | 12 | const qsCode = ` 13 | // ===== BEGIN of all query sets 14 | 15 | {{ range .Configs }} 16 | {{ $ft := printf "%s%s" .StructName "DBSchemaField" }} 17 | // ===== BEGIN of query set {{ .Name }} 18 | 19 | // {{ .Name }} is an queryset type for {{ .StructName }} 20 | type {{ .Name }} struct { 21 | db *gorm.DB 22 | } 23 | 24 | // New{{ .Name }} constructs new {{ .Name }} 25 | func New{{ .Name }}(db *gorm.DB) {{ .Name }} { 26 | return {{ .Name }}{ 27 | db: db.Model(&{{ .StructName }}{}), 28 | } 29 | } 30 | 31 | func (qs {{ .Name }}) w(db *gorm.DB) {{ .Name }} { 32 | return New{{ .Name }}(db) 33 | } 34 | 35 | func (qs {{ .Name }}) Select(fields ...{{ $ft }}) {{ .Name }} { 36 | names := []string{} 37 | for _, f := range fields { 38 | names = append(names, f.String()) 39 | } 40 | 41 | return qs.w(qs.db.Select(strings.Join(names, ","))) 42 | } 43 | 44 | {{ range .Methods }} 45 | {{ .GetDoc .GetMethodName }} 46 | func ({{ .GetReceiverDeclaration }}) {{ .GetMethodName }}({{ .GetArgsDeclaration }}) 47 | {{- .GetReturnValuesDeclaration }} { 48 | {{ .GetBody }} 49 | } 50 | {{ end }} 51 | 52 | // ===== END of query set {{ .Name }} 53 | 54 | // ===== BEGIN of {{ .StructName }} modifiers 55 | 56 | // {{ $ft }} describes database schema field. It requires for method 'Update' 57 | type {{ $ft }} string 58 | 59 | // String method returns string representation of field. 60 | // nolint: dupl 61 | func (f {{ $ft }}) String() string { 62 | return string(f) 63 | } 64 | 65 | // {{ .StructName }}DBSchema stores db field names of {{ .StructName }} 66 | var {{ .StructName }}DBSchema = struct { 67 | {{ range .Fields }} 68 | {{ .Name }} {{ $ft }} 69 | {{- end }} 70 | }{ 71 | {{ range .Fields }} 72 | {{ .Name }}: {{ $ft }}("{{ .DBName }}"), 73 | {{- end }} 74 | } 75 | 76 | // Update updates {{ .StructName }} fields by primary key 77 | // nolint: dupl 78 | func (o *{{ .StructName }}) Update(db *gorm.DB, fields ...{{ $ft }}) error { 79 | dbNameToFieldName := map[string]interface{}{ 80 | {{- range .Fields }} 81 | "{{ .DBName }}": o.{{ .Name }}, 82 | {{- end }} 83 | } 84 | u := map[string]interface{}{} 85 | for _, f := range fields { 86 | fs := f.String() 87 | u[fs] = dbNameToFieldName[fs] 88 | } 89 | if err := db.Model(o).Updates(u).Error; err != nil { 90 | if err == gorm.ErrRecordNotFound { 91 | return err 92 | } 93 | 94 | return fmt.Errorf("can't update {{ .StructName }} %v fields %v: %s", 95 | o, fields, err) 96 | } 97 | 98 | return nil 99 | } 100 | 101 | // {{ .StructName }}Updater is an {{ .StructName }} updates manager 102 | type {{ .StructName }}Updater struct { 103 | fields map[string]interface{} 104 | db *gorm.DB 105 | } 106 | 107 | // New{{ .StructName }}Updater creates new {{ .StructName }} updater 108 | // nolint: dupl 109 | func New{{ .StructName }}Updater(db *gorm.DB) {{ .StructName }}Updater { 110 | return {{ .StructName }}Updater{ 111 | fields: map[string]interface{}{}, 112 | db: db.Model(&{{ .StructName }}{}), 113 | } 114 | } 115 | 116 | // ===== END of {{ .StructName }} modifiers 117 | {{ end }} 118 | 119 | // ===== END of all query sets 120 | ` 121 | -------------------------------------------------------------------------------- /internal/queryset/generator/test/models.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/jirfag/go-queryset/internal/queryset/generator/tmp" 6 | ) 7 | 8 | //go:generate go run ../../../../cmd/goqueryset/goqueryset.go -in models.go 9 | 10 | // User is a usual user 11 | // gen:qs 12 | type User struct { 13 | gorm.Model 14 | 15 | //Posts []Post 16 | Name string 17 | Surname *string `gorm:"column:user_surname"` 18 | Email string 19 | } 20 | 21 | // Blog is a blog 22 | // gen:qs 23 | type Blog struct { 24 | gorm.Model 25 | 26 | Name string `gorm:"column:myname"` 27 | } 28 | 29 | // Post is an article 30 | // gen:qs 31 | type Post struct { 32 | gorm.Model 33 | 34 | Blog *Blog // may be no blog 35 | User User 36 | Title *string 37 | Str tmp.StringDef 38 | Unused int `gorm:"-"` 39 | } 40 | 41 | // String is just for testing purposes 42 | func (p *Post) String() string { 43 | return "" 44 | } 45 | 46 | // SomeMethod is just for testing purposes 47 | func (b *Blog) SomeMethod() string { 48 | if b.ID%2 == 0 { 49 | return "1" 50 | } 51 | 52 | return "0" 53 | } 54 | 55 | // CheckReservedKeywords is a struct for checking 56 | // work of fields with reserved keywords names 57 | // gen:qs 58 | type CheckReservedKeywords struct { 59 | Type string 60 | Struct int 61 | } 62 | -------------------------------------------------------------------------------- /internal/queryset/generator/test/pkgimport/autogenerated_models.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-queryset. DO NOT EDIT. 2 | package models 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/jinzhu/gorm" 10 | forex "github.com/jirfag/go-queryset/internal/queryset/generator/test/pkgimport/forex/v1" 11 | ) 12 | 13 | // ===== BEGIN of all query sets 14 | 15 | // ===== BEGIN of query set ExampleQuerySet 16 | 17 | // ExampleQuerySet is an queryset type for Example 18 | type ExampleQuerySet struct { 19 | db *gorm.DB 20 | } 21 | 22 | // NewExampleQuerySet constructs new ExampleQuerySet 23 | func NewExampleQuerySet(db *gorm.DB) ExampleQuerySet { 24 | return ExampleQuerySet{ 25 | db: db.Model(&Example{}), 26 | } 27 | } 28 | 29 | func (qs ExampleQuerySet) w(db *gorm.DB) ExampleQuerySet { 30 | return NewExampleQuerySet(db) 31 | } 32 | 33 | func (qs ExampleQuerySet) Select(fields ...ExampleDBSchemaField) ExampleQuerySet { 34 | names := []string{} 35 | for _, f := range fields { 36 | names = append(names, f.String()) 37 | } 38 | 39 | return qs.w(qs.db.Select(strings.Join(names, ","))) 40 | } 41 | 42 | // Create is an autogenerated method 43 | // nolint: dupl 44 | func (o *Example) Create(db *gorm.DB) error { 45 | return db.Create(o).Error 46 | } 47 | 48 | // Delete is an autogenerated method 49 | // nolint: dupl 50 | func (o *Example) Delete(db *gorm.DB) error { 51 | return db.Delete(o).Error 52 | } 53 | 54 | // All is an autogenerated method 55 | // nolint: dupl 56 | func (qs ExampleQuerySet) All(ret *[]Example) error { 57 | return qs.db.Find(ret).Error 58 | } 59 | 60 | // Count is an autogenerated method 61 | // nolint: dupl 62 | func (qs ExampleQuerySet) Count() (int, error) { 63 | var count int 64 | err := qs.db.Count(&count).Error 65 | return count, err 66 | } 67 | 68 | // Currency1Eq is an autogenerated method 69 | // nolint: dupl 70 | func (qs ExampleQuerySet) Currency1Eq(currency1 forex.Currency1) ExampleQuerySet { 71 | return qs.w(qs.db.Where("currency1 = ?", currency1)) 72 | } 73 | 74 | // Currency1Gt is an autogenerated method 75 | // nolint: dupl 76 | func (qs ExampleQuerySet) Currency1Gt(currency1 forex.Currency1) ExampleQuerySet { 77 | return qs.w(qs.db.Where("currency1 > ?", currency1)) 78 | } 79 | 80 | // Currency1Gte is an autogenerated method 81 | // nolint: dupl 82 | func (qs ExampleQuerySet) Currency1Gte(currency1 forex.Currency1) ExampleQuerySet { 83 | return qs.w(qs.db.Where("currency1 >= ?", currency1)) 84 | } 85 | 86 | // Currency1In is an autogenerated method 87 | // nolint: dupl 88 | func (qs ExampleQuerySet) Currency1In(currency1 ...forex.Currency1) ExampleQuerySet { 89 | if len(currency1) == 0 { 90 | qs.db.AddError(errors.New("must at least pass one currency1 in Currency1In")) 91 | return qs.w(qs.db) 92 | } 93 | return qs.w(qs.db.Where("currency1 IN (?)", currency1)) 94 | } 95 | 96 | // Currency1Lt is an autogenerated method 97 | // nolint: dupl 98 | func (qs ExampleQuerySet) Currency1Lt(currency1 forex.Currency1) ExampleQuerySet { 99 | return qs.w(qs.db.Where("currency1 < ?", currency1)) 100 | } 101 | 102 | // Currency1Lte is an autogenerated method 103 | // nolint: dupl 104 | func (qs ExampleQuerySet) Currency1Lte(currency1 forex.Currency1) ExampleQuerySet { 105 | return qs.w(qs.db.Where("currency1 <= ?", currency1)) 106 | } 107 | 108 | // Currency1Ne is an autogenerated method 109 | // nolint: dupl 110 | func (qs ExampleQuerySet) Currency1Ne(currency1 forex.Currency1) ExampleQuerySet { 111 | return qs.w(qs.db.Where("currency1 != ?", currency1)) 112 | } 113 | 114 | // Currency1NotIn is an autogenerated method 115 | // nolint: dupl 116 | func (qs ExampleQuerySet) Currency1NotIn(currency1 ...forex.Currency1) ExampleQuerySet { 117 | if len(currency1) == 0 { 118 | qs.db.AddError(errors.New("must at least pass one currency1 in Currency1NotIn")) 119 | return qs.w(qs.db) 120 | } 121 | return qs.w(qs.db.Where("currency1 NOT IN (?)", currency1)) 122 | } 123 | 124 | // Currency2Eq is an autogenerated method 125 | // nolint: dupl 126 | func (qs ExampleQuerySet) Currency2Eq(currency2 forex.Currency2) ExampleQuerySet { 127 | return qs.w(qs.db.Where("currency2 = ?", currency2)) 128 | } 129 | 130 | // Currency2Gt is an autogenerated method 131 | // nolint: dupl 132 | func (qs ExampleQuerySet) Currency2Gt(currency2 forex.Currency2) ExampleQuerySet { 133 | return qs.w(qs.db.Where("currency2 > ?", currency2)) 134 | } 135 | 136 | // Currency2Gte is an autogenerated method 137 | // nolint: dupl 138 | func (qs ExampleQuerySet) Currency2Gte(currency2 forex.Currency2) ExampleQuerySet { 139 | return qs.w(qs.db.Where("currency2 >= ?", currency2)) 140 | } 141 | 142 | // Currency2In is an autogenerated method 143 | // nolint: dupl 144 | func (qs ExampleQuerySet) Currency2In(currency2 ...forex.Currency2) ExampleQuerySet { 145 | if len(currency2) == 0 { 146 | qs.db.AddError(errors.New("must at least pass one currency2 in Currency2In")) 147 | return qs.w(qs.db) 148 | } 149 | return qs.w(qs.db.Where("currency2 IN (?)", currency2)) 150 | } 151 | 152 | // Currency2Like is an autogenerated method 153 | // nolint: dupl 154 | func (qs ExampleQuerySet) Currency2Like(currency2 forex.Currency2) ExampleQuerySet { 155 | return qs.w(qs.db.Where("currency2 LIKE ?", currency2)) 156 | } 157 | 158 | // Currency2Lt is an autogenerated method 159 | // nolint: dupl 160 | func (qs ExampleQuerySet) Currency2Lt(currency2 forex.Currency2) ExampleQuerySet { 161 | return qs.w(qs.db.Where("currency2 < ?", currency2)) 162 | } 163 | 164 | // Currency2Lte is an autogenerated method 165 | // nolint: dupl 166 | func (qs ExampleQuerySet) Currency2Lte(currency2 forex.Currency2) ExampleQuerySet { 167 | return qs.w(qs.db.Where("currency2 <= ?", currency2)) 168 | } 169 | 170 | // Currency2Ne is an autogenerated method 171 | // nolint: dupl 172 | func (qs ExampleQuerySet) Currency2Ne(currency2 forex.Currency2) ExampleQuerySet { 173 | return qs.w(qs.db.Where("currency2 != ?", currency2)) 174 | } 175 | 176 | // Currency2NotIn is an autogenerated method 177 | // nolint: dupl 178 | func (qs ExampleQuerySet) Currency2NotIn(currency2 ...forex.Currency2) ExampleQuerySet { 179 | if len(currency2) == 0 { 180 | qs.db.AddError(errors.New("must at least pass one currency2 in Currency2NotIn")) 181 | return qs.w(qs.db) 182 | } 183 | return qs.w(qs.db.Where("currency2 NOT IN (?)", currency2)) 184 | } 185 | 186 | // Currency2Notlike is an autogenerated method 187 | // nolint: dupl 188 | func (qs ExampleQuerySet) Currency2Notlike(currency2 forex.Currency2) ExampleQuerySet { 189 | return qs.w(qs.db.Where("currency2 NOT LIKE ?", currency2)) 190 | } 191 | 192 | // Currency3Eq is an autogenerated method 193 | // nolint: dupl 194 | func (qs ExampleQuerySet) Currency3Eq(currency3 forex.Currency3) ExampleQuerySet { 195 | return qs.w(qs.db.Where("currency3 = ?", currency3)) 196 | } 197 | 198 | // Currency3Gt is an autogenerated method 199 | // nolint: dupl 200 | func (qs ExampleQuerySet) Currency3Gt(currency3 forex.Currency3) ExampleQuerySet { 201 | return qs.w(qs.db.Where("currency3 > ?", currency3)) 202 | } 203 | 204 | // Currency3Gte is an autogenerated method 205 | // nolint: dupl 206 | func (qs ExampleQuerySet) Currency3Gte(currency3 forex.Currency3) ExampleQuerySet { 207 | return qs.w(qs.db.Where("currency3 >= ?", currency3)) 208 | } 209 | 210 | // Currency3In is an autogenerated method 211 | // nolint: dupl 212 | func (qs ExampleQuerySet) Currency3In(currency3 ...forex.Currency3) ExampleQuerySet { 213 | if len(currency3) == 0 { 214 | qs.db.AddError(errors.New("must at least pass one currency3 in Currency3In")) 215 | return qs.w(qs.db) 216 | } 217 | return qs.w(qs.db.Where("currency3 IN (?)", currency3)) 218 | } 219 | 220 | // Currency3Like is an autogenerated method 221 | // nolint: dupl 222 | func (qs ExampleQuerySet) Currency3Like(currency3 forex.Currency3) ExampleQuerySet { 223 | return qs.w(qs.db.Where("currency3 LIKE ?", currency3)) 224 | } 225 | 226 | // Currency3Lt is an autogenerated method 227 | // nolint: dupl 228 | func (qs ExampleQuerySet) Currency3Lt(currency3 forex.Currency3) ExampleQuerySet { 229 | return qs.w(qs.db.Where("currency3 < ?", currency3)) 230 | } 231 | 232 | // Currency3Lte is an autogenerated method 233 | // nolint: dupl 234 | func (qs ExampleQuerySet) Currency3Lte(currency3 forex.Currency3) ExampleQuerySet { 235 | return qs.w(qs.db.Where("currency3 <= ?", currency3)) 236 | } 237 | 238 | // Currency3Ne is an autogenerated method 239 | // nolint: dupl 240 | func (qs ExampleQuerySet) Currency3Ne(currency3 forex.Currency3) ExampleQuerySet { 241 | return qs.w(qs.db.Where("currency3 != ?", currency3)) 242 | } 243 | 244 | // Currency3NotIn is an autogenerated method 245 | // nolint: dupl 246 | func (qs ExampleQuerySet) Currency3NotIn(currency3 ...forex.Currency3) ExampleQuerySet { 247 | if len(currency3) == 0 { 248 | qs.db.AddError(errors.New("must at least pass one currency3 in Currency3NotIn")) 249 | return qs.w(qs.db) 250 | } 251 | return qs.w(qs.db.Where("currency3 NOT IN (?)", currency3)) 252 | } 253 | 254 | // Currency3Notlike is an autogenerated method 255 | // nolint: dupl 256 | func (qs ExampleQuerySet) Currency3Notlike(currency3 forex.Currency3) ExampleQuerySet { 257 | return qs.w(qs.db.Where("currency3 NOT LIKE ?", currency3)) 258 | } 259 | 260 | // Delete is an autogenerated method 261 | // nolint: dupl 262 | func (qs ExampleQuerySet) Delete() error { 263 | return qs.db.Delete(Example{}).Error 264 | } 265 | 266 | // DeleteNum is an autogenerated method 267 | // nolint: dupl 268 | func (qs ExampleQuerySet) DeleteNum() (int64, error) { 269 | db := qs.db.Delete(Example{}) 270 | return db.RowsAffected, db.Error 271 | } 272 | 273 | // DeleteNumUnscoped is an autogenerated method 274 | // nolint: dupl 275 | func (qs ExampleQuerySet) DeleteNumUnscoped() (int64, error) { 276 | db := qs.db.Unscoped().Delete(Example{}) 277 | return db.RowsAffected, db.Error 278 | } 279 | 280 | // GetDB is an autogenerated method 281 | // nolint: dupl 282 | func (qs ExampleQuerySet) GetDB() *gorm.DB { 283 | return qs.db 284 | } 285 | 286 | // GetUpdater is an autogenerated method 287 | // nolint: dupl 288 | func (qs ExampleQuerySet) GetUpdater() ExampleUpdater { 289 | return NewExampleUpdater(qs.db) 290 | } 291 | 292 | // Limit is an autogenerated method 293 | // nolint: dupl 294 | func (qs ExampleQuerySet) Limit(limit int) ExampleQuerySet { 295 | return qs.w(qs.db.Limit(limit)) 296 | } 297 | 298 | // Offset is an autogenerated method 299 | // nolint: dupl 300 | func (qs ExampleQuerySet) Offset(offset int) ExampleQuerySet { 301 | return qs.w(qs.db.Offset(offset)) 302 | } 303 | 304 | // One is used to retrieve one result. It returns gorm.ErrRecordNotFound 305 | // if nothing was fetched 306 | func (qs ExampleQuerySet) One(ret *Example) error { 307 | return qs.db.First(ret).Error 308 | } 309 | 310 | // OrderAscByCurrency1 is an autogenerated method 311 | // nolint: dupl 312 | func (qs ExampleQuerySet) OrderAscByCurrency1() ExampleQuerySet { 313 | return qs.w(qs.db.Order("currency1 ASC")) 314 | } 315 | 316 | // OrderAscByCurrency2 is an autogenerated method 317 | // nolint: dupl 318 | func (qs ExampleQuerySet) OrderAscByCurrency2() ExampleQuerySet { 319 | return qs.w(qs.db.Order("currency2 ASC")) 320 | } 321 | 322 | // OrderAscByCurrency3 is an autogenerated method 323 | // nolint: dupl 324 | func (qs ExampleQuerySet) OrderAscByCurrency3() ExampleQuerySet { 325 | return qs.w(qs.db.Order("currency3 ASC")) 326 | } 327 | 328 | // OrderAscByPriceID is an autogenerated method 329 | // nolint: dupl 330 | func (qs ExampleQuerySet) OrderAscByPriceID() ExampleQuerySet { 331 | return qs.w(qs.db.Order("price_id ASC")) 332 | } 333 | 334 | // OrderDescByCurrency1 is an autogenerated method 335 | // nolint: dupl 336 | func (qs ExampleQuerySet) OrderDescByCurrency1() ExampleQuerySet { 337 | return qs.w(qs.db.Order("currency1 DESC")) 338 | } 339 | 340 | // OrderDescByCurrency2 is an autogenerated method 341 | // nolint: dupl 342 | func (qs ExampleQuerySet) OrderDescByCurrency2() ExampleQuerySet { 343 | return qs.w(qs.db.Order("currency2 DESC")) 344 | } 345 | 346 | // OrderDescByCurrency3 is an autogenerated method 347 | // nolint: dupl 348 | func (qs ExampleQuerySet) OrderDescByCurrency3() ExampleQuerySet { 349 | return qs.w(qs.db.Order("currency3 DESC")) 350 | } 351 | 352 | // OrderDescByPriceID is an autogenerated method 353 | // nolint: dupl 354 | func (qs ExampleQuerySet) OrderDescByPriceID() ExampleQuerySet { 355 | return qs.w(qs.db.Order("price_id DESC")) 356 | } 357 | 358 | // PriceIDEq is an autogenerated method 359 | // nolint: dupl 360 | func (qs ExampleQuerySet) PriceIDEq(priceID int64) ExampleQuerySet { 361 | return qs.w(qs.db.Where("price_id = ?", priceID)) 362 | } 363 | 364 | // PriceIDGt is an autogenerated method 365 | // nolint: dupl 366 | func (qs ExampleQuerySet) PriceIDGt(priceID int64) ExampleQuerySet { 367 | return qs.w(qs.db.Where("price_id > ?", priceID)) 368 | } 369 | 370 | // PriceIDGte is an autogenerated method 371 | // nolint: dupl 372 | func (qs ExampleQuerySet) PriceIDGte(priceID int64) ExampleQuerySet { 373 | return qs.w(qs.db.Where("price_id >= ?", priceID)) 374 | } 375 | 376 | // PriceIDIn is an autogenerated method 377 | // nolint: dupl 378 | func (qs ExampleQuerySet) PriceIDIn(priceID ...int64) ExampleQuerySet { 379 | if len(priceID) == 0 { 380 | qs.db.AddError(errors.New("must at least pass one priceID in PriceIDIn")) 381 | return qs.w(qs.db) 382 | } 383 | return qs.w(qs.db.Where("price_id IN (?)", priceID)) 384 | } 385 | 386 | // PriceIDLt is an autogenerated method 387 | // nolint: dupl 388 | func (qs ExampleQuerySet) PriceIDLt(priceID int64) ExampleQuerySet { 389 | return qs.w(qs.db.Where("price_id < ?", priceID)) 390 | } 391 | 392 | // PriceIDLte is an autogenerated method 393 | // nolint: dupl 394 | func (qs ExampleQuerySet) PriceIDLte(priceID int64) ExampleQuerySet { 395 | return qs.w(qs.db.Where("price_id <= ?", priceID)) 396 | } 397 | 398 | // PriceIDNe is an autogenerated method 399 | // nolint: dupl 400 | func (qs ExampleQuerySet) PriceIDNe(priceID int64) ExampleQuerySet { 401 | return qs.w(qs.db.Where("price_id != ?", priceID)) 402 | } 403 | 404 | // PriceIDNotIn is an autogenerated method 405 | // nolint: dupl 406 | func (qs ExampleQuerySet) PriceIDNotIn(priceID ...int64) ExampleQuerySet { 407 | if len(priceID) == 0 { 408 | qs.db.AddError(errors.New("must at least pass one priceID in PriceIDNotIn")) 409 | return qs.w(qs.db) 410 | } 411 | return qs.w(qs.db.Where("price_id NOT IN (?)", priceID)) 412 | } 413 | 414 | // SetCurrency1 is an autogenerated method 415 | // nolint: dupl 416 | func (u ExampleUpdater) SetCurrency1(currency1 forex.Currency1) ExampleUpdater { 417 | u.fields[string(ExampleDBSchema.Currency1)] = currency1 418 | return u 419 | } 420 | 421 | // SetCurrency2 is an autogenerated method 422 | // nolint: dupl 423 | func (u ExampleUpdater) SetCurrency2(currency2 forex.Currency2) ExampleUpdater { 424 | u.fields[string(ExampleDBSchema.Currency2)] = currency2 425 | return u 426 | } 427 | 428 | // SetCurrency3 is an autogenerated method 429 | // nolint: dupl 430 | func (u ExampleUpdater) SetCurrency3(currency3 forex.Currency3) ExampleUpdater { 431 | u.fields[string(ExampleDBSchema.Currency3)] = currency3 432 | return u 433 | } 434 | 435 | // SetPriceID is an autogenerated method 436 | // nolint: dupl 437 | func (u ExampleUpdater) SetPriceID(priceID int64) ExampleUpdater { 438 | u.fields[string(ExampleDBSchema.PriceID)] = priceID 439 | return u 440 | } 441 | 442 | // Update is an autogenerated method 443 | // nolint: dupl 444 | func (u ExampleUpdater) Update() error { 445 | return u.db.Updates(u.fields).Error 446 | } 447 | 448 | // UpdateNum is an autogenerated method 449 | // nolint: dupl 450 | func (u ExampleUpdater) UpdateNum() (int64, error) { 451 | db := u.db.Updates(u.fields) 452 | return db.RowsAffected, db.Error 453 | } 454 | 455 | // ===== END of query set ExampleQuerySet 456 | 457 | // ===== BEGIN of Example modifiers 458 | 459 | // ExampleDBSchemaField describes database schema field. It requires for method 'Update' 460 | type ExampleDBSchemaField string 461 | 462 | // String method returns string representation of field. 463 | // nolint: dupl 464 | func (f ExampleDBSchemaField) String() string { 465 | return string(f) 466 | } 467 | 468 | // ExampleDBSchema stores db field names of Example 469 | var ExampleDBSchema = struct { 470 | PriceID ExampleDBSchemaField 471 | Currency1 ExampleDBSchemaField 472 | Currency2 ExampleDBSchemaField 473 | Currency3 ExampleDBSchemaField 474 | }{ 475 | 476 | PriceID: ExampleDBSchemaField("price_id"), 477 | Currency1: ExampleDBSchemaField("currency1"), 478 | Currency2: ExampleDBSchemaField("currency2"), 479 | Currency3: ExampleDBSchemaField("currency3"), 480 | } 481 | 482 | // Update updates Example fields by primary key 483 | // nolint: dupl 484 | func (o *Example) Update(db *gorm.DB, fields ...ExampleDBSchemaField) error { 485 | dbNameToFieldName := map[string]interface{}{ 486 | "price_id": o.PriceID, 487 | "currency1": o.Currency1, 488 | "currency2": o.Currency2, 489 | "currency3": o.Currency3, 490 | } 491 | u := map[string]interface{}{} 492 | for _, f := range fields { 493 | fs := f.String() 494 | u[fs] = dbNameToFieldName[fs] 495 | } 496 | if err := db.Model(o).Updates(u).Error; err != nil { 497 | if err == gorm.ErrRecordNotFound { 498 | return err 499 | } 500 | 501 | return fmt.Errorf("can't update Example %v fields %v: %s", 502 | o, fields, err) 503 | } 504 | 505 | return nil 506 | } 507 | 508 | // ExampleUpdater is an Example updates manager 509 | type ExampleUpdater struct { 510 | fields map[string]interface{} 511 | db *gorm.DB 512 | } 513 | 514 | // NewExampleUpdater creates new Example updater 515 | // nolint: dupl 516 | func NewExampleUpdater(db *gorm.DB) ExampleUpdater { 517 | return ExampleUpdater{ 518 | fields: map[string]interface{}{}, 519 | db: db.Model(&Example{}), 520 | } 521 | } 522 | 523 | // ===== END of Example modifiers 524 | 525 | // ===== END of all query sets 526 | -------------------------------------------------------------------------------- /internal/queryset/generator/test/pkgimport/forex/v1/types.go: -------------------------------------------------------------------------------- 1 | package forex 2 | 3 | // Currency1 is a test type 4 | type Currency1 int 5 | 6 | // Currency2 is a test type 7 | type Currency2 string 8 | 9 | // Currency3 is a test type 10 | type Currency3 Currency2 11 | -------------------------------------------------------------------------------- /internal/queryset/generator/test/pkgimport/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | //go:generate goqueryset -in models.go 4 | 5 | import ( 6 | forex "github.com/jirfag/go-queryset/internal/queryset/generator/test/pkgimport/forex/v1" 7 | forexAlias "github.com/jirfag/go-queryset/internal/queryset/generator/test/pkgimport/forex/v1" 8 | ) 9 | 10 | // Example is a test struct 11 | // gen:qs 12 | type Example struct { 13 | PriceID int64 14 | Currency1 forexAlias.Currency1 15 | Currency2 forex.Currency2 16 | Currency3 forex.Currency3 17 | } 18 | -------------------------------------------------------------------------------- /internal/queryset/generator/tmp/tmp.go: -------------------------------------------------------------------------------- 1 | package tmp 2 | 3 | // StringDef is named type 4 | type StringDef string 5 | -------------------------------------------------------------------------------- /internal/queryset/methods/base.go: -------------------------------------------------------------------------------- 1 | package methods 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Method represents method (func with receiver) 9 | type Method interface { 10 | GetMethodName() string 11 | GetReceiverDeclaration() string 12 | GetArgsDeclaration() string 13 | GetReturnValuesDeclaration() string 14 | GetBody() string 15 | GetDoc(methodName string) string 16 | } 17 | 18 | // receiverMethod 19 | 20 | type receiverMethod struct { 21 | receiverDeclaration string 22 | } 23 | 24 | // GetReceiverDeclaration returns receiver declaration 25 | func (m receiverMethod) GetReceiverDeclaration() string { 26 | return m.receiverDeclaration 27 | } 28 | 29 | func newReceiverMethod(decl string) *receiverMethod { 30 | return &receiverMethod{ 31 | receiverDeclaration: decl, 32 | } 33 | } 34 | 35 | // structMethod 36 | 37 | type structMethod struct { 38 | *receiverMethod 39 | structTypeName string 40 | } 41 | 42 | func newStructMethod(receiverArgName, structTypeName string) structMethod { 43 | receiverDecl := fmt.Sprintf("%s %s", receiverArgName, structTypeName) 44 | return structMethod{ 45 | receiverMethod: newReceiverMethod(receiverDecl), 46 | structTypeName: structTypeName, 47 | } 48 | } 49 | 50 | // namedMethod 51 | 52 | type namedMethod struct { 53 | name string 54 | doc string 55 | } 56 | 57 | func newNamedMethod(name string) namedMethod { 58 | return namedMethod{ 59 | name: name, 60 | } 61 | } 62 | 63 | // GetMethodName returns name of method 64 | func (m namedMethod) GetMethodName() string { 65 | return m.name 66 | } 67 | 68 | // GetDoc returns default doc 69 | func (m namedMethod) GetDoc(methodName string) string { 70 | if m.doc != "" { 71 | return m.doc 72 | } 73 | 74 | return fmt.Sprintf(`// %s is an autogenerated method 75 | // nolint: dupl`, methodName) 76 | } 77 | 78 | func (m *namedMethod) setDoc(doc string) { 79 | m.doc = doc 80 | } 81 | 82 | // oneArgMethod 83 | 84 | type oneArgMethod struct { 85 | argName string 86 | argTypeName string 87 | } 88 | 89 | func (m oneArgMethod) getArgName() string { 90 | return m.argName 91 | } 92 | 93 | // GetArgsDeclaration returns declaration of arguments list for func decl 94 | func (m oneArgMethod) GetArgsDeclaration() string { 95 | return fmt.Sprintf("%s %s", m.getArgName(), m.argTypeName) 96 | } 97 | 98 | func newOneArgMethod(argName, argTypeName string) oneArgMethod { 99 | return oneArgMethod{ 100 | argName: argName, 101 | argTypeName: argTypeName, 102 | } 103 | } 104 | 105 | type nArgsMethod struct { 106 | args []oneArgMethod 107 | } 108 | 109 | func (m nArgsMethod) getArgName(n int) string { 110 | return m.args[n].getArgName() 111 | } 112 | 113 | // GetArgsDeclaration returns declaration of arguments list for func decl 114 | func (m nArgsMethod) GetArgsDeclaration() string { 115 | decls := []string{} 116 | for _, a := range m.args { 117 | decls = append(decls, a.GetArgsDeclaration()) 118 | } 119 | return strings.Join(decls, ",") 120 | } 121 | 122 | func newNArgsMethod(args ...oneArgMethod) nArgsMethod { 123 | return nArgsMethod{ 124 | args: args, 125 | } 126 | } 127 | 128 | // noArgsMethod 129 | 130 | type noArgsMethod struct{} 131 | 132 | // GetArgsDeclaration returns declaration of arguments list for func decl 133 | func (m noArgsMethod) GetArgsDeclaration() string { 134 | return "" 135 | } 136 | 137 | // errorRetMethod 138 | 139 | type errorRetMethod struct{} 140 | 141 | func (m errorRetMethod) GetReturnValuesDeclaration() string { 142 | return "error" 143 | } 144 | 145 | // constBodyMethod 146 | 147 | type constBodyMethod struct { 148 | body string 149 | } 150 | 151 | // GetBody returns const body 152 | func (m constBodyMethod) GetBody() string { 153 | return m.body 154 | } 155 | 156 | func newConstBodyMethod(format string, args ...interface{}) constBodyMethod { 157 | return constBodyMethod{ 158 | body: fmt.Sprintf(format, args...), 159 | } 160 | } 161 | 162 | // constRetMethod 163 | 164 | type constRetMethod struct { 165 | ret string 166 | } 167 | 168 | func (m constRetMethod) GetReturnValuesDeclaration() string { 169 | return m.ret 170 | } 171 | 172 | func newConstRetMethod(ret string) constRetMethod { 173 | return constRetMethod{ 174 | ret: ret, 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /internal/queryset/methods/context.go: -------------------------------------------------------------------------------- 1 | package methods 2 | 3 | import ( 4 | "github.com/jirfag/go-queryset/internal/parser" 5 | "github.com/jirfag/go-queryset/internal/queryset/field" 6 | ) 7 | 8 | type QsStructContext struct { 9 | s parser.ParsedStruct 10 | } 11 | 12 | func NewQsStructContext(s parser.ParsedStruct) QsStructContext { 13 | return QsStructContext{ 14 | s: s, 15 | } 16 | } 17 | 18 | func (ctx QsStructContext) qsTypeName() string { 19 | return ctx.s.TypeName + "QuerySet" 20 | } 21 | 22 | func (ctx QsStructContext) FieldCtx(f field.Info) QsFieldContext { 23 | return QsFieldContext{ 24 | f: f, 25 | QsStructContext: ctx, 26 | } 27 | } 28 | 29 | // QsFieldContext is a query set field context 30 | type QsFieldContext struct { 31 | f field.Info 32 | operationName string 33 | 34 | QsStructContext 35 | } 36 | 37 | func (ctx QsFieldContext) fieldName() string { 38 | return ctx.f.Name 39 | } 40 | 41 | func (ctx QsFieldContext) fieldDBName() string { 42 | return ctx.f.DBName 43 | } 44 | 45 | func (ctx QsFieldContext) fieldTypeName() string { 46 | return ctx.f.TypeName 47 | } 48 | 49 | func (ctx QsFieldContext) onFieldMethod() onFieldMethod { 50 | return newOnFieldMethod(ctx.operationName, ctx.fieldName()) 51 | } 52 | 53 | func (ctx QsFieldContext) chainedQuerySetMethod() chainedQuerySetMethod { 54 | return newChainedQuerySetMethod(ctx.qsTypeName()) 55 | } 56 | 57 | // WithOperationName return ctx with changed operation's name 58 | func (ctx QsFieldContext) WithOperationName(operationName string) QsFieldContext { 59 | ctx.operationName = operationName 60 | return ctx 61 | } 62 | -------------------------------------------------------------------------------- /internal/queryset/methods/field_utils.go: -------------------------------------------------------------------------------- 1 | package methods 2 | 3 | import "go/token" 4 | 5 | // commonInitialisms is a set of common initialisms. 6 | // Only add entries that are highly unlikely to be non-initialisms. 7 | // For instance, "ID" is fine (Freudian code is rare), but "AND" is not. 8 | // XXX: copy-pasted from golint. 9 | var commonInitialisms = map[string]bool{ 10 | "ACL": true, 11 | "API": true, 12 | "ASCII": true, 13 | "CPU": true, 14 | "CSS": true, 15 | "DNS": true, 16 | "EOF": true, 17 | "GUID": true, 18 | "HTML": true, 19 | "HTTP": true, 20 | "HTTPS": true, 21 | "ID": true, 22 | "IP": true, 23 | "JSON": true, 24 | "LHS": true, 25 | "QPS": true, 26 | "RAM": true, 27 | "RHS": true, 28 | "RPC": true, 29 | "SLA": true, 30 | "SMTP": true, 31 | "SQL": true, 32 | "SSH": true, 33 | "TCP": true, 34 | "TLS": true, 35 | "TTL": true, 36 | "UDP": true, 37 | "UI": true, 38 | "UID": true, 39 | "UUID": true, 40 | "URI": true, 41 | "URL": true, 42 | "UTF8": true, 43 | "VM": true, 44 | "XML": true, 45 | "XMPP": true, 46 | "XSRF": true, 47 | "XSS": true, 48 | } 49 | 50 | func fieldNameToArgName(fieldName string) string { 51 | if commonInitialisms[fieldName] { 52 | return fieldName 53 | } 54 | 55 | argName := LowercaseFirstRune(fieldName) 56 | if token.Lookup(argName).IsKeyword() { 57 | return argName + "Value" 58 | } 59 | return argName 60 | } 61 | -------------------------------------------------------------------------------- /internal/queryset/methods/field_utils_test.go: -------------------------------------------------------------------------------- 1 | package methods 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFieldNameToArgName(t *testing.T) { 10 | t.Parallel() 11 | cases := []struct{ in, out string }{ 12 | {"field", "field"}, 13 | {"Field", "field"}, 14 | {"MyField", "myField"}, 15 | {"Type", "typeValue"}, // reserved keyword 16 | {"ID", "ID"}, 17 | {"SOMENAME", "sOMENAME"}, // TODO 18 | } 19 | 20 | for _, c := range cases { 21 | assert.Equal(t, c.out, fieldNameToArgName(c.in)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/queryset/methods/fields.go: -------------------------------------------------------------------------------- 1 | package methods 2 | 3 | import "strings" 4 | 5 | // onFieldMethod 6 | 7 | type onFieldMethod struct { 8 | namedMethod 9 | fieldName string 10 | isFieldNameFirst bool 11 | } 12 | 13 | func (m *onFieldMethod) setFieldNameFirst(isFieldNameFirst bool) { 14 | m.isFieldNameFirst = isFieldNameFirst 15 | } 16 | 17 | // GetMethodName returns name of method 18 | func (m onFieldMethod) GetMethodName() string { 19 | args := []string{m.fieldName, strings.Title(m.name)} 20 | if !m.isFieldNameFirst { 21 | args[0], args[1] = args[1], args[0] 22 | } 23 | return args[0] + args[1] 24 | } 25 | 26 | func newOnFieldMethod(name, fieldName string) onFieldMethod { 27 | return onFieldMethod{ 28 | namedMethod: newNamedMethod(name), 29 | fieldName: fieldName, 30 | isFieldNameFirst: true, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/queryset/methods/gorm.go: -------------------------------------------------------------------------------- 1 | package methods 2 | 3 | import "fmt" 4 | 5 | func wrapToGormScope(code string) string { 6 | const tmpl = `return qs.w(%s)` 7 | return fmt.Sprintf(tmpl, code) 8 | } 9 | 10 | // callGormMethod 11 | type callGormMethod struct { 12 | gormMethodName string 13 | gormMethodArgs string 14 | gormVarName string 15 | } 16 | 17 | func (m *callGormMethod) setGormMethodName(name string) { 18 | m.gormMethodName = name 19 | } 20 | 21 | func (m callGormMethod) getGormMethodName() string { 22 | return m.gormMethodName 23 | } 24 | 25 | func (m callGormMethod) getGormMethodArgs() string { 26 | return m.gormMethodArgs 27 | } 28 | 29 | func (m *callGormMethod) setGormMethodArgs(args string) { 30 | m.gormMethodArgs = args 31 | } 32 | 33 | func (m callGormMethod) getGormVarName() string { 34 | return m.gormVarName 35 | } 36 | 37 | func (m callGormMethod) GetBody() string { 38 | return fmt.Sprintf("%s.%s(%s)", 39 | m.getGormVarName(), m.getGormMethodName(), m.getGormMethodArgs()) 40 | } 41 | 42 | func newCallGormMethod(name, args, varName string) callGormMethod { 43 | return callGormMethod{ 44 | gormMethodName: name, 45 | gormMethodArgs: args, 46 | gormVarName: varName, 47 | } 48 | } 49 | 50 | // dbArgMethod 51 | 52 | type dbArgMethod struct { 53 | oneArgMethod 54 | } 55 | 56 | func newDbArgMethod() dbArgMethod { 57 | return dbArgMethod{ 58 | oneArgMethod: newOneArgMethod("db", "*gorm.DB"), 59 | } 60 | } 61 | 62 | // gormErroredMethod 63 | type gormErroredMethod struct { 64 | errorRetMethod 65 | callGormMethod 66 | } 67 | 68 | // GetBody returns body of method 69 | func (m gormErroredMethod) GetBody() string { 70 | return "return " + m.callGormMethod.GetBody() + ".Error" 71 | } 72 | 73 | func newGormErroredMethod(name, args, varName string) gormErroredMethod { 74 | return gormErroredMethod{ 75 | callGormMethod: newCallGormMethod(name, args, varName), 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/queryset/methods/queryset.go: -------------------------------------------------------------------------------- 1 | package methods 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | const qsReceiverName = "qs" 11 | const qsDbName = qsReceiverName + ".db" 12 | 13 | // retQuerySetMethod 14 | 15 | type retQuerySetMethod struct { 16 | qsTypeName string 17 | } 18 | 19 | // GetReturnValuesDeclaration gets return values declaration 20 | func (m retQuerySetMethod) GetReturnValuesDeclaration() string { 21 | return m.qsTypeName 22 | } 23 | 24 | func newRetQuerySetMethod(qsTypeName string) retQuerySetMethod { 25 | return retQuerySetMethod{ 26 | qsTypeName: qsTypeName, 27 | } 28 | } 29 | 30 | type GetDBMethod struct { 31 | namedMethod 32 | baseQuerySetMethod 33 | constRetMethod 34 | noArgsMethod 35 | constBodyMethod 36 | } 37 | 38 | func NewGetDBMethod(qsTypeName string) GetDBMethod { 39 | return GetDBMethod{ 40 | namedMethod: newNamedMethod("GetDB"), 41 | baseQuerySetMethod: newBaseQuerySetMethod(qsTypeName), 42 | constRetMethod: newConstRetMethod("*gorm.DB"), 43 | constBodyMethod: newConstBodyMethod("return qs.db"), 44 | } 45 | } 46 | 47 | // baseQuerySetMethod 48 | 49 | type baseQuerySetMethod struct { 50 | structMethod 51 | } 52 | 53 | func newBaseQuerySetMethod(qsTypeName string) baseQuerySetMethod { 54 | return baseQuerySetMethod{ 55 | structMethod: newStructMethod(qsReceiverName, qsTypeName), 56 | } 57 | } 58 | 59 | // chainedQuerySetMethod 60 | type chainedQuerySetMethod struct { 61 | baseQuerySetMethod 62 | retQuerySetMethod 63 | } 64 | 65 | func newChainedQuerySetMethod(qsTypeName string) chainedQuerySetMethod { 66 | return chainedQuerySetMethod{ 67 | baseQuerySetMethod: newBaseQuerySetMethod(qsTypeName), 68 | retQuerySetMethod: newRetQuerySetMethod(qsTypeName), 69 | } 70 | } 71 | 72 | // FieldOperationNoArgsMethod is for unary operations: preload, orderby, etc 73 | type FieldOperationNoArgsMethod struct { 74 | qsCallGormMethod 75 | onFieldMethod 76 | noArgsMethod 77 | chainedQuerySetMethod 78 | } 79 | 80 | func newFieldOperationNoArgsMethod(ctx QsFieldContext, transformFieldName bool) FieldOperationNoArgsMethod { 81 | 82 | gormArgName := ctx.fieldName() 83 | if transformFieldName { 84 | gormArgName = ctx.fieldDBName() 85 | } 86 | 87 | r := FieldOperationNoArgsMethod{ 88 | onFieldMethod: ctx.onFieldMethod(), 89 | qsCallGormMethod: newQsCallGormMethod(ctx.operationName, `"%s"`, gormArgName), 90 | chainedQuerySetMethod: ctx.chainedQuerySetMethod(), 91 | } 92 | r.setFieldNameFirst(false) // UserPreload -> PreloadUser 93 | return r 94 | } 95 | 96 | // LowercaseFirstRune lowercases first rune of string 97 | func LowercaseFirstRune(s string) string { 98 | r := []rune(s) 99 | r[0] = unicode.ToLower(r[0]) 100 | return string(r) 101 | } 102 | 103 | // StructOperationOneArgMethod is for struct operations with one arg 104 | type StructOperationOneArgMethod struct { 105 | namedMethod 106 | chainedQuerySetMethod 107 | oneArgMethod 108 | qsCallGormMethod 109 | } 110 | 111 | func newStructOperationOneArgMethod(name, argTypeName, qsTypeName string) StructOperationOneArgMethod { 112 | argName := strings.ToLower(name) 113 | return StructOperationOneArgMethod{ 114 | namedMethod: newNamedMethod(name), 115 | chainedQuerySetMethod: newChainedQuerySetMethod(qsTypeName), 116 | oneArgMethod: newOneArgMethod(argName, argTypeName), 117 | qsCallGormMethod: newQsCallGormMethod(name, argName), 118 | } 119 | } 120 | 121 | type qsCallGormMethod struct { 122 | callGormMethod 123 | } 124 | 125 | func (m qsCallGormMethod) GetBody() string { 126 | return wrapToGormScope(m.callGormMethod.GetBody()) 127 | } 128 | 129 | func newQsCallGormMethod(name, argsFmt string, argsArgs ...interface{}) qsCallGormMethod { 130 | return qsCallGormMethod{ 131 | callGormMethod: newCallGormMethod(name, fmt.Sprintf(argsFmt, argsArgs...), qsDbName), 132 | } 133 | } 134 | 135 | // BinaryFilterMethod is a binary filter method 136 | type BinaryFilterMethod struct { 137 | chainedQuerySetMethod 138 | onFieldMethod 139 | oneArgMethod 140 | qsCallGormMethod 141 | } 142 | 143 | // NewBinaryFilterMethod create new binary filter method 144 | func NewBinaryFilterMethod(ctx QsFieldContext) BinaryFilterMethod { 145 | argName := fieldNameToArgName(ctx.fieldName()) 146 | return BinaryFilterMethod{ 147 | onFieldMethod: ctx.onFieldMethod(), 148 | oneArgMethod: newOneArgMethod(argName, ctx.fieldTypeName()), 149 | chainedQuerySetMethod: ctx.chainedQuerySetMethod(), 150 | qsCallGormMethod: newQsCallGormMethod("Where", "\"%s %s\", %s", 151 | ctx.fieldDBName(), getWhereCondition(ctx.operationName), argName), 152 | } 153 | } 154 | 155 | // InFilterMethod filters with IN condition 156 | type InFilterMethod struct { 157 | chainedQuerySetMethod 158 | onFieldMethod 159 | nArgsMethod 160 | qsCallGormMethod 161 | } 162 | 163 | func (m InFilterMethod) GetBody() string { 164 | tmpl := `if len(%s) == 0 { 165 | qs.db.AddError(errors.New("must at least pass one %s in %s")) 166 | return qs.w(qs.db) 167 | } 168 | ` 169 | return fmt.Sprintf(tmpl, m.getArgName(0), m.getArgName(0), m.GetMethodName()) + m.qsCallGormMethod.GetBody() 170 | } 171 | 172 | func newInFilterMethodImpl(ctx QsFieldContext, operationName, sql string) InFilterMethod { 173 | ctx = ctx.WithOperationName(operationName) 174 | argName := fieldNameToArgName(ctx.fieldName()) 175 | args := newNArgsMethod( 176 | newOneArgMethod(argName, "..."+ctx.fieldTypeName()), 177 | ) 178 | return InFilterMethod{ 179 | onFieldMethod: ctx.onFieldMethod(), 180 | nArgsMethod: args, 181 | chainedQuerySetMethod: ctx.chainedQuerySetMethod(), 182 | qsCallGormMethod: newQsCallGormMethod("Where", "\"%s %s (?)\", %s", 183 | ctx.fieldDBName(), sql, argName), 184 | } 185 | } 186 | 187 | // NewInFilterMethod create new IN filter method 188 | func NewInFilterMethod(ctx QsFieldContext) InFilterMethod { 189 | return newInFilterMethodImpl(ctx, "In", "IN") 190 | } 191 | 192 | // NewNotInFilterMethod create new NOT IN filter method 193 | func NewNotInFilterMethod(ctx QsFieldContext) InFilterMethod { 194 | return newInFilterMethodImpl(ctx, "NotIn", "NOT IN") 195 | } 196 | 197 | func getWhereCondition(name string) string { 198 | nameToOp := map[string]string{ 199 | "eq": "=", 200 | "ne": "!=", 201 | "lt": "<", 202 | "lte": "<=", 203 | "gt": ">", 204 | "gte": ">=", 205 | "like": "LIKE", 206 | "notlike": "NOT LIKE", 207 | } 208 | op := nameToOp[name] 209 | if op == "" { 210 | log.Fatalf("no operation for filter %q", name) 211 | } 212 | 213 | return fmt.Sprintf("%s ?", op) 214 | } 215 | 216 | // UnaryFilterMethod represents unary filter 217 | type UnaryFilterMethod struct { 218 | onFieldMethod 219 | noArgsMethod 220 | chainedQuerySetMethod 221 | qsCallGormMethod 222 | } 223 | 224 | func newUnaryFilterMethod(ctx QsFieldContext, op string) UnaryFilterMethod { 225 | r := UnaryFilterMethod{ 226 | onFieldMethod: ctx.onFieldMethod(), 227 | qsCallGormMethod: newQsCallGormMethod("Where", `"%s %s"`, 228 | ctx.fieldDBName(), op), 229 | chainedQuerySetMethod: ctx.chainedQuerySetMethod(), 230 | } 231 | return r 232 | } 233 | 234 | // unaryFilerMethod 235 | 236 | // SelectMethod is a select field (all, one, etc) 237 | type SelectMethod struct { 238 | namedMethod 239 | oneArgMethod 240 | baseQuerySetMethod 241 | gormErroredMethod 242 | } 243 | 244 | func newSelectMethod(name, gormName, argTypeName, qsTypeName string) SelectMethod { 245 | return SelectMethod{ 246 | namedMethod: newNamedMethod(name), 247 | baseQuerySetMethod: newBaseQuerySetMethod(qsTypeName), 248 | oneArgMethod: newOneArgMethod("ret", argTypeName), 249 | gormErroredMethod: newGormErroredMethod(gormName, "ret", qsDbName), 250 | } 251 | } 252 | 253 | // GetUpdaterMethod creates GetUpdater method 254 | type GetUpdaterMethod struct { 255 | baseQuerySetMethod 256 | namedMethod 257 | noArgsMethod 258 | constRetMethod 259 | constBodyMethod 260 | } 261 | 262 | // NewGetUpdaterMethod creates GetUpdaterMethod 263 | func NewGetUpdaterMethod(qsTypeName, updaterTypeMethod string) GetUpdaterMethod { 264 | return GetUpdaterMethod{ 265 | baseQuerySetMethod: newBaseQuerySetMethod(qsTypeName), 266 | namedMethod: newNamedMethod("GetUpdater"), 267 | constRetMethod: newConstRetMethod(updaterTypeMethod), 268 | constBodyMethod: newConstBodyMethod("return New%s(%s)", updaterTypeMethod, qsDbName), 269 | } 270 | } 271 | 272 | // DeleteMethod creates Delete method 273 | type DeleteMethod struct { 274 | baseQuerySetMethod 275 | 276 | namedMethod 277 | noArgsMethod 278 | 279 | gormErroredMethod 280 | } 281 | 282 | // NewDeleteMethod creates Delete method 283 | func NewDeleteMethod(qsTypeName, structTypeName string) DeleteMethod { 284 | return DeleteMethod{ 285 | 286 | namedMethod: newNamedMethod("Delete"), 287 | baseQuerySetMethod: newBaseQuerySetMethod(qsTypeName), 288 | gormErroredMethod: newGormErroredMethod("Delete", structTypeName+"{}", qsDbName), 289 | } 290 | } 291 | 292 | // DeleteNumMethod creates DeleteNum method 293 | type DeleteNumMethod struct { 294 | namedMethod 295 | baseQuerySetMethod 296 | 297 | noArgsMethod 298 | constBodyMethod 299 | constRetMethod 300 | } 301 | 302 | // NewDeleteNumMethod delete row count 303 | func NewDeleteNumMethod(qsTypeName, structTypeName string) DeleteNumMethod { 304 | return DeleteNumMethod{ 305 | namedMethod: newNamedMethod("DeleteNum"), 306 | baseQuerySetMethod: newBaseQuerySetMethod(qsTypeName), 307 | constRetMethod: newConstRetMethod("(int64, error)"), 308 | constBodyMethod: newConstBodyMethod( 309 | strings.Join([]string{ 310 | "db := qs.db.Delete(" + structTypeName + "{}" + ")", 311 | "return db.RowsAffected, db.Error", 312 | }, "\n"), 313 | ), 314 | } 315 | } 316 | 317 | // DeleteNumUnscopedMethod creates DeleteNumUnscoped method for performing hard deletes 318 | type DeleteNumUnscopedMethod struct { 319 | namedMethod 320 | baseQuerySetMethod 321 | 322 | noArgsMethod 323 | constBodyMethod 324 | constRetMethod 325 | } 326 | 327 | // NewDeleteNumUnscopedMethod delete row count for hard deletes 328 | func NewDeleteNumUnscopedMethod(qsTypeName, structTypeName string) DeleteNumUnscopedMethod { 329 | return DeleteNumUnscopedMethod{ 330 | namedMethod: newNamedMethod("DeleteNumUnscoped"), 331 | baseQuerySetMethod: newBaseQuerySetMethod(qsTypeName), 332 | constRetMethod: newConstRetMethod("(int64, error)"), 333 | constBodyMethod: newConstBodyMethod( 334 | strings.Join([]string{ 335 | "db := qs.db.Unscoped().Delete(" + structTypeName + "{}" + ")", 336 | "return db.RowsAffected, db.Error", 337 | }, "\n"), 338 | ), 339 | } 340 | } 341 | 342 | // CountMethod creates Count method 343 | type CountMethod struct { 344 | baseQuerySetMethod 345 | namedMethod 346 | noArgsMethod 347 | constRetMethod 348 | constBodyMethod 349 | } 350 | 351 | // NewCountMethod returns new CountMethod 352 | func NewCountMethod(qsTypeName string) CountMethod { 353 | return CountMethod{ 354 | baseQuerySetMethod: newBaseQuerySetMethod(qsTypeName), 355 | namedMethod: newNamedMethod("Count"), 356 | constRetMethod: newConstRetMethod("(int, error)"), 357 | constBodyMethod: newConstBodyMethod(`var count int 358 | err := %s.Count(&count).Error 359 | return count, err`, qsDbName), 360 | } 361 | } 362 | 363 | // Concrete methods 364 | 365 | // NewPreloadMethod creates new Preload method 366 | func NewPreloadMethod(ctx QsFieldContext) FieldOperationNoArgsMethod { 367 | r := newFieldOperationNoArgsMethod(ctx.WithOperationName("Preload"), false) 368 | return r 369 | } 370 | 371 | // NewOrderAscByMethod creates new OrderBy method ascending 372 | func NewOrderAscByMethod(ctx QsFieldContext) FieldOperationNoArgsMethod { 373 | r := newFieldOperationNoArgsMethod(ctx.WithOperationName("OrderAscBy"), true) 374 | r.setGormMethodName("Order") 375 | r.setGormMethodArgs(fmt.Sprintf(`"%s ASC"`, ctx.fieldDBName())) 376 | return r 377 | } 378 | 379 | // NewOrderDescByMethod creates new OrderBy method descending 380 | func NewOrderDescByMethod(ctx QsFieldContext) FieldOperationNoArgsMethod { 381 | r := newFieldOperationNoArgsMethod(ctx.WithOperationName("OrderDescBy"), true) 382 | r.setGormMethodName("Order") 383 | r.setGormMethodArgs(fmt.Sprintf(`"%s DESC"`, ctx.fieldDBName())) 384 | return r 385 | } 386 | 387 | // NewLimitMethod creates Limit method 388 | func NewLimitMethod(qsTypeName string) StructOperationOneArgMethod { 389 | return newStructOperationOneArgMethod("Limit", "int", qsTypeName) 390 | } 391 | 392 | // NewOffsetMethod creates Offset method 393 | func NewOffsetMethod(qsTypeName string) StructOperationOneArgMethod { 394 | return newStructOperationOneArgMethod("Offset", "int", qsTypeName) 395 | } 396 | 397 | // NewAllMethod creates All method 398 | func NewAllMethod(structName, qsTypeName string) SelectMethod { 399 | return newSelectMethod("All", "Find", fmt.Sprintf("*[]%s", structName), qsTypeName) 400 | } 401 | 402 | // NewOneMethod creates One method 403 | func NewOneMethod(structName, qsTypeName string) SelectMethod { 404 | r := newSelectMethod("One", "First", fmt.Sprintf("*%s", structName), qsTypeName) 405 | const doc = `// One is used to retrieve one result. It returns gorm.ErrRecordNotFound 406 | // if nothing was fetched` 407 | r.setDoc(doc) 408 | return r 409 | } 410 | 411 | // NewIsNullMethod create IsNull method 412 | func NewIsNullMethod(ctx QsFieldContext) UnaryFilterMethod { 413 | return newUnaryFilterMethod(ctx.WithOperationName("IsNull"), "IS NULL") 414 | } 415 | 416 | // NewIsNotNullMethod create IsNotNull method 417 | func NewIsNotNullMethod(ctx QsFieldContext) UnaryFilterMethod { 418 | return newUnaryFilterMethod(ctx.WithOperationName("IsNotNull"), "IS NOT NULL") 419 | } 420 | -------------------------------------------------------------------------------- /internal/queryset/methods/struct.go: -------------------------------------------------------------------------------- 1 | package methods 2 | 3 | // StructModifierMethod represents method, modifying current struct 4 | type StructModifierMethod struct { 5 | namedMethod 6 | structMethod 7 | dbArgMethod 8 | gormErroredMethod 9 | } 10 | 11 | // NewStructModifierMethod create StructModifierMethod method 12 | func NewStructModifierMethod(name, structTypeName string) StructModifierMethod { 13 | r := StructModifierMethod{ 14 | namedMethod: newNamedMethod(name), 15 | dbArgMethod: newDbArgMethod(), 16 | structMethod: newStructMethod("o", "*"+structTypeName), 17 | gormErroredMethod: newGormErroredMethod(name, "o", "db"), 18 | } 19 | return r 20 | } 21 | -------------------------------------------------------------------------------- /internal/queryset/methods/updater.go: -------------------------------------------------------------------------------- 1 | package methods 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // baseUpdaterMethod 8 | type baseUpdaterMethod struct { 9 | structMethod 10 | updaterTypeName string 11 | } 12 | 13 | func newBaseUpdaterMethod(updaterTypeName string) baseUpdaterMethod { 14 | return baseUpdaterMethod{ 15 | updaterTypeName: updaterTypeName, 16 | structMethod: newStructMethod("u", updaterTypeName), 17 | } 18 | } 19 | 20 | // UpdaterSetMethod generates Set method 21 | type UpdaterSetMethod struct { 22 | onFieldMethod 23 | oneArgMethod 24 | baseUpdaterMethod 25 | constRetMethod 26 | constBodyMethod 27 | 28 | dbSchemaTypeName string 29 | } 30 | 31 | // NewUpdaterSetMethod create new SetField method 32 | func NewUpdaterSetMethod(fieldName, fieldTypeName, 33 | updaterTypeName, dbSchemaTypeName string) UpdaterSetMethod { 34 | 35 | argName := fieldNameToArgName(fieldName) 36 | cbm := newConstBodyMethod( 37 | `u.fields[string(%s.%s)] = %s 38 | return u`, 39 | dbSchemaTypeName, 40 | fieldName, 41 | argName) 42 | 43 | r := UpdaterSetMethod{ 44 | onFieldMethod: newOnFieldMethod("Set", fieldName), 45 | oneArgMethod: newOneArgMethod(argName, fieldTypeName), 46 | baseUpdaterMethod: newBaseUpdaterMethod(updaterTypeName), 47 | constRetMethod: newConstRetMethod(updaterTypeName), 48 | constBodyMethod: cbm, 49 | dbSchemaTypeName: dbSchemaTypeName, 50 | } 51 | r.setFieldNameFirst(false) 52 | return r 53 | } 54 | 55 | // UpdaterUpdateMethod creates Update method 56 | type UpdaterUpdateMethod struct { 57 | namedMethod 58 | baseUpdaterMethod 59 | noArgsMethod 60 | errorRetMethod 61 | constBodyMethod 62 | } 63 | 64 | // NewUpdaterUpdateMethod create new Update method 65 | func NewUpdaterUpdateMethod(updaterTypeName string) UpdaterUpdateMethod { 66 | return UpdaterUpdateMethod{ 67 | namedMethod: newNamedMethod("Update"), 68 | baseUpdaterMethod: newBaseUpdaterMethod(updaterTypeName), 69 | constBodyMethod: newConstBodyMethod("return u.db.Updates(u.fields).Error"), 70 | } 71 | } 72 | 73 | // UpdaterUpdateNumMethod describes UpdateNum method 74 | type UpdaterUpdateNumMethod struct { 75 | namedMethod 76 | baseUpdaterMethod 77 | noArgsMethod 78 | constRetMethod 79 | constBodyMethod 80 | } 81 | 82 | // NewUpdaterUpdateNumMethod creates new UpdateNum method 83 | func NewUpdaterUpdateNumMethod(updaterTypeName string) UpdaterUpdateNumMethod { 84 | return UpdaterUpdateNumMethod{ 85 | namedMethod: newNamedMethod("UpdateNum"), 86 | baseUpdaterMethod: newBaseUpdaterMethod(updaterTypeName), 87 | constRetMethod: newConstRetMethod("(int64, error)"), 88 | constBodyMethod: newConstBodyMethod( 89 | strings.Join([]string{ 90 | "db := u.db.Updates(u.fields)", 91 | "return db.RowsAffected, db.Error", 92 | }, "\n"), 93 | ), 94 | } 95 | } 96 | --------------------------------------------------------------------------------