├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── main.yaml ├── .gitignore ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── common.go ├── dialect ├── dialect.go ├── logger.go └── sql │ ├── builder.go │ ├── builder_ext.go │ ├── builder_test.go │ ├── driver.go │ ├── driver_ext.go │ ├── entity.go │ ├── mutation.go │ ├── mutation_test.go │ ├── pagination.go │ ├── pagination_test.go │ ├── query.go │ ├── query_test.go │ ├── scan │ ├── alloc.go │ ├── alloc_test.go │ ├── column.go │ ├── iter.go │ ├── iter_test.go │ ├── mock │ │ └── model.go │ ├── query.go │ ├── query_test.go │ ├── scan.go │ ├── scan_test.go │ ├── suite_test.go │ ├── value.go │ └── value_test.go │ └── suite_test.go ├── error.go ├── error_test.go ├── example ├── README.md ├── cmd │ └── example-api │ │ └── main.go └── database │ ├── ent │ └── schema.go │ ├── gateway.go │ ├── migration │ ├── 00060524000000_setup.sql │ ├── 20180406190015_users.sql │ └── content.go │ ├── repository.go │ └── routine │ ├── content.go │ └── routine.sql ├── example_test.go ├── gateway.go ├── gateway_engine.go ├── gateway_option.go ├── gateway_test.go ├── gateway_tx.go ├── go.mod ├── go.sum ├── media └── img │ └── logo.png └── suite_test.go /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@phogolabs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please explain the changes you made here. 4 | 5 | ### Checklist 6 | 7 | - [ ] Code compiles correctly 8 | - [ ] Created tests that fail without the change (if possible) 9 | - [ ] All tests passing 10 | - [ ] Extended the README.md / documentation, if necessary 11 | - [ ] Added me / the copyright holder to the CONTRIBUTORS file 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: github.com/onsi/ginkgo 10 | versions: 11 | - 1.14.2 12 | - 1.15.0 13 | - 1.15.1 14 | - 1.15.2 15 | - 1.16.0 16 | - dependency-name: github.com/a8m/rql 17 | versions: 18 | - 1.3.0 19 | - dependency-name: github.com/onsi/gomega 20 | versions: 21 | - 1.10.4 22 | - 1.10.5 23 | - dependency-name: github.com/jmoiron/sqlx 24 | versions: 25 | - 1.3.1 26 | - dependency-name: github.com/mattn/go-sqlite3 27 | versions: 28 | - 1.14.6 29 | - dependency-name: github.com/lib/pq 30 | versions: 31 | - 1.9.0 32 | - dependency-name: github.com/apex/log 33 | versions: 34 | - 1.9.0 35 | - dependency-name: github.com/facebookincubator/ent 36 | versions: 37 | - 0.3.0 38 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | ignore-tags: 8 | - 'v*' 9 | pull_request: 10 | 11 | jobs: 12 | pipeline: 13 | name: pipeline 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v2 18 | - name: Set up Golang 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: '1.19.x' 22 | - name: Run Tests 23 | run: go test -race -coverprofile=coverage.txt -covermode=atomic 24 | env: 25 | TEST_DB_URL: postgres://postgres:postgres@postgres:${{ job.services.postgres.ports[5432] }}/postgres?sslmode=disable 26 | - name: Upload tests coverage to codeconv.io 27 | uses: codecov/codecov-action@v1 28 | with: 29 | token: ${{ secrets.CODECOV_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .DS_Store 15 | .glide/ 16 | *.coverprofile 17 | vendor/ 18 | 19 | # Ignore SQLite database 20 | *.db 21 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the list of Parcell contributors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history in source control. 6 | 7 | Svetlin Ralchev - https://github.com/svett 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Phogo Labs Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ORM 2 | 3 | [![Documentation][godoc-img]][godoc-url] 4 | ![License][license-img] 5 | [![Build Status][action-img]][action-url] 6 | [![Coverage][codecov-img]][codecov-url] 7 | [![Go Report Card][report-img]][report-url] 8 | 9 | The package facilitates execution of SQL scripts generated by 10 | [prana][prana-url]. Also it provides a query builder and object relation mapper. 11 | Note that it is in BETA. We may introduce breaking changes until we reach 12 | version 1.0. 13 | 14 | [![ORM][orm-img]][orm-url] 15 | 16 | ## Installation 17 | 18 | ```console 19 | $ go get -u github.com/phogolabs/orm 20 | ``` 21 | 22 | ## Getting Started 23 | 24 | Let's first import all required packages: 25 | 26 | ```golang 27 | import ( 28 | "github.com/phogolabs/orm" 29 | ) 30 | ``` 31 | 32 | and then establish the connection: 33 | 34 | ```golang 35 | gateway, err := orm.Open("sqlite3", "example.db", orm.WithRoutine(routine.Statement)) 36 | if err != nil { 37 | return err 38 | } 39 | ``` 40 | 41 | ## SQL Migrations 42 | 43 | You can execute the migration generated by [prana][prana-url]. For that you have 44 | to use either [embed][embed-url] package or [os][os-url] package. 45 | 46 | ```golang 47 | if err := gateway.Migrate(resource); err != nil { 48 | return err 49 | } 50 | ``` 51 | 52 | ## SQL Queries 53 | 54 | The package provides a way to work with embeddable SQL scripts. It understands predefined files with [SQL Scripts](https://github.com/phogolabs/prana#sql-scripts-and-commands). 55 | 56 | It executes them as standard SQL queries. Let's define a SQL routines named `insert-user` and `select-all-users`: 57 | 58 | ``` 59 | -- name: insert-user 60 | INSERT INTO users (id, first_name, last_name) 61 | VALUES (:id, :first_name, :last_name); 62 | 63 | -- named: select-all-users 64 | SELECT * FROM users; 65 | ``` 66 | 67 | Then you can execute the desired script by just passing its name: 68 | 69 | ```golang 70 | routine := orm.Routine("select-all-users") 71 | // execute the routine 72 | _, err = gateway.All(context.TODO(), routine, &users) 73 | ``` 74 | 75 | ```golang 76 | routine := orm.Routine("insert-user", &user) 77 | // execute the routine 78 | _, err = gateway.Exec(context.TODO(), routine) 79 | ``` 80 | 81 | Also you can execute raw SQL Scripts from your code: 82 | 83 | ```golang 84 | query := orm.Query("SELECT * FROM users WHERE id = ?", 5432) 85 | // fetch the records as a slice of users 86 | rows, err := gateway.Only(context.TODO(), query, &user) 87 | ``` 88 | 89 | ## Example 90 | 91 | You can check our [Getting Started Example](/example). 92 | 93 | For more information, how you can change the default behavior you can read the 94 | help documentation by executing: 95 | 96 | ## Contributing 97 | 98 | We are open for any contributions. Just fork the 99 | [project](https://github.com/phogolabs/orm). 100 | 101 | *logo made by [Free Pik][logo-author-url]* 102 | 103 | [report-img]: https://goreportcard.com/badge/github.com/phogolabs/orm 104 | [report-url]: https://goreportcard.com/report/github.com/phogolabs/orm 105 | [logo-author-url]: https://www.freepik.com/free-photos-vectors/tree 106 | [logo-license]: http://creativecommons.org/licenses/by/3.0/ 107 | [orm-url]: https://github.com/phogolabs/orm 108 | [orm-img]: media/img/logo.png 109 | [codecov-url]: https://codecov.io/gh/phogolabs/orm 110 | [codecov-img]: https://codecov.io/gh/phogolabs/orm/branch/master/graph/badge.svg 111 | [action-img]: https://github.com/phogolabs/orm/workflows/main/badge.svg 112 | [action-url]: https://github.com/phogolabs/orm/actions 113 | [orm-url]: https://github.com/phogolabs/orm 114 | [godoc-url]: https://godoc.org/github.com/phogolabs/orm 115 | [godoc-img]: https://godoc.org/github.com/phogolabs/orm?status.svg 116 | [license-img]: https://img.shields.io/badge/license-MIT-blue.svg 117 | [software-license-url]: LICENSE 118 | [embed-url]: https://golang.org/pkg/embed 119 | [os-url]: https://golang.org/pkg/os 120 | [prana-url]: https://github.com/phogolabs/prana 121 | [sqlx-url]: https://github.com/jmoiron/sqlx 122 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/phogolabs/orm/dialect/sql" 7 | ) 8 | 9 | var ( 10 | // SQL represents an SQL command 11 | Query = sql.Query 12 | // Routine represents an SQL routine 13 | Routine = sql.Routine 14 | ) 15 | 16 | var ( 17 | // NewDelete creates a Mutation that deletes the entity with given primary key. 18 | NewDelete = sql.NewDelete 19 | 20 | // NewInsert creates a Mutation that will save the entity src into the db 21 | NewInsert = sql.NewInsert 22 | 23 | // NewUpdate creates a Mutation that updates the entity into the db 24 | NewUpdate = sql.NewUpdate 25 | ) 26 | 27 | type ( 28 | // A NamedArg is a named argument. NamedArg values may be used as 29 | // arguments to Query or Exec and bind to the corresponding named 30 | // parameter in the SQL statement. 31 | NamedArg = sql.NamedArg 32 | 33 | // NameQuery is a named query that uses named arguments 34 | NamedQuery = sql.NamedQuery 35 | 36 | // RoutineQuery represents a named routine 37 | RoutineQuery = sql.RoutineQuery 38 | 39 | // FileSystem represents the SQL filesystem 40 | FileSystem = sql.FileSystem 41 | ) 42 | 43 | // Querier executes the commands 44 | type Querier interface { 45 | // All executes the query and returns a list of entities. 46 | All(ctx context.Context, q sql.Querier, v interface{}) error 47 | 48 | // Only returns the only entity in the query, returns an error if not 49 | // exactly one entity was returned. 50 | Only(ctx context.Context, q sql.Querier, v interface{}) error 51 | 52 | // First returns the first entity in the query. Returns *NotFoundError 53 | // when no records were found. 54 | First(ctx context.Context, q sql.Querier, v interface{}) error 55 | 56 | // Query executes a query that returns rows, typically a SELECT in SQL. 57 | // It scans the result into the pointer v. In SQL, you it's usually *sql.Rows. 58 | Query(ctx context.Context, q sql.Querier) (*sql.Rows, error) 59 | 60 | // Exec executes a query that doesn't return rows. For example, in SQL, INSERT 61 | // or UPDATE. It scans the result into the pointer v. In SQL, you it's usually 62 | // sql.Result. 63 | Exec(ctx context.Context, q sql.Querier) (sql.Result, error) 64 | } 65 | -------------------------------------------------------------------------------- /dialect/dialect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-present Facebook Inc. All rights reserved. 2 | // This source code is licensed under the Apache 2.0 license found 3 | // in the LICENSE file in the root directory of this source tree. 4 | 5 | package dialect 6 | 7 | import ( 8 | "context" 9 | "database/sql/driver" 10 | "io/fs" 11 | ) 12 | 13 | // Dialect names for external usage. 14 | const ( 15 | MySQL = "mysql" 16 | SQLite = "sqlite3" 17 | Postgres = "postgres" 18 | ) 19 | 20 | // FileSystem represents a file sytem storage 21 | type FileSystem = fs.FS 22 | 23 | // ExecQuerier wraps the standard Exec and Query methods. 24 | type ExecQuerier interface { 25 | Execer 26 | Querier 27 | } 28 | 29 | // Execer wraps the exec database operations. 30 | type Execer interface { 31 | // Exec executes a query that doesn't return rows. For example, in SQL, INSERT or UPDATE. 32 | // It scans the result into the pointer v. In SQL, you it's usually sql.Result. 33 | Exec(ctx context.Context, query string, args, v interface{}) error 34 | } 35 | 36 | // Querier wraps the query database operations. 37 | type Querier interface { 38 | // Query executes a query that returns rows, typically a SELECT in SQL. 39 | // It scans the result into the pointer v. In SQL, you it's usually *sql.Rows. 40 | Query(ctx context.Context, query string, args, v interface{}) error 41 | } 42 | 43 | // Driver is the interface that wraps all necessary operations for ent clients. 44 | type Driver interface { 45 | // ExecQuerier inheritance 46 | ExecQuerier 47 | // Dialect returns the dialect name of the driver. 48 | Dialect() string 49 | // Tx starts and returns a new transaction. 50 | // The provided context is used until the transaction is committed or rolled back. 51 | Tx(context.Context) (Tx, error) 52 | // Migrate runs the migrations 53 | Migrate(FileSystem) error 54 | // Ping sends a ping request 55 | Ping(context.Context) error 56 | // Close closes the underlying connection. 57 | Close() error 58 | } 59 | 60 | // Tx wraps the Exec and Query operations in transaction. 61 | type Tx interface { 62 | // ExecQuerier inheritance 63 | ExecQuerier 64 | // actual transaction 65 | driver.Tx 66 | } 67 | 68 | type nopTx struct { 69 | Driver 70 | } 71 | 72 | func (nopTx) Commit() error { return nil } 73 | func (nopTx) Rollback() error { return nil } 74 | 75 | // NopTx returns a Tx with a no-op Commit / Rollback methods wrapping 76 | // the provided Driver d. 77 | func NopTx(d Driver) Tx { 78 | return nopTx{d} 79 | } 80 | -------------------------------------------------------------------------------- /dialect/logger.go: -------------------------------------------------------------------------------- 1 | package dialect 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "time" 8 | 9 | "github.com/phogolabs/log" 10 | ) 11 | 12 | // Logger represents a logger 13 | type Logger = log.Logger 14 | 15 | // LoggerDriver is a driver that logs all driver operations. 16 | type LoggerDriver struct { 17 | Driver 18 | logger Logger 19 | } 20 | 21 | // Log gets a driver and an optional logging function, and returns 22 | // a new debugged-driver that prints all outgoing operations. 23 | func Log(d Driver, logger Logger) Driver { 24 | return &LoggerDriver{d, logger} 25 | } 26 | 27 | // Exec logs its params and calls the underlying driver Exec method. 28 | func (d *LoggerDriver) Exec(ctx context.Context, query string, args, v interface{}) error { 29 | var ( 30 | start = time.Now() 31 | logger = d.logger 32 | ) 33 | 34 | err := d.Driver.Exec(ctx, query, args, v) 35 | 36 | logger = logger.WithField("sql.query", query) 37 | logger = logger.WithField("sql.param", args) 38 | logger = logger.WithField("sql.duration", time.Since(start).String()) 39 | 40 | if err != nil { 41 | logger.WithError(err).Errorf("query.exec fail") 42 | return err 43 | } 44 | 45 | logger.Infof("query.exec success") 46 | return nil 47 | } 48 | 49 | // Query logs its params and calls the underlying driver Query method. 50 | func (d *LoggerDriver) Query(ctx context.Context, query string, args, v interface{}) error { 51 | var ( 52 | start = time.Now() 53 | logger = d.logger 54 | ) 55 | 56 | err := d.Driver.Query(ctx, query, args, v) 57 | 58 | logger = logger.WithField("sql.query", query) 59 | logger = logger.WithField("sql.param", args) 60 | logger = logger.WithField("sql.duration", time.Since(start).String()) 61 | 62 | if err != nil { 63 | logger.WithError(err).Errorf("query.exec fail") 64 | return err 65 | } 66 | 67 | logger.Infof("query.exec success") 68 | return nil 69 | } 70 | 71 | // Tx adds an log-id for the transaction and calls the underlying driver Tx command. 72 | func (d *LoggerDriver) Tx(ctx context.Context) (Tx, error) { 73 | logger := d.logger.WithField("sql.tx", d.random()) 74 | 75 | tx, err := d.Driver.Tx(ctx) 76 | if err != nil { 77 | logger.WithError(err).Errorf("tx.start fail") 78 | return nil, err 79 | } 80 | 81 | logger.Infof("tx.start success") 82 | return &LoggerTx{tx, logger, ctx}, nil 83 | } 84 | 85 | func (d *LoggerDriver) random() string { 86 | var ( 87 | size = 12 88 | buffer = make([]byte, size) 89 | ) 90 | 91 | rand.Read(buffer) 92 | // encode the buffer as a string 93 | vector := base64.StdEncoding.EncodeToString(buffer) 94 | // Base 64 can be longer than len 95 | return vector[:size] 96 | } 97 | 98 | // LoggerTx is a transaction implementation that logs all transaction operations. 99 | type LoggerTx struct { 100 | Tx // underlying transaction. 101 | logger Logger // log function. defaults to fmt.Println. 102 | ctx context.Context // underlying transaction context. 103 | } 104 | 105 | // Exec logs its params and calls the underlying transaction Exec method. 106 | func (d *LoggerTx) Exec(ctx context.Context, query string, args, v interface{}) error { 107 | var ( 108 | start = time.Now() 109 | logger = d.logger 110 | ) 111 | 112 | err := d.Tx.Exec(ctx, query, args, v) 113 | 114 | logger = logger.WithField("sql.query", query) 115 | logger = logger.WithField("sql.param", args) 116 | logger = logger.WithField("sql.duration", time.Since(start).String()) 117 | 118 | if err != nil { 119 | logger.WithError(err).Errorf("query.exec fail") 120 | return err 121 | } 122 | 123 | logger.Infof("query.exec success") 124 | return nil 125 | } 126 | 127 | // Query logs its params and calls the underlying transaction Query method. 128 | func (d *LoggerTx) Query(ctx context.Context, query string, args, v interface{}) error { 129 | var ( 130 | start = time.Now() 131 | logger = d.logger 132 | ) 133 | 134 | err := d.Tx.Query(ctx, query, args, v) 135 | 136 | logger = logger.WithField("sql.query", query) 137 | logger = logger.WithField("sql.param", args) 138 | logger = logger.WithField("sql.duration", time.Since(start).String()) 139 | 140 | if err != nil { 141 | logger.WithError(err).Errorf("query.exec fail") 142 | return err 143 | } 144 | 145 | logger.Infof("query.exec success") 146 | return nil 147 | } 148 | 149 | // Commit logs this step and calls the underlying transaction Commit method. 150 | func (d *LoggerTx) Commit() error { 151 | var ( 152 | logger = d.logger 153 | err = d.Tx.Commit() 154 | ) 155 | 156 | if err != nil { 157 | logger = d.logger.WithError(err) 158 | logger.Errorf("tx.commit fail") 159 | return err 160 | } 161 | 162 | logger.Infof("tx.commit success") 163 | return nil 164 | } 165 | 166 | // Rollback logs this step and calls the underlying transaction Rollback method. 167 | func (d *LoggerTx) Rollback() error { 168 | var ( 169 | logger = d.logger 170 | err = d.Tx.Rollback() 171 | ) 172 | 173 | if err != nil { 174 | logger = d.logger.WithError(err) 175 | logger.Errorf("tx.rollback fail") 176 | return err 177 | } 178 | 179 | logger.Infof("tx.rollback success") 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /dialect/sql/builder_ext.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Name returns the name 10 | func (s *Selector) Name() string { 11 | switch view := s.from.(type) { 12 | case *WithBuilder: 13 | return view.name 14 | case *SelectTable: 15 | return view.name 16 | case *Selector: 17 | return view.as 18 | default: 19 | panic(fmt.Sprintf("unhandled TableView type %T", s.from)) 20 | } 21 | } 22 | 23 | // TableViews returns the table views 24 | func (s *Selector) TableViews() []TableView { 25 | views := []TableView{} 26 | views = append(views, s.from) 27 | 28 | for _, item := range s.joins { 29 | views = append(views, item.table) 30 | } 31 | 32 | return views 33 | } 34 | 35 | // Name returns the name 36 | func (s *SelectTable) Name() string { 37 | return s.name 38 | } 39 | 40 | // SelectorFunc represents a selector function 41 | type SelectorFunc func(*Selector) 42 | 43 | // Selection represents a selection 44 | type Selection struct { 45 | fn SelectorFunc 46 | } 47 | 48 | // SelectionBy returns a new selection 49 | func SelectionBy(fn SelectorFunc) *Selection { 50 | return &Selection{fn: fn} 51 | } 52 | 53 | // SelectionWith creates a selection 54 | func SelectionWith(selection ...*Selection) *Selection { 55 | fn := func(selector *Selector) { 56 | for _, element := range selection { 57 | // execute the selection 58 | element.Select(selector) 59 | } 60 | } 61 | 62 | return &Selection{fn: fn} 63 | } 64 | 65 | // Select selects the selector 66 | func (s *Selection) Select(selector *Selector) { 67 | if s != nil { 68 | s.fn(selector) 69 | } 70 | } 71 | 72 | // Projection represents an order 73 | type Projection struct { 74 | columns []*ProjectColumn 75 | dialect string 76 | total int 77 | } 78 | 79 | // ProjectBy returns the view 80 | func (s *SelectTable) ProjectBy(clause *Projection) *Projection { 81 | output := &Projection{ 82 | dialect: clause.dialect, 83 | total: clause.total, 84 | } 85 | 86 | for _, element := range clause.columns { 87 | column := element.Clone() 88 | column.name = s.C(column.name) 89 | // append the column 90 | output.columns = append(output.columns, column) 91 | } 92 | 93 | return output 94 | } 95 | 96 | // ProjectBy returns the view 97 | func (w *WithBuilder) ProjectBy(clause *Projection) *Projection { 98 | output := &Projection{ 99 | dialect: clause.dialect, 100 | total: clause.total, 101 | } 102 | 103 | for _, element := range clause.columns { 104 | column := element.Clone() 105 | column.name = w.C(column.name) 106 | // append the column 107 | output.columns = append(output.columns, column) 108 | } 109 | 110 | return output 111 | } 112 | 113 | // ProjectBy returns a seelect by 114 | func ProjectBy(clauses ...string) *Projection { 115 | selection := &Projection{} 116 | 117 | for _, clause := range clauses { 118 | exprs := strings.Split(clause, ",") 119 | // parse each element 120 | for _, expr := range exprs { 121 | if column := ProjectColumnBy(expr); column != nil { 122 | // add the expression the collection 123 | selection.columns = append(selection.columns, column) 124 | } 125 | } 126 | } 127 | 128 | return selection 129 | } 130 | 131 | // SetDialect sets the dialect 132 | func (x *Projection) SetDialect(dialect string) { 133 | x.dialect = dialect 134 | } 135 | 136 | // Dialect returns the dialect 137 | func (x *Projection) Dialect() string { 138 | return x.dialect 139 | } 140 | 141 | // Total returns the total value 142 | func (x *Projection) Total() int { 143 | return x.total 144 | } 145 | 146 | // SetTotal sets the totla arguments 147 | func (x *Projection) SetTotal(total int) { 148 | x.total = total 149 | } 150 | 151 | // Query returns the order by clause 152 | func (x *Projection) Query() (string, []interface{}) { 153 | queriers := []Querier{} 154 | 155 | for _, column := range x.columns { 156 | column.SetDialect(x.dialect) 157 | column.SetTotal(x.total) 158 | // add the column to the collection 159 | queriers = append(queriers, column) 160 | } 161 | 162 | b := &Builder{dialect: x.dialect, total: x.total} 163 | b.JoinComma(queriers...) 164 | return b.String(), nil 165 | } 166 | 167 | // As creates an alias 168 | func (x *Projection) As(prefix string, extra ...string) *Projection { 169 | for _, column := range x.columns { 170 | name := column.name 171 | // prepare the name 172 | name = strings.Replace(name, `"`, "", -1) 173 | name = strings.Replace(name, "`", "", -1) 174 | // split the name 175 | parts := strings.Split(name, ".") 176 | index := len(parts) - 1 177 | // find the relative name 178 | name = parts[index] 179 | 180 | head := []string{} 181 | head = append(head, prefix) 182 | head = append(head, extra...) 183 | head = append(head, name) 184 | // prepare 185 | name = strings.Join(head, ".") 186 | // alias 187 | column.As(name) 188 | } 189 | 190 | return x 191 | } 192 | 193 | // ProjectColumn is a column selector. 194 | type ProjectColumn struct { 195 | dialect string 196 | alias string 197 | name string 198 | total int 199 | } 200 | 201 | // ProjectColumnBy returns a new column selector. 202 | func ProjectColumnBy(name string) *ProjectColumn { 203 | return &ProjectColumn{ 204 | name: name, 205 | } 206 | } 207 | 208 | // Clone clones the column 209 | func (x *ProjectColumn) Clone() *ProjectColumn { 210 | return &ProjectColumn{ 211 | dialect: x.dialect, 212 | alias: x.alias, 213 | name: x.name, 214 | total: x.total, 215 | } 216 | } 217 | 218 | // SetDialect sets the dialect 219 | func (x *ProjectColumn) SetDialect(dialect string) { 220 | x.dialect = dialect 221 | } 222 | 223 | // Dialect returns the dialect 224 | func (x *ProjectColumn) Dialect() string { 225 | return x.dialect 226 | } 227 | 228 | // Total returns the total value 229 | func (x *ProjectColumn) Total() int { 230 | return x.total 231 | } 232 | 233 | // SetTotal sets the totla arguments 234 | func (x *ProjectColumn) SetTotal(total int) { 235 | x.total = total 236 | } 237 | 238 | // As adds the AS clause to the column selector. 239 | func (x *ProjectColumn) As(alias string) *ProjectColumn { 240 | x.alias = alias 241 | return x 242 | } 243 | 244 | // Query returns the select column clause 245 | func (x *ProjectColumn) Query() (string, []interface{}) { 246 | b := &Builder{dialect: x.dialect} 247 | b.Ident(x.name) 248 | if x.alias != "" { 249 | b.Pad().WriteString("AS") 250 | b.Pad().Ident(x.alias) 251 | } 252 | return b.String(), nil 253 | } 254 | 255 | // Order represents an order 256 | type Order struct { 257 | columns []*OrderColumn 258 | dialect string 259 | total int 260 | } 261 | 262 | // Columns returns the column nams 263 | func (x *Order) Columns() []string { 264 | columns := []string{} 265 | // extract the column names 266 | for _, elem := range x.columns { 267 | columns = append(columns, elem.column) 268 | } 269 | 270 | return columns 271 | } 272 | 273 | // String returns the order as string 274 | func (x *Order) String() string { 275 | data, err := x.MarshalText() 276 | if err != nil { 277 | panic(err) 278 | } 279 | return string(data) 280 | } 281 | 282 | // MarshalText encodes the receiver into UTF-8-encoded text and returns the result. 283 | func (x *Order) MarshalText() ([]byte, error) { 284 | buffer := &bytes.Buffer{} 285 | 286 | for _, column := range x.columns { 287 | data, err := column.MarshalText() 288 | if err != nil { 289 | return nil, err 290 | } 291 | 292 | if buffer.Len() > 0 { 293 | buffer.WriteString(", ") 294 | } 295 | 296 | if _, err := buffer.Write(data); err != nil { 297 | return nil, err 298 | } 299 | } 300 | 301 | return buffer.Bytes(), nil 302 | } 303 | 304 | // UnmarshalText must be able to decode the form generated by MarshalText. 305 | // UnmarshalText must copy the text if it wishes to retain the text after 306 | // returning. 307 | func (x *Order) UnmarshalText(data []byte) error { 308 | separator := []byte(",") 309 | // partition the text 310 | clauses := bytes.Split(data, separator) 311 | // parse each element 312 | for _, expr := range clauses { 313 | column := &OrderColumn{} 314 | // unmarsha the column 315 | if err := column.UnmarshalText(expr); err != nil { 316 | return err 317 | } 318 | // add the expression the collection 319 | x.columns = append(x.columns, column) 320 | } 321 | 322 | return nil 323 | } 324 | 325 | // Order returns the order 326 | func (s *Selector) Order() *Order { 327 | columns := []*OrderColumn{} 328 | 329 | for _, elem := range s.order { 330 | switch x := elem.(type) { 331 | case string: 332 | columns = append(columns, OrderColumnBy(x)) 333 | case *Order: 334 | columns = append(columns, x.columns...) 335 | case *OrderColumn: 336 | columns = append(columns, x) 337 | default: 338 | panic("sql: order column ambiguous") 339 | } 340 | } 341 | 342 | return &Order{ 343 | columns: columns, 344 | total: s.total, 345 | dialect: s.dialect, 346 | } 347 | } 348 | 349 | // OrderBy returns the order 350 | func (s *SelectTable) OrderBy(clause *Order) *Order { 351 | order := &Order{ 352 | dialect: clause.dialect, 353 | total: clause.total, 354 | } 355 | 356 | for _, element := range clause.columns { 357 | element = element.Clone() 358 | element.column = s.C(element.column) 359 | // append the column 360 | order.columns = append(order.columns, element) 361 | } 362 | 363 | return order 364 | } 365 | 366 | // OrderBy returns the order 367 | func (w *WithBuilder) OrderBy(clause *Order) *Order { 368 | order := &Order{ 369 | dialect: clause.dialect, 370 | total: clause.total, 371 | } 372 | 373 | for _, element := range clause.columns { 374 | element = element.Clone() 375 | element.column = w.C(element.column) 376 | // append the column 377 | order.columns = append(order.columns, element) 378 | } 379 | 380 | return order 381 | } 382 | 383 | // OrderBy returns an order by 384 | func OrderBy(clauses ...string) *Order { 385 | order := &Order{} 386 | 387 | for _, clause := range clauses { 388 | data := []byte(clause) 389 | order.UnmarshalText(data) 390 | } 391 | 392 | return order 393 | } 394 | 395 | // OrderWith returns an order from 396 | func OrderWith(orders ...*Order) *Order { 397 | order := &Order{} 398 | 399 | for _, element := range orders { 400 | order.dialect = element.dialect 401 | order.total += element.total 402 | 403 | for _, clause := range element.columns { 404 | order.columns = append(order.columns, clause.Clone()) 405 | } 406 | } 407 | 408 | return order 409 | } 410 | 411 | // Prepend prepends the clauses 412 | func (x *Order) Prepend(clauses ...string) *Order { 413 | head := OrderBy(clauses...) 414 | // return a new order 415 | return OrderWith(head, x) 416 | } 417 | 418 | // Map maps the order based on the provided mapping 419 | func (x *Order) Map(mapping map[string]string) *Order { 420 | order := &Order{ 421 | dialect: x.dialect, 422 | total: x.total, 423 | } 424 | 425 | for _, element := range x.columns { 426 | name := element.column 427 | // prepare the name 428 | name = strings.Replace(name, `"`, "", -1) 429 | name = strings.Replace(name, "`", "", -1) 430 | // split the name 431 | parts := strings.Split(name, ".") 432 | index := len(parts) - 1 433 | // find the relative name 434 | name = parts[index] 435 | 436 | if nick, ok := mapping[element.column]; ok { 437 | element = element.Clone() 438 | element.column = strings.Replace(element.column, name, nick, -1) 439 | // append the order 440 | order.columns = append(order.columns, element) 441 | } 442 | } 443 | 444 | return order 445 | } 446 | 447 | // SetDialect sets the dialect 448 | func (x *Order) SetDialect(dialect string) { 449 | x.dialect = dialect 450 | } 451 | 452 | // Dialect returns the dialect 453 | func (x *Order) Dialect() string { 454 | return x.dialect 455 | } 456 | 457 | // Total returns the total value 458 | func (x *Order) Total() int { 459 | return x.total 460 | } 461 | 462 | // SetTotal sets the totla arguments 463 | func (x *Order) SetTotal(total int) { 464 | x.total = total 465 | } 466 | 467 | // Query returns the order by clause 468 | func (x *Order) Query() (string, []interface{}) { 469 | queriers := []Querier{} 470 | 471 | for _, column := range x.columns { 472 | column.SetDialect(x.dialect) 473 | column.SetTotal(x.total) 474 | // add the column to the collection 475 | queriers = append(queriers, column) 476 | } 477 | 478 | b := &Builder{dialect: x.dialect, total: x.total} 479 | b.JoinComma(queriers...) 480 | return b.String(), nil 481 | } 482 | 483 | // OrderColumn represents an order by column 484 | type OrderColumn struct { 485 | dialect string 486 | column string 487 | order string 488 | total int 489 | err error 490 | } 491 | 492 | // MarshalText encodes the receiver into UTF-8-encoded text and returns the result. 493 | func (x *OrderColumn) MarshalText() ([]byte, error) { 494 | if x.err == nil { 495 | text := fmt.Sprintf("%s %s", x.column, x.order) 496 | return []byte(text), nil 497 | } 498 | 499 | return nil, x.err 500 | } 501 | 502 | // UnmarshalText must be able to decode the form generated by MarshalText. 503 | // UnmarshalText must copy the text if it wishes to retain the text after 504 | // returning. 505 | func (x *OrderColumn) UnmarshalText(data []byte) error { 506 | data = bytes.ToLower(data) 507 | data = bytes.TrimSpace(data) 508 | 509 | var ( 510 | name string 511 | order string 512 | ) 513 | 514 | elem := bytes.Fields(data) 515 | // prepare the name 516 | if len(elem) > 0 { 517 | name = string(elem[0]) 518 | // name = strings.Replace(name, "`", "", -1) 519 | } 520 | 521 | switch len(elem) { 522 | case 0: 523 | return nil 524 | case 1: 525 | order = string(name[0]) 526 | // convert the expression 527 | switch order { 528 | case "+": 529 | order = "asc" 530 | name = name[1:] 531 | case "-": 532 | order = "desc" 533 | name = name[1:] 534 | default: 535 | order = "asc" 536 | } 537 | case 2: 538 | order = string(elem[1]) 539 | } 540 | 541 | switch order { 542 | case "asc": 543 | *x = OrderColumn{ 544 | column: name, 545 | order: order, 546 | } 547 | case "desc": 548 | *x = OrderColumn{ 549 | column: name, 550 | order: order, 551 | } 552 | default: 553 | *x = OrderColumn{ 554 | err: fmt.Errorf("expression %q is not valid", string(data)), 555 | } 556 | 557 | return x.err 558 | } 559 | 560 | return nil 561 | } 562 | 563 | // OrderColumnBy returns a order column 564 | func OrderColumnBy(expr string) *OrderColumn { 565 | data := []byte(expr) 566 | // unmarshal 567 | x := &OrderColumn{} 568 | x.UnmarshalText(data) 569 | // done! 570 | return x 571 | } 572 | 573 | // Clone clones the column 574 | func (x *OrderColumn) Clone() *OrderColumn { 575 | return &OrderColumn{ 576 | dialect: x.dialect, 577 | column: x.column, 578 | order: x.order, 579 | total: x.total, 580 | err: x.err, 581 | } 582 | } 583 | 584 | // SetDialect sets the dialect 585 | func (x *OrderColumn) SetDialect(dialect string) { 586 | x.dialect = dialect 587 | } 588 | 589 | // Dialect returns the dialect 590 | func (x *OrderColumn) Dialect() string { 591 | return x.dialect 592 | } 593 | 594 | // Total returns the total value 595 | func (x *OrderColumn) Total() int { 596 | return x.total 597 | } 598 | 599 | // SetTotal sets the totla arguments 600 | func (x *OrderColumn) SetTotal(total int) { 601 | x.total = total 602 | } 603 | 604 | // Err returns the error 605 | func (x *OrderColumn) Err() error { 606 | return x.err 607 | } 608 | 609 | // Equal returns true the expressions are equal; otherwise false. 610 | func (x *OrderColumn) Equal(y *OrderColumn) bool { 611 | return strings.EqualFold(x.column, y.column) && strings.EqualFold(x.order, y.order) 612 | } 613 | 614 | // Query returns the order by clause 615 | func (x *OrderColumn) Query() (string, []interface{}) { 616 | b := &Builder{dialect: x.dialect, total: x.total} 617 | b.Ident(x.column) 618 | 619 | switch x.order { 620 | case "asc": 621 | b.WriteString(" ASC") 622 | case "desc": 623 | b.WriteString(" DESC") 624 | default: 625 | b.WriteString(" ASC") 626 | } 627 | 628 | return b.String(), nil 629 | } 630 | 631 | // SetDialect sets the dialect 632 | func (n Queries) SetDialect(dialect string) { 633 | type QuerierDialect interface { 634 | SetDialect(dialect string) 635 | } 636 | 637 | for _, querier := range n { 638 | if r, ok := querier.(QuerierDialect); ok { 639 | r.SetDialect(dialect) 640 | } 641 | } 642 | } 643 | 644 | // ReturningExpr adds the `RETURNING` clause to the insert statement. PostgreSQL only. 645 | func (i *InsertBuilder) ReturningExpr(queriers ...Querier) *InsertBuilder { 646 | type Projection interface { 647 | Columns() []string 648 | } 649 | 650 | i.returning = []string{} 651 | 652 | for _, querier := range queriers { 653 | if projection, ok := querier.(Projection); ok { 654 | i.returning = append(i.returning, projection.Columns()...) 655 | } else { 656 | column, _ := querier.Query() 657 | // append the actual column 658 | i.returning = append(i.returning, column) 659 | } 660 | } 661 | 662 | return i 663 | } 664 | 665 | // Or sets the next coming predicate with OR operator (disjunction). 666 | func (u *UpdateBuilder) Or() *UpdateBuilder { 667 | u.or = true 668 | return u 669 | } 670 | 671 | // Not sets the next coming predicate with not. 672 | func (u *UpdateBuilder) Not() *UpdateBuilder { 673 | u.not = true 674 | return u 675 | } 676 | 677 | // Returning adds the `RETURNING` clause to the insert statement. PostgreSQL only. 678 | func (u *UpdateBuilder) Returning(columns ...string) *UpdateBuilder { 679 | u.returning = columns 680 | return u 681 | } 682 | 683 | // ReturningExpr adds the `RETURNING` clause to the insert statement. PostgreSQL only. 684 | func (u *UpdateBuilder) ReturningExpr(queriers ...Querier) *UpdateBuilder { 685 | type Projection interface { 686 | Columns() []string 687 | } 688 | 689 | u.returning = []string{} 690 | 691 | for _, querier := range queriers { 692 | if selection, ok := querier.(Projection); ok { 693 | u.returning = append(u.returning, selection.Columns()...) 694 | } else { 695 | column, _ := querier.Query() 696 | // append the actual column 697 | u.returning = append(u.returning, column) 698 | } 699 | } 700 | 701 | return u 702 | } 703 | 704 | // Or sets the next coming predicate with OR operator (disjunction). 705 | func (d *DeleteBuilder) Or() *DeleteBuilder { 706 | d.or = true 707 | return d 708 | } 709 | 710 | // Not sets the next coming predicate with not. 711 | func (d *DeleteBuilder) Not() *DeleteBuilder { 712 | d.not = true 713 | return d 714 | } 715 | 716 | // Returning adds the `RETURNING` clause to the insert statement. PostgreSQL only. 717 | func (d *DeleteBuilder) Returning(columns ...string) *DeleteBuilder { 718 | d.returning = columns 719 | return d 720 | } 721 | 722 | // ReturningExpr adds the `RETURNING` clause to the insert statement. PostgreSQL only. 723 | func (d *DeleteBuilder) ReturningExpr(queriers ...Querier) *DeleteBuilder { 724 | type Projection interface { 725 | Columns() []string 726 | } 727 | 728 | d.returning = []string{} 729 | 730 | for _, querier := range queriers { 731 | if selection, ok := querier.(Projection); ok { 732 | d.returning = append(d.returning, selection.Columns()...) 733 | } else { 734 | column, _ := querier.Query() 735 | // append the actual column 736 | d.returning = append(d.returning, column) 737 | } 738 | } 739 | 740 | return d 741 | } 742 | -------------------------------------------------------------------------------- /dialect/sql/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-present Facebook Inc. All rights reserved. 2 | // This source code is licensed under the Apache 2.0 license found 3 | // in the LICENSE file in the root directory of this source tree. 4 | 5 | package sql 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | "database/sql/driver" 11 | "fmt" 12 | "strings" 13 | 14 | "github.com/phogolabs/orm/dialect" 15 | ) 16 | 17 | var _ dialect.Driver = (*Driver)(nil) 18 | 19 | // Driver is a dialect.Driver implementation for SQL based databases. 20 | type Driver struct { 21 | // Querier 22 | dialect.ExecQuerier 23 | // Dialect name 24 | name string 25 | } 26 | 27 | // Open wraps the database/sql.Open method and returns a dialect.Driver that implements the an ent/dialect.Driver interface. 28 | func Open(name, source string) (*Driver, error) { 29 | db, err := sql.Open(name, source) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | driver := &Driver{ 35 | ExecQuerier: &Conn{db}, 36 | name: name, 37 | } 38 | 39 | return driver, nil 40 | } 41 | 42 | // OpenDB wraps the given database/sql.DB method with a Driver. 43 | func OpenDB(name string, db *sql.DB) *Driver { 44 | driver := &Driver{ 45 | ExecQuerier: &Conn{db}, 46 | name: name, 47 | } 48 | 49 | return driver 50 | } 51 | 52 | // DB returns the underlying *sql.DB instance. 53 | func (d Driver) DB() *sql.DB { 54 | conn := d.ExecQuerier.(*Conn) 55 | // the underlying database 56 | return conn.ExecQuerier.(*sql.DB) 57 | } 58 | 59 | // Dialect implements the dialect.Dialect method. 60 | func (d Driver) Dialect() string { 61 | // If the underlying driver is wrapped with opencensus driver. 62 | for _, name := range []string{dialect.MySQL, dialect.SQLite, dialect.Postgres} { 63 | if strings.HasPrefix(d.name, name) { 64 | return name 65 | } 66 | } 67 | return d.name 68 | } 69 | 70 | // Tx starts and returns a transaction. 71 | func (d *Driver) Tx(ctx context.Context) (dialect.Tx, error) { 72 | return d.BeginTx(ctx, nil) 73 | } 74 | 75 | // BeginTx starts a transaction with options. 76 | func (d *Driver) BeginTx(ctx context.Context, opts *TxOptions) (dialect.Tx, error) { 77 | tx, err := d.DB().BeginTx(ctx, opts) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | dtx := &Tx{ 83 | ExecQuerier: &Conn{tx}, 84 | Tx: tx, 85 | } 86 | 87 | return dtx, nil 88 | } 89 | 90 | // Close closes the underlying connection. 91 | func (d *Driver) Close() error { return d.DB().Close() } 92 | 93 | // Tx implements dialect.Tx interface. 94 | type Tx struct { 95 | // Querier for the current transaction 96 | dialect.ExecQuerier 97 | // Inheri from the actual driver 98 | driver.Tx 99 | } 100 | 101 | // ExecQuerier wraps the standard Exec and Query methods. 102 | type ExecQuerier interface { 103 | ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) 104 | QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) 105 | } 106 | 107 | // Conn implements dialect.ExecQuerier given ExecQuerier. 108 | type Conn struct { 109 | ExecQuerier 110 | } 111 | 112 | // Exec implements the dialect.Exec method. 113 | func (c *Conn) Exec(ctx context.Context, query string, args, v interface{}) error { 114 | argv, ok := args.([]interface{}) 115 | if !ok { 116 | return fmt.Errorf("dialect/sql: invalid type %T. expect []interface{} for args", v) 117 | } 118 | switch v := v.(type) { 119 | case nil: 120 | if _, err := c.ExecContext(ctx, query, argv...); err != nil { 121 | return err 122 | } 123 | case *sql.Result: 124 | res, err := c.ExecContext(ctx, query, argv...) 125 | if err != nil { 126 | return err 127 | } 128 | *v = res 129 | default: 130 | return fmt.Errorf("dialect/sql: invalid type %T. expect *sql.Result", v) 131 | } 132 | return nil 133 | } 134 | 135 | // Query implements the dialect.Query method. 136 | func (c *Conn) Query(ctx context.Context, query string, args, v interface{}) error { 137 | vr, ok := v.(*Rows) 138 | if !ok { 139 | return fmt.Errorf("dialect/sql: invalid type %T. expect *sql.Rows", v) 140 | } 141 | argv, ok := args.([]interface{}) 142 | if !ok { 143 | return fmt.Errorf("dialect/sql: invalid type %T. expect []interface{} for args", args) 144 | } 145 | rows, err := c.QueryContext(ctx, query, argv...) 146 | if err != nil { 147 | return err 148 | } 149 | *vr = Rows{rows} 150 | return nil 151 | } 152 | 153 | type ( 154 | // Rows wraps the sql.Rows to avoid locks copy. 155 | Rows struct{ ColumnScanner } 156 | // Result is an alias to sql.Result. 157 | Result = sql.Result 158 | // NullBool is an alias to sql.NullBool. 159 | NullBool = sql.NullBool 160 | // NullInt64 is an alias to sql.NullInt64. 161 | NullInt64 = sql.NullInt64 162 | // NullString is an alias to sql.NullString. 163 | NullString = sql.NullString 164 | // NullFloat64 is an alias to sql.NullFloat64. 165 | NullFloat64 = sql.NullFloat64 166 | // NullTime represents a time.Time that may be null. 167 | NullTime = sql.NullTime 168 | // TxOptions holds the transaction options to be used in DB.BeginTx. 169 | TxOptions = sql.TxOptions 170 | ) 171 | 172 | // NullScanner represents an sql.Scanner that may be null. 173 | // NullScanner implements the sql.Scanner interface so it can 174 | // be used as a scan destination, similar to the types above. 175 | type NullScanner struct { 176 | S sql.Scanner 177 | Valid bool // Valid is true if the Scan value is not NULL. 178 | } 179 | 180 | // Scan implements the Scanner interface. 181 | func (n *NullScanner) Scan(value interface{}) error { 182 | n.Valid = value != nil 183 | if n.Valid { 184 | return n.S.Scan(value) 185 | } 186 | return nil 187 | } 188 | 189 | // ColumnScanner is the interface that wraps the standard 190 | // sql.Rows methods used for scanning database rows. 191 | type ColumnScanner interface { 192 | Close() error 193 | ColumnTypes() ([]*sql.ColumnType, error) 194 | Columns() ([]string, error) 195 | Err() error 196 | Next() bool 197 | NextResultSet() bool 198 | Scan(dest ...interface{}) error 199 | } 200 | -------------------------------------------------------------------------------- /dialect/sql/driver_ext.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "io/fs" 7 | 8 | "github.com/jmoiron/sqlx" 9 | "github.com/phogolabs/prana/sqlexec" 10 | "github.com/phogolabs/prana/sqlmigr" 11 | ) 12 | 13 | // ErrNoRows is returned by Scan when QueryRow doesn't return a 14 | // row. In such a case, QueryRow returns a placeholder *Row value that 15 | // defers this error until a Scan. 16 | var ErrNoRows = sql.ErrNoRows 17 | 18 | // FileSystem represents the SQL filesystem 19 | type FileSystem = fs.FS 20 | 21 | // Provider represents a routine provider 22 | type Provider = sqlexec.Provider 23 | 24 | // Migrate runs the migrations 25 | func (d Driver) Migrate(storage FileSystem) error { 26 | db := sqlx.NewDb(d.DB(), d.name) 27 | // execute the migration 28 | return sqlmigr.RunAll(db, storage) 29 | } 30 | 31 | // Ping pings the server 32 | func (d Driver) Ping(ctx context.Context) error { 33 | return d.DB().PingContext(ctx) 34 | } 35 | -------------------------------------------------------------------------------- /dialect/sql/entity.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import "time" 4 | 5 | // EntityState is the entity state 6 | type EntityState int 7 | 8 | const ( 9 | // EntityStateDetached: the entity is being tracked by the context and 10 | // exists in the database, but has been marked for deletion from the 11 | // database the next time SaveChanges is called 12 | EntityStateDetached EntityState = iota 13 | 14 | // EntityStateCreated: the entity is being tracked by the context but does 15 | // not yet exist in the database 16 | EntityStateCreated 17 | 18 | // EntityStateUpdated: the entity is being tracked by the context and 19 | // exists in the database, and some or all of its property values have been 20 | // modified 21 | EntityStateUpdated 22 | ) 23 | 24 | // Entity represents an entity 25 | type Entity interface { 26 | // GetUpdatedAt returns the time when the entity was modified 27 | GetUpdatedAt() time.Time 28 | // GetCreatedAt returns the time when the entity was created 29 | GetCreatedAt() time.Time 30 | } 31 | 32 | // State returns the entity state 33 | // Accoring to StackOverflow the now() function is transactional 34 | // so in order to find out whether the contact has been changed we should 35 | // just check whether both created_at == updated_at 36 | // 37 | // ref: https://stackoverflow.com/a/49935987 38 | func GetEntityState(entity Entity) EntityState { 39 | var ( 40 | createdAt = entity.GetCreatedAt() 41 | updatedAt = entity.GetUpdatedAt() 42 | ) 43 | 44 | if createdAt.IsZero() || updatedAt.IsZero() { 45 | return EntityStateDetached 46 | } 47 | 48 | if createdAt.Equal(updatedAt) { 49 | return EntityStateCreated 50 | } 51 | 52 | return EntityStateUpdated 53 | } 54 | -------------------------------------------------------------------------------- /dialect/sql/mutation.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "github.com/phogolabs/orm/dialect/sql/scan" 5 | ) 6 | 7 | // InsertMutation represents an insert mutation 8 | type InsertMutation struct { 9 | builder *InsertBuilder 10 | } 11 | 12 | // NewInsert creates a Mutation that will save the entity src into the db 13 | func NewInsert(table string) *InsertMutation { 14 | return &InsertMutation{ 15 | builder: Insert(table), 16 | } 17 | } 18 | 19 | // Entity returns the builder 20 | func (d *InsertMutation) Entity(src interface{}) *InsertBuilder { 21 | var ( 22 | iterator = scan.IteratorOf(src) 23 | columns = make([]string, 0) 24 | values = make([]interface{}, 0) 25 | ) 26 | 27 | for iterator.Next() { 28 | var ( 29 | column = iterator.Column() 30 | value = iterator.Value().Interface() 31 | ) 32 | 33 | if column.HasOption("auto") { 34 | if scan.IsEmpty(value) { 35 | continue 36 | } 37 | } 38 | 39 | columns = append(columns, column.Name) 40 | values = append(values, value) 41 | } 42 | 43 | return d.builder. 44 | Columns(columns...). 45 | Values(values...) 46 | } 47 | 48 | // UpdateMutation represents an update mutation 49 | type UpdateMutation struct { 50 | builder *UpdateBuilder 51 | conflict bool 52 | } 53 | 54 | // NewUpdate creates a Mutation that updates the entity into the db 55 | func NewUpdate(table ...string) *UpdateMutation { 56 | table = append(table, "") 57 | 58 | return &UpdateMutation{ 59 | builder: Update(table[0]), 60 | } 61 | } 62 | 63 | // ResolveWithEntity resolves the conflict with the entity provided values 64 | func ResolveWithEntity(src interface{}, columns ...string) ConflictOption { 65 | return func(c *conflict) { 66 | c.action.update = append(c.action.update, func(u *UpdateSet) { 67 | mutation := &UpdateMutation{builder: u.update, conflict: true} 68 | mutation.Entity(src, columns...) 69 | }) 70 | } 71 | } 72 | 73 | // Entity returns the builder 74 | func (d *UpdateMutation) Entity(src interface{}, columns ...string) *UpdateBuilder { 75 | var ( 76 | builder = d.builder 77 | empty = len(columns) == 0 78 | iterator = scan.IteratorOf(src) 79 | updateable = make(map[string]interface{}) 80 | ) 81 | 82 | for iterator.Next() { 83 | var ( 84 | column = iterator.Column() 85 | value = iterator.Value().Interface() 86 | ) 87 | 88 | if empty { 89 | columns = append(columns, column.Name) 90 | } 91 | 92 | immutable := column.HasOption("read_only") || column.HasOption("immutable") || column.HasOption("primary_key") 93 | // we can update only immutable columns 94 | if !immutable { 95 | updateable[column.Name] = value 96 | } 97 | 98 | if d.conflict { 99 | continue 100 | } 101 | 102 | if column.HasOption("primary_key") { 103 | // TODO: we may use immutable & unique column with not null values as part of the where 104 | builder.Where(EQ(column.Name, value)) 105 | } 106 | } 107 | 108 | for _, name := range columns { 109 | if value, ok := updateable[name]; ok { 110 | if scan.IsNil(value) { 111 | builder.SetNull(name) 112 | } else { 113 | builder.Set(name, value) 114 | } 115 | } 116 | } 117 | 118 | return builder 119 | } 120 | 121 | // DeleteMutation represents a delete mutation 122 | type DeleteMutation struct { 123 | builder *DeleteBuilder 124 | } 125 | 126 | // NewDelete creates a Mutation that deletes the entity with given primary key. 127 | func NewDelete(table string) *DeleteMutation { 128 | return &DeleteMutation{ 129 | builder: Delete(table), 130 | } 131 | } 132 | 133 | // Entity returns the builder 134 | func (d *DeleteMutation) Entity(src interface{}) *DeleteBuilder { 135 | var ( 136 | builder = d.builder 137 | iterator = scan.IteratorOf(src) 138 | ) 139 | 140 | for iterator.Next() { 141 | if column := iterator.Column(); column.HasOption("primary_key") { 142 | // where condition 143 | builder = builder.Where( 144 | EQ(column.Name, iterator.Value().Interface()), 145 | ) 146 | } 147 | } 148 | 149 | return builder 150 | } 151 | -------------------------------------------------------------------------------- /dialect/sql/mutation_test.go: -------------------------------------------------------------------------------- 1 | package sql_test 2 | 3 | import ( 4 | "github.com/phogolabs/orm/dialect/sql" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | . "github.com/phogolabs/orm/dialect/sql/scan/mock" 9 | ) 10 | 11 | var _ = Describe("Mutation", func() { 12 | var entity *User 13 | 14 | BeforeEach(func() { 15 | email := "jack@example.com" 16 | // mock it 17 | entity = &User{ 18 | ID: "007", 19 | Name: "Jack", 20 | Email: &email, 21 | Group: &Group{ 22 | ID: "555", 23 | Name: "guest", 24 | }, 25 | } 26 | }) 27 | 28 | Describe("InsertMutation", func() { 29 | It("creates new insert mutation", func() { 30 | query, params := sql.NewInsert("users").Entity(entity).Query() 31 | 32 | Expect(query).To(Equal("INSERT INTO `users` (`id`, `name`, `email`, `group_id`) VALUES (?, ?, ?, ?)")) 33 | Expect(params).To(HaveLen(4)) 34 | Expect(params[0]).To(Equal(entity.ID)) 35 | Expect(params[1]).To(Equal(entity.Name)) 36 | Expect(params[2]).To(Equal(entity.Email)) 37 | Expect(params[3]).To(Equal(entity.Group.ID)) 38 | }) 39 | }) 40 | 41 | Describe("UpdateMutation", func() { 42 | It("creates new update mutation", func() { 43 | query, params := sql.NewUpdate("users").Entity(entity, "id", "name", "email", "group_id").Query() 44 | 45 | Expect(query).To(Equal("UPDATE `users` SET `name` = ?, `email` = ?, `group_id` = ? WHERE `id` = ?")) 46 | Expect(params).To(HaveLen(4)) 47 | Expect(params[3]).To(Equal(entity.ID)) 48 | Expect(params[0]).To(Equal(entity.Name)) 49 | Expect(params[1]).To(Equal(entity.Email)) 50 | Expect(params[2]).To(Equal(entity.Group.ID)) 51 | }) 52 | 53 | Context("when the columns are not provided", func() { 54 | It("updates all columns", func() { 55 | query, params := sql.NewUpdate("users").Entity(entity).Query() 56 | 57 | Expect(query).To(Equal("UPDATE `users` SET `name` = ?, `email` = ?, `group_id` = ? WHERE `id` = ?")) 58 | Expect(params).To(HaveLen(4)) 59 | Expect(params[3]).To(Equal(entity.ID)) 60 | Expect(params[0]).To(Equal(entity.Name)) 61 | Expect(params[1]).To(Equal(entity.Email)) 62 | Expect(params[2]).To(Equal(entity.Group.ID)) 63 | }) 64 | }) 65 | 66 | Context("when the value is nil", func() { 67 | BeforeEach(func() { 68 | entity.Email = nil 69 | }) 70 | 71 | It("sets it to null", func() { 72 | query, params := sql.NewUpdate("users").Entity(entity, "email").Query() 73 | 74 | Expect(query).To(Equal("UPDATE `users` SET `email` = NULL WHERE `id` = ?")) 75 | Expect(params).To(HaveLen(1)) 76 | Expect(params[0]).To(Equal(entity.ID)) 77 | }) 78 | }) 79 | }) 80 | 81 | Describe("DeleteMutation", func() { 82 | It("creates new delete mutation", func() { 83 | query, params := sql.NewDelete("users").Entity(entity).Query() 84 | 85 | Expect(query).To(Equal("DELETE FROM `users` WHERE `id` = ?")) 86 | Expect(params).To(HaveLen(1)) 87 | Expect(params[0]).To(Equal(entity.ID)) 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /dialect/sql/pagination.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "reflect" 9 | 10 | "github.com/phogolabs/orm/dialect/sql/scan" 11 | ) 12 | 13 | // PaginateTable paginates a given selector 14 | type PaginateTable struct { 15 | selector *Selector 16 | cursor *Cursor 17 | err error 18 | } 19 | 20 | // PaginateBy paginates the given selecttor 21 | func (x *Selector) PaginateBy(args ...*Cursor) *PaginateTable { 22 | if len(args) == 0 { 23 | args = append(args, &Cursor{}) 24 | } 25 | 26 | paginator := &PaginateTable{ 27 | selector: x.Clone(), 28 | cursor: args[0], 29 | } 30 | 31 | return paginator.seek() 32 | } 33 | 34 | // Cursor returns the underlying cursor 35 | func (pg *PaginateTable) Cursor() *Cursor { 36 | if pg.cursor.valid() { 37 | return pg.cursor 38 | } 39 | 40 | return nil 41 | } 42 | 43 | // Err returns the underlying error 44 | func (pg *PaginateTable) Err() error { 45 | err := pg.selector.Err() 46 | // check the paginator error 47 | if err == nil { 48 | err = pg.err 49 | } 50 | 51 | return err 52 | } 53 | 54 | // Dialect returns the dialect 55 | func (pg *PaginateTable) Dialect() string { 56 | return pg.selector.dialect 57 | } 58 | 59 | // SetDialect sets the dialect 60 | func (pg *PaginateTable) SetDialect(dialect string) { 61 | pg.selector.SetDialect(dialect) 62 | } 63 | 64 | // Query returns the query 65 | func (pg *PaginateTable) Query() (string, []interface{}) { 66 | return pg.selector.Query() 67 | } 68 | 69 | // Scan scans the target 70 | func (pg *PaginateTable) Scan(target interface{}) error { 71 | value := reflect.Indirect(reflect.ValueOf(target)) 72 | 73 | if value.Kind() != reflect.Slice { 74 | return fmt.Errorf("sql: invalid type %T. expect []interface{}", target) 75 | } 76 | 77 | count := value.Len() 78 | // if we do not have any items we should not proceed 79 | if count == 0 { 80 | // reset the token 81 | pg.cursor = &Cursor{} 82 | return nil 83 | } 84 | 85 | if limit := pg.selector.limit; limit != nil { 86 | if int(count) < *limit { 87 | // reset the token 88 | pg.cursor = &Cursor{} 89 | return nil 90 | } 91 | } 92 | 93 | // get the last item 94 | item := value.Index(count - 1) 95 | // remove the last item 96 | value.Set(value.Slice(0, count-1)) 97 | // selector order 98 | orderBy := pg.selector.Order() 99 | 100 | columns := orderBy.Columns() 101 | // extract the column values 102 | whereAt, err := scan.Values(item.Interface(), columns...) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | if len(columns) != len(whereAt) { 108 | return fmt.Errorf("sql: the order clause should have valid cursor vector") 109 | } 110 | 111 | pg.cursor = &Cursor{ 112 | OrderBy: orderBy, 113 | WhereAt: whereAt, 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (pg *PaginateTable) seek() *PaginateTable { 120 | if limit := pg.selector.limit; limit != nil { 121 | next := *limit + 1 122 | pg.selector.limit = &next 123 | } 124 | 125 | if err := pg.cursor.where(pg.selector); err != nil { 126 | pg.err = err 127 | return pg 128 | } 129 | 130 | if err := pg.cursor.order(pg.selector); err != nil { 131 | pg.err = err 132 | return pg 133 | } 134 | 135 | if len(pg.selector.order) == 0 { 136 | pg.err = fmt.Errorf("sql: query should have at least one order by clause") 137 | } 138 | 139 | return pg 140 | } 141 | 142 | // Cursor represents the pagination position 143 | type Cursor struct { 144 | OrderBy *Order `json:"order_by"` 145 | WhereAt []interface{} `json:"where_at"` 146 | } 147 | 148 | // MarshalBinary encodes the receiver into a binary form and returns the result. 149 | func (c *Cursor) MarshalBinary() ([]byte, error) { 150 | var data []byte 151 | 152 | if c.valid() { 153 | source, err := json.Marshal(c) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | size := len(source) 159 | // prepare the target 160 | target := make([]byte, base64.URLEncoding.EncodedLen(size)) 161 | // encode 162 | base64.URLEncoding.Encode(target, source) 163 | // prepare the target 164 | data = bytes.Trim(target, "=") 165 | } 166 | 167 | // done! 168 | return data, nil 169 | } 170 | 171 | // UnmarshalBinary must be able to decode the form generated by MarshalBinary. 172 | // UnmarshalBinary must copy the data if it wishes to retain the data after 173 | // returning. 174 | func (c *Cursor) UnmarshalBinary(source []byte) error { 175 | if len(source) > 0 { 176 | if n := len(source) % 4; n != 0 { 177 | suffix := []byte("=") 178 | suffix = bytes.Repeat(suffix, 4-n) 179 | // prepare the data 180 | source = append(source, suffix...) 181 | } 182 | 183 | size := len(source) 184 | // prepare the target 185 | target := make([]byte, base64.URLEncoding.DecodedLen(size)) 186 | // decode 187 | if _, err := base64.URLEncoding.Decode(target, source); err != nil { 188 | return err 189 | } 190 | 191 | target = bytes.Trim(target, "\x00") 192 | // move back to json 193 | if err := json.Unmarshal(target, c); err != nil { 194 | return err 195 | } 196 | } 197 | // done! 198 | return nil 199 | } 200 | 201 | func (c *Cursor) valid() bool { 202 | return c != nil && c.OrderBy != nil && c.WhereAt != nil 203 | } 204 | 205 | func (c *Cursor) where(selector *Selector) error { 206 | if c.OrderBy == nil { 207 | return nil 208 | } 209 | 210 | if predicate := c.predicate(0); predicate != nil { 211 | selector.Where(predicate) 212 | } 213 | 214 | return nil 215 | } 216 | 217 | func (c *Cursor) order(selector *Selector) error { 218 | if c.OrderBy == nil { 219 | return nil 220 | } 221 | 222 | var ( 223 | ccount = len(c.OrderBy.columns) 224 | porder = selector.Order() 225 | pcount = len(porder.columns) 226 | pindex = 0 227 | ) 228 | 229 | for cindex, vector := range c.OrderBy.columns { 230 | source := &OrderColumn{ 231 | column: vector.column, 232 | order: vector.order, 233 | } 234 | 235 | switch { 236 | case pcount == 0, cindex > pindex: 237 | selector.order = append(selector.order, source) 238 | default: 239 | if target := porder.columns[pindex]; !source.Equal(target) { 240 | return fmt.Errorf("sql: pagination cursor order by mismatch") 241 | } 242 | } 243 | 244 | if pindex+1 < pcount { 245 | pindex++ 246 | } 247 | 248 | if cindex != ccount-1 { 249 | continue 250 | } 251 | } 252 | 253 | return nil 254 | } 255 | 256 | func (c *Cursor) predicate(index int) *Predicate { 257 | if index >= len(c.OrderBy.columns) { 258 | return nil 259 | } 260 | 261 | var ( 262 | order = c.OrderBy.columns[index] 263 | value = c.WhereAt[index] 264 | ) 265 | 266 | var ( 267 | predicateCmp *Predicate 268 | predicateEQ = EQ(order.column, value) 269 | ) 270 | 271 | switch order.order { 272 | case "asc": 273 | predicateCmp = GT(order.column, value) 274 | case "desc": 275 | predicateCmp = LT(order.column, value) 276 | } 277 | 278 | predicate := c.predicate(index + 1) 279 | 280 | switch { 281 | case predicate != nil: 282 | predicate = Or(predicateCmp, And(predicateEQ, predicate)) 283 | case predicate == nil: 284 | predicate = Or(predicateCmp, predicateEQ) 285 | } 286 | 287 | return predicate 288 | } 289 | -------------------------------------------------------------------------------- /dialect/sql/pagination_test.go: -------------------------------------------------------------------------------- 1 | package sql_test 2 | 3 | import ( 4 | "github.com/phogolabs/orm/dialect/sql" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("PaginateBy", func() { 11 | var query *sql.Selector 12 | 13 | BeforeEach(func() { 14 | query = sql.Select(). 15 | From(sql.Table("users")). 16 | Where(sql.Like("name", "john")). 17 | OrderExpr(sql.OrderColumnBy("name")). 18 | Limit(100) 19 | }) 20 | 21 | It("returns a paginator", func() { 22 | paginator := query.PaginateBy() 23 | 24 | query, args := paginator.Query() 25 | Expect(query).To(Equal("SELECT * FROM `users` WHERE `name` LIKE ? ORDER BY `name` ASC LIMIT 101")) 26 | Expect(paginator.Cursor()).To(BeNil()) 27 | Expect(args).To(HaveLen(1)) 28 | }) 29 | 30 | Context("when the cursor is provided", func() { 31 | var cursor *sql.Cursor 32 | 33 | BeforeEach(func() { 34 | cursor = &sql.Cursor{ 35 | OrderBy: sql.OrderBy("name"), 36 | WhereAt: []interface{}{"john"}, 37 | } 38 | }) 39 | 40 | It("returns a paginator", func() { 41 | paginator := query.PaginateBy(cursor) 42 | query, args := paginator.Query() 43 | Expect(query).To(Equal("SELECT * FROM `users` WHERE `name` LIKE ? AND (`name` > ? OR `name` = ?) ORDER BY `name` ASC LIMIT 101")) 44 | Expect(paginator.Cursor()).To(Equal(cursor)) 45 | Expect(args).To(HaveLen(3)) 46 | }) 47 | }) 48 | 49 | Context("when the query is not sorted", func() { 50 | BeforeEach(func() { 51 | query = sql.Select(). 52 | From(sql.Table("users")). 53 | Limit(100) 54 | }) 55 | 56 | It("returns an error", func() { 57 | paginator := query.PaginateBy() 58 | Expect(paginator.Err()).To(MatchError("sql: query should have at least one order by clause")) 59 | }) 60 | }) 61 | 62 | Describe("Scan", func() { 63 | type User struct { 64 | ID int `db:"id"` 65 | Name string `db:"name"` 66 | } 67 | 68 | It("returns the next page token", func() { 69 | users := []*User{ 70 | {ID: 4, Name: "Mike"}, 71 | {ID: 3, Name: "Peter"}, 72 | {ID: 2, Name: "Brown"}, 73 | } 74 | 75 | paginator := query.Limit(2).PaginateBy() 76 | Expect(paginator.Scan(&users)).To(Succeed()) 77 | 78 | cursor := paginator.Cursor() 79 | Expect(cursor).NotTo(BeNil()) 80 | Expect(cursor.OrderBy).NotTo(BeNil()) 81 | Expect(cursor.OrderBy.String()).To(Equal("name asc")) 82 | Expect(cursor.WhereAt).To(HaveLen(1)) 83 | Expect(cursor.WhereAt).To(ContainElement("Brown")) 84 | 85 | Expect(users).To(HaveLen(2)) 86 | Expect(users[0].Name).To(Equal("Mike")) 87 | Expect(users[1].Name).To(Equal("Peter")) 88 | }) 89 | 90 | Context("when the target is not a slice", func() { 91 | It("returns an error", func() { 92 | user := &User{} 93 | paginator := query.Limit(2).PaginateBy() 94 | Expect(paginator.Scan(&user)).To(MatchError("sql: invalid type **sql_test.User. expect []interface{}")) 95 | }) 96 | }) 97 | }) 98 | 99 | Describe("SetDialect", func() { 100 | It("sets the dialect", func() { 101 | paginator := query.PaginateBy() 102 | paginator.SetDialect("postgres") 103 | Expect(paginator.Dialect()).To(Equal("postgres")) 104 | }) 105 | }) 106 | 107 | Describe("Query", func() { 108 | It("returns the actual query", func() { 109 | paginator := query.OrderExpr(sql.OrderColumnBy("id")).PaginateBy() 110 | query, args := paginator.Query() 111 | Expect(query).To(Equal("SELECT * FROM `users` WHERE `name` LIKE ? ORDER BY `name` ASC, `id` ASC LIMIT 101")) 112 | Expect(args).To(HaveLen(1)) 113 | }) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /dialect/sql/query.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/phogolabs/orm/dialect/sql/scan" 9 | ) 10 | 11 | var _ Querier = &RoutineQuery{} 12 | 13 | // RoutineQuery represents a named routine 14 | type RoutineQuery struct { 15 | name string 16 | dialect string 17 | args []interface{} 18 | stmt *NamedQuery 19 | } 20 | 21 | // Routine create a new routine for given name 22 | func Routine(name string, args ...interface{}) *RoutineQuery { 23 | return &RoutineQuery{ 24 | name: name, 25 | args: args, 26 | } 27 | } 28 | 29 | // Name returns the name of the procedure 30 | func (r *RoutineQuery) Name() string { 31 | return r.name 32 | } 33 | 34 | // Total returns the total count of parameters 35 | func (r *RoutineQuery) Total() int { 36 | if r.stmt == nil { 37 | return len(r.args) 38 | } 39 | return r.stmt.Total() 40 | } 41 | 42 | // Dialect returns the dialect 43 | func (r *RoutineQuery) Dialect() string { 44 | return r.dialect 45 | } 46 | 47 | // SetDialect sets the dialect 48 | func (r *RoutineQuery) SetDialect(dialect string) { 49 | r.dialect = dialect 50 | } 51 | 52 | // SetQuery sets the query 53 | func (r *RoutineQuery) SetQuery(value string) { 54 | r.stmt = Query(value, r.args...) 55 | r.stmt.SetDialect(r.dialect) 56 | } 57 | 58 | // Query returns the query representation of the element 59 | // and its arguments (if any). 60 | func (r *RoutineQuery) Query() (string, []interface{}) { 61 | if r.stmt == nil { 62 | return "", r.args 63 | } 64 | // return the underlying information 65 | return r.stmt.Query() 66 | } 67 | 68 | // Error returns the underlying error 69 | func (r *RoutineQuery) Error() error { 70 | if r.stmt == nil { 71 | return nil 72 | } 73 | // return the underlying error 74 | return r.stmt.err 75 | } 76 | 77 | // A NamedArg is a named argument. NamedArg values may be used as 78 | // arguments to Query or Exec and bind to the corresponding named 79 | // parameter in the SQL statement. 80 | // 81 | // For a more concise way to create NamedArg values, see 82 | // the Named function. 83 | type NamedArg = sql.NamedArg 84 | 85 | var _ Querier = &NamedQuery{} 86 | 87 | // NamedQuery is a named query that uses named arguments 88 | type NamedQuery struct { 89 | err error 90 | dialect string 91 | query string 92 | args []sql.NamedArg 93 | } 94 | 95 | // Query create a new named query 96 | func Query(query string, params ...interface{}) *NamedQuery { 97 | query, columns := scan.NamedQuery(query) 98 | // scane the arguments 99 | args, err := scan.Args(params, columns...) 100 | // create the query 101 | querier := &NamedQuery{ 102 | err: err, 103 | query: query, 104 | } 105 | 106 | for index, name := range columns { 107 | param := NamedArg{ 108 | Name: name, 109 | Value: args[index], 110 | } 111 | querier.args = append(querier.args, param) 112 | } 113 | 114 | return querier 115 | } 116 | 117 | // Dialect returns the dialect 118 | func (r *NamedQuery) Dialect() string { 119 | return r.dialect 120 | } 121 | 122 | // SetDialect sets the dialect 123 | func (r *NamedQuery) SetDialect(dialect string) { 124 | r.dialect = dialect 125 | } 126 | 127 | // Total returns the total count of parameters 128 | func (r *NamedQuery) Total() int { 129 | return len(r.args) 130 | } 131 | 132 | // Error returns the error 133 | func (r *NamedQuery) Error() error { 134 | return r.err 135 | } 136 | 137 | // Query returns the routine 138 | // Query returns the query representation of the element 139 | // and its arguments (if any). 140 | func (r *NamedQuery) Query() (string, []interface{}) { 141 | var ( 142 | query = r.query 143 | args = make([]interface{}, 0) 144 | ) 145 | 146 | for index, param := range r.args { 147 | target := fmt.Sprintf(":%v", param.Name) 148 | 149 | switch r.dialect { 150 | case "postgres": 151 | name := fmt.Sprintf("$%d", index+1) 152 | query = strings.Replace(query, target, name, 1) 153 | args = append(args, param.Value) 154 | case "mysql", "sqlite": 155 | name := "?" 156 | query = strings.Replace(query, target, name, 1) 157 | args = append(args, param.Value) 158 | case "oci8", "ora", "goracle", "godror": 159 | args = append(args, param) 160 | case "sqlserver": 161 | name := fmt.Sprintf("@%v", param.Name) 162 | query = strings.Replace(query, target, name, 1) 163 | args = append(args, param) 164 | default: 165 | args = append(args, param) 166 | } 167 | } 168 | 169 | return query, args 170 | } 171 | -------------------------------------------------------------------------------- /dialect/sql/query_test.go: -------------------------------------------------------------------------------- 1 | package sql_test 2 | 3 | import ( 4 | "github.com/phogolabs/orm/dialect/sql" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("RoutineQuerier", func() { 11 | It("creates new routine successfully", func() { 12 | routine := sql.Routine("my-test-routine", 5432) 13 | routine.SetQuery("SELECT * FROM USER WHERE id = ?") 14 | 15 | Expect(routine.Name()).To(Equal("my-test-routine")) 16 | 17 | query, params := routine.Query() 18 | Expect(query).To(Equal("SELECT * FROM USER WHERE id = :arg0")) 19 | Expect(params).To(HaveLen(1)) 20 | 21 | arg := params[0].(sql.NamedArg) 22 | Expect(arg.Name).To(Equal("arg0")) 23 | Expect(arg.Value).To(Equal(5432)) 24 | }) 25 | }) 26 | 27 | var _ = Describe("NamedQuerier", func() { 28 | Context("when the provided argument is single", func() { 29 | It("creates new command successfully", func() { 30 | routine := sql.Query("SELECT * FROM users WHERE id = ?", 5432) 31 | 32 | query, params := routine.Query() 33 | Expect(query).To(Equal("SELECT * FROM users WHERE id = :arg0")) 34 | Expect(params).To(HaveLen(1)) 35 | 36 | namedArg, ok := params[0].(sql.NamedArg) 37 | Expect(ok).To(BeTrue()) 38 | Expect(namedArg.Name).To(Equal("arg0")) 39 | Expect(namedArg.Value).To(Equal(5432)) 40 | }) 41 | }) 42 | 43 | Context("when the provided argument is a slice", func() { 44 | It("creates new command successfully", func() { 45 | routine := sql.Query("SELECT * FROM users WHERE id = ? AND category_id > ? AND category_name = ?", 1, 77, "fruits") 46 | query, params := routine.Query() 47 | 48 | Expect(query).To(Equal("SELECT * FROM users WHERE id = :arg0 AND category_id > :arg1 AND category_name = :arg2")) 49 | Expect(params).To(HaveLen(3)) 50 | 51 | namedArg, ok := params[0].(sql.NamedArg) 52 | Expect(ok).To(BeTrue()) 53 | Expect(namedArg.Name).To(Equal("arg0")) 54 | Expect(namedArg.Value).To(Equal(1)) 55 | 56 | namedArg, ok = params[1].(sql.NamedArg) 57 | Expect(ok).To(BeTrue()) 58 | Expect(namedArg.Name).To(Equal("arg1")) 59 | Expect(namedArg.Value).To(Equal(77)) 60 | 61 | namedArg, ok = params[2].(sql.NamedArg) 62 | Expect(ok).To(BeTrue()) 63 | Expect(namedArg.Name).To(Equal("arg2")) 64 | Expect(namedArg.Value).To(Equal("fruits")) 65 | }) 66 | }) 67 | 68 | Context("when the provided argument is a map", func() { 69 | It("creates new command successfully", func() { 70 | param := map[string]interface{}{ 71 | "category_id": 99, 72 | "category_name": "nuts", 73 | "id": 1234, 74 | } 75 | 76 | routine := sql.Query("SELECT * FROM users WHERE id = :id AND category_id > :category_id AND category_name = :category_name", param) 77 | query, params := routine.Query() 78 | 79 | Expect(query).To(Equal("SELECT * FROM users WHERE id = :id AND category_id > :category_id AND category_name = :category_name")) 80 | Expect(params).To(HaveLen(3)) 81 | 82 | namedArg, ok := params[0].(sql.NamedArg) 83 | Expect(ok).To(BeTrue()) 84 | Expect(namedArg.Name).To(Equal("id")) 85 | Expect(namedArg.Value).To(Equal(1234)) 86 | 87 | namedArg, ok = params[1].(sql.NamedArg) 88 | Expect(ok).To(BeTrue()) 89 | Expect(namedArg.Name).To(Equal("category_id")) 90 | Expect(namedArg.Value).To(Equal(99)) 91 | 92 | namedArg, ok = params[2].(sql.NamedArg) 93 | Expect(ok).To(BeTrue()) 94 | Expect(namedArg.Name).To(Equal("category_name")) 95 | Expect(namedArg.Value).To(Equal("nuts")) 96 | }) 97 | }) 98 | 99 | Context("when the provided argument is a struct", func() { 100 | type User struct { 101 | ID int `db:"id"` 102 | CategoryID int `db:"category_id"` 103 | CategoryName string `db:"category_name"` 104 | } 105 | 106 | It("creates new command successfully", func() { 107 | user := &User{ 108 | ID: 1234, 109 | CategoryID: 99, 110 | CategoryName: "nuts", 111 | } 112 | 113 | routine := sql.Query("SELECT * FROM users WHERE id = :id AND category_id > :category_id AND category_name = :category_name", user) 114 | routine.SetDialect("postgres") 115 | 116 | query, params := routine.Query() 117 | Expect(query).To(Equal("SELECT * FROM users WHERE id = $1 AND category_id > $2 AND category_name = $3")) 118 | 119 | Expect(params).To(HaveLen(3)) 120 | Expect(params).To(ContainElement(1234)) 121 | Expect(params).To(ContainElement(99)) 122 | Expect(params).To(ContainElement("nuts")) 123 | 124 | routine.SetDialect("sqlite") 125 | query, params = routine.Query() 126 | Expect(query).To(Equal("SELECT * FROM users WHERE id = ? AND category_id > ? AND category_name = ?")) 127 | Expect(params).To(HaveLen(3)) 128 | 129 | routine.SetDialect("oci8") 130 | query, params = routine.Query() 131 | Expect(query).To(Equal("SELECT * FROM users WHERE id = :id AND category_id > :category_id AND category_name = :category_name")) 132 | Expect(params).To(HaveLen(3)) 133 | 134 | routine.SetDialect("sqlserver") 135 | query, params = routine.Query() 136 | Expect(query).To(Equal("SELECT * FROM users WHERE id = @id AND category_id > @category_id AND category_name = @category_name")) 137 | Expect(params).To(HaveLen(3)) 138 | }) 139 | }) 140 | }) 141 | -------------------------------------------------------------------------------- /dialect/sql/scan/alloc.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/go-openapi/inflect" 9 | "github.com/jmoiron/sqlx/reflectx" 10 | ) 11 | 12 | // Allocator allocates values 13 | type Allocator struct { 14 | types []reflect.Type 15 | create func(values []interface{}) reflect.Value 16 | } 17 | 18 | // Create sets the given values 19 | func (r *Allocator) Create(values []interface{}) reflect.Value { 20 | return r.create(values) 21 | } 22 | 23 | // Allocate allocates values 24 | func (r *Allocator) Allocate() []interface{} { 25 | values := make([]interface{}, len(r.types)) 26 | 27 | for index := range r.types { 28 | values[index] = reflect.New(r.types[index]).Interface() 29 | } 30 | 31 | return values 32 | } 33 | 34 | // Set sets the values 35 | func (r *Allocator) Set(value, next reflect.Value, columns []string) { 36 | switch { 37 | case value.Kind() == reflect.Ptr: 38 | r.Set(value.Elem(), next.Elem(), columns) 39 | case value.Kind() == reflect.Struct: 40 | for _, name := range columns { 41 | field := fieldByName(value.Type(), name) 42 | // copy the value from the source to target 43 | source := next.FieldByIndex(field.Index) 44 | target := valueByIndex(value, field.Index) 45 | // set the value 46 | target.Set(source) 47 | } 48 | default: 49 | value.Set(next) 50 | } 51 | } 52 | 53 | // NewAllocator returns allocator for the given reflect.Type. 54 | func NewAllocator(target reflect.Type, columns []string) (*Allocator, error) { 55 | switch k := target.Kind(); { 56 | case k == reflect.Interface && target.NumMethod() == 0: 57 | fallthrough // interface{} 58 | case k == reflect.String || k >= reflect.Bool && k <= reflect.Float64: 59 | return NewAllocatorPrimitive(target), nil 60 | case k == reflect.Ptr: 61 | return NewAllocatorPtr(target, columns) 62 | case k == reflect.Struct: 63 | return NewAllocatorStruct(target, columns) 64 | default: 65 | return nil, fmt.Errorf("sql/scan: unsupported type ([]%s)", k) 66 | } 67 | } 68 | 69 | // NewAllocatorPrimitive allocates primitive type 70 | func NewAllocatorPrimitive(typ reflect.Type) *Allocator { 71 | return &Allocator{ 72 | types: []reflect.Type{typ}, 73 | create: func(v []interface{}) reflect.Value { 74 | return reflect.Indirect(reflect.ValueOf(v[0])) 75 | }, 76 | } 77 | } 78 | 79 | // NewAllocatorStruct returns the a configuration for scanning an sql.Row into a struct. 80 | func NewAllocatorStruct(target reflect.Type, columns []string) (*Allocator, error) { 81 | var ( 82 | types = []reflect.Type{} 83 | indices = make([][]int, 0, target.NumField()) 84 | ) 85 | 86 | for _, name := range columns { 87 | name = strings.ToLower(strings.Split(name, "(")[0]) 88 | 89 | field := fieldByName(target, name) 90 | // check if the field is nil 91 | if field == nil { 92 | return nil, fmt.Errorf("sql/scan: missing struct field for column: %s", name) 93 | } 94 | 95 | indices = append(indices, field.Index) 96 | types = append(types, field.Field.Type) 97 | } 98 | 99 | allocator := &Allocator{ 100 | types: types, 101 | create: func(values []interface{}) reflect.Value { 102 | row := reflect.New(target).Elem() 103 | 104 | for index, value := range values { 105 | vector := indices[index] 106 | column := valueByIndex(row, vector) 107 | column.Set(reflect.Indirect(reflect.ValueOf(value))) 108 | } 109 | 110 | return row 111 | }, 112 | } 113 | 114 | return allocator, nil 115 | } 116 | 117 | // NewAllocatorPtr wraps the underlying type with rowScan. 118 | func NewAllocatorPtr(target reflect.Type, columns []string) (*Allocator, error) { 119 | target = target.Elem() 120 | 121 | allocator, err := NewAllocator(target, columns) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | create := allocator.create 127 | 128 | allocator.create = func(vs []interface{}) reflect.Value { 129 | value := create(vs) 130 | ptrTyp := reflect.PtrTo(value.Type()) 131 | ptr := reflect.New(ptrTyp.Elem()) 132 | ptr.Elem().Set(value) 133 | return ptr 134 | } 135 | 136 | return allocator, nil 137 | } 138 | 139 | func valueByIndex(target reflect.Value, vector []int) reflect.Value { 140 | if len(vector) == 1 { 141 | return target.Field(vector[0]) 142 | } 143 | 144 | for depth, index := range vector { 145 | if depth > 0 && target.Kind() == reflect.Ptr { 146 | valType := target.Type().Elem() 147 | 148 | if valType.Kind() == reflect.Struct && target.IsNil() { 149 | // set the value 150 | target.Set(reflect.New(valType)) 151 | } 152 | 153 | target = target.Elem() 154 | } 155 | 156 | // field 157 | target = target.Field(index) 158 | } 159 | 160 | return target 161 | } 162 | 163 | func fieldByName(target reflect.Type, name string) *reflectx.FieldInfo { 164 | // prepare the metadata 165 | meta := mapper.TypeMap(target) 166 | 167 | // trim function 168 | trim := func(key string) string { 169 | table := strings.ToLower(inflect.Underscore(target.Name())) 170 | // prepare the name 171 | key = unquote(key) 172 | // prefix trim 173 | key = strings.TrimPrefix(key, table) 174 | key = strings.TrimPrefix(key, ".") 175 | 176 | return key 177 | } 178 | 179 | prepare := func(key string) string { 180 | tree := meta.Index 181 | // find the path 182 | path := strings.Split(key, ".") 183 | 184 | // key index 185 | if index := len(path) - 1; index > 0 { 186 | // prepare the key 187 | key = path[index] 188 | // parent key 189 | parent := strings.Join(path[:index], ".") 190 | // find the parent 191 | if field := meta.GetByPath(parent); field != nil { 192 | tree = field.Children 193 | 194 | for _, field := range tree { 195 | if field.Name == key { 196 | return field.Path 197 | } 198 | } 199 | } 200 | } 201 | 202 | for _, field := range tree { 203 | if fkey, ok := field.Options["foreign_key"]; ok && fkey == key { 204 | // prepare the reference key 205 | if ref, ok := field.Options["reference_key"]; ok { 206 | return field.Path + "." + ref 207 | } 208 | } 209 | } 210 | 211 | return key 212 | } 213 | 214 | // trum the field name 215 | name = trim(name) 216 | // prepare the field name 217 | name = prepare(name) 218 | // look up the field 219 | field := meta.GetByPath(name) 220 | // done! 221 | return field 222 | } 223 | 224 | func unquote(key string) string { 225 | // prepare the name 226 | key = strings.Replace(key, `"`, "", -1) 227 | key = strings.Replace(key, "`", "", -1) 228 | // done 229 | return key 230 | } 231 | -------------------------------------------------------------------------------- /dialect/sql/scan/alloc_test.go: -------------------------------------------------------------------------------- 1 | package scan_test 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/phogolabs/orm/dialect/sql/scan" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | . "github.com/phogolabs/orm/dialect/sql/scan/mock" 11 | ) 12 | 13 | var _ = Describe("Allocator", func() { 14 | Describe("Allocate", func() { 15 | It("allocates new values", func() { 16 | entity := &User{ 17 | ID: "007", 18 | Name: "John Doe", 19 | Group: &Group{ 20 | ID: "555", 21 | Name: "guest", 22 | Description: "My Group", 23 | }, 24 | } 25 | 26 | columns := []string{"id", "group_id", "name", "created_at", "group.name", "group.description"} 27 | 28 | allocator, err := scan.NewAllocator(reflect.TypeOf(&User{}), columns) 29 | Expect(err).NotTo(HaveOccurred()) 30 | 31 | values := allocator.Allocate() 32 | Expect(values).To(HaveLen(6)) 33 | 34 | values[0] = &entity.ID 35 | values[1] = &entity.Group.ID 36 | values[2] = &entity.Name 37 | values[3] = &entity.CreatedAt 38 | values[4] = &entity.Group.Name 39 | values[5] = &entity.Group.Description 40 | 41 | record, ok := allocator.Create(values).Interface().(*User) 42 | Expect(ok).To(BeTrue()) 43 | Expect(record).To(Equal(entity)) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /dialect/sql/scan/column.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // Column information 9 | type Column struct { 10 | Name string 11 | Options map[string]string 12 | } 13 | 14 | // HasOption return true if the column has option 15 | func (c *Column) HasOption(name string) bool { 16 | _, ok := c.Options[name] 17 | return ok 18 | } 19 | 20 | // Columns returns the columns for given opts 21 | func Columns(src interface{}, names ...string) ([]*Column, error) { 22 | var ( 23 | value = reflect.Indirect(reflect.ValueOf(src)) 24 | kind = value.Kind() 25 | ) 26 | 27 | if kind != reflect.Struct { 28 | return nil, fmt.Errorf("sql/scan: invalid type %s. expected struct as an argument", kind) 29 | } 30 | 31 | var ( 32 | columns = make([]*Column, 0) 33 | meta = mapper.TypeMap(value.Type()) 34 | ) 35 | 36 | if len(names) == 0 { 37 | for _, field := range meta.Index { 38 | names = append(names, field.Name) 39 | } 40 | } 41 | 42 | for _, name := range names { 43 | field, ok := meta.Names[name] 44 | 45 | if !ok { 46 | continue 47 | } 48 | 49 | column := &Column{ 50 | Name: name, 51 | Options: field.Options, 52 | } 53 | 54 | columns = append(columns, column) 55 | } 56 | 57 | return columns, nil 58 | } 59 | -------------------------------------------------------------------------------- /dialect/sql/scan/iter.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/jmoiron/sqlx/reflectx" 7 | ) 8 | 9 | // Iterator iterates over a struct fields 10 | type Iterator struct { 11 | value reflect.Value 12 | meta *reflectx.StructMap 13 | index int 14 | } 15 | 16 | // IteratorOf creates a new iterator 17 | func IteratorOf(src interface{}) *Iterator { 18 | var ( 19 | value = reflect.Indirect(reflect.ValueOf(src)) 20 | meta = mapper.TypeMap(value.Type()) 21 | ) 22 | 23 | return &Iterator{ 24 | meta: meta, 25 | value: value, 26 | index: -1, 27 | } 28 | } 29 | 30 | // Next progress 31 | func (iter *Iterator) Next() bool { 32 | count := len(iter.meta.Tree.Children) 33 | 34 | // if we are at the end return 35 | if count <= 0 { 36 | return false 37 | } 38 | 39 | if next := iter.index + 1; next < count { 40 | iter.index = next 41 | // done 42 | return true 43 | } 44 | 45 | return false 46 | } 47 | 48 | // Column returns the column 49 | func (iter *Iterator) Column() *Column { 50 | // current field 51 | parent := iter.meta.Tree.Children[iter.index] 52 | 53 | column := &Column{ 54 | Name: parent.Name, 55 | Options: parent.Options, 56 | } 57 | 58 | if name, ok := parent.Options["foreign_key"]; ok { 59 | column.Name = name 60 | } 61 | 62 | return column 63 | } 64 | 65 | // Value returns the underlying value 66 | func (iter *Iterator) Value() reflect.Value { 67 | parent := iter.meta.Tree.Children[iter.index] 68 | // fetch the value 69 | value := iter.value.FieldByIndex(parent.Index) 70 | 71 | if name, ok := parent.Options["reference_key"]; ok { 72 | value = reflect.Indirect(value) 73 | // prepare the meta mapper 74 | meta := mapper.TypeMap(value.Type()) 75 | // find the actual value 76 | value = value.FieldByIndex(meta.GetByPath(name).Index) 77 | } 78 | 79 | // fetch the field value 80 | return value 81 | } 82 | -------------------------------------------------------------------------------- /dialect/sql/scan/iter_test.go: -------------------------------------------------------------------------------- 1 | package scan_test 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/phogolabs/orm/dialect/sql/scan" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("Iter", func() { 13 | type University struct { 14 | ID string `db:"id,primary_key"` 15 | Name string `db:"name"` 16 | } 17 | 18 | type Student struct { 19 | ID string `db:"id,primary_key"` 20 | Name string `db:"name"` 21 | University *University `db:"university,foreign_key=university_id,reference_key=id"` 22 | CreatedAt time.Time `db:"created_at,read_only"` 23 | } 24 | 25 | It("iterates over the fields", func() { 26 | student := &Student{ 27 | ID: "001", 28 | Name: "Jack", 29 | University: &University{ 30 | ID: "007", 31 | Name: "Sofia University", 32 | }, 33 | CreatedAt: time.Now(), 34 | } 35 | 36 | iter := scan.IteratorOf(student) 37 | Expect(iter.Next()).To(BeTrue()) 38 | 39 | column := iter.Column() 40 | Expect(column).NotTo(BeNil()) 41 | Expect(column.Name).To(Equal("id")) 42 | Expect(column.HasOption("primary_key")).To(BeTrue()) 43 | Expect(iter.Value().Interface()).To(Equal("001")) 44 | Expect(iter.Next()).To(BeTrue()) 45 | 46 | column = iter.Column() 47 | Expect(column).NotTo(BeNil()) 48 | Expect(column.Name).To(Equal("name")) 49 | Expect(column.Options).To(BeEmpty()) 50 | Expect(iter.Value().Interface()).To(Equal("Jack")) 51 | Expect(iter.Next()).To(BeTrue()) 52 | 53 | column = iter.Column() 54 | Expect(column).NotTo(BeNil()) 55 | Expect(column.Name).To(Equal("university_id")) 56 | Expect(column.HasOption("foreign_key")).To(BeTrue()) 57 | Expect(column.HasOption("reference_key")).To(BeTrue()) 58 | Expect(iter.Value().Interface()).To(Equal("007")) 59 | Expect(iter.Next()).To(BeTrue()) 60 | 61 | column = iter.Column() 62 | Expect(column).NotTo(BeNil()) 63 | Expect(column.Name).To(Equal("created_at")) 64 | Expect(column.HasOption("read_only")).To(BeTrue()) 65 | Expect(iter.Next()).To(BeFalse()) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /dialect/sql/scan/mock/model.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import "time" 4 | 5 | type Group struct { 6 | ID string `db:"id"` 7 | Name string `db:"name"` 8 | Description string `db:"description"` 9 | CreatedAt time.Time `db:"created_at,auto,read_only"` 10 | UpdatedAt time.Time `db:"created_at,auto,read_only"` 11 | } 12 | 13 | type User struct { 14 | ID string `db:"id,primary_key,immutable"` 15 | Name string `db:"name"` 16 | Email *string `db:"email"` 17 | Group *Group `db:"group,foreign_key=group_id,reference_key=id"` 18 | CreatedAt time.Time `db:"created_at,auto,read_only"` 19 | UpdatedAt time.Time `db:"created_at,auto,read_only"` 20 | } 21 | -------------------------------------------------------------------------------- /dialect/sql/scan/query.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | // NamedQuery returns the query renamed 12 | func NamedQuery(query string) (string, []string) { 13 | var ( 14 | buffer = &bytes.Buffer{} 15 | next int 16 | ) 17 | 18 | for index := strings.Index(query, "?"); index != -1; index = strings.Index(query, "?") { 19 | fmt.Fprint(buffer, query[:index]) 20 | fmt.Fprint(buffer, ":") 21 | fmt.Fprintf(buffer, "arg%d", next) 22 | 23 | query = query[index+1:] 24 | next++ 25 | } 26 | 27 | fmt.Fprint(buffer, query) 28 | 29 | query = buffer.String() 30 | 31 | var ( 32 | params = []string{} 33 | underscore = '_' 34 | runes = []*unicode.RangeTable{ 35 | unicode.Letter, 36 | unicode.Digit, 37 | } 38 | ) 39 | 40 | scanner := bufio.NewScanner(buffer) 41 | scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) { 42 | for index := 0; index < len(data); index++ { 43 | if data[index] == ':' { 44 | return index + 1, data[:index+1], nil 45 | } 46 | } 47 | 48 | if !atEOF { 49 | return 0, nil, nil 50 | } 51 | 52 | return 0, data, bufio.ErrFinalToken 53 | }) 54 | 55 | for scanner.Scan() { 56 | part := scanner.Text() 57 | tail := part[len(part)-1] 58 | 59 | for tail == ':' { 60 | if !scanner.Scan() { 61 | break 62 | } 63 | 64 | part = scanner.Text() 65 | tail = part[len(part)-1] 66 | 67 | param := &bytes.Buffer{} 68 | 69 | for _, ch := range part { 70 | if !unicode.IsOneOf(runes, ch) && ch != underscore { 71 | break 72 | } 73 | 74 | fmt.Fprint(param, string(ch)) 75 | } 76 | 77 | params = append(params, param.String()) 78 | } 79 | } 80 | 81 | return query, params 82 | } 83 | -------------------------------------------------------------------------------- /dialect/sql/scan/query_test.go: -------------------------------------------------------------------------------- 1 | package scan_test 2 | 3 | import ( 4 | "github.com/phogolabs/orm/dialect/sql/scan" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("NamedQuery", func() { 11 | Context("when the query uses questionmark parameters", func() { 12 | It("renames the query", func() { 13 | query, params := scan.NamedQuery("SELECT * FROM id = ? AND name LIKE %root% OR name = ?") 14 | Expect(query).To(Equal("SELECT * FROM id = :arg0 AND name LIKE %root% OR name = :arg1")) 15 | Expect(params).To(HaveLen(2)) 16 | Expect(params[0]).To(Equal("arg0")) 17 | Expect(params[1]).To(Equal("arg1")) 18 | }) 19 | }) 20 | 21 | Context("when the query uses named parameters", func() { 22 | It("renames the query", func() { 23 | query, params := scan.NamedQuery("SELECT * FROM id = :arg0 AND name LIKE %root% OR name = :arg1") 24 | Expect(query).To(Equal("SELECT * FROM id = :arg0 AND name LIKE %root% OR name = :arg1")) 25 | Expect(params).To(HaveLen(2)) 26 | Expect(params[0]).To(Equal("arg0")) 27 | Expect(params[1]).To(Equal("arg1")) 28 | }) 29 | 30 | Context("when the args are not separated by spaces", func() { 31 | It("renames the query", func() { 32 | query, params := scan.NamedQuery("SELECT * FROM id IN (:arg0,:arg1)") 33 | Expect(query).To(Equal("SELECT * FROM id IN (:arg0,:arg1)")) 34 | Expect(params).To(HaveLen(2)) 35 | Expect(params[0]).To(Equal("arg0")) 36 | Expect(params[1]).To(Equal("arg1")) 37 | }) 38 | }) 39 | 40 | Context("when the args have underline symbol in their name", func() { 41 | It("renames the query", func() { 42 | query, params := scan.NamedQuery("SELECT * FROM id IN (:category_id,:category_name)") 43 | Expect(query).To(Equal("SELECT * FROM id IN (:category_id,:category_name)")) 44 | Expect(params).To(HaveLen(2)) 45 | Expect(params[0]).To(Equal("category_id")) 46 | Expect(params[1]).To(Equal("category_name")) 47 | }) 48 | }) 49 | }) 50 | 51 | Context("when the query is mixed", func() { 52 | It("renames the query", func() { 53 | query, params := scan.NamedQuery("SELECT * FROM id IN (?,:name)") 54 | Expect(query).To(Equal("SELECT * FROM id IN (:arg0,:name)")) 55 | Expect(params).To(HaveLen(2)) 56 | Expect(params[0]).To(Equal("arg0")) 57 | Expect(params[1]).To(Equal("name")) 58 | }) 59 | }) 60 | 61 | Context("when the arguments are duplicated", func() { 62 | It("renames the query", func() { 63 | query, params := scan.NamedQuery("SELECT * FROM id = :arg0 AND name LIKE :arg1 OR name = :arg1") 64 | Expect(query).To(Equal("SELECT * FROM id = :arg0 AND name LIKE :arg1 OR name = :arg1")) 65 | Expect(params).To(HaveLen(3)) 66 | Expect(params[0]).To(Equal("arg0")) 67 | Expect(params[1]).To(Equal("arg1")) 68 | Expect(params[2]).To(Equal("arg1")) 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /dialect/sql/scan/scan.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | // ErrOneRow is returned by Row scan when the query returns more than one row 10 | var ErrOneRow = fmt.Errorf("sql/scan: expect exactly one row in result set") 11 | 12 | // Readable provides a scannable interface 13 | type Readable interface { 14 | Scan(interface{}) error 15 | } 16 | 17 | // Scanner is the interface that wraps the 18 | // three sql.Rows methods used for scanning. 19 | type Scanner interface { 20 | Next() bool 21 | Scan(...interface{}) error 22 | Columns() ([]string, error) 23 | } 24 | 25 | // Row scans one row to the given value. It fails if the rows holds more than 1 row. 26 | func Row(scanner Scanner, src interface{}) error { 27 | value, err := valueOf(src) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | columns, err := scanner.Columns() 33 | if err != nil { 34 | return fmt.Errorf("sql/scan: failed getting column names: %v", err) 35 | } 36 | 37 | if !scanner.Next() { 38 | return sql.ErrNoRows 39 | } 40 | 41 | allocator, err := NewAllocator(value.Type(), columns) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | if expected, actual := len(columns), len(allocator.types); expected > actual { 47 | return fmt.Errorf("sql/scan: columns do not match (%d > %d)", expected, actual) 48 | } 49 | 50 | values := allocator.Allocate() 51 | if err := scanner.Scan(values...); err != nil { 52 | return fmt.Errorf("sql/scan: failed scanning rows: %v", err) 53 | } 54 | 55 | next := allocator.Create(values) 56 | allocator.Set(value, next, columns) 57 | 58 | if scanner.Next() { 59 | return ErrOneRow 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // Rows scans the given ColumnScanner (basically, sql.Row or sql.Rows) into the given slice. 66 | func Rows(scanner Scanner, src interface{}) error { 67 | value, err := valueOf(src) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | columns, err := scanner.Columns() 73 | if err != nil { 74 | return fmt.Errorf("sql/scan: failed getting column names: %v", err) 75 | } 76 | 77 | if kind := value.Kind(); kind != reflect.Slice { 78 | return fmt.Errorf("sql/scan: invalid type %s. expected slice as an argument", kind) 79 | } 80 | 81 | allocator, err := NewAllocator(value.Type().Elem(), columns) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | if expected, actual := len(columns), len(allocator.types); expected > actual { 87 | return fmt.Errorf("sql/scan: columns do not match (%d > %d)", expected, actual) 88 | } 89 | 90 | var ( 91 | count = value.Len() 92 | index = 0 93 | ) 94 | 95 | for scanner.Next() { 96 | values := allocator.Allocate() 97 | 98 | if err := scanner.Scan(values...); err != nil { 99 | return fmt.Errorf("sql/scan: failed scanning rows: %v", err) 100 | } 101 | 102 | switch { 103 | case index < count: 104 | allocator.Set(value.Index(index), allocator.Create(values), columns) 105 | default: 106 | value.Set(reflect.Append(value, allocator.Create(values))) 107 | } 108 | 109 | index++ 110 | } 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /dialect/sql/scan/scan_test.go: -------------------------------------------------------------------------------- 1 | package scan_test 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/phogolabs/orm/dialect/sql/scan" 7 | 8 | _ "github.com/mattn/go-sqlite3" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | . "github.com/onsi/gomega/gstruct" 12 | ) 13 | 14 | var _ = Describe("Scan", func() { 15 | var db *sql.DB 16 | 17 | type User struct { 18 | ID int `db:"id"` 19 | Name *string `db:"name"` 20 | Password string `db:"password"` 21 | } 22 | 23 | type UserGroup struct { 24 | Name string `db:"name"` 25 | User *User `db:"user,foreign_key=user_id,reference_key=id"` 26 | } 27 | 28 | BeforeEach(func() { 29 | var err error 30 | 31 | db, err = sql.Open("sqlite3", "file:test.db?cache=shared&mode=memory") 32 | Expect(err).To(BeNil()) 33 | 34 | _, err = db.Exec("CREATE TABLE IF NOT EXISTS users (id int, name varchar(255), password varchar(10))") 35 | Expect(err).To(BeNil()) 36 | 37 | _, err = db.Exec("CREATE TABLE IF NOT EXISTS user_groups (name varchar(255), user_id int)") 38 | Expect(err).To(BeNil()) 39 | }) 40 | 41 | AfterEach(func() { 42 | var err error 43 | 44 | _, err = db.Exec("DELETE FROM users") 45 | Expect(err).To(BeNil()) 46 | 47 | _, err = db.Exec("DELETE FROM user_groups") 48 | Expect(err).To(BeNil()) 49 | 50 | Expect(db.Close()).To(Succeed()) 51 | }) 52 | 53 | Describe("Row", func() { 54 | It("scans the row successfully", func() { 55 | _, err := db.Exec("INSERT INTO users VALUES(1, 'root', 'swordfish')") 56 | Expect(err).To(BeNil()) 57 | 58 | user := &User{} 59 | 60 | rows, err := db.Query("SELECT * FROM users") 61 | Expect(err).To(BeNil()) 62 | 63 | Expect(scan.Row(rows, user)).To(Succeed()) 64 | Expect(user.ID).To(Equal(1)) 65 | Expect(*user.Name).To(Equal("root")) 66 | Expect(user.Password).To(Equal("swordfish")) 67 | }) 68 | 69 | Context("when the collection is nested", func() { 70 | It("scans the row successfully", func() { 71 | _, err := db.Exec("INSERT INTO user_groups VALUES('admin', 7)") 72 | Expect(err).To(BeNil()) 73 | 74 | group := &UserGroup{} 75 | 76 | rows, err := db.Query("SELECT * FROM user_groups") 77 | Expect(err).To(BeNil()) 78 | 79 | Expect(scan.Row(rows, group)).To(Succeed()) 80 | Expect(group.Name).To(Equal("admin")) 81 | Expect(group.User.ID).To(Equal(7)) 82 | }) 83 | }) 84 | }) 85 | 86 | Describe("Rows", func() { 87 | It("scans the row successfully", func() { 88 | _, err := db.Exec("INSERT INTO users VALUES(1, 'root', 'swordfish')") 89 | Expect(err).To(BeNil()) 90 | 91 | _, err = db.Exec("INSERT INTO users VALUES(2, 'admin', 'qwerty')") 92 | Expect(err).To(BeNil()) 93 | 94 | users := []*User{ 95 | &User{ID: 1}, 96 | } 97 | 98 | rows, err := db.Query("SELECT name, password FROM users") 99 | Expect(err).To(BeNil()) 100 | 101 | Expect(scan.Rows(rows, &users)).To(Succeed()) 102 | Expect(users).To(HaveLen(2)) 103 | 104 | Expect(users[0].ID).To(Equal(1)) 105 | Expect(users[0].Name).To(PointTo(Equal("root"))) 106 | Expect(users[0].Password).To(Equal("swordfish")) 107 | 108 | Expect(users[1].ID).To(Equal(0)) 109 | Expect(users[1].Name).To(PointTo(Equal("admin"))) 110 | Expect(users[1].Password).To(Equal("qwerty")) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /dialect/sql/scan/suite_test.go: -------------------------------------------------------------------------------- 1 | package scan_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestScan(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Scan Suite") 13 | } 14 | -------------------------------------------------------------------------------- /dialect/sql/scan/value.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/jmoiron/sqlx/reflectx" 9 | ) 10 | 11 | var ( 12 | mapper = reflectx.NewMapper("db") 13 | namedArgType = reflect.TypeOf(sql.NamedArg{}) 14 | ) 15 | 16 | // IsNil returns true if the value is nil 17 | func IsNil(src interface{}) bool { 18 | if src == nil { 19 | return true 20 | } 21 | 22 | value := reflect.ValueOf(src) 23 | 24 | switch value.Kind() { 25 | case reflect.Ptr: 26 | return value.IsNil() 27 | case reflect.Chan: 28 | return value.IsNil() 29 | case reflect.Func: 30 | return value.IsNil() 31 | case reflect.Map: 32 | return value.IsNil() 33 | case reflect.Interface: 34 | return value.IsNil() 35 | case reflect.Slice: 36 | return value.IsNil() 37 | } 38 | 39 | return false 40 | } 41 | 42 | // IsEmpty returns true if the value is empty 43 | func IsEmpty(src interface{}) bool { 44 | if src == nil { 45 | return true 46 | } 47 | 48 | value := reflect.ValueOf(src) 49 | 50 | switch value.Kind() { 51 | case reflect.Ptr: 52 | return value.IsNil() 53 | case reflect.Chan: 54 | return value.IsNil() 55 | case reflect.Func: 56 | return value.IsNil() 57 | case reflect.Map: 58 | return value.IsNil() 59 | case reflect.Interface: 60 | return value.IsNil() 61 | case reflect.Slice: 62 | return value.IsNil() 63 | } 64 | 65 | return value.IsZero() 66 | } 67 | 68 | // Args returns the arguments 69 | func Args(src []interface{}, columns ...string) ([]interface{}, error) { 70 | if len(columns) == 0 { 71 | return src, nil 72 | } 73 | 74 | args := make([]interface{}, 0) 75 | 76 | for _, arg := range src { 77 | param := reflect.ValueOf(arg) 78 | param = reflect.Indirect(param) 79 | 80 | switch k := param.Type().Kind(); { 81 | case k == reflect.String || k >= reflect.Bool && k <= reflect.Float64: 82 | args = append(args, arg) 83 | default: 84 | values, err := valuesOf(param, columns) 85 | if err != nil { 86 | return nil, err 87 | } 88 | args = append(args, values...) 89 | } 90 | } 91 | 92 | return args, nil 93 | } 94 | 95 | // Values scans a struct and returns the values associated with the columns 96 | // provided. Only simple value types are supported (i.e. Bool, Ints, Uints, 97 | // Floats, Interface, String, NamedArg) 98 | func Values(src interface{}, columns ...string) ([]interface{}, error) { 99 | // prepare the input 100 | for index, column := range columns { 101 | columns[index] = unquote(column) 102 | } 103 | 104 | value, err := valueOf(src) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | return valuesOf(value, columns) 110 | } 111 | 112 | func valuesOf(value reflect.Value, columns []string) ([]interface{}, error) { 113 | switch value.Kind() { 114 | case reflect.Struct: 115 | switch value.Type() { 116 | case namedArgType: 117 | return valuesOfNamedArg(value, columns) 118 | default: 119 | return valuesOfStruct(value, columns) 120 | } 121 | case reflect.Map: 122 | return valuesOfMap(value, columns) 123 | default: 124 | return nil, fmt.Errorf("sql/scan: invalid type %s. expected struct or map as an argument", value.Kind()) 125 | } 126 | } 127 | 128 | func valuesOfStruct(target reflect.Value, columns []string) ([]interface{}, error) { 129 | kind := target.Kind() 130 | 131 | if kind != reflect.Struct { 132 | return nil, fmt.Errorf("sql/scan: invalid type %s. expected struct as an argument", kind) 133 | } 134 | 135 | values := make([]interface{}, 0) 136 | 137 | if len(columns) == 0 { 138 | meta := mapper.TypeMap(target.Type()) 139 | 140 | iter := &Iterator{ 141 | meta: meta, 142 | value: target, 143 | index: -1, 144 | } 145 | 146 | for iter.Next() { 147 | column := iter.Column() 148 | columns = append(columns, column.Name) 149 | } 150 | } 151 | 152 | for _, name := range columns { 153 | if field := fieldByName(target.Type(), name); field != nil { 154 | // find the value 155 | value := valueByIndex(target, field.Index).Interface() 156 | // append it 157 | values = append(values, value) 158 | } 159 | } 160 | 161 | return values, nil 162 | } 163 | 164 | func valuesOfNamedArg(target reflect.Value, columns []string) ([]interface{}, error) { 165 | var ( 166 | values = []interface{}{} 167 | arg = target.Interface().(sql.NamedArg) 168 | ) 169 | 170 | if len(columns) == 0 { 171 | columns = append(columns, arg.Name) 172 | } 173 | 174 | for _, column := range columns { 175 | if column == arg.Name { 176 | values = append(values, arg.Value) 177 | break 178 | } 179 | } 180 | 181 | return values, nil 182 | } 183 | 184 | func valuesOfMap(value reflect.Value, columns []string) ([]interface{}, error) { 185 | var ( 186 | empty = reflect.Value{} 187 | kind = value.Kind() 188 | ) 189 | 190 | if kind != reflect.Map { 191 | return nil, fmt.Errorf("sql/scan: invalid type %s. expected map as an argument", kind) 192 | } 193 | 194 | if keyKind := value.Type().Key().Kind(); keyKind != reflect.String { 195 | return nil, fmt.Errorf("sql/scan: invalid type %s. expected string as an key", keyKind) 196 | } 197 | 198 | if len(columns) == 0 { 199 | for _, key := range value.MapKeys() { 200 | columns = append(columns, key.String()) 201 | } 202 | } 203 | 204 | values := make([]interface{}, 0) 205 | 206 | for _, name := range columns { 207 | param := value.MapIndex(reflect.ValueOf(name)) 208 | 209 | if param == empty { 210 | continue 211 | } 212 | 213 | values = append(values, param.Interface()) 214 | } 215 | 216 | return values, nil 217 | } 218 | 219 | func valueOf(src interface{}) (reflect.Value, error) { 220 | var ( 221 | empty = reflect.Value{} 222 | typ = reflect.TypeOf(src) 223 | kind = typ.Kind() 224 | ) 225 | 226 | if kind != reflect.Ptr { 227 | return empty, fmt.Errorf("sql/scan: invalid type %s. expected pointer as an argument", kind) 228 | } 229 | 230 | value := reflect.ValueOf(src) 231 | value = reflect.Indirect(value) 232 | return value, nil 233 | } 234 | -------------------------------------------------------------------------------- /dialect/sql/scan/value_test.go: -------------------------------------------------------------------------------- 1 | package scan_test 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | 7 | "github.com/phogolabs/orm/dialect/sql/scan" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | . "github.com/phogolabs/orm/dialect/sql/scan/mock" 12 | ) 13 | 14 | var _ = Describe("Values", func() { 15 | var entity *User 16 | 17 | BeforeEach(func() { 18 | email := "john.doe@example.com" 19 | // mock it 20 | entity = &User{ 21 | ID: "007", 22 | Name: "John Doe", 23 | Email: &email, 24 | CreatedAt: time.Now(), 25 | Group: &Group{ 26 | ID: "555", 27 | Name: "guest", 28 | Description: "My Group", 29 | CreatedAt: time.Now(), 30 | }, 31 | } 32 | }) 33 | 34 | Context("when the source is struct", func() { 35 | It("scans the values successfully", func() { 36 | values, err := scan.Values(entity, "name", "email", "group_id") 37 | Expect(err).NotTo(HaveOccurred()) 38 | Expect(values).To(HaveLen(3)) 39 | Expect(values[0]).To(Equal(entity.Name)) 40 | Expect(values[1]).To(Equal(entity.Email)) 41 | Expect(values[2]).To(Equal(entity.Group.ID)) 42 | }) 43 | 44 | Context("when the column is path", func() { 45 | It("scans the values successfully", func() { 46 | values, err := scan.Values(entity, "group.id", "group.name", "group.description") 47 | Expect(err).NotTo(HaveOccurred()) 48 | Expect(values).To(HaveLen(3)) 49 | Expect(values[0]).To(Equal(entity.Group.ID)) 50 | Expect(values[1]).To(Equal(entity.Group.Name)) 51 | Expect(values[2]).To(Equal(entity.Group.Description)) 52 | }) 53 | }) 54 | 55 | Context("when the column is not found", func() { 56 | It("scans the values successfully", func() { 57 | values, err := scan.Values(entity, "name", "email", "description") 58 | Expect(err).NotTo(HaveOccurred()) 59 | Expect(values).To(HaveLen(2)) 60 | Expect(values[0]).To(Equal(entity.Name)) 61 | Expect(values[1]).To(Equal(entity.Email)) 62 | }) 63 | }) 64 | 65 | Context("when the columns are not provided", func() { 66 | It("scans the values successfully", func() { 67 | values, err := scan.Values(entity) 68 | Expect(err).NotTo(HaveOccurred()) 69 | Expect(values).To(HaveLen(6)) 70 | Expect(values[0]).To(Equal(entity.ID)) 71 | Expect(values[1]).To(Equal(entity.Name)) 72 | Expect(values[2]).To(Equal(entity.Email)) 73 | Expect(values[3]).To(Equal(entity.Group.ID)) 74 | Expect(values[4]).To(Equal(entity.CreatedAt)) 75 | }) 76 | }) 77 | }) 78 | 79 | Context("when the source is NamedArg", func() { 80 | It("scans the values successfully", func() { 81 | arg := sql.Named("name", "John Doe") 82 | values, err := scan.Values(&arg, "name") 83 | Expect(err).NotTo(HaveOccurred()) 84 | Expect(values).To(HaveLen(1)) 85 | Expect(values[0]).To(Equal(arg.Value)) 86 | }) 87 | 88 | Context("when the columns are not provided", func() { 89 | It("scans the values successfully", func() { 90 | arg := sql.Named("name", "John Doe") 91 | values, err := scan.Values(&arg) 92 | Expect(err).NotTo(HaveOccurred()) 93 | Expect(values).To(HaveLen(1)) 94 | Expect(values[0]).To(Equal(arg.Value)) 95 | }) 96 | }) 97 | 98 | Context("when the column is not found", func() { 99 | It("scans the values successfully", func() { 100 | arg := sql.Named("name", "John Doe") 101 | values, err := scan.Values(&arg, "age") 102 | Expect(err).NotTo(HaveOccurred()) 103 | Expect(values).To(HaveLen(0)) 104 | }) 105 | }) 106 | }) 107 | 108 | Context("when the source is a map", func() { 109 | var dictionary map[string]interface{} 110 | 111 | BeforeEach(func() { 112 | dictionary = map[string]interface{}{ 113 | "id": "007", 114 | "name": "John Doe", 115 | "email": entity.Email, 116 | "created_at": time.Now(), 117 | "deleted_at": nil, 118 | } 119 | }) 120 | 121 | It("scans the values successfully", func() { 122 | values, err := scan.Values(&dictionary, "id", "name") 123 | Expect(err).NotTo(HaveOccurred()) 124 | Expect(values).To(HaveLen(2)) 125 | Expect(values[0]).To(Equal(dictionary["id"])) 126 | Expect(values[1]).To(Equal(dictionary["name"])) 127 | }) 128 | 129 | Context("when the key is not found", func() { 130 | It("scans the values successfully", func() { 131 | values, err := scan.Values(&dictionary, "id", "address") 132 | Expect(err).NotTo(HaveOccurred()) 133 | Expect(values).To(HaveLen(1)) 134 | Expect(values[0]).To(Equal(dictionary["id"])) 135 | }) 136 | }) 137 | 138 | Context("when the columns are not provided", func() { 139 | It("scans the values successfully", func() { 140 | values, err := scan.Values(&dictionary) 141 | Expect(err).NotTo(HaveOccurred()) 142 | Expect(values).To(HaveLen(5)) 143 | }) 144 | }) 145 | 146 | Context("when the map key is not supported", func() { 147 | It("returns an error", func() { 148 | dictionary := map[int]interface{}{} 149 | values, err := scan.Values(&dictionary) 150 | Expect(err).To(MatchError("sql/scan: invalid type int. expected string as an key")) 151 | Expect(values).To(BeEmpty()) 152 | }) 153 | }) 154 | }) 155 | 156 | Context("when the source is not supported", func() { 157 | It("returns an error", func() { 158 | src := "pointer" 159 | values, err := scan.Values(&src) 160 | Expect(err).To(MatchError("sql/scan: invalid type string. expected struct or map as an argument")) 161 | Expect(values).To(BeEmpty()) 162 | }) 163 | }) 164 | 165 | Context("when the source is not pointer", func() { 166 | It("returns an error", func() { 167 | values, err := scan.Values("not pointer") 168 | Expect(err).To(MatchError("sql/scan: invalid type string. expected pointer as an argument")) 169 | Expect(values).To(BeEmpty()) 170 | }) 171 | }) 172 | }) 173 | 174 | var _ = Describe("Args", func() { 175 | Context("when no columns are provided", func() { 176 | It("returns the actual arguments", func() { 177 | args := []interface{}{1, "root", time.Now()} 178 | values, err := scan.Args(args) 179 | Expect(err).NotTo(HaveOccurred()) 180 | Expect(values).To(Equal(args)) 181 | }) 182 | }) 183 | 184 | Context("when the columns are provided", func() { 185 | It("returns the actual arguments", func() { 186 | args := []interface{}{ 187 | sql.NamedArg{Name: "id", Value: 1}, 188 | sql.NamedArg{Name: "entity", Value: "root"}, 189 | sql.NamedArg{Name: "password", Value: "swordfish"}, 190 | } 191 | 192 | values, err := scan.Args(args, "id", "entity", "password") 193 | Expect(err).NotTo(HaveOccurred()) 194 | Expect(values).To(HaveLen(3)) 195 | Expect(values[0]).To(Equal(1)) 196 | Expect(values[1]).To(Equal("root")) 197 | Expect(values[2]).To(Equal("swordfish")) 198 | }) 199 | }) 200 | }) 201 | -------------------------------------------------------------------------------- /dialect/sql/suite_test.go: -------------------------------------------------------------------------------- 1 | package sql_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSQL(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "SQL Suite") 13 | } 14 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // NotFoundError returns when trying to fetch a specific entity and it was not found in the database. 9 | type NotFoundError struct { 10 | label string 11 | } 12 | 13 | // Error implements the error interface. 14 | func (e *NotFoundError) Error() string { 15 | return "orm: " + e.label + " not found" 16 | } 17 | 18 | // IsNotFound returns a boolean indicating whether the error is a not found error. 19 | func IsNotFound(err error) bool { 20 | if err == nil { 21 | return false 22 | } 23 | var e *NotFoundError 24 | return errors.As(err, &e) 25 | } 26 | 27 | // MaskNotFound masks nor found error. 28 | func MaskNotFound(err error) error { 29 | if IsNotFound(err) { 30 | return nil 31 | } 32 | return err 33 | } 34 | 35 | // NotSingularError returns when trying to fetch a singular entity and more then one was found in the database. 36 | type NotSingularError struct { 37 | label string 38 | } 39 | 40 | // Error implements the error interface. 41 | func (e *NotSingularError) Error() string { 42 | return "orm: " + e.label + " not singular" 43 | } 44 | 45 | // IsNotSingular returns a boolean indicating whether the error is a not singular error. 46 | func IsNotSingular(err error) bool { 47 | if err == nil { 48 | return false 49 | } 50 | var e *NotSingularError 51 | return errors.As(err, &e) 52 | } 53 | 54 | // NotLoadedError returns when trying to get a node that was not loaded by the query. 55 | type NotLoadedError struct { 56 | edge string 57 | } 58 | 59 | // Error implements the error interface. 60 | func (e *NotLoadedError) Error() string { 61 | return "orm: " + e.edge + " edge was not loaded" 62 | } 63 | 64 | // IsNotLoaded returns a boolean indicating whether the error is a not loaded error. 65 | func IsNotLoaded(err error) bool { 66 | if err == nil { 67 | return false 68 | } 69 | var e *NotLoadedError 70 | return errors.As(err, &e) 71 | } 72 | 73 | // ConstraintError returns when trying to create/update one or more entities and 74 | // one or more of their constraints failed. For example, violation of edge or 75 | // field uniqueness. 76 | type ConstraintError struct { 77 | wrap error 78 | name string 79 | } 80 | 81 | // Name returns the constraint name 82 | func (e ConstraintError) Name() string { 83 | return e.name 84 | } 85 | 86 | // Error implements the error interface. 87 | func (e ConstraintError) Error() string { 88 | const prefix = "orm: constraint violation" 89 | 90 | if e.wrap != nil { 91 | return fmt.Sprintf("%s: %s", prefix, e.wrap.Error()) 92 | } 93 | 94 | return prefix 95 | } 96 | 97 | // Unwrap implements the errors.Wrapper interface. 98 | func (e *ConstraintError) Unwrap() error { 99 | return e.wrap 100 | } 101 | 102 | // IsConstraintViolation returns a boolean indicating whether the error is a constraint failure. 103 | func IsConstraintViolation(err error) bool { 104 | if err == nil { 105 | return false 106 | } 107 | var e *ConstraintError 108 | return errors.As(err, &e) 109 | } 110 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package orm_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/phogolabs/orm" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("NotFoundError", func() { 13 | It("returns a not found error", func() { 14 | errx := &orm.NotFoundError{} 15 | Expect(errx.Error()).To(Equal("orm: not found")) 16 | }) 17 | 18 | Describe("IsNotFound", func() { 19 | It("returns true", func() { 20 | errx := &orm.NotFoundError{} 21 | Expect(orm.IsNotFound(errx)).To(BeTrue()) 22 | }) 23 | 24 | Context("when the error is nil", func() { 25 | It("returns false", func() { 26 | Expect(orm.IsNotFound(nil)).To(BeFalse()) 27 | }) 28 | }) 29 | }) 30 | 31 | Describe("MaskNotFound", func() { 32 | It("masks the error", func() { 33 | Expect(orm.MaskNotFound(&orm.NotFoundError{})).To(BeNil()) 34 | }) 35 | 36 | It("masks the error", func() { 37 | Expect(orm.MaskNotFound(fmt.Errorf("oh no"))).To(MatchError("oh no")) 38 | }) 39 | }) 40 | }) 41 | 42 | var _ = Describe("NotSingularError", func() { 43 | It("returns a not singular error", func() { 44 | errx := &orm.NotSingularError{} 45 | Expect(errx.Error()).To(Equal("orm: not singular")) 46 | }) 47 | 48 | Describe("IsNotSingular", func() { 49 | It("returns true", func() { 50 | errx := &orm.NotSingularError{} 51 | Expect(orm.IsNotSingular(errx)).To(BeTrue()) 52 | }) 53 | 54 | Context("when the error is nil", func() { 55 | It("returns false", func() { 56 | Expect(orm.IsNotSingular(nil)).To(BeFalse()) 57 | }) 58 | }) 59 | }) 60 | }) 61 | 62 | var _ = Describe("NotLoadedError", func() { 63 | It("returns a not loaded error", func() { 64 | errx := &orm.NotLoadedError{} 65 | Expect(errx.Error()).To(Equal("orm: edge was not loaded")) 66 | }) 67 | 68 | Describe("IsNotLoaded", func() { 69 | It("returns true", func() { 70 | errx := &orm.NotLoadedError{} 71 | Expect(orm.IsNotLoaded(errx)).To(BeTrue()) 72 | }) 73 | 74 | Context("when the error is nil", func() { 75 | It("returns false", func() { 76 | Expect(orm.IsNotLoaded(nil)).To(BeFalse()) 77 | }) 78 | }) 79 | }) 80 | }) 81 | 82 | var _ = Describe("ConstraintError", func() { 83 | It("returns a constraint error", func() { 84 | errx := &orm.ConstraintError{} 85 | Expect(errx.Error()).To(Equal("orm: constraint violation")) 86 | }) 87 | 88 | Describe("UnWrap", func() { 89 | It("returns the wrapped error", func() { 90 | errx := &orm.ConstraintError{} 91 | Expect(errx.Unwrap()).To(BeNil()) 92 | }) 93 | }) 94 | 95 | Describe("IsConstraintViolation", func() { 96 | It("returns true", func() { 97 | errx := &orm.ConstraintError{} 98 | Expect(orm.IsConstraintViolation(errx)).To(BeTrue()) 99 | }) 100 | 101 | Context("when the error is nil", func() { 102 | It("returns false", func() { 103 | Expect(orm.IsConstraintViolation(nil)).To(BeFalse()) 104 | }) 105 | }) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # OAK Example 2 | 3 | The example shows how to use ORM with 4 | [Prana](https://github.com/phogolabs/prana). 5 | 6 | If you want, you can setup the database via Prana CLI: 7 | 8 | ```bash 9 | $ prana migration run 10 | ``` 11 | 12 | Otherwise when you run the example, it will setup the database by using `Migrate` function. 13 | 14 | You can run the project by executing the `main.go`: 15 | 16 | ```bash 17 | $ go run cmd/example-api/main.go 18 | ``` 19 | -------------------------------------------------------------------------------- /example/cmd/example-api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "os" 7 | "time" 8 | 9 | _ "github.com/mattn/go-sqlite3" 10 | 11 | "github.com/bxcodec/faker/v3" 12 | "github.com/phogolabs/log" 13 | "github.com/phogolabs/orm/example/database" 14 | "github.com/phogolabs/orm/example/database/ent" 15 | ) 16 | 17 | func main() { 18 | gateway, err := database.Open("sqlite3://prana.db") 19 | if err != nil { 20 | log.WithError(err).Fatal("failed to open database connection") 21 | } 22 | // close the connection 23 | defer gateway.Close() 24 | 25 | if err := gateway.Migrate(database.Schema); err != nil { 26 | log.WithError(err).Fatal("failed to run database migration") 27 | } 28 | 29 | repository := &database.UserRepository{ 30 | Gateway: gateway, 31 | } 32 | 33 | if err = generate(repository); err != nil { 34 | log.WithError(err).Fatal("failed to generate all records") 35 | } 36 | 37 | users, err := repository.AllUsers(context.TODO()) 38 | if err != nil { 39 | log.WithError(err).Fatal("failed to select all records") 40 | } 41 | 42 | if err := show(users); err != nil { 43 | log.WithError(err).Fatal("printing the records") 44 | } 45 | } 46 | 47 | func generate(repository *database.UserRepository) error { 48 | ctx := context.TODO() 49 | 50 | for i := 0; i < 10; i++ { 51 | record := &ent.User{ 52 | ID: int(time.Now().UnixNano()), 53 | FirstName: faker.FirstName(), 54 | LastName: pointer(faker.LastName()), 55 | } 56 | 57 | if err := repository.InsertUser(ctx, record); err != nil { 58 | return err 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func show(data interface{}) error { 66 | encoder := json.NewEncoder(os.Stdout) 67 | encoder.SetIndent(" ", " ") 68 | return encoder.Encode(data) 69 | } 70 | 71 | func pointer(v string) *string { 72 | return &v 73 | } 74 | -------------------------------------------------------------------------------- /example/database/ent/schema.go: -------------------------------------------------------------------------------- 1 | // Code generated by prana; DO NOT EDIT. 2 | 3 | // Package model contains an object model of database schema 'default' 4 | // Auto-generated at Thu, 11 Apr 2019 16:39:45 CEST 5 | 6 | package ent 7 | 8 | // User represents a data base table 'users' 9 | type User struct { 10 | // ID represents a database column 'id' of type 'INT PRIMARY KEY NOT NULL' 11 | ID int `db:"id,primary_key,not_null" validate:"required" json:"id"` 12 | // FirstName represents a database column 'first_name' of type 'TEXT NOT NULL' 13 | FirstName string `db:"first_name,not_null" validate:"required,gt=0" json:"first_name"` 14 | // LastName represents a database column 'last_name' of type 'TEXT NULL' 15 | LastName *string `db:"last_name,null" validate:"-" json:"last_name"` 16 | } 17 | -------------------------------------------------------------------------------- /example/database/gateway.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "github.com/phogolabs/log" 5 | "github.com/phogolabs/orm" 6 | "github.com/phogolabs/orm/example/database/migration" 7 | "github.com/phogolabs/orm/example/database/routine" 8 | ) 9 | 10 | // Schema represents the database schema 11 | var Schema = migration.Schema 12 | 13 | // Open opens the connection 14 | func Open(url string) (*orm.Gateway, error) { 15 | logger := log.WithField("component", "database") 16 | 17 | return orm.Connect(url, 18 | orm.WithLogger(logger), 19 | orm.WithRoutine(routine.Statement), 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /example/database/migration/00060524000000_setup.sql: -------------------------------------------------------------------------------- 1 | -- Auto-generated at Thu Apr 19 19:32:18 CEST 2018 2 | -- Please do not change the name attributes 3 | 4 | -- name: up 5 | CREATE TABLE IF NOT EXISTS migrations ( 6 | id TEXT NOT NULL PRIMARY KEY, 7 | description TEXT NOT NULL, 8 | created_at TIMESTAMP NOT NULL 9 | ); 10 | 11 | -- name: down 12 | DROP TABLE IF EXISTS migrations; 13 | -------------------------------------------------------------------------------- /example/database/migration/20180406190015_users.sql: -------------------------------------------------------------------------------- 1 | -- Auto-generated at Fri Apr 6 19:00:15 CEST 2018 2 | -- Please do not change the name attributes 3 | 4 | -- name: up 5 | CREATE TABLE users ( 6 | id INT PRIMARY KEY NOT NULL, 7 | first_name TEXT NOT NULL, 8 | last_name TEXT 9 | ); 10 | 11 | -- name: down 12 | DROP TABLE IF EXISTS users; 13 | -------------------------------------------------------------------------------- /example/database/migration/content.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import "embed" 4 | 5 | //go:embed *.sql 6 | var Schema embed.FS 7 | -------------------------------------------------------------------------------- /example/database/repository.go: -------------------------------------------------------------------------------- 1 | // Package database contains an repository of database schema '' 2 | // Auto-generated at Fri, 12 Apr 2019 13:09:42 CEST 3 | package database 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/phogolabs/orm" 9 | "github.com/phogolabs/orm/example/database/ent" 10 | ) 11 | 12 | // UserRepository represents a repository for 'users' 13 | type UserRepository struct { 14 | // Gateway connects the repository to the underlying database 15 | Gateway *orm.Gateway 16 | } 17 | 18 | // AllUsers returns all User from the database 19 | func (r *UserRepository) AllUsers(ctx context.Context) ([]*ent.User, error) { 20 | var ( 21 | entities = []*ent.User{} 22 | routine = orm.Routine("select-all-users") 23 | ) 24 | 25 | if err := r.Gateway.All(ctx, routine, &entities); err != nil { 26 | return nil, err 27 | } 28 | 29 | return entities, nil 30 | } 31 | 32 | // InsertUser inserts a record of type User into the database 33 | func (r *UserRepository) InsertUser(ctx context.Context, entity *ent.User) error { 34 | routine := orm.Routine("insert-user", entity) 35 | _, err := r.Gateway.Exec(ctx, routine) 36 | return err 37 | } 38 | -------------------------------------------------------------------------------- /example/database/routine/content.go: -------------------------------------------------------------------------------- 1 | package routine 2 | 3 | import "embed" 4 | 5 | //go:embed *.sql 6 | var Statement embed.FS 7 | -------------------------------------------------------------------------------- /example/database/routine/routine.sql: -------------------------------------------------------------------------------- 1 | -- Auto-generated at Thu, 11 Apr 2019 16:15:43 CEST 2 | 3 | -- name: select-all-users 4 | SELECT * FROM users; 5 | 6 | -- name: insert-user 7 | INSERT INTO users (id, first_name, last_name) 8 | VALUES (:id, :first_name, :last_name); 9 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package orm_test 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/phogolabs/orm" 7 | "github.com/phogolabs/orm/dialect/sql" 8 | ) 9 | 10 | func ExampleGateway_First() { 11 | gateway, err := orm.Open("sqlite3", "example.db") 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | user := &User{} 17 | query := orm.Query("SELECT * FROM users ORDER BY created_at") 18 | 19 | if err := gateway.First(context.TODO(), query, user); err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | func ExampleGateway_Only() { 25 | gateway, err := orm.Open("sqlite3", "example.db") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | user := &User{} 31 | query := orm.Query("SELECT * FROM users WHERE id = ?", "007") 32 | 33 | if err := gateway.Only(context.TODO(), query, user); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | func ExampleGateway_Exec() { 39 | gateway, err := orm.Open("sqlite3", "example.db") 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | query := 45 | sql.Insert("users"). 46 | Columns("first_name", "last_name"). 47 | Values("John", "Doe"). 48 | Returning("id") 49 | 50 | if _, err := gateway.Exec(context.TODO(), query); err != nil { 51 | panic(err) 52 | } 53 | } 54 | 55 | func ExampleRoutine() { 56 | gateway, err := orm.Open("sqlite3", "example.db") 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | users := []*User{} 62 | routine := orm.Routine("show-top-5-users") 63 | 64 | if err := gateway.All(context.TODO(), routine, &users); err != nil { 65 | panic(err) 66 | } 67 | } 68 | 69 | func ExampleSQL() { 70 | gateway, err := orm.Open("sqlite3", "example.db") 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | users := []*User{} 76 | query := orm.Query("SELECT name FROM users") 77 | 78 | if err := gateway.All(context.TODO(), query, &users); err != nil { 79 | panic(err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /gateway.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/phogolabs/log" 7 | "github.com/phogolabs/orm/dialect" 8 | "github.com/phogolabs/orm/dialect/sql" 9 | "github.com/phogolabs/prana" 10 | ) 11 | 12 | var _ Querier = &Gateway{} 13 | 14 | // Gateway is connected to a database and can executes SQL queries against it. 15 | type Gateway struct { 16 | engine *engine 17 | } 18 | 19 | // Connect creates a new gateway connecto to the provided URL. 20 | func Connect(url string, opts ...Option) (*Gateway, error) { 21 | driver, source, err := prana.ParseURL(url) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | gateway, err := Open(driver, source, opts...) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | if err = gateway.Ping(context.TODO()); err != nil { 32 | return nil, err 33 | } 34 | 35 | return gateway, nil 36 | } 37 | 38 | // Open creates a new gateway connected to the provided source. 39 | func Open(name, source string, opts ...Option) (*Gateway, error) { 40 | driver, err := sql.Open(name, source) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | dialect := driver.Dialect() 46 | // setup the provider 47 | provider := &sql.Provider{} 48 | provider.SetDialect(dialect) 49 | 50 | gateway := &Gateway{ 51 | engine: &engine{ 52 | querier: driver, 53 | dialect: dialect, 54 | provider: provider, 55 | }, 56 | } 57 | 58 | for _, opt := range opts { 59 | if err := opt.Apply(gateway); err != nil { 60 | return nil, err 61 | } 62 | } 63 | 64 | return gateway, nil 65 | } 66 | 67 | // Ping pins the underlying database 68 | func (g *Gateway) Ping(ctx context.Context) error { 69 | driver := g.engine.querier.(dialect.Driver) 70 | // make a ping request 71 | return driver.Ping(ctx) 72 | } 73 | 74 | // Close closes the connection to the database. 75 | func (g *Gateway) Close() error { 76 | driver := g.engine.querier.(dialect.Driver) 77 | // close the connection 78 | return driver.Close() 79 | } 80 | 81 | // Dialect returns the driver's dialect 82 | func (g *Gateway) Dialect() string { 83 | return g.engine.dialect 84 | } 85 | 86 | // Migrate runs all pending migration 87 | func (g *Gateway) Migrate(storage FileSystem) error { 88 | driver := g.engine.querier.(dialect.Driver) 89 | // run the migration 90 | return driver.Migrate(storage) 91 | } 92 | 93 | // Begin begins a transaction and returns an *Tx 94 | func (g *Gateway) Begin(ctx context.Context) (*GatewayTx, error) { 95 | driver := g.engine.querier.(dialect.Driver) 96 | 97 | tx, err := driver.Tx(ctx) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | gtx := &GatewayTx{ 103 | engine: &engine{ 104 | querier: tx, 105 | dialect: g.engine.dialect, 106 | provider: g.engine.provider, 107 | }, 108 | } 109 | 110 | return gtx, nil 111 | } 112 | 113 | // RunInTx runs a callback function within a transaction. It commits the 114 | // transaction if succeeds, otherwise rollbacks. 115 | func (g *Gateway) RunInTx(ctx context.Context, fn RunTxFunc) error { 116 | logger := log.GetContext(ctx) 117 | 118 | gtx, err := g.Begin(ctx) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | if err := fn(gtx); err != nil { 124 | if rerr := gtx.Rollback(); rerr != nil { 125 | logger.WithError(rerr).Error("cannot rollback") 126 | } 127 | 128 | return err 129 | } 130 | 131 | if cerr := gtx.Commit(); cerr != nil { 132 | logger.WithError(cerr).Error("cannot commit") 133 | return cerr 134 | } 135 | 136 | return nil 137 | } 138 | 139 | // All executes the query and returns a list of entities. 140 | func (g *Gateway) All(ctx context.Context, q sql.Querier, v interface{}) error { 141 | return g.engine.All(ctx, q, v) 142 | } 143 | 144 | // Only returns the only entity in the query, returns an error if not 145 | // exactly one entity was returned. 146 | func (g *Gateway) Only(ctx context.Context, q sql.Querier, v interface{}) error { 147 | return g.engine.Only(ctx, q, v) 148 | } 149 | 150 | // First returns the first entity in the query. Returns *NotFoundError 151 | // when no records were found. 152 | func (g *Gateway) First(ctx context.Context, q sql.Querier, v interface{}) error { 153 | return g.engine.First(ctx, q, v) 154 | } 155 | 156 | // Query executes a query that returns rows, typically a SELECT in SQL. 157 | // It scans the result into the pointer v. In SQL, you it's usually *sql.Rows. 158 | func (g *Gateway) Query(ctx context.Context, q sql.Querier) (*sql.Rows, error) { 159 | return g.engine.Query(ctx, q) 160 | } 161 | 162 | // Exec executes a query that doesn't return rows. For example, in SQL, INSERT 163 | // or UPDATE. It scans the result into the pointer v. In SQL, you it's usually 164 | // sql.Result. 165 | func (g *Gateway) Exec(ctx context.Context, q sql.Querier) (sql.Result, error) { 166 | return g.engine.Exec(ctx, q) 167 | } 168 | -------------------------------------------------------------------------------- /gateway_engine.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/go-openapi/inflect" 10 | "github.com/phogolabs/orm/dialect" 11 | "github.com/phogolabs/orm/dialect/sql" 12 | "github.com/phogolabs/orm/dialect/sql/scan" 13 | "github.com/phogolabs/prana/sqlexec" 14 | ) 15 | 16 | var _ Querier = &engine{} 17 | 18 | type engine struct { 19 | provider *sqlexec.Provider 20 | querier dialect.ExecQuerier 21 | dialect string 22 | } 23 | 24 | // All executes the query and returns a list of entities. 25 | func (g *engine) All(ctx context.Context, q sql.Querier, v interface{}) error { 26 | rows, err := g.Query(ctx, q) 27 | if err != nil { 28 | return err 29 | } 30 | // close the rows 31 | defer rows.Close() 32 | 33 | // scan the rows into the target 34 | if err := scan.Rows(rows, v); err != nil { 35 | return g.wrap(err) 36 | } 37 | 38 | // if the query supports scannable interface 39 | if scanner, ok := q.(scan.Readable); ok { 40 | return scanner.Scan(v) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // Only returns the only entity in the query, returns an error if not 47 | // exactly one entity was returned. 48 | func (g *engine) Only(ctx context.Context, q sql.Querier, v interface{}) error { 49 | rows, err := g.Query(ctx, q) 50 | if err != nil { 51 | return err 52 | } 53 | // close the rows 54 | defer rows.Close() 55 | 56 | // scan the rows into the target 57 | err = scan.Row(rows, v) 58 | 59 | switch { 60 | case err == sql.ErrNoRows: 61 | return &NotFoundError{nameOf(reflect.TypeOf(v))} 62 | case err == scan.ErrOneRow: 63 | return &NotSingularError{nameOf(reflect.TypeOf(v))} 64 | default: 65 | return g.wrap(err) 66 | } 67 | } 68 | 69 | // First returns the first entity in the query. Returns *NotFoundError 70 | // when no user was found. 71 | func (g *engine) First(ctx context.Context, q sql.Querier, v interface{}) error { 72 | rows, err := g.Query(ctx, q) 73 | if err != nil { 74 | return g.wrap(err) 75 | } 76 | // close the rows 77 | defer rows.Close() 78 | 79 | // scan the rows into the target 80 | err = scan.Row(rows, v) 81 | 82 | switch { 83 | case err == sql.ErrNoRows: 84 | return &NotFoundError{nameOf(reflect.TypeOf(v))} 85 | case err == scan.ErrOneRow: 86 | return nil 87 | default: 88 | return g.wrap(err) 89 | } 90 | } 91 | 92 | // Query executes a query that returns rows, typically a SELECT in SQL. 93 | // It scans the result into the pointer v. In SQL, you it's usually *sql.Rows. 94 | func (g *engine) Query(ctx context.Context, q sql.Querier) (*sql.Rows, error) { 95 | // compile the query prior to execution 96 | query, params, err := g.compile(q) 97 | if err != nil { 98 | return nil, g.wrap(err) 99 | } 100 | 101 | rows := &sql.Rows{} 102 | // execute the query into the rows 103 | if err := g.querier.Query(ctx, query, params, rows); err != nil { 104 | return nil, g.wrap(err) 105 | } 106 | 107 | return rows, nil 108 | } 109 | 110 | // Exec executes a query that doesn't return rows. For example, in SQL, INSERT 111 | // or UPDATE. It scans the result into the pointer v. In SQL, you it's usually 112 | // sql.Result. 113 | func (g *engine) Exec(ctx context.Context, q sql.Querier) (sql.Result, error) { 114 | // compile the query prior to execution 115 | query, params, err := g.compile(q) 116 | if err != nil { 117 | return nil, g.wrap(err) 118 | } 119 | 120 | var result sql.Result 121 | // execute the query into the reslt 122 | if err := g.querier.Exec(ctx, query, params, &result); err != nil { 123 | return nil, g.wrap(err) 124 | } 125 | 126 | return result, nil 127 | } 128 | 129 | func (g *engine) compile(stmt sql.Querier) (string, []interface{}, error) { 130 | type QuerierRoutine interface { 131 | Name() string 132 | SetQuery(string) 133 | } 134 | 135 | // find the command if any 136 | if routine, ok := stmt.(QuerierRoutine); ok { 137 | // get the actual SQL query 138 | query, err := g.provider.Query(routine.Name()) 139 | // if getting the query fails 140 | if err != nil { 141 | return "", nil, g.wrap(err) 142 | } 143 | // sets the routine's query 144 | routine.SetQuery(query) 145 | } 146 | 147 | type QuerierDialect interface { 148 | SetDialect(dialect string) 149 | } 150 | 151 | // set the dialect 152 | if query, ok := stmt.(QuerierDialect); ok { 153 | query.SetDialect(g.dialect) 154 | } 155 | 156 | // compile the query 157 | query, params := stmt.Query() 158 | 159 | type QuerierErr interface { 160 | Err() error 161 | } 162 | 163 | // check for errors 164 | if queryErr, ok := stmt.(QuerierErr); ok { 165 | if err := queryErr.Err(); err != nil { 166 | return "", nil, g.wrap(err) 167 | } 168 | } 169 | 170 | return query, params, nil 171 | } 172 | 173 | func (g *engine) wrap(err error) error { 174 | if err == nil { 175 | return nil 176 | } 177 | 178 | var ( 179 | // error as string 180 | errm = err.Error() 181 | // known errors 182 | errors = [...]string{ 183 | // MySQL 1062 error (ER_DUP_ENTRY). 184 | "Error 1062", 185 | // SQLite. 186 | "UNIQUE constraint failed: %s", 187 | // PostgreSQL. 188 | "pq: duplicate key value violates unique constraint %q", 189 | // check constraint 190 | "pq: violates check constraint %q", 191 | // new row check constraint 192 | "pq: new row for relation %q violates check constraint %q", 193 | } 194 | ) 195 | 196 | for _, message := range errors { 197 | // name of the constrain 198 | var name string 199 | // scane the name 200 | fmt.Sscanf(errm, message, &name, &name) 201 | // check 202 | if len(name) > 0 || strings.Contains(errm, message) { 203 | // return the constraint 204 | return &ConstraintError{ 205 | name: name, 206 | wrap: err, 207 | } 208 | } 209 | } 210 | 211 | return err 212 | } 213 | 214 | func nameOf(value reflect.Type) string { 215 | switch value.Kind() { 216 | case reflect.Ptr: 217 | return nameOf(value.Elem()) 218 | case reflect.Slice: 219 | return nameOf(value.Elem()) 220 | case reflect.Array: 221 | return nameOf(value.Elem()) 222 | case reflect.Map: 223 | return "map" 224 | default: 225 | name := inflect.Underscore(value.Name()) 226 | return strings.ToLower(name) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /gateway_option.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/phogolabs/orm/dialect" 7 | "github.com/phogolabs/orm/dialect/sql" 8 | ) 9 | 10 | // Option represents a Gateway option 11 | type Option interface { 12 | Apply(*Gateway) error 13 | } 14 | 15 | // OptionFunc represents a function that can be used to set option 16 | type OptionFunc func(*Gateway) error 17 | 18 | // Apply applies the option 19 | func (fn OptionFunc) Apply(gateway *Gateway) error { 20 | return fn(gateway) 21 | } 22 | 23 | // WithLogger sets the logger 24 | func WithLogger(logger dialect.Logger) Option { 25 | fn := func(g *Gateway) error { 26 | if driver, ok := g.engine.querier.(*sql.Driver); ok { 27 | g.engine.querier = dialect.Log(driver, logger) 28 | } 29 | return nil 30 | } 31 | 32 | return OptionFunc(fn) 33 | } 34 | 35 | // WithMaxIdleConns sets the maximum number of connections in the idle 36 | // connection pool. 37 | // 38 | // If MaxOpenConns is greater than 0 but less than the new MaxIdleConns, 39 | // then the new MaxIdleConns will be reduced to match the MaxOpenConns limit. 40 | // 41 | // If n <= 0, no idle connections are retained. 42 | // 43 | // The default max idle connections is currently 2. This may change in 44 | // a future release. 45 | func WithMaxIdleConns(value int) Option { 46 | fn := func(g *Gateway) error { 47 | if driver, ok := g.engine.querier.(*sql.Driver); ok { 48 | driver.DB().SetMaxIdleConns(value) 49 | } 50 | return nil 51 | } 52 | 53 | return OptionFunc(fn) 54 | } 55 | 56 | // WithMaxOpenConns sets the maximum number of open connections to the database. 57 | // 58 | // If MaxIdleConns is greater than 0 and the new MaxOpenConns is less than 59 | // MaxIdleConns, then MaxIdleConns will be reduced to match the new 60 | // MaxOpenConns limit. 61 | // 62 | // If n <= 0, then there is no limit on the number of open connections. 63 | // The default is 0 (unlimited). 64 | func WithMaxOpenConns(value int) Option { 65 | fn := func(g *Gateway) error { 66 | if driver, ok := g.engine.querier.(*sql.Driver); ok { 67 | driver.DB().SetMaxOpenConns(value) 68 | } 69 | return nil 70 | } 71 | 72 | return OptionFunc(fn) 73 | } 74 | 75 | // WithConnMaxLifetime sets the maximum amount of time a connection may be reused. 76 | // 77 | // Expired connections may be closed lazily before reuse. 78 | // 79 | // If d <= 0, connections are reused forever. 80 | func WithConnMaxLifetime(duration time.Duration) Option { 81 | fn := func(g *Gateway) error { 82 | if driver, ok := g.engine.querier.(*sql.Driver); ok { 83 | driver.DB().SetConnMaxLifetime(duration) 84 | } 85 | return nil 86 | } 87 | 88 | return OptionFunc(fn) 89 | } 90 | 91 | // WithRoutine creates the gateway with a given routine 92 | func WithRoutine(source FileSystem) Option { 93 | fn := func(g *Gateway) error { 94 | if err := g.engine.provider.ReadDir(source); err != nil { 95 | return err 96 | } 97 | return nil 98 | } 99 | 100 | return OptionFunc(fn) 101 | } 102 | -------------------------------------------------------------------------------- /gateway_test.go: -------------------------------------------------------------------------------- 1 | package orm_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/bxcodec/faker/v3" 8 | "github.com/phogolabs/orm" 9 | "github.com/phogolabs/orm/dialect/sql" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("Open", func() { 16 | Context("when cannot open the database", func() { 17 | It("returns an error", func() { 18 | gateway, err := orm.Open("sqlite4", "/tmp/orm.db") 19 | Expect(gateway).To(BeNil()) 20 | Expect(err).To(MatchError(`sql: unknown driver "sqlite4" (forgotten import?)`)) 21 | }) 22 | }) 23 | 24 | Context("when the dns is not wrong", func() { 25 | It("returns an error", func() { 26 | gateway, err := orm.Open("mysql", "localhost") 27 | Expect(gateway).To(BeNil()) 28 | Expect(err).To(MatchError(`invalid DSN: missing the slash separating the database name`)) 29 | }) 30 | }) 31 | }) 32 | 33 | var _ = Describe("Connect", func() { 34 | It("opens the URL successfully", func() { 35 | gateway, err := orm.Connect("sqlite3://orm.db") 36 | Expect(err).To(BeNil()) 37 | Expect(gateway).NotTo(BeNil()) 38 | Expect(gateway.Close()).To(Succeed()) 39 | }) 40 | 41 | Context("when cannot open the database", func() { 42 | It("returns an error", func() { 43 | gateway, err := orm.Connect("unknown://") 44 | Expect(err).To(MatchError(`sql: unknown driver "unknown" (forgotten import?)`)) 45 | Expect(gateway).To(BeNil()) 46 | }) 47 | }) 48 | 49 | Context("when the dns is not wrong", func() { 50 | It("returns an error", func() { 51 | gateway, err := orm.Connect("sqlite3://localhost:5430/db") 52 | Expect(gateway).To(BeNil()) 53 | Expect(err).To(MatchError("unable to open database file: no such file or directory")) 54 | }) 55 | }) 56 | }) 57 | 58 | var _ = Describe("Gateway", func() { 59 | var ( 60 | ctx context.Context 61 | gateway *orm.Gateway 62 | ) 63 | 64 | BeforeEach(func() { 65 | var err error 66 | 67 | gateway, err = orm.Open("sqlite3", "file:test.db?cache=shared&mode=memory") 68 | Expect(err).To(BeNil()) 69 | 70 | ctx = context.TODO() 71 | 72 | query := 73 | sql.CreateTable("users").IfNotExists(). 74 | Columns( 75 | sql.Column("id").Type("int"), 76 | sql.Column("first_name").Type("varchar(255)").Attr("NOT NULL"), 77 | sql.Column("last_name").Type("varchar(255)").Attr("NOT NULL"), 78 | sql.Column("email").Type("varchar(255)").Attr("NULL"), 79 | sql.Column("created_at").Type("timestamp").Attr("NULL"), 80 | ). 81 | PrimaryKey("id") 82 | 83 | _, err = gateway.Exec(ctx, query) 84 | Expect(err).To(Succeed()) 85 | 86 | for i := 0; i < 10; i++ { 87 | query := 88 | sql.Insert("users"). 89 | Columns("id", "first_name", "last_name", "email"). 90 | Values(i, faker.FirstName(), faker.LastName(), faker.Email()). 91 | Returning("id") 92 | 93 | _, err := gateway.Exec(ctx, query) 94 | Expect(err).To(Succeed()) 95 | } 96 | }) 97 | 98 | AfterEach(func() { 99 | _, err := gateway.Exec(ctx, sql.Raw("DELETE FROM users")) 100 | Expect(err).To(Succeed()) 101 | 102 | Expect(gateway.Close()).To(Succeed()) 103 | }) 104 | 105 | Describe("Begin", func() { 106 | It("starts new transaction", func() { 107 | tx, err := gateway.Begin(ctx) 108 | Expect(err).NotTo(HaveOccurred()) 109 | Expect(tx).NotTo(BeNil()) 110 | }) 111 | 112 | Context("when the gateway is closed", func() { 113 | It("returns an error", func() { 114 | gateway, err := orm.Open("sqlite3", "file:test.db?cache=shared&mode=memory") 115 | Expect(err).To(BeNil()) 116 | Expect(gateway.Close()).To(Succeed()) 117 | 118 | tx, err := gateway.Begin(ctx) 119 | Expect(err).To(MatchError("sql: database is closed")) 120 | Expect(tx).To(BeNil()) 121 | }) 122 | }) 123 | }) 124 | 125 | Describe("RunInTx", func() { 126 | It("starts new transaction", func() { 127 | err := gateway.RunInTx(ctx, func(tx *orm.GatewayTx) error { 128 | entities := []*User{} 129 | Expect(tx.All(ctx, sql.Raw("SELECT * FROM users"), &entities)).To(Succeed()) 130 | 131 | entity := &User{} 132 | Expect(tx.Only(ctx, sql.Raw("SELECT * FROM users WHERE id = 0"), entity)).To(Succeed()) 133 | Expect(tx.First(ctx, sql.Raw("SELECT * FROM users WHERE id = 0"), entity)).To(Succeed()) 134 | 135 | _, err := tx.Exec(ctx, sql.Raw("UPDATE users SET created_at = strftime()")) 136 | Expect(err).To(Succeed()) 137 | 138 | rows, err := tx.Query(ctx, sql.Raw("UPDATE users SET created_at = strftime()")) 139 | Expect(err).To(Succeed()) 140 | Expect(rows.Close()).To(Succeed()) 141 | 142 | return nil 143 | }) 144 | 145 | Expect(err).NotTo(HaveOccurred()) 146 | }) 147 | 148 | Context("when the gateway is closed", func() { 149 | It("returns an error", func() { 150 | gateway, err := orm.Open("sqlite3", "file:test.db?cache=shared&mode=memory") 151 | Expect(err).To(BeNil()) 152 | Expect(gateway.Close()).To(Succeed()) 153 | 154 | err = gateway.RunInTx(ctx, func(tx *orm.GatewayTx) error { 155 | return nil 156 | }) 157 | 158 | Expect(err).To(MatchError("sql: database is closed")) 159 | }) 160 | }) 161 | 162 | Context("when the transaction fails", func() { 163 | It("returns an error", func() { 164 | err := gateway.RunInTx(ctx, func(tx *orm.GatewayTx) error { 165 | Expect(tx).NotTo(BeNil()) 166 | return fmt.Errorf("oh no") 167 | }) 168 | 169 | Expect(err).To(MatchError("oh no")) 170 | }) 171 | }) 172 | }) 173 | 174 | Describe("Dialect", func() { 175 | It("returns the dialect", func() { 176 | Expect(gateway.Dialect()).To(Equal("sqlite3")) 177 | }) 178 | }) 179 | 180 | Describe("All", func() { 181 | It("returns all entities", func() { 182 | entities := []*User{} 183 | 184 | Expect(gateway.All(ctx, sql.Raw("SELECT * FROM users"), &entities)).To(Succeed()) 185 | Expect(entities).To(HaveLen(10)) 186 | 187 | Expect(entities[0].ID).To(Equal(0)) 188 | Expect(entities[0].Email).NotTo(BeNil()) 189 | 190 | Expect(entities[1].ID).To(Equal(1)) 191 | Expect(entities[1].Email).NotTo(BeNil()) 192 | }) 193 | 194 | Context("when the database operation fail", func() { 195 | It("returns an error", func() { 196 | entities := []*User{} 197 | 198 | Expect(gateway.All(ctx, sql.Raw("SELECT * FROM unknown.users"), &entities)).To(MatchError("no such table: unknown.users")) 199 | Expect(entities).To(HaveLen(0)) 200 | }) 201 | }) 202 | 203 | Context("when the routine is unknown", func() { 204 | It("returns an error", func() { 205 | entities := []*User{} 206 | 207 | err := gateway.All(ctx, sql.Routine("my-unknown-routine"), entities) 208 | Expect(err).To(MatchError("query 'my-unknown-routine' not found")) 209 | }) 210 | }) 211 | }) 212 | 213 | Describe("Only", func() { 214 | It("returns the first entity", func() { 215 | entity := &User{} 216 | Expect(gateway.Only(ctx, sql.Raw("SELECT * FROM users WHERE id = 0"), entity)).To(Succeed()) 217 | 218 | Expect(entity.ID).To(Equal(0)) 219 | Expect(entity.Email).NotTo(BeNil()) 220 | }) 221 | 222 | Context("when the provided type is not compatible", func() { 223 | It("returns an error", func() { 224 | entity := "root" 225 | Expect(gateway.Only(ctx, sql.Raw("SELECT * FROM users WHERE id = 0"), &entity)).To(MatchError("sql/scan: columns do not match (5 > 1)")) 226 | }) 227 | }) 228 | 229 | Context("when there are more than one entities", func() { 230 | It("returns an error", func() { 231 | entity := &User{} 232 | Expect(gateway.Only(ctx, sql.Raw("SELECT * FROM users"), entity)).To(MatchError("orm: user not singular")) 233 | }) 234 | }) 235 | 236 | Context("when there are not entities", func() { 237 | It("returns an error", func() { 238 | entity := &User{} 239 | Expect(gateway.Only(ctx, sql.Raw("SELECT * FROM users WHERE id > 1000"), entity)).To(MatchError("orm: user not found")) 240 | }) 241 | }) 242 | 243 | Context("when the database operation fail", func() { 244 | It("returns an error", func() { 245 | entity := &User{} 246 | Expect(gateway.Only(ctx, sql.Raw("SELECT * FROM unknown.users"), entity)).To(MatchError("no such table: unknown.users")) 247 | }) 248 | }) 249 | 250 | Context("when the routine is unknown", func() { 251 | It("returns an error", func() { 252 | entity := &User{} 253 | err := gateway.Only(ctx, sql.Routine("my-unknown-routine"), entity) 254 | Expect(err).To(MatchError("query 'my-unknown-routine' not found")) 255 | }) 256 | }) 257 | }) 258 | 259 | Describe("First", func() { 260 | It("returns the first entity", func() { 261 | entity := &User{} 262 | Expect(gateway.First(ctx, sql.Raw("SELECT * FROM users"), entity)).To(Succeed()) 263 | 264 | Expect(entity.ID).To(Equal(0)) 265 | Expect(entity.Email).NotTo(BeNil()) 266 | }) 267 | 268 | Context("when the provided type is not compatible", func() { 269 | It("returns an error", func() { 270 | entity := "root" 271 | Expect(gateway.First(ctx, sql.Raw("SELECT * FROM users WHERE id = 0"), &entity)).To(MatchError("sql/scan: columns do not match (5 > 1)")) 272 | }) 273 | }) 274 | 275 | Context("when there are not entities", func() { 276 | It("returns an error", func() { 277 | entity := &User{} 278 | Expect(gateway.First(ctx, sql.Raw("SELECT * FROM users WHERE id > 1000"), entity)).To(MatchError("orm: user not found")) 279 | }) 280 | }) 281 | 282 | Context("when the database operation fail", func() { 283 | It("returns an error", func() { 284 | entity := &User{} 285 | Expect(gateway.First(ctx, sql.Raw("SELECT * FROM unknown.users"), entity)).To(MatchError("no such table: unknown.users")) 286 | }) 287 | }) 288 | 289 | Context("when the routine is unknown", func() { 290 | It("returns an error", func() { 291 | entity := &User{} 292 | err := gateway.First(ctx, sql.Routine("my-unknown-routine"), entity) 293 | Expect(err).To(MatchError("query 'my-unknown-routine' not found")) 294 | }) 295 | }) 296 | }) 297 | 298 | Describe("Exec", func() { 299 | Context("when the query has wrong syntax", func() { 300 | It("returns an error", func() { 301 | _, err := gateway.Exec(ctx, sql.Raw("SELECT * FROM unknown.users")) 302 | Expect(err).To(MatchError("no such table: unknown.users")) 303 | }) 304 | }) 305 | 306 | Context("when the routine is unknown", func() { 307 | It("returns an error", func() { 308 | _, err := gateway.Exec(ctx, sql.Routine("my-unknown-routine")) 309 | Expect(err).To(MatchError("query 'my-unknown-routine' not found")) 310 | }) 311 | }) 312 | }) 313 | }) 314 | -------------------------------------------------------------------------------- /gateway_tx.go: -------------------------------------------------------------------------------- 1 | package orm 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/phogolabs/orm/dialect" 7 | "github.com/phogolabs/orm/dialect/sql" 8 | ) 9 | 10 | var _ Querier = &GatewayTx{} 11 | 12 | // RunTxFunc is a transaction function 13 | type RunTxFunc func(gateway *GatewayTx) error 14 | 15 | // GatewayTx represents a gateway in transaction 16 | type GatewayTx struct { 17 | engine *engine 18 | } 19 | 20 | // All executes the query and returns a list of entities. 21 | func (g *GatewayTx) All(ctx context.Context, q sql.Querier, v interface{}) error { 22 | return g.engine.All(ctx, q, v) 23 | } 24 | 25 | // Only returns the only entity in the query, returns an error if not 26 | // exactly one entity was returned. 27 | func (g *GatewayTx) Only(ctx context.Context, q sql.Querier, v interface{}) error { 28 | return g.engine.Only(ctx, q, v) 29 | } 30 | 31 | // First returns the first entity in the query. Returns *NotFoundError 32 | // when no user was found. 33 | func (g *GatewayTx) First(ctx context.Context, q sql.Querier, v interface{}) error { 34 | return g.engine.First(ctx, q, v) 35 | } 36 | 37 | // Query executes a query that returns rows, typically a SELECT in SQL. 38 | // It scans the result into the pointer v. In SQL, you it's usually *sql.Rows. 39 | func (g *GatewayTx) Query(ctx context.Context, q sql.Querier) (*sql.Rows, error) { 40 | return g.engine.Query(ctx, q) 41 | } 42 | 43 | // Exec executes a query that doesn't return rows. For example, in SQL, INSERT 44 | // or UPDATE. It scans the result into the pointer v. In SQL, you it's usually 45 | // sql.Result. 46 | func (g *GatewayTx) Exec(ctx context.Context, q sql.Querier) (sql.Result, error) { 47 | return g.engine.Exec(ctx, q) 48 | } 49 | 50 | // Commit commits the transaction 51 | func (g *GatewayTx) Commit() error { 52 | tx := g.engine.querier.(dialect.Tx) 53 | return tx.Commit() 54 | } 55 | 56 | // Rollback rollbacks the transaction 57 | func (g *GatewayTx) Rollback() error { 58 | tx := g.engine.querier.(dialect.Tx) 59 | return tx.Rollback() 60 | } 61 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/phogolabs/orm 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/bxcodec/faker/v3 v3.8.0 7 | github.com/go-openapi/inflect v0.19.0 8 | github.com/jmoiron/sqlx v1.3.5 9 | github.com/mattn/go-sqlite3 v1.14.16 10 | github.com/onsi/ginkgo/v2 v2.7.0 11 | github.com/onsi/gomega v1.24.1 12 | github.com/phogolabs/log v0.0.0-20210430125128-bb23cd1dfac5 13 | github.com/phogolabs/prana v1.0.0-rc10 14 | github.com/stretchr/testify v1.8.1 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/fatih/color v1.13.0 // indirect 20 | github.com/go-logr/logr v1.2.3 // indirect 21 | github.com/go-sql-driver/mysql v1.6.0 // indirect 22 | github.com/golang/protobuf v1.5.2 // indirect 23 | github.com/google/go-cmp v0.5.9 // indirect 24 | github.com/gosuri/uitable v0.0.4 // indirect 25 | github.com/mattn/go-colorable v0.1.13 // indirect 26 | github.com/mattn/go-isatty v0.0.16 // indirect 27 | github.com/mattn/go-runewidth v0.0.14 // indirect 28 | github.com/olekukonko/tablewriter v0.0.5 // indirect 29 | github.com/phogolabs/flaw v0.0.0-20210430130223-f948049b189e // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/rivo/uniseg v0.4.3 // indirect 32 | golang.org/x/net v0.3.0 // indirect 33 | golang.org/x/sys v0.3.0 // indirect 34 | golang.org/x/text v0.5.0 // indirect 35 | google.golang.org/genproto v0.0.0-20221116193143-41c2ba794472 // indirect 36 | google.golang.org/grpc v1.50.1 // indirect 37 | google.golang.org/protobuf v1.28.1 // indirect 38 | gopkg.in/yaml.v3 v3.0.1 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 17 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 18 | cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= 19 | cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= 20 | cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= 21 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 22 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 23 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 24 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 25 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 26 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 27 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 28 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 29 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 30 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 31 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 32 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 33 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 34 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 35 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 36 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 37 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 42 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 43 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 44 | github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= 45 | github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= 46 | github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= 47 | github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 48 | github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= 49 | github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 50 | github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 51 | github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 52 | github.com/aws/aws-sdk-go v1.25.43/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 53 | github.com/aws/aws-sdk-go v1.38.29/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= 54 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= 55 | github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= 56 | github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 57 | github.com/blang/vfs v1.0.0/go.mod h1:jjuNUc/IKcRNNWC9NUCvz4fR9PZLPIKxEygtPs/4tSI= 58 | github.com/bxcodec/faker/v3 v3.8.0 h1:F59Qqnsh0BOtZRC+c4cXoB/VNYDMS3R5mlSpxIap1oU= 59 | github.com/bxcodec/faker/v3 v3.8.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM= 60 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 61 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 62 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 63 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 64 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 65 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 66 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 67 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 68 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 69 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 70 | github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= 71 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 72 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 73 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 74 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 75 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 76 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 77 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 78 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 79 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 80 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= 81 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 82 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 83 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 84 | github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 85 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 86 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 87 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 88 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 89 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 90 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 91 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 92 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 93 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 94 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 95 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 96 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 97 | github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= 98 | github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= 99 | github.com/go-playground/ansi v2.1.0+incompatible h1:f9ldskdk1seTFmYjbmPaYB+WYsDKWc4UXcGb+e9JrN8= 100 | github.com/go-playground/ansi v2.1.0+incompatible/go.mod h1:OCdnfTFO/GfFtp+ktUt+PhElbGOwyTRUuRUsA+Y5pSU= 101 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 102 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 103 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 104 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 105 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 106 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 107 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 108 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 109 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 110 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 111 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 112 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 113 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 114 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 115 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 116 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 117 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 118 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 119 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 120 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 121 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 122 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 123 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 124 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 125 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 126 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 127 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 128 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 129 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 130 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 131 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 132 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 133 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 134 | github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= 135 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 136 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 137 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 138 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 139 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 140 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 141 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 142 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 143 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 144 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 145 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 146 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 147 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 148 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 149 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 150 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 151 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 152 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 153 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 154 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 155 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 156 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 157 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 158 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 159 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 160 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 161 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 162 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 163 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 164 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 165 | github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 166 | github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 167 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 168 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 169 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 170 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 171 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 172 | github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= 173 | github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= 174 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 175 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 176 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 177 | github.com/hashicorp/hcl/v2 v2.10.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= 178 | github.com/hashicorp/hcl/v2 v2.10.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= 179 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 180 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 181 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 182 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 183 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 184 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 185 | github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= 186 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 187 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 188 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 189 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 190 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 191 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 192 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 193 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 194 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 195 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 196 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 197 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 198 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 199 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 200 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 201 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 202 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 203 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 204 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 205 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 206 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 207 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 208 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 209 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 210 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 211 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 212 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 213 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 214 | github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 215 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 216 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 217 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 218 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 219 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 220 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 221 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 222 | github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 223 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 224 | github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 225 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 226 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 227 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 228 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 229 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 230 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 231 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 232 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 233 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 234 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 235 | github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 236 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 237 | github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= 238 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 239 | github.com/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= 240 | github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= 241 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 242 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 243 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 244 | github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= 245 | github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= 246 | github.com/phogolabs/cli v0.0.0-20191212161310-ce689d871370/go.mod h1:grzrc/EIac+v5wd6EjBB4a9obKGGIdsgWhPIsqjBGLo= 247 | github.com/phogolabs/cli v0.0.0-20210430125239-bcee8250ce56/go.mod h1:i5jRM2v76D0igq/IAXgQppkc6vyzinF2smhpyx38rMM= 248 | github.com/phogolabs/flaw v0.0.0-20210306145544-6984b46f2d8f/go.mod h1:P+5bxKo3KEVGUc/bnK5w2M9dx/90ap6PZiDVZ3uYpQ4= 249 | github.com/phogolabs/flaw v0.0.0-20210430130223-f948049b189e h1:C1p4v5IGXYyAbZr5j0G2rLEBgrCWP6P+grVufV7kkKM= 250 | github.com/phogolabs/flaw v0.0.0-20210430130223-f948049b189e/go.mod h1:XDzw4DJjytL63OyIsJyKlrzV3dsPb656hZEt2sGL5h0= 251 | github.com/phogolabs/log v0.0.0-20210430124854-15b4a15a74cc/go.mod h1:IvLsZtJyw+eLkm7BNaRFrfz33UNKtrsNNz+hj6RVC+A= 252 | github.com/phogolabs/log v0.0.0-20210430125128-bb23cd1dfac5 h1:MlaknYljrhKCT3wBN35sDwJvaZH5db/vh8pgl2FEP6E= 253 | github.com/phogolabs/log v0.0.0-20210430125128-bb23cd1dfac5/go.mod h1:IvLsZtJyw+eLkm7BNaRFrfz33UNKtrsNNz+hj6RVC+A= 254 | github.com/phogolabs/parcello v0.8.1/go.mod h1:/HlY+yKSdyM8MUX9YvwT3+sED9SKXizc5zfuHDh6+to= 255 | github.com/phogolabs/parcello v0.8.2/go.mod h1:rtKgAK1yF6KZOllQDfcHvOzYYwV3kCt61LJybif3SCE= 256 | github.com/phogolabs/prana v1.0.0-rc10 h1:ybUEW5td+wvPmVeQqZMpo9pqKN5JXGk30JTyvj/3nR0= 257 | github.com/phogolabs/prana v1.0.0-rc10/go.mod h1:y92Ktk/rxzeRC8/uf4Iq0X6AeTU+/KOwIPejZ564aiU= 258 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 259 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 260 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 261 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 262 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 263 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 264 | github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= 265 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 266 | github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 267 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 268 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 269 | github.com/rollbar/rollbar-go v1.4.0 h1:0hbintwgQUMvgGVAZiV3vOAtL71316PBI1o49PGzIzw= 270 | github.com/rollbar/rollbar-go v1.4.0/go.mod h1:kLQ9gP3WCRGrvJmF0ueO3wK9xWocej8GRX98D8sa39w= 271 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 272 | github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= 273 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= 274 | github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= 275 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 276 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 277 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 278 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 279 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 280 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 281 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 282 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 283 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 284 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 285 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 286 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 287 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 288 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 289 | github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= 290 | github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= 291 | github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= 292 | github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= 293 | github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= 294 | github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= 295 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 296 | github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 297 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 298 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 299 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 300 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 301 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 302 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 303 | github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= 304 | github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= 305 | github.com/zclconf/go-cty v1.8.2/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= 306 | github.com/zclconf/go-cty v1.9.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= 307 | github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= 308 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 309 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 310 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 311 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 312 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 313 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 314 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 315 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 316 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 317 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 318 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 319 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 320 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 321 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 322 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 323 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 324 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 325 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 326 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 327 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 328 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 329 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 330 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 331 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 332 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 333 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 334 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 335 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 336 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 337 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 338 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 339 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 340 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 341 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 342 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 343 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 344 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 345 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 346 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 347 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 348 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 349 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 350 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 351 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 352 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 353 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 354 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 355 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 356 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 357 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 358 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 359 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 360 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 361 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 362 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 363 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 364 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 365 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 366 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 367 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 368 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 369 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 370 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 371 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 372 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 373 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 374 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 375 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 376 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 377 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 378 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 379 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 380 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 381 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 382 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 383 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 384 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 385 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 386 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 387 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 388 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 389 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 390 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 391 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 392 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 393 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= 394 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 395 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 396 | golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= 397 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 398 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 399 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 400 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 401 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 402 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 403 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 404 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 405 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 406 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 407 | golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 408 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 409 | golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 410 | golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 411 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 412 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 413 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 414 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 415 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 416 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 417 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 418 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 419 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 420 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 421 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 422 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 423 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 424 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 425 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 426 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 427 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 428 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 429 | golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 430 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 431 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 432 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 433 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 434 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 435 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 436 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 437 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 438 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 439 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 440 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 441 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 442 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 443 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 444 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 445 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 448 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 449 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 450 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 451 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 452 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 453 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 454 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 455 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 456 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 457 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 458 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 459 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 460 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 461 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 462 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 463 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 464 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 465 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 466 | golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 467 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 468 | golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 469 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 470 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 471 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 472 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 473 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 474 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 475 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 476 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 477 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 478 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 479 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 480 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 481 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 482 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 483 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 484 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 485 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 486 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 487 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 488 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 489 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 490 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 491 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 492 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 493 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 494 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 495 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 496 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 497 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 498 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 499 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 500 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 501 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 502 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 503 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 504 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 505 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 506 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 507 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 508 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 509 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 510 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 511 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 512 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 513 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 514 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 515 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 516 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 517 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 518 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 519 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 520 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 521 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 522 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 523 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 524 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 525 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 526 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 527 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 528 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 529 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 530 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 531 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 532 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 533 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 534 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 535 | golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= 536 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 537 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 538 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 539 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 540 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 541 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 542 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 543 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 544 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 545 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 546 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 547 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 548 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 549 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 550 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 551 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 552 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 553 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 554 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 555 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 556 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 557 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 558 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 559 | google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= 560 | google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= 561 | google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= 562 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 563 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 564 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 565 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 566 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 567 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 568 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 569 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 570 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 571 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 572 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 573 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 574 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 575 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 576 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 577 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 578 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 579 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 580 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 581 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 582 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 583 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 584 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 585 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 586 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 587 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 588 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 589 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 590 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 591 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 592 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 593 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 594 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 595 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 596 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 597 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 598 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 599 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 600 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 601 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 602 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 603 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 604 | google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 605 | google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 606 | google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 607 | google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 608 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= 609 | google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= 610 | google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= 611 | google.golang.org/genproto v0.0.0-20210726200206-e7812ac95cc0/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= 612 | google.golang.org/genproto v0.0.0-20221116193143-41c2ba794472 h1:kIfItBRE5gkUKpH4H5lNGciZbka1JrmRli3ArqrKFkA= 613 | google.golang.org/genproto v0.0.0-20221116193143-41c2ba794472/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= 614 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 615 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 616 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 617 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 618 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 619 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 620 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 621 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 622 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 623 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 624 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 625 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 626 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 627 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 628 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 629 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 630 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 631 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 632 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 633 | google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 634 | google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= 635 | google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= 636 | google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= 637 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 638 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 639 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 640 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 641 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 642 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 643 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 644 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 645 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 646 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 647 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 648 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 649 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 650 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 651 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 652 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 653 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 654 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 655 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 656 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 657 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 658 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 659 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 660 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 661 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 662 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 663 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 664 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 665 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 666 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 667 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 668 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 669 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 670 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 671 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 672 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 673 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 674 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 675 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 676 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 677 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 678 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 679 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 680 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 681 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 682 | -------------------------------------------------------------------------------- /media/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phogolabs/orm/694567495a4884083ae22f712e48fa2950afcdc5/media/img/logo.png -------------------------------------------------------------------------------- /suite_test.go: -------------------------------------------------------------------------------- 1 | package orm_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | _ "github.com/mattn/go-sqlite3" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestORM(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "ORM Suite") 15 | } 16 | 17 | // User represents a user 18 | type User struct { 19 | ID int `db:"id,primary_key,not_null,read_only"` 20 | FirstName string `db:"first_name,not_null"` 21 | LastName string `db:"last_name,not_null"` 22 | Email *string `db:"email"` 23 | CreatedAt *time.Time `db:"created_at"` 24 | } 25 | --------------------------------------------------------------------------------