├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── adapter.go ├── adapter ├── cockroachdb │ ├── README.md │ ├── cockroachdb.go │ ├── collection.go │ ├── connection.go │ ├── connection_pgx.go │ ├── connection_pgx_test.go │ ├── connection_pq.go │ ├── connection_pq_test.go │ ├── custom_types.go │ ├── custom_types_pgx.go │ ├── custom_types_pq.go │ ├── custom_types_test.go │ ├── database.go │ ├── database_pgx.go │ ├── database_pq.go │ ├── template.go │ └── template_test.go ├── mongo │ ├── README.md │ ├── collection.go │ ├── connection.go │ ├── connection_test.go │ ├── database.go │ └── result.go ├── mssql │ ├── README.md │ ├── collection.go │ ├── connection.go │ ├── connection_test.go │ ├── database.go │ ├── mssql.go │ ├── template.go │ └── template_test.go ├── mysql │ ├── README.md │ ├── collection.go │ ├── connection.go │ ├── connection_test.go │ ├── custom_types.go │ ├── database.go │ ├── mysql.go │ ├── template.go │ └── template_test.go ├── postgresql │ ├── README.md │ ├── collection.go │ ├── connection.go │ ├── connection_pgx.go │ ├── connection_pgx_test.go │ ├── connection_pq.go │ ├── connection_pq_test.go │ ├── custom_types.go │ ├── custom_types_pgx.go │ ├── custom_types_pq.go │ ├── custom_types_test.go │ ├── database.go │ ├── database_pgx.go │ ├── database_pq.go │ ├── postgresql.go │ ├── template.go │ └── template_test.go ├── ql │ ├── collection.go │ ├── connection.go │ ├── connection_test.go │ ├── database.go │ ├── generic_test.go │ ├── helper_test.go │ ├── ql.go │ ├── sql_test.go │ ├── template.go │ └── template_test.go └── sqlite │ ├── README.md │ ├── collection.go │ ├── connection.go │ ├── connection_test.go │ ├── database.go │ ├── generic_test.go │ ├── helper_test.go │ ├── record_test.go │ ├── sql_test.go │ ├── sqlite.go │ ├── sqlite_test.go │ ├── template.go │ └── template_test.go ├── clauses.go ├── collection.go ├── comparison.go ├── comparison_test.go ├── cond.go ├── cond_test.go ├── connection_url.go ├── context.go ├── db.go ├── errors.go ├── function.go ├── function_test.go ├── go.mod ├── go.sum ├── internal ├── adapter │ ├── comparison.go │ ├── constraint.go │ ├── func.go │ ├── logical_expr.go │ └── raw.go ├── cache │ ├── cache.go │ ├── cache_test.go │ ├── hash.go │ └── interface.go ├── immutable │ └── immutable.go ├── reflectx │ ├── LICENSE │ ├── README.md │ ├── reflect.go │ └── reflect_test.go ├── sqladapter │ ├── collection.go │ ├── compat │ │ ├── query.go │ │ └── query_go18.go │ ├── exql │ │ ├── column.go │ │ ├── column_test.go │ │ ├── column_value.go │ │ ├── column_value_test.go │ │ ├── columns.go │ │ ├── columns_test.go │ │ ├── database.go │ │ ├── database_test.go │ │ ├── default.go │ │ ├── errors.go │ │ ├── group_by.go │ │ ├── group_by_test.go │ │ ├── interfaces.go │ │ ├── join.go │ │ ├── join_test.go │ │ ├── order_by.go │ │ ├── order_by_test.go │ │ ├── raw.go │ │ ├── raw_test.go │ │ ├── returning.go │ │ ├── statement.go │ │ ├── statement_test.go │ │ ├── table.go │ │ ├── table_test.go │ │ ├── template.go │ │ ├── types.go │ │ ├── utilities.go │ │ ├── utilities_test.go │ │ ├── value.go │ │ ├── value_test.go │ │ ├── where.go │ │ └── where_test.go │ ├── hash.go │ ├── record.go │ ├── result.go │ ├── session.go │ ├── sqladapter.go │ ├── sqladapter_test.go │ └── statement.go └── sqlbuilder │ ├── batch.go │ ├── builder.go │ ├── builder_test.go │ ├── comparison.go │ ├── convert.go │ ├── custom_types.go │ ├── delete.go │ ├── errors.go │ ├── fetch.go │ ├── insert.go │ ├── paginate.go │ ├── placeholder_test.go │ ├── scanner.go │ ├── select.go │ ├── sqlbuilder.go │ ├── template.go │ ├── update.go │ └── wrapper.go ├── intersection.go ├── iterator.go ├── logger.go ├── marshal.go ├── raw.go ├── record.go ├── result.go ├── session.go ├── settings.go ├── sql.go ├── store.go ├── tests ├── Makefile ├── adapter_config_test.go ├── ansible │ ├── Makefile │ ├── ansible.cfg │ ├── inventory.yml │ ├── playbook.yml │ ├── requirements.txt │ └── roles │ │ ├── cockroachdb │ │ └── tasks │ │ │ └── main.yml │ │ ├── mongodb │ │ └── tasks │ │ │ └── main.yml │ │ ├── mssql │ │ └── tasks │ │ │ └── main.yml │ │ ├── mysql │ │ └── tasks │ │ │ └── main.yml │ │ └── postgresql │ │ └── tasks │ │ └── main.yml ├── cockroachdb │ ├── cockroachdb.go │ └── helper.go ├── entrypoint_test.go ├── generic_suite_test.go ├── mongo │ └── helper.go ├── mssql │ └── helper.go ├── mysql │ ├── helper.go │ └── mysql.go ├── postgresql │ ├── helper.go │ └── postgresql.go ├── record_suite_test.go └── sql_suite_test.go └── union.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | name: unit-tests 8 | 9 | jobs: 10 | 11 | test: 12 | 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | go-version: [1.22.x, 1.23.x, 1.24.x] 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | 24 | - name: Install Go 25 | uses: actions/setup-go@v2 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | - name: Configure environment 30 | run: | 31 | echo "GOARCH=amd64" >> $GITHUB_ENV 32 | echo "DB_HOST=127.0.0.1" >> $GITHUB_ENV 33 | echo "UPPER_DB_LOG=ERROR" >> $GITHUB_ENV 34 | 35 | - name: Run tests 36 | run: make test 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | *.db 3 | *.tmp 4 | .venv 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | UPPER_DB_LOG ?= ERROR 2 | 3 | export UPPER_DB_LOG 4 | 5 | bench: 6 | go test -v -benchtime=500ms -bench=. ./internal/... 7 | 8 | test: 9 | $(MAKE) -C tests 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | upper/db unit tests status 7 |

8 | 9 | # upper/db 10 | 11 | `upper/db` is a productive data access layer (DAL) for [Go](https://golang.org) 12 | that provides agnostic tools to work with different data sources, such as: 13 | 14 | * [PostgreSQL](https://upper.io/v4/adapter/postgresql) 15 | * [MySQL](https://upper.io/v4/adapter/mysql) 16 | * [MSSQL](https://upper.io/v4/adapter/mssql) 17 | * [CockroachDB](https://upper.io/v4/adapter/cockroachdb) 18 | * [MongoDB](https://upper.io/v4/adapter/mongo) 19 | * [QL](https://upper.io/v4/adapter/ql) 20 | * [SQLite](https://upper.io/v4/adapter/sqlite) 21 | 22 | See [upper.io/v4](//upper.io/v4) for documentation and code samples. 23 | 24 | ## The tour 25 | 26 | ![tour](https://user-images.githubusercontent.com/385670/91495824-c6fabb00-e880-11ea-925b-a30b94474610.png) 27 | 28 | Take the [tour](https://tour.upper.io) to see real live examples in your 29 | browser. 30 | 31 | ## License 32 | 33 | Licensed under [MIT License](./LICENSE) 34 | 35 | ## Contributors 36 | 37 | See the [list of contributors](https://github.com/upper/db/graphs/contributors). 38 | -------------------------------------------------------------------------------- /adapter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package db 23 | 24 | import ( 25 | "fmt" 26 | "sync" 27 | ) 28 | 29 | var ( 30 | adapterMap = make(map[string]Adapter) 31 | adapterMapMu sync.RWMutex 32 | ) 33 | 34 | // Adapter interface defines an adapter 35 | type Adapter interface { 36 | Open(ConnectionURL) (Session, error) 37 | } 38 | 39 | type missingAdapter struct { 40 | name string 41 | } 42 | 43 | func (ma *missingAdapter) Open(ConnectionURL) (Session, error) { 44 | return nil, fmt.Errorf("upper: Missing adapter %q, did you forget to import it?", ma.name) 45 | } 46 | 47 | // RegisterAdapter registers a generic database adapter. 48 | func RegisterAdapter(name string, adapter Adapter) { 49 | adapterMapMu.Lock() 50 | defer adapterMapMu.Unlock() 51 | 52 | if name == "" { 53 | panic(`Missing adapter name`) 54 | } 55 | if _, ok := adapterMap[name]; ok { 56 | panic(`db.RegisterAdapter() called twice for adapter: ` + name) 57 | } 58 | adapterMap[name] = adapter 59 | } 60 | 61 | // LookupAdapter returns a previously registered adapter by name. 62 | func LookupAdapter(name string) Adapter { 63 | adapterMapMu.RLock() 64 | defer adapterMapMu.RUnlock() 65 | 66 | if adapter, ok := adapterMap[name]; ok { 67 | return adapter 68 | } 69 | return &missingAdapter{name: name} 70 | } 71 | 72 | // Open attempts to stablish a connection with a database. 73 | func Open(adapterName string, settings ConnectionURL) (Session, error) { 74 | return LookupAdapter(adapterName).Open(settings) 75 | } 76 | -------------------------------------------------------------------------------- /adapter/cockroachdb/README.md: -------------------------------------------------------------------------------- 1 | # CockroachDB adapter for upper/db 2 | 3 | Please read the full docs, acknowledgements and examples at 4 | [https://upper.io/v4/adapter/cockroachdb/](https://upper.io/v4/adapter/cockroachdb/). 5 | -------------------------------------------------------------------------------- /adapter/cockroachdb/cockroachdb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package cockroachdb 23 | 24 | import ( 25 | "database/sql" 26 | 27 | db "github.com/upper/db/v4" 28 | "github.com/upper/db/v4/internal/sqladapter" 29 | "github.com/upper/db/v4/internal/sqlbuilder" 30 | ) 31 | 32 | // Adapter is the unique name that you can use to refer to this adapter. 33 | const Adapter = `cockroachdb` 34 | 35 | var registeredAdapter = sqladapter.RegisterAdapter(Adapter, &database{}) 36 | 37 | // Open establishes a connection to the database server and returns a 38 | // db.Session instance (which is compatible with db.Session). 39 | func Open(connURL db.ConnectionURL) (db.Session, error) { 40 | return registeredAdapter.OpenDSN(connURL) 41 | } 42 | 43 | // NewTx creates a sqlbuilder.Tx instance by wrapping a *sql.Tx value. 44 | func NewTx(sqlTx *sql.Tx) (sqlbuilder.Tx, error) { 45 | return registeredAdapter.NewTx(sqlTx) 46 | } 47 | 48 | // New creates a sqlbuilder.Sesion instance by wrapping a *sql.DB value. 49 | func New(sqlDB *sql.DB) (db.Session, error) { 50 | return registeredAdapter.New(sqlDB) 51 | } 52 | -------------------------------------------------------------------------------- /adapter/cockroachdb/collection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package cockroachdb 23 | 24 | import ( 25 | db "github.com/upper/db/v4" 26 | "github.com/upper/db/v4/internal/sqladapter" 27 | ) 28 | 29 | type collectionAdapter struct { 30 | } 31 | 32 | func (*collectionAdapter) Insert(col sqladapter.Collection, item interface{}) (interface{}, error) { 33 | pKey, err := col.PrimaryKeys() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | q := col.SQL().InsertInto(col.Name()).Values(item) 39 | 40 | if len(pKey) == 0 { 41 | // There is no primary key. 42 | res, err := q.Exec() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | // Attempt to use LastInsertId() (probably won't work, but the Exec() 48 | // succeeded, so we can safely ignore the error from LastInsertId()). 49 | lastID, err := res.LastInsertId() 50 | if err != nil { 51 | return 0, nil 52 | } 53 | return lastID, nil 54 | } 55 | 56 | // Asking the database to return the primary key after insertion. 57 | q = q.Returning(pKey...) 58 | 59 | var keyMap db.Cond 60 | if err := q.Iterator().One(&keyMap); err != nil { 61 | return nil, err 62 | } 63 | 64 | // The IDSetter interface does not match, look for another interface match. 65 | if len(keyMap) == 1 { 66 | return keyMap[pKey[0]], nil 67 | } 68 | 69 | // This was a compound key and no interface matched it, let's return a map. 70 | return keyMap, nil 71 | } 72 | -------------------------------------------------------------------------------- /adapter/cockroachdb/connection_pgx.go: -------------------------------------------------------------------------------- 1 | //go:build !pq 2 | // +build !pq 3 | 4 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | package cockroachdb 26 | 27 | import ( 28 | "net" 29 | "sort" 30 | "strings" 31 | ) 32 | 33 | // String reassembles the parsed PostgreSQL connection URL into a valid DSN. 34 | func (c ConnectionURL) String() (s string) { 35 | u := []string{} 36 | 37 | // TODO: This surely needs some sort of escaping. 38 | if c.User != "" { 39 | u = append(u, "user="+escaper.Replace(c.User)) 40 | } 41 | 42 | if c.Password != "" { 43 | u = append(u, "password="+escaper.Replace(c.Password)) 44 | } 45 | 46 | if c.Host != "" { 47 | host, port, err := net.SplitHostPort(c.Host) 48 | if err == nil { 49 | if host == "" { 50 | host = "127.0.0.1" 51 | } 52 | u = append(u, "host="+escaper.Replace(host)) 53 | } else { 54 | u = append(u, "host="+escaper.Replace(c.Host)) 55 | } 56 | if port == "" { 57 | port = "26257" 58 | } 59 | u = append(u, "port="+escaper.Replace(port)) 60 | } 61 | 62 | if c.Socket != "" { 63 | u = append(u, "host="+escaper.Replace(c.Socket)) 64 | } 65 | 66 | if c.Database != "" { 67 | u = append(u, "dbname="+escaper.Replace(c.Database)) 68 | } 69 | 70 | // Is there actually any connection data? 71 | if len(u) == 0 { 72 | return "" 73 | } 74 | 75 | if c.Options == nil { 76 | c.Options = map[string]string{} 77 | } 78 | 79 | // If not present, SSL mode is assumed "prefer". 80 | if sslMode, ok := c.Options["sslmode"]; !ok || sslMode == "" { 81 | c.Options["sslmode"] = "prefer" 82 | } 83 | 84 | c.Options["default_query_exec_mode"] = "cache_describe" 85 | 86 | for k, v := range c.Options { 87 | u = append(u, escaper.Replace(k)+"="+escaper.Replace(v)) 88 | } 89 | 90 | sort.Strings(u) 91 | 92 | return strings.Join(u, " ") 93 | } 94 | -------------------------------------------------------------------------------- /adapter/cockroachdb/connection_pq.go: -------------------------------------------------------------------------------- 1 | //go:build pq 2 | // +build pq 3 | 4 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | package cockroachdb 26 | 27 | import ( 28 | "net" 29 | "sort" 30 | "strings" 31 | ) 32 | 33 | // String reassembles the parsed PostgreSQL connection URL into a valid DSN. 34 | func (c ConnectionURL) String() (s string) { 35 | u := []string{} 36 | 37 | // TODO: This surely needs some sort of escaping. 38 | if c.User != "" { 39 | u = append(u, "user="+escaper.Replace(c.User)) 40 | } 41 | 42 | if c.Password != "" { 43 | u = append(u, "password="+escaper.Replace(c.Password)) 44 | } 45 | 46 | if c.Host != "" { 47 | host, port, err := net.SplitHostPort(c.Host) 48 | if err == nil { 49 | if host == "" { 50 | host = "127.0.0.1" 51 | } 52 | u = append(u, "host="+escaper.Replace(host)) 53 | } else { 54 | u = append(u, "host="+escaper.Replace(c.Host)) 55 | } 56 | if port == "" { 57 | port = "26257" 58 | } 59 | u = append(u, "port="+escaper.Replace(port)) 60 | } 61 | 62 | if c.Socket != "" { 63 | u = append(u, "host="+escaper.Replace(c.Socket)) 64 | } 65 | 66 | if c.Database != "" { 67 | u = append(u, "dbname="+escaper.Replace(c.Database)) 68 | } 69 | 70 | // Is there actually any connection data? 71 | if len(u) == 0 { 72 | return "" 73 | } 74 | 75 | if c.Options == nil { 76 | c.Options = map[string]string{} 77 | } 78 | 79 | // If not present, SSL mode is assumed "prefer". 80 | if sslMode, ok := c.Options["sslmode"]; !ok || sslMode == "" { 81 | c.Options["sslmode"] = "prefer" 82 | } 83 | 84 | for k, v := range c.Options { 85 | u = append(u, escaper.Replace(k)+"="+escaper.Replace(v)) 86 | } 87 | 88 | sort.Strings(u) 89 | 90 | return strings.Join(u, " ") 91 | } 92 | -------------------------------------------------------------------------------- /adapter/cockroachdb/custom_types_test.go: -------------------------------------------------------------------------------- 1 | package cockroachdb 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type testStruct struct { 11 | X int `json:"x"` 12 | Z string `json:"z"` 13 | V interface{} `json:"v"` 14 | } 15 | 16 | func TestScanJSONB(t *testing.T) { 17 | { 18 | a := testStruct{} 19 | err := ScanJSONB(&a, []byte(`{"x": 5, "z": "Hello", "v": 1}`)) 20 | assert.NoError(t, err) 21 | assert.Equal(t, "Hello", a.Z) 22 | assert.Equal(t, float64(1), a.V) 23 | assert.Equal(t, 5, a.X) 24 | } 25 | { 26 | a := testStruct{} 27 | err := ScanJSONB(&a, []byte(`{"x": 5, "z": "Hello", "v": null}`)) 28 | assert.NoError(t, err) 29 | assert.Equal(t, "Hello", a.Z) 30 | assert.Equal(t, nil, a.V) 31 | assert.Equal(t, 5, a.X) 32 | } 33 | { 34 | a := testStruct{} 35 | err := ScanJSONB(&a, []byte(`{"x": 5, "z": "Hello"}`)) 36 | assert.NoError(t, err) 37 | assert.Equal(t, "Hello", a.Z) 38 | assert.Equal(t, nil, a.V) 39 | assert.Equal(t, 5, a.X) 40 | } 41 | { 42 | a := testStruct{} 43 | err := ScanJSONB(&a, []byte(`{"v": "Hello"}`)) 44 | assert.NoError(t, err) 45 | assert.Equal(t, "Hello", a.V) 46 | } 47 | { 48 | a := testStruct{} 49 | err := ScanJSONB(&a, []byte(`{"v": true}`)) 50 | assert.NoError(t, err) 51 | assert.Equal(t, true, a.V) 52 | } 53 | { 54 | a := testStruct{} 55 | err := ScanJSONB(&a, []byte(`{}`)) 56 | assert.NoError(t, err) 57 | assert.Equal(t, nil, a.V) 58 | } 59 | { 60 | a := []*testStruct{} 61 | err := json.Unmarshal([]byte(`[{}]`), &a) 62 | assert.NoError(t, err) 63 | assert.Equal(t, 1, len(a)) 64 | assert.Nil(t, a[0].V) 65 | } 66 | { 67 | a := []*testStruct{} 68 | err := json.Unmarshal([]byte(`[{"v": true}]`), &a) 69 | assert.NoError(t, err) 70 | assert.Equal(t, 1, len(a)) 71 | assert.Equal(t, true, a[0].V) 72 | } 73 | { 74 | a := []*testStruct{} 75 | err := json.Unmarshal([]byte(`[{"v": null}]`), &a) 76 | assert.NoError(t, err) 77 | assert.Equal(t, 1, len(a)) 78 | assert.Nil(t, a[0].V) 79 | } 80 | { 81 | a := []*testStruct{} 82 | err := json.Unmarshal([]byte(`[{"v": 12.34}]`), &a) 83 | assert.NoError(t, err) 84 | assert.Equal(t, 1, len(a)) 85 | assert.Equal(t, 12.34, a[0].V) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /adapter/cockroachdb/database_pgx.go: -------------------------------------------------------------------------------- 1 | //go:build !pq 2 | // +build !pq 3 | 4 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | package cockroachdb 26 | 27 | import ( 28 | "context" 29 | "database/sql" 30 | 31 | "time" 32 | 33 | _ "github.com/jackc/pgx/v5/stdlib" 34 | "github.com/upper/db/v4" 35 | "github.com/upper/db/v4/internal/sqladapter" 36 | ) 37 | 38 | func (*database) OpenDSN(sess sqladapter.Session, dsn string) (*sql.DB, error) { 39 | connURL, err := ParseURL(dsn) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if tz := connURL.Options["timezone"]; tz != "" { 44 | loc, err := time.LoadLocation(tz) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | ctx := context.WithValue(sess.Context(), db.ContextKey("timezone"), loc) 50 | sess.SetContext(ctx) 51 | } 52 | return sql.Open("pgx", dsn) 53 | } 54 | -------------------------------------------------------------------------------- /adapter/cockroachdb/database_pq.go: -------------------------------------------------------------------------------- 1 | //go:build pq 2 | // +build pq 3 | 4 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | package cockroachdb 26 | 27 | import ( 28 | "context" 29 | "database/sql" 30 | "time" 31 | 32 | _ "github.com/lib/pq" 33 | db "github.com/upper/db/v4" 34 | "github.com/upper/db/v4/internal/sqladapter" 35 | ) 36 | 37 | func (*database) OpenDSN(sess sqladapter.Session, dsn string) (*sql.DB, error) { 38 | connURL, err := ParseURL(dsn) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if tz := connURL.Options["timezone"]; tz != "" { 43 | loc, err := time.LoadLocation(tz) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | ctx := context.WithValue(sess.Context(), db.ContextKey("timezone"), loc) 49 | sess.SetContext(ctx) 50 | } 51 | return sql.Open("postgres", dsn) 52 | } 53 | -------------------------------------------------------------------------------- /adapter/mongo/README.md: -------------------------------------------------------------------------------- 1 | # MongoDB adapter for upper/db 2 | 3 | Please read the full docs, acknowledgements and examples at 4 | [https://upper.io/v4/adapter/mongo/](https://upper.io/v4/adapter/mongo/). 5 | -------------------------------------------------------------------------------- /adapter/mssql/README.md: -------------------------------------------------------------------------------- 1 | # SQLServer adapter for upper/db 2 | 3 | Please read the full docs, acknowledgements and examples at 4 | [https://upper.io/v4/adapter/mssql/](https://upper.io/v4/adapter/mssql/). 5 | -------------------------------------------------------------------------------- /adapter/mssql/connection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package mssql 23 | 24 | import ( 25 | "errors" 26 | "net/url" 27 | ) 28 | 29 | // ConnectionURL implements a MSSQL connection struct. 30 | type ConnectionURL struct { 31 | User string 32 | Password string 33 | Database string 34 | Host string 35 | Socket string 36 | Options map[string]string 37 | } 38 | 39 | func (c ConnectionURL) String() (s string) { 40 | if c.Host == "" && c.Database == "" && c.User == "" && c.Password == "" { 41 | return "" 42 | } 43 | 44 | if c.Host == "" { 45 | c.Host = "127.0.0.1" 46 | } 47 | 48 | if c.Database == "" { 49 | c.Database = "master" 50 | } 51 | 52 | params := url.Values{} 53 | for k, v := range c.Options { 54 | if k == "instance" { 55 | continue 56 | } 57 | params.Add(k, v) 58 | } 59 | params.Set("database", c.Database) 60 | 61 | u := url.URL{ 62 | Scheme: "sqlserver", 63 | Host: c.Host, 64 | RawQuery: params.Encode(), 65 | } 66 | 67 | u.Path = c.Options["instance"] 68 | 69 | if c.User != "" || c.Password != "" { 70 | u.User = url.UserPassword(c.User, c.Password) 71 | } 72 | 73 | return u.String() 74 | } 75 | 76 | // ParseURL parses s into a ConnectionURL struct. 77 | func ParseURL(s string) (conn ConnectionURL, err error) { 78 | var u *url.URL 79 | 80 | u, err = url.Parse(s) 81 | if err != nil { 82 | return 83 | } 84 | 85 | if u.Scheme != "sqlserver" && u.Scheme != "mssql" { 86 | return conn, errors.New(`Expecting "sqlserver" or "mssql" schema`) 87 | } 88 | 89 | if u.Host == "" { 90 | conn.Host = "127.0.0.1" 91 | } else { 92 | conn.Host = u.Host 93 | } 94 | 95 | if u.User != nil { 96 | conn.User = u.User.Username() 97 | conn.Password, _ = u.User.Password() 98 | } 99 | 100 | q := u.Query() 101 | for k := range q { 102 | if k == "database" { 103 | conn.Database = q.Get(k) 104 | continue 105 | } 106 | if conn.Options == nil { 107 | conn.Options = make(map[string]string) 108 | } 109 | conn.Options[k] = q.Get(k) 110 | } 111 | 112 | return 113 | } 114 | -------------------------------------------------------------------------------- /adapter/mssql/connection_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package mssql 23 | 24 | import ( 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | func TestConnectionURL(t *testing.T) { 32 | c := ConnectionURL{} 33 | 34 | // Zero value equals to an empty string. 35 | assert.Equal(t, "", c.String(), "Expecting default connectiong string to be empty") 36 | 37 | // Adding a database name. 38 | c.Database = "mydbname" 39 | assert.Equal(t, "sqlserver://127.0.0.1?database=mydbname", c.String()) 40 | 41 | // Adding an option. 42 | c.Options = map[string]string{ 43 | "connection timeout": "30", 44 | "param1": "value1", 45 | "instance": "instance1", 46 | } 47 | 48 | assert.Equal(t, "sqlserver://127.0.0.1/instance1?connection+timeout=30&database=mydbname¶m1=value1", c.String()) 49 | 50 | // Setting default options 51 | c.Options = nil 52 | 53 | // Setting user and password. 54 | c.User = "user" 55 | c.Password = "pass" 56 | 57 | assert.Equal(t, `sqlserver://user:pass@127.0.0.1?database=mydbname`, c.String()) 58 | 59 | // Setting host. 60 | c.Host = "1.2.3.4:1433" 61 | assert.Equal(t, `sqlserver://user:pass@1.2.3.4:1433?database=mydbname`, c.String()) 62 | } 63 | 64 | func TestParseConnectionURL(t *testing.T) { 65 | var u ConnectionURL 66 | var s string 67 | var err error 68 | 69 | s = "sqlserver://user:pass@127.0.0.1:1433?connection+timeout=30&database=mydbname¶m1=value1" 70 | 71 | u, err = ParseURL(s) 72 | require.NoError(t, err) 73 | 74 | assert.Equal(t, "user", u.User) 75 | assert.Equal(t, "pass", u.Password) 76 | assert.Equal(t, "127.0.0.1:1433", u.Host) 77 | assert.Equal(t, "mydbname", u.Database) 78 | } 79 | -------------------------------------------------------------------------------- /adapter/mssql/mssql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package mssql 23 | 24 | import ( 25 | "database/sql" 26 | 27 | db "github.com/upper/db/v4" 28 | "github.com/upper/db/v4/internal/sqladapter" 29 | "github.com/upper/db/v4/internal/sqlbuilder" 30 | ) 31 | 32 | // Adapter is the public name of the adapter. 33 | const Adapter = `mssql` 34 | 35 | var registeredAdapter = sqladapter.RegisterAdapter(Adapter, &database{}) 36 | 37 | // Open establishes a connection to the database server and returns a 38 | // db.Session instance (which is compatible with db.Session). 39 | func Open(connURL db.ConnectionURL) (db.Session, error) { 40 | return registeredAdapter.OpenDSN(connURL) 41 | } 42 | 43 | // NewTx creates a sqlbuilder.Tx instance by wrapping a *sql.Tx value. 44 | func NewTx(sqlTx *sql.Tx) (sqlbuilder.Tx, error) { 45 | return registeredAdapter.NewTx(sqlTx) 46 | } 47 | 48 | // New creates a sqlbuilder.Sesion instance by wrapping a *sql.DB value. 49 | func New(sqlDB *sql.DB) (db.Session, error) { 50 | return registeredAdapter.New(sqlDB) 51 | } 52 | -------------------------------------------------------------------------------- /adapter/mysql/README.md: -------------------------------------------------------------------------------- 1 | # MySQL adapter for upper/db 2 | 3 | Please read the full docs, acknowledgements and examples at 4 | [https://upper.io/v4/adapter/mysql/](https://upper.io/v4/adapter/mysql/). 5 | 6 | -------------------------------------------------------------------------------- /adapter/mysql/collection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package mysql 23 | 24 | import ( 25 | db "github.com/upper/db/v4" 26 | "github.com/upper/db/v4/internal/sqladapter" 27 | "github.com/upper/db/v4/internal/sqlbuilder" 28 | ) 29 | 30 | type collectionAdapter struct { 31 | } 32 | 33 | func (*collectionAdapter) Insert(col sqladapter.Collection, item interface{}) (interface{}, error) { 34 | columnNames, columnValues, err := sqlbuilder.Map(item, nil) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | pKey, err := col.PrimaryKeys() 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | q := col.SQL().InsertInto(col.Name()). 45 | Columns(columnNames...). 46 | Values(columnValues...) 47 | 48 | res, err := q.Exec() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | lastID, err := res.LastInsertId() 54 | if err == nil && len(pKey) <= 1 { 55 | return lastID, nil 56 | } 57 | 58 | keyMap := db.Cond{} 59 | for i := range columnNames { 60 | for j := 0; j < len(pKey); j++ { 61 | if pKey[j] == columnNames[i] { 62 | keyMap[pKey[j]] = columnValues[i] 63 | } 64 | } 65 | } 66 | 67 | // There was an auto column among primary keys, let's search for it. 68 | if lastID > 0 { 69 | for j := 0; j < len(pKey); j++ { 70 | if keyMap[pKey[j]] == nil { 71 | keyMap[pKey[j]] = lastID 72 | } 73 | } 74 | } 75 | 76 | return keyMap, nil 77 | } 78 | -------------------------------------------------------------------------------- /adapter/mysql/connection_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package mysql 23 | 24 | import ( 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | func TestConnectionURL(t *testing.T) { 32 | 33 | c := ConnectionURL{} 34 | 35 | // Zero value equals to an empty string. 36 | assert.Equal(t, "", c.String(), "Expecting default connectiong string to be empty") 37 | 38 | // Adding a database name. 39 | c.Database = "mydbname" 40 | assert.Equal(t, "/mydbname?charset=utf8&parseTime=true", c.String()) 41 | 42 | // Adding options 43 | c.Options = map[string]string{ 44 | "charset": "utf8mb4,utf8", 45 | "sys_var": "esc@ped", 46 | } 47 | 48 | assert.Equal(t, "/mydbname?charset=utf8mb4%2Cutf8&parseTime=true&sys_var=esc%40ped", c.String()) 49 | 50 | // Setting default options 51 | c.Options = nil 52 | 53 | // Setting user and password. 54 | c.User = "user" 55 | c.Password = "pass" 56 | 57 | assert.Equal(t, "user:pass@/mydbname?charset=utf8&parseTime=true", c.String()) 58 | 59 | // Setting host. 60 | c.Host = "1.2.3.4:3306" 61 | 62 | assert.Equal(t, `user:pass@tcp(1.2.3.4:3306)/mydbname?charset=utf8&parseTime=true`, c.String()) 63 | 64 | // Setting socket. 65 | c.Socket = "/path/to/socket" 66 | 67 | assert.Equal(t, `user:pass@unix(/path/to/socket)/mydbname?charset=utf8&parseTime=true`, c.String()) 68 | } 69 | 70 | func TestParseConnectionURL(t *testing.T) { 71 | var u ConnectionURL 72 | var s string 73 | var err error 74 | 75 | s = "user:pass@unix(/path/to/socket)/mydbname?charset=utf8" 76 | 77 | u, err = ParseURL(s) 78 | require.NoError(t, err) 79 | 80 | assert.Equal(t, "user", u.User) 81 | assert.Equal(t, "pass", u.Password) 82 | assert.Equal(t, "/path/to/socket", u.Socket) 83 | assert.Equal(t, "mydbname", u.Database) 84 | assert.Equal(t, "utf8", u.Options["charset"]) 85 | 86 | s = "user:pass@tcp(1.2.3.4:5678)/mydbname?charset=utf8" 87 | u, err = ParseURL(s) 88 | require.NoError(t, err) 89 | 90 | assert.Equal(t, "user", u.User) 91 | assert.Equal(t, "pass", u.Password) 92 | assert.Equal(t, "1.2.3.4:5678", u.Host) 93 | assert.Equal(t, "mydbname", u.Database) 94 | assert.Equal(t, "utf8", u.Options["charset"]) 95 | } 96 | -------------------------------------------------------------------------------- /adapter/mysql/mysql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package mysql 23 | 24 | import ( 25 | "database/sql" 26 | 27 | db "github.com/upper/db/v4" 28 | "github.com/upper/db/v4/internal/sqladapter" 29 | "github.com/upper/db/v4/internal/sqlbuilder" 30 | ) 31 | 32 | // Adapter is the public name of the adapter. 33 | const Adapter = `mysql` 34 | 35 | var registeredAdapter = sqladapter.RegisterAdapter(Adapter, &database{}) 36 | 37 | // Open establishes a connection to the database server and returns a 38 | // db.Session instance (which is compatible with db.Session). 39 | func Open(connURL db.ConnectionURL) (db.Session, error) { 40 | return registeredAdapter.OpenDSN(connURL) 41 | } 42 | 43 | // NewTx creates a sqlbuilder.Tx instance by wrapping a *sql.Tx value. 44 | func NewTx(sqlTx *sql.Tx) (sqlbuilder.Tx, error) { 45 | return registeredAdapter.NewTx(sqlTx) 46 | } 47 | 48 | // New creates a sqlbuilder.Sesion instance by wrapping a *sql.DB value. 49 | func New(sqlDB *sql.DB) (db.Session, error) { 50 | return registeredAdapter.New(sqlDB) 51 | } 52 | -------------------------------------------------------------------------------- /adapter/postgresql/README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL adapter for upper/db 2 | 3 | Please read the full docs, acknowledgements and examples at 4 | [https://upper.io/v4/adapter/postgresql/](https://upper.io/v4/adapter/postgresql/). 5 | 6 | -------------------------------------------------------------------------------- /adapter/postgresql/collection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package postgresql 23 | 24 | import ( 25 | db "github.com/upper/db/v4" 26 | "github.com/upper/db/v4/internal/sqladapter" 27 | ) 28 | 29 | type collectionAdapter struct { 30 | } 31 | 32 | func (*collectionAdapter) Insert(col sqladapter.Collection, item interface{}) (interface{}, error) { 33 | pKey, err := col.PrimaryKeys() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | q := col.SQL().InsertInto(col.Name()).Values(item) 39 | 40 | if len(pKey) == 0 { 41 | // There is no primary key. 42 | res, err := q.Exec() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | // Attempt to use LastInsertId() (probably won't work, but the Exec() 48 | // succeeded, so we can safely ignore the error from LastInsertId()). 49 | lastID, err := res.LastInsertId() 50 | if err != nil { 51 | return nil, nil 52 | } 53 | return lastID, nil 54 | } 55 | 56 | // Asking the database to return the primary key after insertion. 57 | q = q.Returning(pKey...) 58 | 59 | var keyMap db.Cond 60 | if err := q.Iterator().One(&keyMap); err != nil { 61 | return nil, err 62 | } 63 | 64 | // The IDSetter interface does not match, look for another interface match. 65 | if len(keyMap) == 1 { 66 | return keyMap[pKey[0]], nil 67 | } 68 | 69 | // This was a compound key and no interface matched it, let's return a map. 70 | return keyMap, nil 71 | } 72 | -------------------------------------------------------------------------------- /adapter/postgresql/connection_pgx.go: -------------------------------------------------------------------------------- 1 | //go:build !pq 2 | // +build !pq 3 | 4 | package postgresql 5 | 6 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining 9 | // a copy of this software and associated documentation files (the 10 | // "Software"), to deal in the Software without restriction, including 11 | // without limitation the rights to use, copy, modify, merge, publish, 12 | // distribute, sublicense, and/or sell copies of the Software, and to 13 | // permit persons to whom the Software is furnished to do so, subject to 14 | // the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be 17 | // included in all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | import ( 28 | "net" 29 | "sort" 30 | "strings" 31 | ) 32 | 33 | // String reassembles the parsed PostgreSQL connection URL into a valid DSN. 34 | func (c ConnectionURL) String() (s string) { 35 | u := []string{} 36 | 37 | // TODO: This surely needs some sort of escaping. 38 | if c.User != "" { 39 | u = append(u, "user="+escaper.Replace(c.User)) 40 | } 41 | 42 | if c.Password != "" { 43 | u = append(u, "password="+escaper.Replace(c.Password)) 44 | } 45 | 46 | if c.Host != "" { 47 | host, port, err := net.SplitHostPort(c.Host) 48 | if err == nil { 49 | if host == "" { 50 | host = "127.0.0.1" 51 | } 52 | if port == "" { 53 | port = "5432" 54 | } 55 | u = append(u, "host="+escaper.Replace(host)) 56 | u = append(u, "port="+escaper.Replace(port)) 57 | } else { 58 | u = append(u, "host="+escaper.Replace(c.Host)) 59 | } 60 | } 61 | 62 | if c.Socket != "" { 63 | u = append(u, "host="+escaper.Replace(c.Socket)) 64 | } 65 | 66 | if c.Database != "" { 67 | u = append(u, "dbname="+escaper.Replace(c.Database)) 68 | } 69 | 70 | // Is there actually any connection data? 71 | if len(u) == 0 { 72 | return "" 73 | } 74 | 75 | if c.Options == nil { 76 | c.Options = map[string]string{} 77 | } 78 | 79 | // If not present, SSL mode is assumed "prefer". 80 | if sslMode, ok := c.Options["sslmode"]; !ok || sslMode == "" { 81 | c.Options["sslmode"] = "prefer" 82 | } 83 | 84 | // Disabled by default 85 | c.Options["default_query_exec_mode"] = "cache_describe" 86 | 87 | for k, v := range c.Options { 88 | u = append(u, escaper.Replace(k)+"="+escaper.Replace(v)) 89 | } 90 | 91 | sort.Strings(u) 92 | 93 | return strings.Join(u, " ") 94 | } 95 | -------------------------------------------------------------------------------- /adapter/postgresql/connection_pq.go: -------------------------------------------------------------------------------- 1 | //go:build pq 2 | // +build pq 3 | 4 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining 7 | // a copy of this software and associated documentation files (the 8 | // "Software"), to deal in the Software without restriction, including 9 | // without limitation the rights to use, copy, modify, merge, publish, 10 | // distribute, sublicense, and/or sell copies of the Software, and to 11 | // permit persons to whom the Software is furnished to do so, subject to 12 | // the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be 15 | // included in all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | package postgresql 26 | 27 | import ( 28 | "net" 29 | "sort" 30 | "strings" 31 | ) 32 | 33 | // String reassembles the parsed PostgreSQL connection URL into a valid DSN. 34 | func (c ConnectionURL) String() (s string) { 35 | u := []string{} 36 | 37 | // TODO: This surely needs some sort of escaping. 38 | if c.User != "" { 39 | u = append(u, "user="+escaper.Replace(c.User)) 40 | } 41 | 42 | if c.Password != "" { 43 | u = append(u, "password="+escaper.Replace(c.Password)) 44 | } 45 | 46 | if c.Host != "" { 47 | host, port, err := net.SplitHostPort(c.Host) 48 | if err == nil { 49 | if host == "" { 50 | host = "127.0.0.1" 51 | } 52 | if port == "" { 53 | port = "5432" 54 | } 55 | u = append(u, "host="+escaper.Replace(host)) 56 | u = append(u, "port="+escaper.Replace(port)) 57 | } else { 58 | u = append(u, "host="+escaper.Replace(c.Host)) 59 | } 60 | } 61 | 62 | if c.Socket != "" { 63 | u = append(u, "host="+escaper.Replace(c.Socket)) 64 | } 65 | 66 | if c.Database != "" { 67 | u = append(u, "dbname="+escaper.Replace(c.Database)) 68 | } 69 | 70 | // Is there actually any connection data? 71 | if len(u) == 0 { 72 | return "" 73 | } 74 | 75 | if c.Options == nil { 76 | c.Options = map[string]string{} 77 | } 78 | 79 | // If not present, SSL mode is assumed "prefer". 80 | if sslMode, ok := c.Options["sslmode"]; !ok || sslMode == "" { 81 | c.Options["sslmode"] = "prefer" 82 | } 83 | 84 | for k, v := range c.Options { 85 | u = append(u, escaper.Replace(k)+"="+escaper.Replace(v)) 86 | } 87 | 88 | sort.Strings(u) 89 | 90 | return strings.Join(u, " ") 91 | } 92 | -------------------------------------------------------------------------------- /adapter/postgresql/custom_types_test.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type testStruct struct { 11 | X int `json:"x"` 12 | Z string `json:"z"` 13 | V interface{} `json:"v"` 14 | } 15 | 16 | func TestScanJSONB(t *testing.T) { 17 | { 18 | a := testStruct{} 19 | err := ScanJSONB(&a, []byte(`{"x": 5, "z": "Hello", "v": 1}`)) 20 | assert.NoError(t, err) 21 | assert.Equal(t, "Hello", a.Z) 22 | assert.Equal(t, float64(1), a.V) 23 | assert.Equal(t, 5, a.X) 24 | } 25 | { 26 | a := testStruct{} 27 | err := ScanJSONB(&a, []byte(`{"x": 5, "z": "Hello", "v": null}`)) 28 | assert.NoError(t, err) 29 | assert.Equal(t, "Hello", a.Z) 30 | assert.Equal(t, nil, a.V) 31 | assert.Equal(t, 5, a.X) 32 | } 33 | { 34 | a := testStruct{} 35 | err := ScanJSONB(&a, []byte(`{"x": 5, "z": "Hello"}`)) 36 | assert.NoError(t, err) 37 | assert.Equal(t, "Hello", a.Z) 38 | assert.Equal(t, nil, a.V) 39 | assert.Equal(t, 5, a.X) 40 | } 41 | { 42 | a := testStruct{} 43 | err := ScanJSONB(&a, []byte(`{"v": "Hello"}`)) 44 | assert.NoError(t, err) 45 | assert.Equal(t, "Hello", a.V) 46 | } 47 | { 48 | a := testStruct{} 49 | err := ScanJSONB(&a, []byte(`{"v": true}`)) 50 | assert.NoError(t, err) 51 | assert.Equal(t, true, a.V) 52 | } 53 | { 54 | a := testStruct{} 55 | err := ScanJSONB(&a, []byte(`{}`)) 56 | assert.NoError(t, err) 57 | assert.Equal(t, nil, a.V) 58 | } 59 | { 60 | var a []byte 61 | err := ScanJSONB(&a, []byte(`{"age":[{"\u003e":"1h"}]}`)) 62 | assert.NoError(t, err) 63 | assert.Equal(t, `{"age":[{"\u003e":"1h"}]}`, string(a)) 64 | } 65 | { 66 | var a json.RawMessage 67 | err := ScanJSONB(&a, []byte(`{"age":[{"\u003e":"1h"}]}`)) 68 | assert.NoError(t, err) 69 | assert.Equal(t, `{"age":[{"\u003e":"1h"}]}`, string(a)) 70 | } 71 | { 72 | var a json.RawMessage 73 | err := ScanJSONB(&a, []byte("{\"age\":[{\"\u003e\":\"1h\"}]}")) 74 | assert.NoError(t, err) 75 | assert.Equal(t, `{"age":[{">":"1h"}]}`, string(a)) 76 | } 77 | { 78 | a := []*testStruct{} 79 | err := json.Unmarshal([]byte(`[{}]`), &a) 80 | assert.NoError(t, err) 81 | assert.Equal(t, 1, len(a)) 82 | assert.Nil(t, a[0].V) 83 | } 84 | { 85 | a := []*testStruct{} 86 | err := json.Unmarshal([]byte(`[{"v": true}]`), &a) 87 | assert.NoError(t, err) 88 | assert.Equal(t, 1, len(a)) 89 | assert.Equal(t, true, a[0].V) 90 | } 91 | { 92 | a := []*testStruct{} 93 | err := json.Unmarshal([]byte(`[{"v": null}]`), &a) 94 | assert.NoError(t, err) 95 | assert.Equal(t, 1, len(a)) 96 | assert.Nil(t, a[0].V) 97 | } 98 | { 99 | a := []*testStruct{} 100 | err := json.Unmarshal([]byte(`[{"v": 12.34}]`), &a) 101 | assert.NoError(t, err) 102 | assert.Equal(t, 1, len(a)) 103 | assert.Equal(t, 12.34, a[0].V) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /adapter/postgresql/database_pgx.go: -------------------------------------------------------------------------------- 1 | //go:build !pq 2 | // +build !pq 3 | 4 | package postgresql 5 | 6 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining 9 | // a copy of this software and associated documentation files (the 10 | // "Software"), to deal in the Software without restriction, including 11 | // without limitation the rights to use, copy, modify, merge, publish, 12 | // distribute, sublicense, and/or sell copies of the Software, and to 13 | // permit persons to whom the Software is furnished to do so, subject to 14 | // the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be 17 | // included in all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | import ( 28 | "context" 29 | "database/sql" 30 | "time" 31 | 32 | _ "github.com/jackc/pgx/v5/stdlib" 33 | "github.com/upper/db/v4" 34 | "github.com/upper/db/v4/internal/sqladapter" 35 | ) 36 | 37 | func (*database) OpenDSN(sess sqladapter.Session, dsn string) (*sql.DB, error) { 38 | connURL, err := ParseURL(dsn) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | if tz := connURL.Options["timezone"]; tz != "" { 44 | loc, err := time.LoadLocation(tz) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | ctx := context.WithValue(sess.Context(), db.ContextKey("timezone"), loc) 50 | sess.SetContext(ctx) 51 | } 52 | 53 | return sql.Open("pgx", dsn) 54 | } 55 | -------------------------------------------------------------------------------- /adapter/postgresql/database_pq.go: -------------------------------------------------------------------------------- 1 | //go:build pq 2 | // +build pq 3 | 4 | package postgresql 5 | 6 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining 9 | // a copy of this software and associated documentation files (the 10 | // "Software"), to deal in the Software without restriction, including 11 | // without limitation the rights to use, copy, modify, merge, publish, 12 | // distribute, sublicense, and/or sell copies of the Software, and to 13 | // permit persons to whom the Software is furnished to do so, subject to 14 | // the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be 17 | // included in all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | import ( 28 | "context" 29 | "database/sql" 30 | "time" 31 | 32 | _ "github.com/lib/pq" 33 | db "github.com/upper/db/v4" 34 | "github.com/upper/db/v4/internal/sqladapter" 35 | ) 36 | 37 | func (*database) OpenDSN(sess sqladapter.Session, dsn string) (*sql.DB, error) { 38 | connURL, err := ParseURL(dsn) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if tz := connURL.Options["timezone"]; tz != "" { 43 | loc, err := time.LoadLocation(tz) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | ctx := context.WithValue(sess.Context(), db.ContextKey("timezone"), loc) 49 | sess.SetContext(ctx) 50 | } 51 | return sql.Open("postgres", dsn) 52 | } 53 | -------------------------------------------------------------------------------- /adapter/postgresql/postgresql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package postgresql 23 | 24 | import ( 25 | "database/sql" 26 | 27 | db "github.com/upper/db/v4" 28 | "github.com/upper/db/v4/internal/sqladapter" 29 | "github.com/upper/db/v4/internal/sqlbuilder" 30 | ) 31 | 32 | // Adapter is the internal name of the adapter. 33 | const Adapter = "postgresql" 34 | 35 | var registeredAdapter = sqladapter.RegisterAdapter(Adapter, &database{}) 36 | 37 | // Open establishes a connection to the database server and returns a 38 | // sqlbuilder.Session instance (which is compatible with db.Session). 39 | func Open(connURL db.ConnectionURL) (db.Session, error) { 40 | return registeredAdapter.OpenDSN(connURL) 41 | } 42 | 43 | // NewTx creates a sqlbuilder.Tx instance by wrapping a *sql.Tx value. 44 | func NewTx(sqlTx *sql.Tx) (sqlbuilder.Tx, error) { 45 | return registeredAdapter.NewTx(sqlTx) 46 | } 47 | 48 | // New creates a sqlbuilder.Sesion instance by wrapping a *sql.DB value. 49 | func New(sqlDB *sql.DB) (db.Session, error) { 50 | return registeredAdapter.New(sqlDB) 51 | } 52 | -------------------------------------------------------------------------------- /adapter/ql/collection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package ql 23 | 24 | import ( 25 | "database/sql" 26 | 27 | db "github.com/upper/db/v4" 28 | "github.com/upper/db/v4/internal/sqladapter" 29 | "github.com/upper/db/v4/internal/sqlbuilder" 30 | ) 31 | 32 | type resultProxy struct { 33 | db.Result 34 | 35 | col sqladapter.Collection 36 | } 37 | 38 | func (r *resultProxy) Select(fields ...interface{}) db.Result { 39 | if len(fields) == 1 { 40 | if s, ok := fields[0].(string); ok && s == "*" { 41 | var columns []struct { 42 | Name string `db:"Name"` 43 | } 44 | err := r.col.SQL().Select("Name"). 45 | From("__Column"). 46 | Where("TableName", r.col.Name()). 47 | Iterator().All(&columns) 48 | if err == nil { 49 | fields = make([]interface{}, 0, len(columns)+1) 50 | fields = append(fields, "id() AS id") 51 | for _, column := range columns { 52 | fields = append(fields, column.Name) 53 | } 54 | } 55 | } 56 | } 57 | return r.Result.Select(fields...) 58 | } 59 | 60 | type collectionAdapter struct { 61 | } 62 | 63 | func (*collectionAdapter) FilterConds(conds ...interface{}) []interface{} { 64 | if len(conds) == 1 { 65 | switch conds[0].(type) { 66 | case int, int64, uint, uint64: 67 | // This is an special QL index, I'm not sure if it allows the user to 68 | // create special indexes with custom names. 69 | conds[0] = db.Cond{"id()": db.Eq(conds[0])} 70 | } 71 | } 72 | return conds 73 | } 74 | 75 | func (*collectionAdapter) Find(col sqladapter.Collection, res *sqladapter.Result, conds ...interface{}) db.Result { 76 | proxy := &resultProxy{ 77 | Result: res, 78 | col: col, 79 | } 80 | return proxy.Select("*") 81 | } 82 | 83 | func (*collectionAdapter) Insert(col sqladapter.Collection, item interface{}) (interface{}, error) { 84 | columnNames, columnValues, err := sqlbuilder.Map(item, nil) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | q := col.SQL().InsertInto(col.Name()). 90 | Columns(columnNames...). 91 | Values(columnValues...) 92 | 93 | var res sql.Result 94 | if res, err = q.Exec(); err != nil { 95 | return nil, err 96 | } 97 | 98 | return res.LastInsertId() 99 | } 100 | -------------------------------------------------------------------------------- /adapter/ql/connection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package ql 23 | 24 | import ( 25 | "fmt" 26 | "net/url" 27 | "path/filepath" 28 | "strings" 29 | ) 30 | 31 | const defaultConnectionScheme = `file` 32 | 33 | // ConnectionURL implements a QL connection struct. 34 | type ConnectionURL struct { 35 | Scheme string 36 | Database string 37 | Options map[string]string 38 | } 39 | 40 | func (c ConnectionURL) String() (s string) { 41 | vv := url.Values{} 42 | 43 | if c.Database == "" { 44 | return "" 45 | } 46 | 47 | // Does the database have an existing name? 48 | if !strings.HasPrefix(c.Database, "/") { 49 | c.Database, _ = filepath.Abs(c.Database) 50 | } 51 | 52 | // Do we have any options? 53 | if c.Options == nil { 54 | c.Options = map[string]string{} 55 | } 56 | 57 | // Converting options into URL values. 58 | for k, v := range c.Options { 59 | vv.Set(k, v) 60 | } 61 | 62 | // Building URL. 63 | u := url.URL{ 64 | Scheme: c.Scheme, 65 | Path: c.Database, 66 | RawQuery: vv.Encode(), 67 | } 68 | if u.Scheme == "" { 69 | u.Scheme = defaultConnectionScheme 70 | } 71 | 72 | return u.String() 73 | } 74 | 75 | // ParseURL parses s into a ConnectionURL struct. 76 | func ParseURL(s string) (conn ConnectionURL, err error) { 77 | var u *url.URL 78 | 79 | if u, err = url.Parse(s); err != nil { 80 | return conn, err 81 | } 82 | 83 | if u.Scheme != "file" && u.Scheme != "memory" && u.Scheme != "" { 84 | return conn, fmt.Errorf(`Expecting file:// or memory:// connection scheme.`) 85 | } 86 | 87 | var vv url.Values 88 | if vv, err = url.ParseQuery(u.RawQuery); err != nil { 89 | return conn, err 90 | } 91 | 92 | conn.Scheme = u.Scheme 93 | conn.Database = u.Host + u.Path 94 | conn.Options = map[string]string{} 95 | for k := range vv { 96 | conn.Options[k] = vv.Get(k) 97 | } 98 | 99 | return conn, err 100 | } 101 | -------------------------------------------------------------------------------- /adapter/ql/connection_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package ql 23 | 24 | import ( 25 | "path/filepath" 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestConnectionURL(t *testing.T) { 32 | c := ConnectionURL{} 33 | 34 | assert.Zero(t, c.String()) 35 | 36 | // Adding a database name. 37 | c.Database = "file://myfilename" 38 | absoluteName, _ := filepath.Abs(c.Database) 39 | 40 | assert.Equal(t, "file://"+absoluteName, c.String()) 41 | 42 | // Adding an option. 43 | c.Options = map[string]string{ 44 | "cache": "foobar", 45 | "mode": "ro", 46 | } 47 | 48 | assert.Equal(t, "file://"+absoluteName+"?cache=foobar&mode=ro", c.String()) 49 | 50 | // Setting another database. 51 | c.Database = "/another/database" 52 | assert.Equal(t, `file:///another/database?cache=foobar&mode=ro`, c.String()) 53 | } 54 | 55 | func TestParseConnectionURL(t *testing.T) { 56 | var u ConnectionURL 57 | var s string 58 | var err error 59 | 60 | s = "file://mydatabase.db" 61 | u, err = ParseURL(s) 62 | assert.NoError(t, err) 63 | assert.Equal(t, "mydatabase.db", u.Database) 64 | 65 | s = "file:///path/to/my/database.db?mode=ro&cache=foobar" 66 | u, err = ParseURL(s) 67 | assert.NoError(t, err) 68 | assert.Equal(t, "/path/to/my/database.db", u.Database) 69 | 70 | s = "memory:///path/to/my/database.db?mode=ro&cache=foobar" 71 | u, err = ParseURL(s) 72 | assert.NoError(t, err) 73 | assert.Equal(t, "/path/to/my/database.db", u.Database) 74 | 75 | assert.Equal(t, "foobar", u.Options["cache"]) 76 | assert.Equal(t, "ro", u.Options["mode"]) 77 | 78 | s = "http://example.org" 79 | u, err = ParseURL(s) 80 | assert.Error(t, err) 81 | assert.Zero(t, u.Database) 82 | } 83 | -------------------------------------------------------------------------------- /adapter/ql/generic_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-today The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package ql 23 | 24 | import ( 25 | "testing" 26 | 27 | "github.com/stretchr/testify/suite" 28 | "github.com/upper/db/v4/internal/testsuite" 29 | ) 30 | 31 | type GenericTests struct { 32 | testsuite.GenericTestSuite 33 | } 34 | 35 | func (s *GenericTests) SetupSuite() { 36 | s.Helper = &Helper{} 37 | } 38 | 39 | func TestGeneric(t *testing.T) { 40 | suite.Run(t, &GenericTests{}) 41 | } 42 | -------------------------------------------------------------------------------- /adapter/ql/ql.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package ql 23 | 24 | import ( 25 | "database/sql" 26 | 27 | db "github.com/upper/db/v4" 28 | "github.com/upper/db/v4/internal/sqladapter" 29 | "github.com/upper/db/v4/internal/sqlbuilder" 30 | ) 31 | 32 | // Adapter is the public name of the adapter. 33 | const Adapter = `ql` 34 | 35 | var registeredAdapter = sqladapter.RegisterAdapter(Adapter, &database{}) 36 | 37 | // Open establishes a connection to the database server and returns a 38 | // db.Session instance (which is compatible with db.Session). 39 | func Open(connURL db.ConnectionURL) (db.Session, error) { 40 | return registeredAdapter.OpenDSN(connURL) 41 | } 42 | 43 | // NewTx creates a sqlbuilder.Tx instance by wrapping a *sql.Tx value. 44 | func NewTx(sqlTx *sql.Tx) (sqlbuilder.Tx, error) { 45 | return registeredAdapter.NewTx(sqlTx) 46 | } 47 | 48 | // New creates a sqlbuilder.Sesion instance by wrapping a *sql.DB value. 49 | func New(sqlDB *sql.DB) (db.Session, error) { 50 | return registeredAdapter.New(sqlDB) 51 | } 52 | -------------------------------------------------------------------------------- /adapter/ql/sql_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-today The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package ql 23 | 24 | import ( 25 | "testing" 26 | 27 | "github.com/stretchr/testify/suite" 28 | "github.com/upper/db/v4/internal/testsuite" 29 | ) 30 | 31 | type SQLTests struct { 32 | testsuite.SQLTestSuite 33 | } 34 | 35 | func (s *SQLTests) SetupSuite() { 36 | s.Helper = &Helper{} 37 | } 38 | 39 | func TestSQL(t *testing.T) { 40 | suite.Run(t, &SQLTests{}) 41 | } 42 | -------------------------------------------------------------------------------- /adapter/sqlite/README.md: -------------------------------------------------------------------------------- 1 | # SQLite adapter for upper/db 2 | 3 | Please read the full docs, acknowledgements and examples at 4 | [https://upper.io/v4/adapter/sqlite/](https://upper.io/v4/adapter/sqlite/). 5 | -------------------------------------------------------------------------------- /adapter/sqlite/collection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package sqlite 23 | 24 | import ( 25 | "database/sql" 26 | 27 | db "github.com/upper/db/v4" 28 | "github.com/upper/db/v4/internal/sqladapter" 29 | "github.com/upper/db/v4/internal/sqlbuilder" 30 | ) 31 | 32 | type collectionAdapter struct { 33 | } 34 | 35 | func (*collectionAdapter) Insert(col sqladapter.Collection, item interface{}) (interface{}, error) { 36 | columnNames, columnValues, err := sqlbuilder.Map(item, nil) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | pKey, err := col.PrimaryKeys() 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | q := col.SQL().InsertInto(col.Name()). 47 | Columns(columnNames...). 48 | Values(columnValues...) 49 | 50 | var res sql.Result 51 | if res, err = q.Exec(); err != nil { 52 | return nil, err 53 | } 54 | 55 | if len(pKey) <= 1 { 56 | return res.LastInsertId() 57 | } 58 | 59 | keyMap := db.Cond{} 60 | 61 | for i := range columnNames { 62 | for j := 0; j < len(pKey); j++ { 63 | if pKey[j] == columnNames[i] { 64 | keyMap[pKey[j]] = columnValues[i] 65 | } 66 | } 67 | } 68 | 69 | return keyMap, nil 70 | } 71 | -------------------------------------------------------------------------------- /adapter/sqlite/connection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package sqlite 23 | 24 | import ( 25 | "fmt" 26 | "net/url" 27 | "path/filepath" 28 | "runtime" 29 | "strings" 30 | ) 31 | 32 | const connectionScheme = `file` 33 | 34 | // ConnectionURL implements a SQLite connection struct. 35 | type ConnectionURL struct { 36 | Database string 37 | Options map[string]string 38 | } 39 | 40 | func (c ConnectionURL) String() (s string) { 41 | vv := url.Values{} 42 | 43 | if c.Database == "" { 44 | return "" 45 | } 46 | 47 | // Did the user provided a full database path? 48 | if !strings.HasPrefix(c.Database, "/") { 49 | c.Database, _ = filepath.Abs(c.Database) 50 | if runtime.GOOS == "windows" { 51 | // Closes https://github.com/upper/db/issues/60 52 | c.Database = "/" + strings.Replace(c.Database, `\`, `/`, -1) 53 | } 54 | } 55 | 56 | // Do we have any options? 57 | if c.Options == nil { 58 | c.Options = map[string]string{} 59 | } 60 | 61 | if _, ok := c.Options["_busy_timeout"]; !ok { 62 | c.Options["_busy_timeout"] = "10000" 63 | } 64 | 65 | // Converting options into URL values. 66 | for k, v := range c.Options { 67 | vv.Set(k, v) 68 | } 69 | 70 | // Building URL. 71 | u := url.URL{ 72 | Scheme: connectionScheme, 73 | Path: c.Database, 74 | RawQuery: vv.Encode(), 75 | } 76 | 77 | return u.String() 78 | } 79 | 80 | // ParseURL parses s into a ConnectionURL struct. 81 | func ParseURL(s string) (conn ConnectionURL, err error) { 82 | var u *url.URL 83 | 84 | if !strings.HasPrefix(s, connectionScheme+"://") { 85 | return conn, fmt.Errorf(`Expecting file:// connection scheme.`) 86 | } 87 | 88 | if u, err = url.Parse(s); err != nil { 89 | return conn, err 90 | } 91 | 92 | conn.Database = u.Host + u.Path 93 | conn.Options = map[string]string{} 94 | 95 | var vv url.Values 96 | 97 | if vv, err = url.ParseQuery(u.RawQuery); err != nil { 98 | return conn, err 99 | } 100 | 101 | for k := range vv { 102 | conn.Options[k] = vv.Get(k) 103 | } 104 | 105 | if _, ok := conn.Options["cache"]; !ok { 106 | conn.Options["cache"] = "shared" 107 | } 108 | 109 | return conn, err 110 | } 111 | -------------------------------------------------------------------------------- /adapter/sqlite/connection_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package sqlite 23 | 24 | import ( 25 | "path/filepath" 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/require" 30 | ) 31 | 32 | func TestConnectionURL(t *testing.T) { 33 | 34 | c := ConnectionURL{} 35 | 36 | assert.Equal(t, "", c.String(), "Expecting default connectiong string to be empty") 37 | 38 | // Adding a database name. 39 | c.Database = "myfilename" 40 | 41 | absoluteName, _ := filepath.Abs(c.Database) 42 | 43 | assert.Equal(t, "file://"+absoluteName+"?_busy_timeout=10000", c.String()) 44 | 45 | // Adding an option. 46 | c.Options = map[string]string{ 47 | "cache": "foobar", 48 | "mode": "ro", 49 | } 50 | 51 | assert.Equal(t, "file://"+absoluteName+"?_busy_timeout=10000&cache=foobar&mode=ro", c.String()) 52 | 53 | // Setting another database. 54 | c.Database = "/another/database" 55 | 56 | assert.Equal(t, "file:///another/database?_busy_timeout=10000&cache=foobar&mode=ro", c.String()) 57 | } 58 | 59 | func TestParseConnectionURL(t *testing.T) { 60 | var u ConnectionURL 61 | var s string 62 | var err error 63 | 64 | s = "file://mydatabase.db" 65 | 66 | u, err = ParseURL(s) 67 | require.NoError(t, err) 68 | 69 | assert.Equal(t, "mydatabase.db", u.Database) 70 | 71 | assert.Equal(t, "shared", u.Options["cache"]) 72 | 73 | s = "file:///path/to/my/database.db?_busy_timeout=10000&mode=ro&cache=foobar" 74 | 75 | u, err = ParseURL(s) 76 | require.NoError(t, err) 77 | 78 | assert.Equal(t, "/path/to/my/database.db", u.Database) 79 | 80 | assert.Equal(t, "foobar", u.Options["cache"]) 81 | assert.Equal(t, "ro", u.Options["mode"]) 82 | 83 | s = "http://example.org" 84 | _, err = ParseURL(s) 85 | require.Error(t, err) 86 | } 87 | -------------------------------------------------------------------------------- /adapter/sqlite/generic_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-today The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package sqlite 23 | 24 | import ( 25 | "testing" 26 | 27 | "github.com/stretchr/testify/suite" 28 | "github.com/upper/db/v4/internal/testsuite" 29 | ) 30 | 31 | type GenericTests struct { 32 | testsuite.GenericTestSuite 33 | } 34 | 35 | func (s *GenericTests) SetupSuite() { 36 | s.Helper = &Helper{} 37 | } 38 | 39 | func TestGeneric(t *testing.T) { 40 | suite.Run(t, &GenericTests{}) 41 | } 42 | -------------------------------------------------------------------------------- /adapter/sqlite/record_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-today The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package sqlite 23 | 24 | import ( 25 | "testing" 26 | 27 | "github.com/stretchr/testify/suite" 28 | "github.com/upper/db/v4/internal/testsuite" 29 | ) 30 | 31 | type RecordTests struct { 32 | testsuite.RecordTestSuite 33 | } 34 | 35 | func (s *RecordTests) SetupSuite() { 36 | s.Helper = &Helper{} 37 | } 38 | 39 | func TestRecord(t *testing.T) { 40 | suite.Run(t, &RecordTests{}) 41 | } 42 | -------------------------------------------------------------------------------- /adapter/sqlite/sql_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-today The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package sqlite 23 | 24 | import ( 25 | "testing" 26 | 27 | "github.com/stretchr/testify/suite" 28 | "github.com/upper/db/v4/internal/testsuite" 29 | ) 30 | 31 | type SQLTests struct { 32 | testsuite.SQLTestSuite 33 | } 34 | 35 | func (s *SQLTests) SetupSuite() { 36 | s.Helper = &Helper{} 37 | } 38 | 39 | func TestSQL(t *testing.T) { 40 | suite.Run(t, &SQLTests{}) 41 | } 42 | -------------------------------------------------------------------------------- /adapter/sqlite/sqlite.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package sqlite 23 | 24 | import ( 25 | "database/sql" 26 | 27 | db "github.com/upper/db/v4" 28 | 29 | "github.com/upper/db/v4/internal/sqladapter" 30 | "github.com/upper/db/v4/internal/sqlbuilder" 31 | ) 32 | 33 | // Adapter is the public name of the adapter. 34 | const Adapter = `sqlite` 35 | 36 | var registeredAdapter = sqladapter.RegisterAdapter(Adapter, &database{}) 37 | 38 | // Open establishes a connection to the database server and returns a 39 | // db.Session instance (which is compatible with db.Session). 40 | func Open(connURL db.ConnectionURL) (db.Session, error) { 41 | return registeredAdapter.OpenDSN(connURL) 42 | } 43 | 44 | // NewTx creates a sqlbuilder.Tx instance by wrapping a *sql.Tx value. 45 | func NewTx(sqlTx *sql.Tx) (sqlbuilder.Tx, error) { 46 | return registeredAdapter.NewTx(sqlTx) 47 | } 48 | 49 | // New creates a sqlbuilder.Sesion instance by wrapping a *sql.DB value. 50 | func New(sqlDB *sql.DB) (db.Session, error) { 51 | return registeredAdapter.New(sqlDB) 52 | } 53 | -------------------------------------------------------------------------------- /adapter/sqlite/sqlite_test.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "database/sql" 8 | 9 | "github.com/stretchr/testify/suite" 10 | "github.com/upper/db/v4/internal/testsuite" 11 | ) 12 | 13 | type AdapterTests struct { 14 | testsuite.Suite 15 | } 16 | 17 | func (s *AdapterTests) SetupSuite() { 18 | s.Helper = &Helper{} 19 | } 20 | 21 | func (s *AdapterTests) Test_Issue633_OpenSession() { 22 | sess, err := Open(settings) 23 | s.NoError(err) 24 | defer sess.Close() 25 | 26 | absoluteName, _ := filepath.Abs(settings.Database) 27 | s.Equal(absoluteName, sess.Name()) 28 | } 29 | 30 | func (s *AdapterTests) Test_Issue633_NewAdapterWithFile() { 31 | sqldb, err := sql.Open("sqlite3", settings.Database) 32 | s.NoError(err) 33 | 34 | sess, err := New(sqldb) 35 | s.NoError(err) 36 | defer sess.Close() 37 | 38 | absoluteName, _ := filepath.Abs(settings.Database) 39 | s.Equal(absoluteName, sess.Name()) 40 | } 41 | 42 | func (s *AdapterTests) Test_Issue633_NewAdapterWithMemory() { 43 | sqldb, err := sql.Open("sqlite3", ":memory:") 44 | s.NoError(err) 45 | 46 | sess, err := New(sqldb) 47 | s.NoError(err) 48 | defer sess.Close() 49 | 50 | s.Equal("main", sess.Name()) 51 | } 52 | 53 | func TestAdapter(t *testing.T) { 54 | suite.Run(t, &AdapterTests{}) 55 | } 56 | -------------------------------------------------------------------------------- /collection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package db 23 | 24 | // Collection defines methods to work with database tables or collections. 25 | type Collection interface { 26 | 27 | // Name returns the name of the collection. 28 | Name() string 29 | 30 | // Session returns the Session that was used to create the collection 31 | // reference. 32 | Session() Session 33 | 34 | // Find defines a new result set. 35 | Find(...interface{}) Result 36 | 37 | Count() (uint64, error) 38 | 39 | // Insert inserts a new item into the collection, the type of this item could 40 | // be a map, a struct or pointer to either of them. If the call succeeds and 41 | // if the collection has a primary key, Insert returns the ID of the newly 42 | // added element as an `interface{}`. The underlying type of this ID depends 43 | // on both the database adapter and the column storing the ID. The ID 44 | // returned by Insert() could be passed directly to Find() to retrieve the 45 | // newly added element. 46 | Insert(interface{}) (InsertResult, error) 47 | 48 | // InsertReturning is like Insert() but it takes a pointer to map or struct 49 | // and, if the operation succeeds, updates it with data from the newly 50 | // inserted row. If the database does not support transactions this method 51 | // returns db.ErrUnsupported. 52 | InsertReturning(interface{}) error 53 | 54 | // UpdateReturning takes a pointer to a map or struct and tries to update the 55 | // row the item is refering to. If the element is updated sucessfully, 56 | // UpdateReturning will fetch the row and update the fields of the passed 57 | // item. If the database does not support transactions this method returns 58 | // db.ErrUnsupported 59 | UpdateReturning(interface{}) error 60 | 61 | // Exists returns true if the collection exists, false otherwise. 62 | Exists() (bool, error) 63 | 64 | // Truncate removes all elements on the collection. 65 | Truncate() error 66 | } 67 | -------------------------------------------------------------------------------- /cond_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCond(t *testing.T) { 10 | t.Run("Base", func(t *testing.T) { 11 | var c Cond 12 | 13 | c = Cond{} 14 | assert.True(t, c.Empty()) 15 | 16 | c = Cond{"id": 1} 17 | assert.False(t, c.Empty()) 18 | }) 19 | 20 | t.Run("And", func(t *testing.T) { 21 | var a *AndExpr 22 | 23 | a = And() 24 | assert.True(t, a.Empty()) 25 | 26 | _ = a.And(Cond{"id": 1}) 27 | assert.True(t, a.Empty(), "conditions are immutable") 28 | 29 | a = a.And(Cond{"name": "Ana"}) 30 | assert.False(t, a.Empty()) 31 | 32 | a = a.And().And() 33 | assert.False(t, a.Empty()) 34 | }) 35 | 36 | t.Run("Or", func(t *testing.T) { 37 | var a *OrExpr 38 | 39 | a = Or() 40 | assert.True(t, a.Empty()) 41 | 42 | _ = a.Or(Cond{"id": 1}) 43 | assert.True(t, a.Empty(), "conditions are immutable") 44 | 45 | a = a.Or(Cond{"name": "Ana"}) 46 | assert.False(t, a.Empty()) 47 | 48 | a = a.Or().Or() 49 | assert.False(t, a.Empty()) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /connection_url.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package db 23 | 24 | // ConnectionURL represents a data source name (DSN). 25 | type ConnectionURL interface { 26 | // String returns the connection string that is going to be passed to the 27 | // adapter. 28 | String() string 29 | } 30 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | type ContextKey string 4 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | // Package db (or upper/db) provides an agnostic data access layer to work with 23 | // different databases. 24 | // 25 | // Install upper/db: 26 | // 27 | // go get github.com/upper/db 28 | // 29 | // Usage 30 | // 31 | // package main 32 | // 33 | // import ( 34 | // "log" 35 | // 36 | // "github.com/upper/db/v4/adapter/postgresql" // Imports the postgresql adapter. 37 | // ) 38 | // 39 | // var settings = postgresql.ConnectionURL{ 40 | // Database: `booktown`, 41 | // Host: `demo.upper.io`, 42 | // User: `demouser`, 43 | // Password: `demop4ss`, 44 | // } 45 | // 46 | // // Book represents a book. 47 | // type Book struct { 48 | // ID uint `db:"id"` 49 | // Title string `db:"title"` 50 | // AuthorID uint `db:"author_id"` 51 | // SubjectID uint `db:"subject_id"` 52 | // } 53 | // 54 | // func main() { 55 | // sess, err := postgresql.Open(settings) 56 | // if err != nil { 57 | // log.Fatal(err) 58 | // } 59 | // defer sess.Close() 60 | // 61 | // var books []Book 62 | // if err := sess.Collection("books").Find().OrderBy("title").All(&books); err != nil { 63 | // log.Fatal(err) 64 | // } 65 | // 66 | // log.Println("Books:") 67 | // for _, book := range books { 68 | // log.Printf("%q (ID: %d)\n", book.Title, book.ID) 69 | // } 70 | // } 71 | package db 72 | -------------------------------------------------------------------------------- /function.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package db 23 | 24 | import ( 25 | "github.com/upper/db/v4/internal/adapter" 26 | ) 27 | 28 | // FuncExpr represents functions. 29 | type FuncExpr = adapter.FuncExpr 30 | 31 | // Func returns a database function expression. 32 | // 33 | // Examples: 34 | // 35 | // // MOD(29, 9) 36 | // db.Func("MOD", 29, 9) 37 | // 38 | // // CONCAT("foo", "bar") 39 | // db.Func("CONCAT", "foo", "bar") 40 | // 41 | // // NOW() 42 | // db.Func("NOW") 43 | // 44 | // // RTRIM("Hello ") 45 | // db.Func("RTRIM", "Hello ") 46 | func Func(name string, args ...interface{}) *FuncExpr { 47 | return adapter.NewFuncExpr(name, args) 48 | } 49 | -------------------------------------------------------------------------------- /function_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCustomFunctions(t *testing.T) { 10 | t.Run("Nil arguments", func(t *testing.T) { 11 | fn := Func("HELLO") 12 | assert.Equal(t, "HELLO", fn.Name()) 13 | assert.Equal(t, []interface{}(nil), fn.Arguments()) 14 | }) 15 | 16 | t.Run("Single argument", func(t *testing.T) { 17 | fn := Func("CONCAT", "a") 18 | assert.Equal(t, "CONCAT", fn.Name()) 19 | assert.Equal(t, []interface{}{"a"}, fn.Arguments()) 20 | }) 21 | 22 | t.Run("Two arguments", func(t *testing.T) { 23 | fn := Func("MOD", 29, 9) 24 | assert.Equal(t, "MOD", fn.Name()) 25 | assert.Equal(t, []interface{}{29, 9}, fn.Arguments()) 26 | }) 27 | 28 | t.Run("Multiple arguments", func(t *testing.T) { 29 | fn := Func("CONCAT", "a", "b", "c") 30 | assert.Equal(t, "CONCAT", fn.Name()) 31 | assert.Equal(t, []interface{}{"a", "b", "c"}, fn.Arguments()) 32 | }) 33 | 34 | t.Run("Slice argument", func(t *testing.T) { 35 | fn := Func("IN", []interface{}{"a", "b", "c"}) 36 | assert.Equal(t, "IN", fn.Name()) 37 | assert.Equal(t, []interface{}{[]interface{}{"a", "b", "c"}}, fn.Arguments()) 38 | }) 39 | 40 | t.Run("Slice argument with one element", func(t *testing.T) { 41 | fn := Func("IN", []interface{}{"a"}) 42 | assert.Equal(t, "IN", fn.Name()) 43 | assert.Equal(t, []interface{}{[]interface{}{"a"}}, fn.Arguments()) 44 | }) 45 | 46 | t.Run("Nil slice argument", func(t *testing.T) { 47 | fn := Func("IN", []interface{}(nil)) 48 | assert.Equal(t, "IN", fn.Name()) 49 | assert.Equal(t, []interface{}{[]interface{}(nil)}, fn.Arguments()) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/upper/db/v4 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/denisenkom/go-mssqldb v0.12.3 9 | github.com/go-sql-driver/mysql v1.9.0 10 | github.com/google/uuid v1.1.1 11 | github.com/ipfs/go-detect-race v0.0.1 12 | github.com/jackc/pgtype v1.14.4 13 | github.com/jackc/pgx/v5 v5.7.2 14 | github.com/lib/pq v1.10.9 15 | github.com/mattn/go-sqlite3 v1.14.24 16 | github.com/segmentio/fasthash v1.0.3 17 | github.com/sirupsen/logrus v1.9.3 18 | github.com/stretchr/testify v1.10.0 19 | go.mongodb.org/mongo-driver v1.17.3 20 | modernc.org/ql v1.4.11 21 | ) 22 | 23 | require ( 24 | filippo.io/edwards25519 v1.1.0 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/edsrzf/mmap-go v1.2.0 // indirect 27 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 28 | github.com/golang-sql/sqlexp v0.1.0 // indirect 29 | github.com/golang/snappy v1.0.0 // indirect 30 | github.com/jackc/pgio v1.0.0 // indirect 31 | github.com/jackc/pgpassfile v1.0.0 // indirect 32 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 33 | github.com/jackc/pgx/v4 v4.18.3 // indirect 34 | github.com/jackc/puddle/v2 v2.2.2 // indirect 35 | github.com/klauspost/compress v1.18.0 // indirect 36 | github.com/kr/text v0.2.0 // indirect 37 | github.com/montanaflynn/stats v0.7.1 // indirect 38 | github.com/pmezard/go-difflib v1.0.0 // indirect 39 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 40 | github.com/rogpeppe/go-internal v1.6.1 // indirect 41 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 42 | github.com/xdg-go/scram v1.1.2 // indirect 43 | github.com/xdg-go/stringprep v1.0.4 // indirect 44 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 45 | golang.org/x/crypto v0.36.0 // indirect 46 | golang.org/x/sync v0.12.0 // indirect 47 | golang.org/x/sys v0.31.0 // indirect 48 | golang.org/x/text v0.23.0 // indirect 49 | gopkg.in/yaml.v3 v3.0.1 // indirect 50 | modernc.org/b v1.1.0 // indirect 51 | modernc.org/db v1.0.13 // indirect 52 | modernc.org/file v1.0.9 // indirect 53 | modernc.org/fileutil v1.3.0 // indirect 54 | modernc.org/golex v1.1.0 // indirect 55 | modernc.org/internal v1.1.1 // indirect 56 | modernc.org/lldb v1.0.8 // indirect 57 | modernc.org/mathutil v1.7.1 // indirect 58 | modernc.org/sortutil v1.2.1 // indirect 59 | modernc.org/strutil v1.2.1 // indirect 60 | modernc.org/zappy v1.1.0 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /internal/adapter/comparison.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | // ComparisonOperator is the base type for comparison operators. 4 | type ComparisonOperator uint8 5 | 6 | // Comparison operators 7 | const ( 8 | ComparisonOperatorNone ComparisonOperator = iota 9 | ComparisonOperatorCustom 10 | 11 | ComparisonOperatorEqual 12 | ComparisonOperatorNotEqual 13 | 14 | ComparisonOperatorLessThan 15 | ComparisonOperatorGreaterThan 16 | 17 | ComparisonOperatorLessThanOrEqualTo 18 | ComparisonOperatorGreaterThanOrEqualTo 19 | 20 | ComparisonOperatorBetween 21 | ComparisonOperatorNotBetween 22 | 23 | ComparisonOperatorIn 24 | ComparisonOperatorNotIn 25 | 26 | ComparisonOperatorIs 27 | ComparisonOperatorIsNot 28 | 29 | ComparisonOperatorLike 30 | ComparisonOperatorNotLike 31 | 32 | ComparisonOperatorRegExp 33 | ComparisonOperatorNotRegExp 34 | ) 35 | 36 | type Comparison struct { 37 | t ComparisonOperator 38 | op string 39 | v interface{} 40 | } 41 | 42 | func (c *Comparison) CustomOperator() string { 43 | return c.op 44 | } 45 | 46 | func (c *Comparison) Operator() ComparisonOperator { 47 | return c.t 48 | } 49 | 50 | func (c *Comparison) Value() interface{} { 51 | return c.v 52 | } 53 | 54 | func NewComparisonOperator(t ComparisonOperator, v interface{}) *Comparison { 55 | return &Comparison{t: t, v: v} 56 | } 57 | 58 | func NewCustomComparisonOperator(op string, v interface{}) *Comparison { 59 | return &Comparison{t: ComparisonOperatorCustom, op: op, v: v} 60 | } 61 | -------------------------------------------------------------------------------- /internal/adapter/constraint.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package adapter 23 | 24 | // ConstraintValuer allows constraints to use specific values of their own. 25 | type ConstraintValuer interface { 26 | ConstraintValue() interface{} 27 | } 28 | 29 | // Constraint interface represents a single condition, like "a = 1". where `a` 30 | // is the key and `1` is the value. This is an exported interface but it's 31 | // rarely used directly, you may want to use the `db.Cond{}` map instead. 32 | type Constraint interface { 33 | // Key is the leftmost part of the constraint and usually contains a column 34 | // name. 35 | Key() interface{} 36 | 37 | // Value if the rightmost part of the constraint and usually contains a 38 | // column value. 39 | Value() interface{} 40 | } 41 | 42 | // Constraints interface represents an array of constraints, like "a = 1, b = 43 | // 2, c = 3". 44 | type Constraints interface { 45 | // Constraints returns an array of constraints. 46 | Constraints() []Constraint 47 | } 48 | 49 | type constraint struct { 50 | k interface{} 51 | v interface{} 52 | } 53 | 54 | func (c constraint) Key() interface{} { 55 | return c.k 56 | } 57 | 58 | func (c constraint) Value() interface{} { 59 | if constraintValuer, ok := c.v.(ConstraintValuer); ok { 60 | return constraintValuer.ConstraintValue() 61 | } 62 | return c.v 63 | } 64 | 65 | // NewConstraint creates a constraint. 66 | func NewConstraint(key interface{}, value interface{}) Constraint { 67 | return &constraint{k: key, v: value} 68 | } 69 | 70 | var ( 71 | _ = Constraint(&constraint{}) 72 | ) 73 | -------------------------------------------------------------------------------- /internal/adapter/func.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package adapter 23 | 24 | type FuncExpr struct { 25 | name string 26 | args []interface{} 27 | } 28 | 29 | func (f *FuncExpr) Arguments() []interface{} { 30 | return f.args 31 | } 32 | 33 | func (f *FuncExpr) Name() string { 34 | return f.name 35 | } 36 | 37 | func NewFuncExpr(name string, args []interface{}) *FuncExpr { 38 | return &FuncExpr{name: name, args: args} 39 | } 40 | -------------------------------------------------------------------------------- /internal/adapter/raw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package adapter 23 | 24 | // RawExpr interface represents values that can bypass SQL filters. This is an 25 | // exported interface but it's rarely used directly, you may want to use the 26 | // `db.Raw()` function instead. 27 | type RawExpr struct { 28 | value string 29 | args *[]interface{} 30 | } 31 | 32 | func (r *RawExpr) Arguments() []interface{} { 33 | if r.args != nil { 34 | return *r.args 35 | } 36 | return nil 37 | } 38 | 39 | func (r RawExpr) Raw() string { 40 | return r.value 41 | } 42 | 43 | func (r RawExpr) String() string { 44 | return r.Raw() 45 | } 46 | 47 | // Expressions returns a logical expressio.n 48 | func (r *RawExpr) Expressions() []LogicalExpr { 49 | return []LogicalExpr{r} 50 | } 51 | 52 | // Operator returns the default compound operator. 53 | func (r RawExpr) Operator() LogicalOperator { 54 | return LogicalOperatorNone 55 | } 56 | 57 | // Empty return false if this struct has no value. 58 | func (r *RawExpr) Empty() bool { 59 | return r.value == "" 60 | } 61 | 62 | func NewRawExpr(value string, args []interface{}) *RawExpr { 63 | r := &RawExpr{value: value, args: nil} 64 | if len(args) > 0 { 65 | r.args = &args 66 | } 67 | return r 68 | } 69 | 70 | var _ = LogicalExpr(&RawExpr{}) 71 | -------------------------------------------------------------------------------- /internal/cache/cache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-present José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package cache 23 | 24 | import ( 25 | "fmt" 26 | "hash/fnv" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | type cacheableT struct { 33 | Name string 34 | } 35 | 36 | func (ct *cacheableT) Hash() uint64 { 37 | s := fnv.New64() 38 | s.Sum([]byte(ct.Name)) 39 | return s.Sum64() 40 | } 41 | 42 | func TestCache(t *testing.T) { 43 | var c *Cache 44 | 45 | var ( 46 | key = cacheableT{"foo"} 47 | value = "bar" 48 | ) 49 | 50 | t.Run("New", func(t *testing.T) { 51 | c = NewCache() 52 | assert.NotNil(t, c) 53 | }) 54 | 55 | t.Run("ReadNonExistentValue", func(t *testing.T) { 56 | _, ok := c.Read(&key) 57 | assert.False(t, ok) 58 | }) 59 | 60 | t.Run("Write", func(t *testing.T) { 61 | c.Write(&key, value) 62 | c.Write(&key, value) 63 | }) 64 | 65 | t.Run("ReadExistentValue", func(t *testing.T) { 66 | v, ok := c.Read(&key) 67 | assert.True(t, ok) 68 | assert.Equal(t, value, v) 69 | }) 70 | } 71 | 72 | func BenchmarkNewCache(b *testing.B) { 73 | for i := 0; i < b.N; i++ { 74 | NewCache() 75 | } 76 | } 77 | 78 | func BenchmarkNewCacheAndClear(b *testing.B) { 79 | for i := 0; i < b.N; i++ { 80 | c := NewCache() 81 | c.Clear() 82 | } 83 | } 84 | 85 | func BenchmarkReadNonExistentValue(b *testing.B) { 86 | key := cacheableT{"foo"} 87 | 88 | z := NewCache() 89 | for i := 0; i < b.N; i++ { 90 | z.Read(&key) 91 | } 92 | } 93 | 94 | func BenchmarkWriteSameValue(b *testing.B) { 95 | key := cacheableT{"foo"} 96 | value := "bar" 97 | 98 | z := NewCache() 99 | for i := 0; i < b.N; i++ { 100 | z.Write(&key, value) 101 | } 102 | } 103 | 104 | func BenchmarkWriteNewValue(b *testing.B) { 105 | value := "bar" 106 | 107 | z := NewCache() 108 | for i := 0; i < b.N; i++ { 109 | key := cacheableT{fmt.Sprintf("item-%d", i)} 110 | z.Write(&key, value) 111 | } 112 | } 113 | 114 | func BenchmarkReadExistentValue(b *testing.B) { 115 | key := cacheableT{"foo"} 116 | value := "bar" 117 | 118 | z := NewCache() 119 | z.Write(&key, value) 120 | for i := 0; i < b.N; i++ { 121 | z.Read(&key) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /internal/cache/hash.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/segmentio/fasthash/fnv1a" 7 | ) 8 | 9 | const ( 10 | hashTypeInt uint64 = 1 << iota 11 | hashTypeSignedInt 12 | hashTypeBool 13 | hashTypeString 14 | hashTypeHashable 15 | hashTypeNil 16 | ) 17 | 18 | type hasher struct { 19 | t uint64 20 | v interface{} 21 | } 22 | 23 | func (h *hasher) Hash() uint64 { 24 | return NewHash(h.t, h.v) 25 | } 26 | 27 | func NewHashable(t uint64, v interface{}) Hashable { 28 | return &hasher{t: t, v: v} 29 | } 30 | 31 | func InitHash(t uint64) uint64 { 32 | return fnv1a.AddUint64(fnv1a.Init64, t) 33 | } 34 | 35 | func NewHash(t uint64, in ...interface{}) uint64 { 36 | return AddToHash(InitHash(t), in...) 37 | } 38 | 39 | func AddToHash(h uint64, in ...interface{}) uint64 { 40 | for i := range in { 41 | if in[i] == nil { 42 | continue 43 | } 44 | h = addToHash(h, in[i]) 45 | } 46 | return h 47 | } 48 | 49 | func addToHash(h uint64, in interface{}) uint64 { 50 | switch v := in.(type) { 51 | case uint64: 52 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), v) 53 | case uint32: 54 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v)) 55 | case uint16: 56 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v)) 57 | case uint8: 58 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v)) 59 | case uint: 60 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v)) 61 | case int64: 62 | if v < 0 { 63 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeSignedInt), uint64(-v)) 64 | } else { 65 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v)) 66 | } 67 | case int32: 68 | if v < 0 { 69 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeSignedInt), uint64(-v)) 70 | } else { 71 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v)) 72 | } 73 | case int16: 74 | if v < 0 { 75 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeSignedInt), uint64(-v)) 76 | } else { 77 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v)) 78 | } 79 | case int8: 80 | if v < 0 { 81 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeSignedInt), uint64(-v)) 82 | } else { 83 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v)) 84 | } 85 | case int: 86 | if v < 0 { 87 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeSignedInt), uint64(-v)) 88 | } else { 89 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeInt), uint64(v)) 90 | } 91 | case bool: 92 | if v { 93 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeBool), 1) 94 | } else { 95 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeBool), 2) 96 | } 97 | case string: 98 | return fnv1a.AddString64(fnv1a.AddUint64(h, hashTypeString), v) 99 | case Hashable: 100 | if in == nil { 101 | panic(fmt.Sprintf("could not hash nil element %T", in)) 102 | } 103 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeHashable), v.Hash()) 104 | case nil: 105 | return fnv1a.AddUint64(fnv1a.AddUint64(h, hashTypeNil), 0) 106 | default: 107 | panic(fmt.Sprintf("unsupported value type %T", in)) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /internal/cache/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-present José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package cache 23 | 24 | // Hashable types must implement a method that returns a key. This key will be 25 | // associated with a cached value. 26 | type Hashable interface { 27 | Hash() uint64 28 | } 29 | 30 | // HasOnEvict type is (optionally) implemented by cache objects to clean after 31 | // themselves. 32 | type HasOnEvict interface { 33 | OnEvict() 34 | } 35 | -------------------------------------------------------------------------------- /internal/immutable/immutable.go: -------------------------------------------------------------------------------- 1 | package immutable 2 | 3 | // Immutable represents an immutable chain that, if passed to FastForward, 4 | // applies Fn() to every element of a chain, the first element of this chain is 5 | // represented by Base(). 6 | type Immutable interface { 7 | // Prev is the previous element on a chain. 8 | Prev() Immutable 9 | // Fn a function that is able to modify the passed element. 10 | Fn(interface{}) error 11 | // Base is the first element on a chain, there's no previous element before 12 | // the Base element. 13 | Base() interface{} 14 | } 15 | 16 | // FastForward applies all Fn methods in order on the given new Base. 17 | func FastForward(curr Immutable) (interface{}, error) { 18 | prev := curr.Prev() 19 | if prev == nil { 20 | return curr.Base(), nil 21 | } 22 | in, err := FastForward(prev) 23 | if err != nil { 24 | return nil, err 25 | } 26 | err = curr.Fn(in) 27 | return in, err 28 | } 29 | -------------------------------------------------------------------------------- /internal/reflectx/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Jason Moiron 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /internal/reflectx/README.md: -------------------------------------------------------------------------------- 1 | # reflectx 2 | 3 | The sqlx package has special reflect needs. In particular, it needs to: 4 | 5 | * be able to map a name to a field 6 | * understand embedded structs 7 | * understand mapping names to fields by a particular tag 8 | * user specified name -> field mapping functions 9 | 10 | These behaviors mimic the behaviors by the standard library marshallers and also the 11 | behavior of standard Go accessors. 12 | 13 | The first two are amply taken care of by `Reflect.Value.FieldByName`, and the third is 14 | addressed by `Reflect.Value.FieldByNameFunc`, but these don't quite understand struct 15 | tags in the ways that are vital to most marshalers, and they are slow. 16 | 17 | This reflectx package extends reflect to achieve these goals. 18 | -------------------------------------------------------------------------------- /internal/sqladapter/compat/query.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.8 2 | // +build !go1.8 3 | 4 | package compat 5 | 6 | import ( 7 | "context" 8 | "database/sql" 9 | ) 10 | 11 | type PreparedExecer interface { 12 | Exec(...interface{}) (sql.Result, error) 13 | } 14 | 15 | func PreparedExecContext(p PreparedExecer, ctx context.Context, args []interface{}) (sql.Result, error) { 16 | return p.Exec(args...) 17 | } 18 | 19 | type Execer interface { 20 | Exec(string, ...interface{}) (sql.Result, error) 21 | } 22 | 23 | func ExecContext(p Execer, ctx context.Context, query string, args []interface{}) (sql.Result, error) { 24 | return p.Exec(query, args...) 25 | } 26 | 27 | type PreparedQueryer interface { 28 | Query(...interface{}) (*sql.Rows, error) 29 | } 30 | 31 | func PreparedQueryContext(p PreparedQueryer, ctx context.Context, args []interface{}) (*sql.Rows, error) { 32 | return p.Query(args...) 33 | } 34 | 35 | type Queryer interface { 36 | Query(string, ...interface{}) (*sql.Rows, error) 37 | } 38 | 39 | func QueryContext(p Queryer, ctx context.Context, query string, args []interface{}) (*sql.Rows, error) { 40 | return p.Query(query, args...) 41 | } 42 | 43 | type PreparedRowQueryer interface { 44 | QueryRow(...interface{}) *sql.Row 45 | } 46 | 47 | func PreparedQueryRowContext(p PreparedRowQueryer, ctx context.Context, args []interface{}) *sql.Row { 48 | return p.QueryRow(args...) 49 | } 50 | 51 | type RowQueryer interface { 52 | QueryRow(string, ...interface{}) *sql.Row 53 | } 54 | 55 | func QueryRowContext(p RowQueryer, ctx context.Context, query string, args []interface{}) *sql.Row { 56 | return p.QueryRow(query, args...) 57 | } 58 | 59 | type Preparer interface { 60 | Prepare(string) (*sql.Stmt, error) 61 | } 62 | 63 | func PrepareContext(p Preparer, ctx context.Context, query string) (*sql.Stmt, error) { 64 | return p.Prepare(query) 65 | } 66 | 67 | type TxStarter interface { 68 | Begin() (*sql.Tx, error) 69 | } 70 | 71 | func BeginTx(p TxStarter, ctx context.Context, opts interface{}) (*sql.Tx, error) { 72 | return p.Begin() 73 | } 74 | -------------------------------------------------------------------------------- /internal/sqladapter/compat/query_go18.go: -------------------------------------------------------------------------------- 1 | //go:build go1.8 2 | // +build go1.8 3 | 4 | package compat 5 | 6 | import ( 7 | "context" 8 | "database/sql" 9 | ) 10 | 11 | type PreparedExecer interface { 12 | ExecContext(context.Context, ...interface{}) (sql.Result, error) 13 | } 14 | 15 | func PreparedExecContext(p PreparedExecer, ctx context.Context, args []interface{}) (sql.Result, error) { 16 | return p.ExecContext(ctx, args...) 17 | } 18 | 19 | type Execer interface { 20 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 21 | } 22 | 23 | func ExecContext(p Execer, ctx context.Context, query string, args []interface{}) (sql.Result, error) { 24 | return p.ExecContext(ctx, query, args...) 25 | } 26 | 27 | type PreparedQueryer interface { 28 | QueryContext(context.Context, ...interface{}) (*sql.Rows, error) 29 | } 30 | 31 | func PreparedQueryContext(p PreparedQueryer, ctx context.Context, args []interface{}) (*sql.Rows, error) { 32 | return p.QueryContext(ctx, args...) 33 | } 34 | 35 | type Queryer interface { 36 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 37 | } 38 | 39 | func QueryContext(p Queryer, ctx context.Context, query string, args []interface{}) (*sql.Rows, error) { 40 | return p.QueryContext(ctx, query, args...) 41 | } 42 | 43 | type PreparedRowQueryer interface { 44 | QueryRowContext(context.Context, ...interface{}) *sql.Row 45 | } 46 | 47 | func PreparedQueryRowContext(p PreparedRowQueryer, ctx context.Context, args []interface{}) *sql.Row { 48 | return p.QueryRowContext(ctx, args...) 49 | } 50 | 51 | type RowQueryer interface { 52 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 53 | } 54 | 55 | func QueryRowContext(p RowQueryer, ctx context.Context, query string, args []interface{}) *sql.Row { 56 | return p.QueryRowContext(ctx, query, args...) 57 | } 58 | 59 | type Preparer interface { 60 | PrepareContext(context.Context, string) (*sql.Stmt, error) 61 | } 62 | 63 | func PrepareContext(p Preparer, ctx context.Context, query string) (*sql.Stmt, error) { 64 | return p.PrepareContext(ctx, query) 65 | } 66 | 67 | type TxStarter interface { 68 | BeginTx(context.Context, *sql.TxOptions) (*sql.Tx, error) 69 | } 70 | 71 | func BeginTx(p TxStarter, ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { 72 | return p.BeginTx(ctx, opts) 73 | } 74 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/column.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/upper/db/v4/internal/cache" 8 | ) 9 | 10 | type columnWithAlias struct { 11 | Name string 12 | Alias string 13 | } 14 | 15 | // Column represents a SQL column. 16 | type Column struct { 17 | Name interface{} 18 | } 19 | 20 | var _ = Fragment(&Column{}) 21 | 22 | // ColumnWithName creates and returns a Column with the given name. 23 | func ColumnWithName(name string) *Column { 24 | return &Column{Name: name} 25 | } 26 | 27 | // Hash returns a unique identifier for the struct. 28 | func (c *Column) Hash() uint64 { 29 | if c == nil { 30 | return cache.NewHash(FragmentType_Column, nil) 31 | } 32 | return cache.NewHash(FragmentType_Column, c.Name) 33 | } 34 | 35 | // Compile transforms the ColumnValue into an equivalent SQL representation. 36 | func (c *Column) Compile(layout *Template) (compiled string, err error) { 37 | if z, ok := layout.Read(c); ok { 38 | return z, nil 39 | } 40 | 41 | var alias string 42 | switch value := c.Name.(type) { 43 | case string: 44 | value = trimString(value) 45 | 46 | chunks := separateByAS(value) 47 | if len(chunks) == 1 { 48 | chunks = separateBySpace(value) 49 | } 50 | 51 | name := chunks[0] 52 | nameChunks := strings.SplitN(name, layout.ColumnSeparator, 2) 53 | 54 | for i := range nameChunks { 55 | nameChunks[i] = trimString(nameChunks[i]) 56 | if nameChunks[i] == "*" { 57 | continue 58 | } 59 | nameChunks[i] = layout.MustCompile(layout.IdentifierQuote, Raw{Value: nameChunks[i]}) 60 | } 61 | 62 | compiled = strings.Join(nameChunks, layout.ColumnSeparator) 63 | 64 | if len(chunks) > 1 { 65 | alias = trimString(chunks[1]) 66 | alias = layout.MustCompile(layout.IdentifierQuote, Raw{Value: alias}) 67 | } 68 | case compilable: 69 | compiled, err = value.Compile(layout) 70 | if err != nil { 71 | return "", err 72 | } 73 | default: 74 | return "", fmt.Errorf(errExpectingHashableFmt, c.Name) 75 | } 76 | 77 | if alias != "" { 78 | compiled = layout.MustCompile(layout.ColumnAliasLayout, columnWithAlias{compiled, alias}) 79 | } 80 | 81 | layout.Write(c, compiled) 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/column_test.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestColumnString(t *testing.T) { 10 | column := Column{Name: "role.name"} 11 | s, err := column.Compile(defaultTemplate) 12 | assert.NoError(t, err) 13 | assert.Equal(t, `"role"."name"`, s) 14 | } 15 | 16 | func TestColumnAs(t *testing.T) { 17 | column := Column{Name: "role.name as foo"} 18 | s, err := column.Compile(defaultTemplate) 19 | assert.NoError(t, err) 20 | assert.Equal(t, `"role"."name" AS "foo"`, s) 21 | } 22 | 23 | func TestColumnImplicitAs(t *testing.T) { 24 | column := Column{Name: "role.name foo"} 25 | s, err := column.Compile(defaultTemplate) 26 | assert.NoError(t, err) 27 | assert.Equal(t, `"role"."name" AS "foo"`, s) 28 | } 29 | 30 | func TestColumnRaw(t *testing.T) { 31 | column := Column{Name: &Raw{Value: "role.name As foo"}} 32 | s, err := column.Compile(defaultTemplate) 33 | assert.NoError(t, err) 34 | assert.Equal(t, `role.name As foo`, s) 35 | } 36 | 37 | func BenchmarkColumnWithName(b *testing.B) { 38 | for i := 0; i < b.N; i++ { 39 | _ = ColumnWithName("a") 40 | } 41 | } 42 | 43 | func BenchmarkColumnHash(b *testing.B) { 44 | c := Column{Name: "name"} 45 | b.ResetTimer() 46 | for i := 0; i < b.N; i++ { 47 | c.Hash() 48 | } 49 | } 50 | 51 | func BenchmarkColumnCompile(b *testing.B) { 52 | c := Column{Name: "name"} 53 | b.ResetTimer() 54 | for i := 0; i < b.N; i++ { 55 | _, _ = c.Compile(defaultTemplate) 56 | } 57 | } 58 | 59 | func BenchmarkColumnCompileNoCache(b *testing.B) { 60 | for i := 0; i < b.N; i++ { 61 | c := Column{Name: "name"} 62 | _, _ = c.Compile(defaultTemplate) 63 | } 64 | } 65 | 66 | func BenchmarkColumnWithDotCompile(b *testing.B) { 67 | c := Column{Name: "role.name"} 68 | b.ResetTimer() 69 | for i := 0; i < b.N; i++ { 70 | _, _ = c.Compile(defaultTemplate) 71 | } 72 | } 73 | 74 | func BenchmarkColumnWithImplicitAsKeywordCompile(b *testing.B) { 75 | c := Column{Name: "role.name foo"} 76 | b.ResetTimer() 77 | for i := 0; i < b.N; i++ { 78 | _, _ = c.Compile(defaultTemplate) 79 | } 80 | } 81 | 82 | func BenchmarkColumnWithAsKeywordCompile(b *testing.B) { 83 | c := Column{Name: "role.name AS foo"} 84 | b.ResetTimer() 85 | for i := 0; i < b.N; i++ { 86 | _, _ = c.Compile(defaultTemplate) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/column_value.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/upper/db/v4/internal/cache" 7 | ) 8 | 9 | // ColumnValue represents a bundle between a column and a corresponding value. 10 | type ColumnValue struct { 11 | Column Fragment 12 | Operator string 13 | Value Fragment 14 | } 15 | 16 | var _ = Fragment(&ColumnValue{}) 17 | 18 | type columnValueT struct { 19 | Column string 20 | Operator string 21 | Value string 22 | } 23 | 24 | // Hash returns a unique identifier for the struct. 25 | func (c *ColumnValue) Hash() uint64 { 26 | if c == nil { 27 | return cache.NewHash(FragmentType_ColumnValue, nil) 28 | } 29 | return cache.NewHash(FragmentType_ColumnValue, c.Column, c.Operator, c.Value) 30 | } 31 | 32 | // Compile transforms the ColumnValue into an equivalent SQL representation. 33 | func (c *ColumnValue) Compile(layout *Template) (compiled string, err error) { 34 | if z, ok := layout.Read(c); ok { 35 | return z, nil 36 | } 37 | 38 | column, err := c.Column.Compile(layout) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | data := columnValueT{ 44 | Column: column, 45 | Operator: c.Operator, 46 | } 47 | 48 | if c.Value != nil { 49 | data.Value, err = c.Value.Compile(layout) 50 | if err != nil { 51 | return "", err 52 | } 53 | } 54 | 55 | compiled = strings.TrimSpace(layout.MustCompile(layout.ColumnValue, data)) 56 | 57 | layout.Write(c, compiled) 58 | 59 | return 60 | } 61 | 62 | // ColumnValues represents an array of ColumnValue 63 | type ColumnValues struct { 64 | ColumnValues []Fragment 65 | } 66 | 67 | var _ = Fragment(&ColumnValues{}) 68 | 69 | // JoinColumnValues returns an array of ColumnValue 70 | func JoinColumnValues(values ...Fragment) *ColumnValues { 71 | return &ColumnValues{ColumnValues: values} 72 | } 73 | 74 | // Insert adds a column to the columns array. 75 | func (c *ColumnValues) Insert(values ...Fragment) *ColumnValues { 76 | c.ColumnValues = append(c.ColumnValues, values...) 77 | return c 78 | } 79 | 80 | // Hash returns a unique identifier for the struct. 81 | func (c *ColumnValues) Hash() uint64 { 82 | h := cache.InitHash(FragmentType_ColumnValues) 83 | for i := range c.ColumnValues { 84 | h = cache.AddToHash(h, c.ColumnValues[i]) 85 | } 86 | return h 87 | } 88 | 89 | // Compile transforms the ColumnValues into its SQL representation. 90 | func (c *ColumnValues) Compile(layout *Template) (compiled string, err error) { 91 | 92 | if z, ok := layout.Read(c); ok { 93 | return z, nil 94 | } 95 | 96 | l := len(c.ColumnValues) 97 | 98 | out := make([]string, l) 99 | 100 | for i := range c.ColumnValues { 101 | out[i], err = c.ColumnValues[i].Compile(layout) 102 | if err != nil { 103 | return "", err 104 | } 105 | } 106 | 107 | compiled = strings.TrimSpace(strings.Join(out, layout.IdentifierSeparator)) 108 | 109 | layout.Write(c, compiled) 110 | 111 | return 112 | } 113 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/columns.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/upper/db/v4/internal/cache" 7 | ) 8 | 9 | // Columns represents an array of Column. 10 | type Columns struct { 11 | Columns []Fragment 12 | } 13 | 14 | var _ = Fragment(&Columns{}) 15 | 16 | // Hash returns a unique identifier. 17 | func (c *Columns) Hash() uint64 { 18 | if c == nil { 19 | return cache.NewHash(FragmentType_Columns, nil) 20 | } 21 | h := cache.InitHash(FragmentType_Columns) 22 | for i := range c.Columns { 23 | h = cache.AddToHash(h, c.Columns[i]) 24 | } 25 | return h 26 | } 27 | 28 | // JoinColumns creates and returns an array of Column. 29 | func JoinColumns(columns ...Fragment) *Columns { 30 | return &Columns{Columns: columns} 31 | } 32 | 33 | // OnConditions creates and retuens a new On. 34 | func OnConditions(conditions ...Fragment) *On { 35 | return &On{Conditions: conditions} 36 | } 37 | 38 | // UsingColumns builds a Using from the given columns. 39 | func UsingColumns(columns ...Fragment) *Using { 40 | return &Using{Columns: columns} 41 | } 42 | 43 | // Append 44 | func (c *Columns) Append(a *Columns) *Columns { 45 | c.Columns = append(c.Columns, a.Columns...) 46 | return c 47 | } 48 | 49 | // IsEmpty 50 | func (c *Columns) IsEmpty() bool { 51 | if c == nil || len(c.Columns) < 1 { 52 | return true 53 | } 54 | return false 55 | } 56 | 57 | // Compile transforms the Columns into an equivalent SQL representation. 58 | func (c *Columns) Compile(layout *Template) (compiled string, err error) { 59 | if z, ok := layout.Read(c); ok { 60 | return z, nil 61 | } 62 | 63 | l := len(c.Columns) 64 | 65 | if l > 0 { 66 | out := make([]string, l) 67 | 68 | for i := 0; i < l; i++ { 69 | out[i], err = c.Columns[i].Compile(layout) 70 | if err != nil { 71 | return "", err 72 | } 73 | } 74 | 75 | compiled = strings.Join(out, layout.IdentifierSeparator) 76 | } else { 77 | compiled = "*" 78 | } 79 | 80 | layout.Write(c, compiled) 81 | 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/columns_test.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestColumns(t *testing.T) { 10 | columns := JoinColumns( 11 | &Column{Name: "id"}, 12 | &Column{Name: "customer"}, 13 | &Column{Name: "service_id"}, 14 | &Column{Name: "role.name"}, 15 | &Column{Name: "role.id"}, 16 | ) 17 | 18 | s, err := columns.Compile(defaultTemplate) 19 | assert.NoError(t, err) 20 | assert.Equal(t, `"id", "customer", "service_id", "role"."name", "role"."id"`, s) 21 | } 22 | 23 | func BenchmarkJoinColumns(b *testing.B) { 24 | for i := 0; i < b.N; i++ { 25 | _ = JoinColumns( 26 | &Column{Name: "a"}, 27 | &Column{Name: "b"}, 28 | &Column{Name: "c"}, 29 | ) 30 | } 31 | } 32 | 33 | func BenchmarkColumnsHash(b *testing.B) { 34 | c := JoinColumns( 35 | &Column{Name: "id"}, 36 | &Column{Name: "customer"}, 37 | &Column{Name: "service_id"}, 38 | &Column{Name: "role.name"}, 39 | &Column{Name: "role.id"}, 40 | ) 41 | b.ResetTimer() 42 | for i := 0; i < b.N; i++ { 43 | c.Hash() 44 | } 45 | } 46 | 47 | func BenchmarkColumnsCompile(b *testing.B) { 48 | c := JoinColumns( 49 | &Column{Name: "id"}, 50 | &Column{Name: "customer"}, 51 | &Column{Name: "service_id"}, 52 | &Column{Name: "role.name"}, 53 | &Column{Name: "role.id"}, 54 | ) 55 | b.ResetTimer() 56 | for i := 0; i < b.N; i++ { 57 | _, _ = c.Compile(defaultTemplate) 58 | } 59 | } 60 | 61 | func BenchmarkColumnsCompileNoCache(b *testing.B) { 62 | for i := 0; i < b.N; i++ { 63 | c := JoinColumns( 64 | &Column{Name: "id"}, 65 | &Column{Name: "customer"}, 66 | &Column{Name: "service_id"}, 67 | &Column{Name: "role.name"}, 68 | &Column{Name: "role.id"}, 69 | ) 70 | _, _ = c.Compile(defaultTemplate) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/database.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "github.com/upper/db/v4/internal/cache" 5 | ) 6 | 7 | // Database represents a SQL database. 8 | type Database struct { 9 | Name string 10 | } 11 | 12 | var _ = Fragment(&Database{}) 13 | 14 | // DatabaseWithName returns a Database with the given name. 15 | func DatabaseWithName(name string) *Database { 16 | return &Database{Name: name} 17 | } 18 | 19 | // Hash returns a unique identifier for the struct. 20 | func (d *Database) Hash() uint64 { 21 | if d == nil { 22 | return cache.NewHash(FragmentType_Database, nil) 23 | } 24 | return cache.NewHash(FragmentType_Database, d.Name) 25 | } 26 | 27 | // Compile transforms the Database into an equivalent SQL representation. 28 | func (d *Database) Compile(layout *Template) (compiled string, err error) { 29 | if c, ok := layout.Read(d); ok { 30 | return c, nil 31 | } 32 | 33 | compiled = layout.MustCompile(layout.IdentifierQuote, Raw{Value: d.Name}) 34 | 35 | layout.Write(d, compiled) 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/database_test.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDatabaseCompile(t *testing.T) { 11 | column := Database{Name: "name"} 12 | s, err := column.Compile(defaultTemplate) 13 | assert.NoError(t, err) 14 | assert.Equal(t, `"name"`, s) 15 | } 16 | 17 | func BenchmarkDatabaseHash(b *testing.B) { 18 | c := Database{Name: "name"} 19 | b.ResetTimer() 20 | for i := 0; i < b.N; i++ { 21 | c.Hash() 22 | } 23 | } 24 | 25 | func BenchmarkDatabaseCompile(b *testing.B) { 26 | c := Database{Name: "name"} 27 | b.ResetTimer() 28 | for i := 0; i < b.N; i++ { 29 | _, _ = c.Compile(defaultTemplate) 30 | } 31 | } 32 | 33 | func BenchmarkDatabaseCompileNoCache(b *testing.B) { 34 | for i := 0; i < b.N; i++ { 35 | c := Database{Name: "name"} 36 | _, _ = c.Compile(defaultTemplate) 37 | } 38 | } 39 | 40 | func BenchmarkDatabaseCompileNoCache2(b *testing.B) { 41 | for i := 0; i < b.N; i++ { 42 | c := Database{Name: strconv.Itoa(i)} 43 | _, _ = c.Compile(defaultTemplate) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/errors.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | const ( 4 | errExpectingHashableFmt = "expecting hashable value, got %T" 5 | ) 6 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/group_by.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "github.com/upper/db/v4/internal/cache" 5 | ) 6 | 7 | // GroupBy represents a SQL's "group by" statement. 8 | type GroupBy struct { 9 | Columns Fragment 10 | } 11 | 12 | var _ = Fragment(&GroupBy{}) 13 | 14 | type groupByT struct { 15 | GroupColumns string 16 | } 17 | 18 | // Hash returns a unique identifier. 19 | func (g *GroupBy) Hash() uint64 { 20 | if g == nil { 21 | return cache.NewHash(FragmentType_GroupBy, nil) 22 | } 23 | return cache.NewHash(FragmentType_GroupBy, g.Columns) 24 | } 25 | 26 | // GroupByColumns creates and returns a GroupBy with the given column. 27 | func GroupByColumns(columns ...Fragment) *GroupBy { 28 | return &GroupBy{Columns: JoinColumns(columns...)} 29 | } 30 | 31 | func (g *GroupBy) IsEmpty() bool { 32 | if g == nil || g.Columns == nil { 33 | return true 34 | } 35 | return g.Columns.(hasIsEmpty).IsEmpty() 36 | } 37 | 38 | // Compile transforms the GroupBy into an equivalent SQL representation. 39 | func (g *GroupBy) Compile(layout *Template) (compiled string, err error) { 40 | 41 | if c, ok := layout.Read(g); ok { 42 | return c, nil 43 | } 44 | 45 | if g.Columns != nil { 46 | columns, err := g.Columns.Compile(layout) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | data := groupByT{ 52 | GroupColumns: columns, 53 | } 54 | compiled = layout.MustCompile(layout.GroupByLayout, data) 55 | } 56 | 57 | layout.Write(g, compiled) 58 | 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/group_by_test.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGroupBy(t *testing.T) { 10 | columns := GroupByColumns( 11 | &Column{Name: "id"}, 12 | &Column{Name: "customer"}, 13 | &Column{Name: "service_id"}, 14 | &Column{Name: "role.name"}, 15 | &Column{Name: "role.id"}, 16 | ) 17 | 18 | s := mustTrim(columns.Compile(defaultTemplate)) 19 | assert.Equal(t, `GROUP BY "id", "customer", "service_id", "role"."name", "role"."id"`, s) 20 | } 21 | 22 | func BenchmarkGroupByColumns(b *testing.B) { 23 | for i := 0; i < b.N; i++ { 24 | _ = GroupByColumns( 25 | &Column{Name: "a"}, 26 | &Column{Name: "b"}, 27 | &Column{Name: "c"}, 28 | ) 29 | } 30 | } 31 | 32 | func BenchmarkGroupByHash(b *testing.B) { 33 | c := GroupByColumns( 34 | &Column{Name: "id"}, 35 | &Column{Name: "customer"}, 36 | &Column{Name: "service_id"}, 37 | &Column{Name: "role.name"}, 38 | &Column{Name: "role.id"}, 39 | ) 40 | b.ResetTimer() 41 | for i := 0; i < b.N; i++ { 42 | c.Hash() 43 | } 44 | } 45 | 46 | func BenchmarkGroupByCompile(b *testing.B) { 47 | c := GroupByColumns( 48 | &Column{Name: "id"}, 49 | &Column{Name: "customer"}, 50 | &Column{Name: "service_id"}, 51 | &Column{Name: "role.name"}, 52 | &Column{Name: "role.id"}, 53 | ) 54 | b.ResetTimer() 55 | for i := 0; i < b.N; i++ { 56 | _, _ = c.Compile(defaultTemplate) 57 | } 58 | } 59 | 60 | func BenchmarkGroupByCompileNoCache(b *testing.B) { 61 | for i := 0; i < b.N; i++ { 62 | c := GroupByColumns( 63 | &Column{Name: "id"}, 64 | &Column{Name: "customer"}, 65 | &Column{Name: "service_id"}, 66 | &Column{Name: "role.name"}, 67 | &Column{Name: "role.id"}, 68 | ) 69 | _, _ = c.Compile(defaultTemplate) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/interfaces.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "github.com/upper/db/v4/internal/cache" 5 | ) 6 | 7 | // Fragment is any interface that can be both cached and compiled. 8 | type Fragment interface { 9 | cache.Hashable 10 | 11 | compilable 12 | } 13 | 14 | type compilable interface { 15 | Compile(*Template) (string, error) 16 | } 17 | 18 | type hasIsEmpty interface { 19 | IsEmpty() bool 20 | } 21 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/raw.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/upper/db/v4/internal/cache" 7 | ) 8 | 9 | var ( 10 | _ = fmt.Stringer(&Raw{}) 11 | ) 12 | 13 | // Raw represents a value that is meant to be used in a query without escaping. 14 | type Raw struct { 15 | Value string 16 | } 17 | 18 | func NewRawValue(v interface{}) (*Raw, error) { 19 | switch t := v.(type) { 20 | case string: 21 | return &Raw{Value: t}, nil 22 | case int, uint, int64, uint64, int32, uint32, int16, uint16: 23 | return &Raw{Value: fmt.Sprintf("%d", t)}, nil 24 | case fmt.Stringer: 25 | return &Raw{Value: t.String()}, nil 26 | } 27 | return nil, fmt.Errorf("unexpected type: %T", v) 28 | } 29 | 30 | // Hash returns a unique identifier for the struct. 31 | func (r *Raw) Hash() uint64 { 32 | if r == nil { 33 | return cache.NewHash(FragmentType_Raw, nil) 34 | } 35 | return cache.NewHash(FragmentType_Raw, r.Value) 36 | } 37 | 38 | // Compile returns the raw value. 39 | func (r *Raw) Compile(*Template) (string, error) { 40 | return r.Value, nil 41 | } 42 | 43 | // String returns the raw value. 44 | func (r *Raw) String() string { 45 | return r.Value 46 | } 47 | 48 | var _ = Fragment(&Raw{}) 49 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/raw_test.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRawString(t *testing.T) { 10 | raw := &Raw{Value: "foo"} 11 | s, err := raw.Compile(defaultTemplate) 12 | assert.NoError(t, err) 13 | assert.Equal(t, `foo`, s) 14 | } 15 | 16 | func TestRawCompile(t *testing.T) { 17 | raw := &Raw{Value: "foo"} 18 | s, err := raw.Compile(defaultTemplate) 19 | assert.NoError(t, err) 20 | assert.Equal(t, `foo`, s) 21 | } 22 | 23 | func BenchmarkRawCreate(b *testing.B) { 24 | for i := 0; i < b.N; i++ { 25 | _ = Raw{Value: "foo"} 26 | } 27 | } 28 | 29 | func BenchmarkRawString(b *testing.B) { 30 | raw := &Raw{Value: "foo"} 31 | b.ResetTimer() 32 | for i := 0; i < b.N; i++ { 33 | _ = raw.String() 34 | } 35 | } 36 | 37 | func BenchmarkRawCompile(b *testing.B) { 38 | raw := &Raw{Value: "foo"} 39 | b.ResetTimer() 40 | for i := 0; i < b.N; i++ { 41 | _, _ = raw.Compile(defaultTemplate) 42 | } 43 | } 44 | 45 | func BenchmarkRawHash(b *testing.B) { 46 | raw := &Raw{Value: "foo"} 47 | b.ResetTimer() 48 | for i := 0; i < b.N; i++ { 49 | raw.Hash() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/returning.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "github.com/upper/db/v4/internal/cache" 5 | ) 6 | 7 | // Returning represents a RETURNING clause. 8 | type Returning struct { 9 | *Columns 10 | } 11 | 12 | // Hash returns a unique identifier for the struct. 13 | func (r *Returning) Hash() uint64 { 14 | if r == nil { 15 | return cache.NewHash(FragmentType_Returning, nil) 16 | } 17 | return cache.NewHash(FragmentType_Returning, r.Columns) 18 | } 19 | 20 | var _ = Fragment(&Returning{}) 21 | 22 | // ReturningColumns creates and returns an array of Column. 23 | func ReturningColumns(columns ...Fragment) *Returning { 24 | return &Returning{Columns: &Columns{Columns: columns}} 25 | } 26 | 27 | // Compile transforms the clause into its equivalent SQL representation. 28 | func (r *Returning) Compile(layout *Template) (compiled string, err error) { 29 | if z, ok := layout.Read(r); ok { 30 | return z, nil 31 | } 32 | 33 | compiled, err = r.Columns.Compile(layout) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | layout.Write(r, compiled) 39 | 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/statement.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/upper/db/v4/internal/cache" 9 | ) 10 | 11 | var errUnknownTemplateType = errors.New("Unknown template type") 12 | 13 | // represents different kinds of SQL statements. 14 | type Statement struct { 15 | Type 16 | Table Fragment 17 | Database Fragment 18 | Columns Fragment 19 | Values Fragment 20 | Distinct bool 21 | ColumnValues Fragment 22 | OrderBy Fragment 23 | GroupBy Fragment 24 | Joins Fragment 25 | Where Fragment 26 | Returning Fragment 27 | 28 | Limit 29 | Offset 30 | 31 | SQL string 32 | 33 | amendFn func(string) string 34 | } 35 | 36 | func (layout *Template) doCompile(c Fragment) (string, error) { 37 | if c != nil && !reflect.ValueOf(c).IsNil() { 38 | return c.Compile(layout) 39 | } 40 | return "", nil 41 | } 42 | 43 | // Hash returns a unique identifier for the struct. 44 | func (s *Statement) Hash() uint64 { 45 | if s == nil { 46 | return cache.NewHash(FragmentType_Statement, nil) 47 | } 48 | return cache.NewHash( 49 | FragmentType_Statement, 50 | s.Type, 51 | s.Table, 52 | s.Database, 53 | s.Columns, 54 | s.Values, 55 | s.Distinct, 56 | s.ColumnValues, 57 | s.OrderBy, 58 | s.GroupBy, 59 | s.Joins, 60 | s.Where, 61 | s.Returning, 62 | s.Limit, 63 | s.Offset, 64 | s.SQL, 65 | ) 66 | } 67 | 68 | func (s *Statement) SetAmendment(amendFn func(string) string) { 69 | s.amendFn = amendFn 70 | } 71 | 72 | func (s *Statement) Amend(in string) string { 73 | if s.amendFn == nil { 74 | return in 75 | } 76 | return s.amendFn(in) 77 | } 78 | 79 | func (s *Statement) template(layout *Template) (string, error) { 80 | switch s.Type { 81 | case Truncate: 82 | return layout.TruncateLayout, nil 83 | case DropTable: 84 | return layout.DropTableLayout, nil 85 | case DropDatabase: 86 | return layout.DropDatabaseLayout, nil 87 | case Count: 88 | return layout.CountLayout, nil 89 | case Select: 90 | return layout.SelectLayout, nil 91 | case Delete: 92 | return layout.DeleteLayout, nil 93 | case Update: 94 | return layout.UpdateLayout, nil 95 | case Insert: 96 | return layout.InsertLayout, nil 97 | default: 98 | return "", errUnknownTemplateType 99 | } 100 | } 101 | 102 | // Compile transforms the Statement into an equivalent SQL query. 103 | func (s *Statement) Compile(layout *Template) (compiled string, err error) { 104 | if s.Type == SQL { 105 | // No need to hit the cache. 106 | return s.SQL, nil 107 | } 108 | 109 | if z, ok := layout.Read(s); ok { 110 | return s.Amend(z), nil 111 | } 112 | 113 | tpl, err := s.template(layout) 114 | if err != nil { 115 | return "", err 116 | } 117 | 118 | compiled = layout.MustCompile(tpl, s) 119 | 120 | compiled = strings.TrimSpace(compiled) 121 | layout.Write(s, compiled) 122 | 123 | return s.Amend(compiled), nil 124 | } 125 | 126 | // RawSQL represents a raw SQL statement. 127 | func RawSQL(s string) *Statement { 128 | return &Statement{ 129 | Type: SQL, 130 | SQL: s, 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/table.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/upper/db/v4/internal/cache" 7 | ) 8 | 9 | type tableT struct { 10 | Name string 11 | Alias string 12 | } 13 | 14 | // Table struct represents a SQL table. 15 | type Table struct { 16 | Name interface{} 17 | } 18 | 19 | var _ = Fragment(&Table{}) 20 | 21 | func quotedTableName(layout *Template, input string) string { 22 | input = trimString(input) 23 | 24 | // chunks := reAliasSeparator.Split(input, 2) 25 | chunks := separateByAS(input) 26 | 27 | if len(chunks) == 1 { 28 | // chunks = reSpaceSeparator.Split(input, 2) 29 | chunks = separateBySpace(input) 30 | } 31 | 32 | name := chunks[0] 33 | 34 | nameChunks := strings.SplitN(name, layout.ColumnSeparator, 2) 35 | 36 | for i := range nameChunks { 37 | // nameChunks[i] = strings.TrimSpace(nameChunks[i]) 38 | nameChunks[i] = trimString(nameChunks[i]) 39 | nameChunks[i] = layout.MustCompile(layout.IdentifierQuote, Raw{Value: nameChunks[i]}) 40 | } 41 | 42 | name = strings.Join(nameChunks, layout.ColumnSeparator) 43 | 44 | var alias string 45 | 46 | if len(chunks) > 1 { 47 | // alias = strings.TrimSpace(chunks[1]) 48 | alias = trimString(chunks[1]) 49 | alias = layout.MustCompile(layout.IdentifierQuote, Raw{Value: alias}) 50 | } 51 | 52 | return layout.MustCompile(layout.TableAliasLayout, tableT{name, alias}) 53 | } 54 | 55 | // TableWithName creates an returns a Table with the given name. 56 | func TableWithName(name string) *Table { 57 | return &Table{Name: name} 58 | } 59 | 60 | // Hash returns a string hash of the table value. 61 | func (t *Table) Hash() uint64 { 62 | if t == nil { 63 | return cache.NewHash(FragmentType_Table, nil) 64 | } 65 | return cache.NewHash(FragmentType_Table, t.Name) 66 | } 67 | 68 | // Compile transforms a table struct into a SQL chunk. 69 | func (t *Table) Compile(layout *Template) (compiled string, err error) { 70 | 71 | if z, ok := layout.Read(t); ok { 72 | return z, nil 73 | } 74 | 75 | switch value := t.Name.(type) { 76 | case string: 77 | if t.Name == "" { 78 | return 79 | } 80 | 81 | // Splitting tables by a comma 82 | parts := separateByComma(value) 83 | 84 | l := len(parts) 85 | 86 | for i := 0; i < l; i++ { 87 | parts[i] = quotedTableName(layout, parts[i]) 88 | } 89 | 90 | compiled = strings.Join(parts, layout.IdentifierSeparator) 91 | case Raw: 92 | compiled = value.String() 93 | } 94 | 95 | layout.Write(t, compiled) 96 | 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/table_test.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | 6 | "testing" 7 | ) 8 | 9 | func TestTableSimple(t *testing.T) { 10 | table := TableWithName("artist") 11 | assert.Equal(t, `"artist"`, mustTrim(table.Compile(defaultTemplate))) 12 | } 13 | 14 | func TestTableCompound(t *testing.T) { 15 | table := TableWithName("artist.foo") 16 | assert.Equal(t, `"artist"."foo"`, mustTrim(table.Compile(defaultTemplate))) 17 | } 18 | 19 | func TestTableCompoundAlias(t *testing.T) { 20 | table := TableWithName("artist.foo AS baz") 21 | 22 | assert.Equal(t, `"artist"."foo" AS "baz"`, mustTrim(table.Compile(defaultTemplate))) 23 | } 24 | 25 | func TestTableImplicitAlias(t *testing.T) { 26 | table := TableWithName("artist.foo baz") 27 | 28 | assert.Equal(t, `"artist"."foo" AS "baz"`, mustTrim(table.Compile(defaultTemplate))) 29 | } 30 | 31 | func TestTableMultiple(t *testing.T) { 32 | table := TableWithName("artist.foo, artist.bar, artist.baz") 33 | 34 | assert.Equal(t, `"artist"."foo", "artist"."bar", "artist"."baz"`, mustTrim(table.Compile(defaultTemplate))) 35 | } 36 | 37 | func TestTableMultipleAlias(t *testing.T) { 38 | table := TableWithName("artist.foo AS foo, artist.bar as bar, artist.baz As baz") 39 | 40 | assert.Equal(t, `"artist"."foo" AS "foo", "artist"."bar" AS "bar", "artist"."baz" AS "baz"`, mustTrim(table.Compile(defaultTemplate))) 41 | } 42 | 43 | func TestTableMinimal(t *testing.T) { 44 | table := TableWithName("a") 45 | 46 | assert.Equal(t, `"a"`, mustTrim(table.Compile(defaultTemplate))) 47 | } 48 | 49 | func TestTableEmpty(t *testing.T) { 50 | table := TableWithName("") 51 | 52 | assert.Equal(t, "", mustTrim(table.Compile(defaultTemplate))) 53 | } 54 | 55 | func BenchmarkTableWithName(b *testing.B) { 56 | for i := 0; i < b.N; i++ { 57 | _ = TableWithName("foo") 58 | } 59 | } 60 | 61 | func BenchmarkTableHash(b *testing.B) { 62 | t := TableWithName("name") 63 | b.ResetTimer() 64 | for i := 0; i < b.N; i++ { 65 | t.Hash() 66 | } 67 | } 68 | 69 | func BenchmarkTableCompile(b *testing.B) { 70 | t := TableWithName("name") 71 | b.ResetTimer() 72 | for i := 0; i < b.N; i++ { 73 | _, _ = t.Compile(defaultTemplate) 74 | } 75 | } 76 | 77 | func BenchmarkTableCompileNoCache(b *testing.B) { 78 | for i := 0; i < b.N; i++ { 79 | t := TableWithName("name") 80 | _, _ = t.Compile(defaultTemplate) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/types.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | const ( 4 | FragmentType_None uint64 = iota + 713910251627 5 | 6 | FragmentType_And 7 | FragmentType_Column 8 | FragmentType_ColumnValue 9 | FragmentType_ColumnValues 10 | FragmentType_Columns 11 | FragmentType_Database 12 | FragmentType_GroupBy 13 | FragmentType_Join 14 | FragmentType_Joins 15 | FragmentType_Nil 16 | FragmentType_Or 17 | FragmentType_Limit 18 | FragmentType_Offset 19 | FragmentType_OrderBy 20 | FragmentType_Order 21 | FragmentType_Raw 22 | FragmentType_Returning 23 | FragmentType_SortBy 24 | FragmentType_SortColumn 25 | FragmentType_SortColumns 26 | FragmentType_Statement 27 | FragmentType_StatementType 28 | FragmentType_Table 29 | FragmentType_Value 30 | FragmentType_On 31 | FragmentType_Using 32 | FragmentType_ValueGroups 33 | FragmentType_Values 34 | FragmentType_Where 35 | ) 36 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/utilities.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // isBlankSymbol returns true if the given byte is either space, tab, carriage 8 | // return or newline. 9 | func isBlankSymbol(in byte) bool { 10 | return in == ' ' || in == '\t' || in == '\r' || in == '\n' 11 | } 12 | 13 | // trimString returns a slice of s with a leading and trailing blank symbols 14 | // (as defined by isBlankSymbol) removed. 15 | func trimString(s string) string { 16 | 17 | // This conversion is rather slow. 18 | // return string(trimBytes([]byte(s))) 19 | 20 | start, end := 0, len(s)-1 21 | 22 | if end < start { 23 | return "" 24 | } 25 | 26 | for isBlankSymbol(s[start]) { 27 | start++ 28 | if start >= end { 29 | return "" 30 | } 31 | } 32 | 33 | for isBlankSymbol(s[end]) { 34 | end-- 35 | } 36 | 37 | return s[start : end+1] 38 | } 39 | 40 | // trimBytes returns a slice of s with a leading and trailing blank symbols (as 41 | // defined by isBlankSymbol) removed. 42 | func trimBytes(s []byte) []byte { 43 | 44 | start, end := 0, len(s)-1 45 | 46 | if end < start { 47 | return []byte{} 48 | } 49 | 50 | for isBlankSymbol(s[start]) { 51 | start++ 52 | if start >= end { 53 | return []byte{} 54 | } 55 | } 56 | 57 | for isBlankSymbol(s[end]) { 58 | end-- 59 | } 60 | 61 | return s[start : end+1] 62 | } 63 | 64 | /* 65 | // Separates by a comma, ignoring spaces too. 66 | // This was slower than strings.Split. 67 | func separateByComma(in string) (out []string) { 68 | 69 | out = []string{} 70 | 71 | start, lim := 0, len(in)-1 72 | 73 | for start < lim { 74 | var end int 75 | 76 | for end = start; end <= lim; end++ { 77 | // Is a comma? 78 | if in[end] == ',' { 79 | break 80 | } 81 | } 82 | 83 | out = append(out, trimString(in[start:end])) 84 | 85 | start = end + 1 86 | } 87 | 88 | return 89 | } 90 | */ 91 | 92 | // Separates by a comma, ignoring spaces too. 93 | func separateByComma(in string) (out []string) { 94 | out = strings.Split(in, ",") 95 | for i := range out { 96 | out[i] = trimString(out[i]) 97 | } 98 | return 99 | } 100 | 101 | // Separates by spaces, ignoring spaces too. 102 | func separateBySpace(in string) (out []string) { 103 | if len(in) == 0 { 104 | return []string{""} 105 | } 106 | 107 | pre := strings.Split(in, " ") 108 | out = make([]string, 0, len(pre)) 109 | 110 | for i := range pre { 111 | pre[i] = trimString(pre[i]) 112 | if pre[i] != "" { 113 | out = append(out, pre[i]) 114 | } 115 | } 116 | 117 | return 118 | } 119 | 120 | func separateByAS(in string) (out []string) { 121 | out = []string{} 122 | 123 | if len(in) < 6 { 124 | // The minimum expression with the AS keyword is "x AS y", 6 chars. 125 | return []string{in} 126 | } 127 | 128 | start, lim := 0, len(in)-1 129 | 130 | for start <= lim { 131 | var end int 132 | 133 | for end = start; end <= lim; end++ { 134 | if end > 3 && isBlankSymbol(in[end]) && isBlankSymbol(in[end-3]) { 135 | if (in[end-1] == 's' || in[end-1] == 'S') && (in[end-2] == 'a' || in[end-2] == 'A') { 136 | break 137 | } 138 | } 139 | } 140 | 141 | if end < lim { 142 | out = append(out, trimString(in[start:end-3])) 143 | } else { 144 | out = append(out, trimString(in[start:end])) 145 | } 146 | 147 | start = end + 1 148 | } 149 | 150 | return 151 | } 152 | -------------------------------------------------------------------------------- /internal/sqladapter/exql/value_test.go: -------------------------------------------------------------------------------- 1 | package exql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestValue(t *testing.T) { 10 | val := NewValue(1) 11 | 12 | s, err := val.Compile(defaultTemplate) 13 | assert.NoError(t, err) 14 | assert.Equal(t, `'1'`, s) 15 | 16 | val = NewValue(&Raw{Value: "NOW()"}) 17 | 18 | s, err = val.Compile(defaultTemplate) 19 | assert.NoError(t, err) 20 | assert.Equal(t, `NOW()`, s) 21 | } 22 | 23 | func TestSameRawValue(t *testing.T) { 24 | { 25 | val := NewValue(&Raw{Value: `"1"`}) 26 | 27 | s, err := val.Compile(defaultTemplate) 28 | assert.NoError(t, err) 29 | assert.Equal(t, `"1"`, s) 30 | } 31 | { 32 | val := NewValue(&Raw{Value: `'1'`}) 33 | 34 | s, err := val.Compile(defaultTemplate) 35 | assert.NoError(t, err) 36 | assert.Equal(t, `'1'`, s) 37 | } 38 | { 39 | val := NewValue(&Raw{Value: `1`}) 40 | 41 | s, err := val.Compile(defaultTemplate) 42 | assert.NoError(t, err) 43 | assert.Equal(t, `1`, s) 44 | } 45 | { 46 | val := NewValue("1") 47 | 48 | s, err := val.Compile(defaultTemplate) 49 | assert.NoError(t, err) 50 | assert.Equal(t, `'1'`, s) 51 | } 52 | { 53 | val := NewValue(1) 54 | 55 | s, err := val.Compile(defaultTemplate) 56 | assert.NoError(t, err) 57 | assert.Equal(t, `'1'`, s) 58 | } 59 | } 60 | 61 | func TestValues(t *testing.T) { 62 | val := NewValueGroup( 63 | &Value{V: &Raw{Value: "1"}}, 64 | &Value{V: &Raw{Value: "2"}}, 65 | &Value{V: "3"}, 66 | ) 67 | 68 | s, err := val.Compile(defaultTemplate) 69 | assert.NoError(t, err) 70 | 71 | assert.Equal(t, `(1, 2, '3')`, s) 72 | } 73 | 74 | func BenchmarkValue(b *testing.B) { 75 | for i := 0; i < b.N; i++ { 76 | _ = NewValue("a") 77 | } 78 | } 79 | 80 | func BenchmarkValueHash(b *testing.B) { 81 | v := NewValue("a") 82 | b.ResetTimer() 83 | for i := 0; i < b.N; i++ { 84 | _ = v.Hash() 85 | } 86 | } 87 | 88 | func BenchmarkValueCompile(b *testing.B) { 89 | v := NewValue("a") 90 | b.ResetTimer() 91 | for i := 0; i < b.N; i++ { 92 | _, _ = v.Compile(defaultTemplate) 93 | } 94 | } 95 | 96 | func BenchmarkValueCompileNoCache(b *testing.B) { 97 | for i := 0; i < b.N; i++ { 98 | v := NewValue("a") 99 | _, _ = v.Compile(defaultTemplate) 100 | } 101 | } 102 | 103 | func BenchmarkValues(b *testing.B) { 104 | for i := 0; i < b.N; i++ { 105 | _ = NewValueGroup(NewValue("a"), NewValue("b")) 106 | } 107 | } 108 | 109 | func BenchmarkValuesHash(b *testing.B) { 110 | vs := NewValueGroup(NewValue("a"), NewValue("b")) 111 | b.ResetTimer() 112 | for i := 0; i < b.N; i++ { 113 | _ = vs.Hash() 114 | } 115 | } 116 | 117 | func BenchmarkValuesCompile(b *testing.B) { 118 | vs := NewValueGroup(NewValue("a"), NewValue("b")) 119 | b.ResetTimer() 120 | for i := 0; i < b.N; i++ { 121 | _, _ = vs.Compile(defaultTemplate) 122 | } 123 | } 124 | 125 | func BenchmarkValuesCompileNoCache(b *testing.B) { 126 | for i := 0; i < b.N; i++ { 127 | vs := NewValueGroup(NewValue("a"), NewValue("b")) 128 | _, _ = vs.Compile(defaultTemplate) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /internal/sqladapter/hash.go: -------------------------------------------------------------------------------- 1 | package sqladapter 2 | 3 | const ( 4 | _ = iota + 345065139389 5 | 6 | hashTypeCollection 7 | hashTypePrimaryKeys 8 | ) 9 | -------------------------------------------------------------------------------- /internal/sqladapter/record.go: -------------------------------------------------------------------------------- 1 | package sqladapter 2 | 3 | import ( 4 | "reflect" 5 | 6 | db "github.com/upper/db/v4" 7 | "github.com/upper/db/v4/internal/sqlbuilder" 8 | ) 9 | 10 | func recordID(store db.Store, record db.Record) (db.Cond, error) { 11 | if record == nil { 12 | return nil, db.ErrNilRecord 13 | } 14 | 15 | if hasConstraints, ok := record.(db.HasConstraints); ok { 16 | return hasConstraints.Constraints(), nil 17 | } 18 | 19 | id := db.Cond{} 20 | 21 | keys, fields, err := recordPrimaryKeyFieldValues(store, record) 22 | if err != nil { 23 | return nil, err 24 | } 25 | for i := range fields { 26 | if fields[i] == reflect.Zero(reflect.TypeOf(fields[i])).Interface() { 27 | return nil, db.ErrRecordIDIsZero 28 | } 29 | id[keys[i]] = fields[i] 30 | } 31 | if len(id) < 1 { 32 | return nil, db.ErrRecordIDIsZero 33 | } 34 | 35 | return id, nil 36 | } 37 | 38 | func recordPrimaryKeyFieldValues(store db.Store, record db.Record) ([]string, []interface{}, error) { 39 | sess := store.Session() 40 | 41 | pKeys, err := sess.(Session).PrimaryKeys(store.Name()) 42 | if err != nil { 43 | return nil, nil, err 44 | } 45 | 46 | fields := sqlbuilder.Mapper.FieldsByName(reflect.ValueOf(record), pKeys) 47 | 48 | values := make([]interface{}, 0, len(fields)) 49 | for i := range fields { 50 | if fields[i].IsValid() { 51 | values = append(values, fields[i].Interface()) 52 | } 53 | } 54 | 55 | return pKeys, values, nil 56 | } 57 | 58 | func recordCreate(store db.Store, record db.Record) error { 59 | sess := store.Session() 60 | 61 | if validator, ok := record.(db.Validator); ok { 62 | if err := validator.Validate(); err != nil { 63 | return err 64 | } 65 | } 66 | 67 | if hook, ok := record.(db.BeforeCreateHook); ok { 68 | if err := hook.BeforeCreate(sess); err != nil { 69 | return err 70 | } 71 | } 72 | 73 | if creator, ok := store.(db.StoreCreator); ok { 74 | if err := creator.Create(record); err != nil { 75 | return err 76 | } 77 | } else { 78 | if err := store.InsertReturning(record); err != nil { 79 | return err 80 | } 81 | } 82 | 83 | if hook, ok := record.(db.AfterCreateHook); ok { 84 | if err := hook.AfterCreate(sess); err != nil { 85 | return err 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | func recordUpdate(store db.Store, record db.Record) error { 92 | sess := store.Session() 93 | 94 | if validator, ok := record.(db.Validator); ok { 95 | if err := validator.Validate(); err != nil { 96 | return err 97 | } 98 | } 99 | 100 | if hook, ok := record.(db.BeforeUpdateHook); ok { 101 | if err := hook.BeforeUpdate(sess); err != nil { 102 | return err 103 | } 104 | } 105 | 106 | if updater, ok := store.(db.StoreUpdater); ok { 107 | if err := updater.Update(record); err != nil { 108 | return err 109 | } 110 | } else { 111 | if err := record.Store(sess).UpdateReturning(record); err != nil { 112 | return err 113 | } 114 | } 115 | 116 | if hook, ok := record.(db.AfterUpdateHook); ok { 117 | if err := hook.AfterUpdate(sess); err != nil { 118 | return err 119 | } 120 | } 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /internal/sqladapter/sqladapter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | // Package sqladapter provides common logic for SQL adapters. 23 | package sqladapter 24 | 25 | import ( 26 | "database/sql" 27 | "database/sql/driver" 28 | 29 | "github.com/upper/db/v4" 30 | "github.com/upper/db/v4/internal/sqlbuilder" 31 | ) 32 | 33 | // IsKeyValue reports whether v is a valid value for a primary key that can be 34 | // used with Find(pKey). 35 | func IsKeyValue(v interface{}) bool { 36 | if v == nil { 37 | return true 38 | } 39 | switch v.(type) { 40 | case int64, int, uint, uint64, 41 | []int64, []int, []uint, []uint64, 42 | []byte, []string, 43 | []interface{}, 44 | driver.Valuer: 45 | return true 46 | } 47 | return false 48 | } 49 | 50 | type sqlAdapterWrapper struct { 51 | adapter AdapterSession 52 | } 53 | 54 | func (w *sqlAdapterWrapper) OpenDSN(dsn db.ConnectionURL) (db.Session, error) { 55 | sess := NewSession(dsn, w.adapter) 56 | if err := sess.Open(); err != nil { 57 | return nil, err 58 | } 59 | return sess, nil 60 | } 61 | 62 | func (w *sqlAdapterWrapper) NewTx(sqlTx *sql.Tx) (sqlbuilder.Tx, error) { 63 | tx, err := NewTx(w.adapter, sqlTx) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return tx, nil 68 | } 69 | 70 | func (w *sqlAdapterWrapper) New(sqlDB *sql.DB) (db.Session, error) { 71 | sess := NewSession(nil, w.adapter) 72 | if err := sess.BindDB(sqlDB); err != nil { 73 | return nil, err 74 | } 75 | return sess, nil 76 | } 77 | 78 | // RegisterAdapter registers a new SQL adapter. 79 | func RegisterAdapter(name string, adapter AdapterSession) sqlbuilder.Adapter { 80 | z := &sqlAdapterWrapper{adapter} 81 | db.RegisterAdapter(name, sqlbuilder.NewCompatAdapter(z)) 82 | return z 83 | } 84 | -------------------------------------------------------------------------------- /internal/sqladapter/sqladapter_test.go: -------------------------------------------------------------------------------- 1 | package sqladapter 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/upper/db/v4" 9 | ) 10 | 11 | var ( 12 | _ db.Collection = &collectionWithSession{} 13 | _ Collection = &collectionWithSession{} 14 | ) 15 | 16 | func TestReplaceWithDollarSign(t *testing.T) { 17 | tests := []struct { 18 | in string 19 | out string 20 | }{ 21 | { 22 | `SELECT ?`, 23 | `SELECT $1`, 24 | }, 25 | { 26 | `SELECT ? FROM ? WHERE ?`, 27 | `SELECT $1 FROM $2 WHERE $3`, 28 | }, 29 | { 30 | `SELECT ?? FROM ? WHERE ??`, 31 | `SELECT ? FROM $1 WHERE ?`, 32 | }, 33 | { 34 | `SELECT ??? FROM ? WHERE ??`, 35 | `SELECT ?$1 FROM $2 WHERE ?`, 36 | }, 37 | { 38 | `SELECT ??? FROM ? WHERE ????`, 39 | `SELECT ?$1 FROM $2 WHERE ??`, 40 | }, 41 | } 42 | 43 | for i, test := range tests { 44 | t.Run(fmt.Sprintf("Case_%03d", i), func(t *testing.T) { 45 | assert.Equal(t, []byte(test.out), ReplaceWithDollarSign([]byte(test.in))) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/sqladapter/statement.go: -------------------------------------------------------------------------------- 1 | package sqladapter 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "sync" 7 | "sync/atomic" 8 | ) 9 | 10 | var ( 11 | activeStatements int64 12 | ) 13 | 14 | // Stmt represents a *sql.Stmt that is cached and provides the 15 | // OnEvict method to allow it to clean after itself. 16 | type Stmt struct { 17 | *sql.Stmt 18 | 19 | query string 20 | mu sync.Mutex 21 | 22 | count int64 23 | dead bool 24 | } 25 | 26 | // NewStatement creates an returns an opened statement 27 | func NewStatement(stmt *sql.Stmt, query string) *Stmt { 28 | s := &Stmt{ 29 | Stmt: stmt, 30 | query: query, 31 | } 32 | atomic.AddInt64(&activeStatements, 1) 33 | return s 34 | } 35 | 36 | // Open marks the statement as in-use 37 | func (c *Stmt) Open() (*Stmt, error) { 38 | c.mu.Lock() 39 | defer c.mu.Unlock() 40 | 41 | if c.dead { 42 | return nil, errors.New("statement is dead") 43 | } 44 | 45 | c.count++ 46 | return c, nil 47 | } 48 | 49 | // Close closes the underlying statement if no other go-routine is using it. 50 | func (c *Stmt) Close() error { 51 | c.mu.Lock() 52 | defer c.mu.Unlock() 53 | 54 | c.count-- 55 | 56 | return c.checkClose() 57 | } 58 | 59 | func (c *Stmt) checkClose() error { 60 | if c.dead && c.count == 0 { 61 | // Statement is dead and we can close it for real. 62 | err := c.Stmt.Close() 63 | if err != nil { 64 | return err 65 | } 66 | // Reduce active statements counter. 67 | atomic.AddInt64(&activeStatements, -1) 68 | } 69 | return nil 70 | } 71 | 72 | // OnEvict marks the statement as ready to be cleaned up. 73 | func (c *Stmt) OnEvict() { 74 | c.mu.Lock() 75 | defer c.mu.Unlock() 76 | 77 | c.dead = true 78 | c.checkClose() 79 | } 80 | 81 | // NumActiveStatements returns the global number of prepared statements in use 82 | // at any point. 83 | func NumActiveStatements() int64 { 84 | return atomic.LoadInt64(&activeStatements) 85 | } 86 | -------------------------------------------------------------------------------- /internal/sqlbuilder/batch.go: -------------------------------------------------------------------------------- 1 | package sqlbuilder 2 | 3 | import ( 4 | "github.com/upper/db/v4" 5 | ) 6 | 7 | // BatchInserter provides a helper that can be used to do massive insertions in 8 | // batches. 9 | type BatchInserter struct { 10 | inserter *inserter 11 | size int 12 | values chan []interface{} 13 | err error 14 | } 15 | 16 | func newBatchInserter(inserter *inserter, size int) *BatchInserter { 17 | if size < 1 { 18 | size = 1 19 | } 20 | b := &BatchInserter{ 21 | inserter: inserter, 22 | size: size, 23 | values: make(chan []interface{}, size), 24 | } 25 | return b 26 | } 27 | 28 | // Values pushes column values to be inserted as part of the batch. 29 | func (b *BatchInserter) Values(values ...interface{}) db.BatchInserter { 30 | b.values <- values 31 | return b 32 | } 33 | 34 | func (b *BatchInserter) nextQuery() *inserter { 35 | ins := &inserter{} 36 | *ins = *b.inserter 37 | i := 0 38 | for values := range b.values { 39 | i++ 40 | ins = ins.Values(values...).(*inserter) 41 | if i == b.size { 42 | break 43 | } 44 | } 45 | if i == 0 { 46 | return nil 47 | } 48 | return ins 49 | } 50 | 51 | // NextResult is useful when using PostgreSQL and Returning(), it dumps the 52 | // next slice of results to dst, which can mean having the IDs of all inserted 53 | // elements in the batch. 54 | func (b *BatchInserter) NextResult(dst interface{}) bool { 55 | clone := b.nextQuery() 56 | if clone == nil { 57 | return false 58 | } 59 | b.err = clone.Iterator().All(dst) 60 | return (b.err == nil) 61 | } 62 | 63 | // Done means that no more elements are going to be added. 64 | func (b *BatchInserter) Done() { 65 | close(b.values) 66 | } 67 | 68 | // Wait blocks until the whole batch is executed. 69 | func (b *BatchInserter) Wait() error { 70 | for { 71 | q := b.nextQuery() 72 | if q == nil { 73 | break 74 | } 75 | if _, err := q.Exec(); err != nil { 76 | b.err = err 77 | break 78 | } 79 | } 80 | return b.Err() 81 | } 82 | 83 | // Err returns any error while executing the batch. 84 | func (b *BatchInserter) Err() error { 85 | return b.err 86 | } 87 | -------------------------------------------------------------------------------- /internal/sqlbuilder/comparison.go: -------------------------------------------------------------------------------- 1 | package sqlbuilder 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | db "github.com/upper/db/v4" 8 | "github.com/upper/db/v4/internal/adapter" 9 | "github.com/upper/db/v4/internal/sqladapter/exql" 10 | ) 11 | 12 | var comparisonOperators = map[adapter.ComparisonOperator]string{ 13 | adapter.ComparisonOperatorEqual: "=", 14 | adapter.ComparisonOperatorNotEqual: "!=", 15 | 16 | adapter.ComparisonOperatorLessThan: "<", 17 | adapter.ComparisonOperatorGreaterThan: ">", 18 | 19 | adapter.ComparisonOperatorLessThanOrEqualTo: "<=", 20 | adapter.ComparisonOperatorGreaterThanOrEqualTo: ">=", 21 | 22 | adapter.ComparisonOperatorBetween: "BETWEEN", 23 | adapter.ComparisonOperatorNotBetween: "NOT BETWEEN", 24 | 25 | adapter.ComparisonOperatorIn: "IN", 26 | adapter.ComparisonOperatorNotIn: "NOT IN", 27 | 28 | adapter.ComparisonOperatorIs: "IS", 29 | adapter.ComparisonOperatorIsNot: "IS NOT", 30 | 31 | adapter.ComparisonOperatorLike: "LIKE", 32 | adapter.ComparisonOperatorNotLike: "NOT LIKE", 33 | 34 | adapter.ComparisonOperatorRegExp: "REGEXP", 35 | adapter.ComparisonOperatorNotRegExp: "NOT REGEXP", 36 | } 37 | 38 | type operatorWrapper struct { 39 | tu *templateWithUtils 40 | cv *exql.ColumnValue 41 | 42 | op *adapter.Comparison 43 | v interface{} 44 | } 45 | 46 | func (ow *operatorWrapper) cmp() *adapter.Comparison { 47 | if ow.op != nil { 48 | return ow.op 49 | } 50 | 51 | if ow.cv.Operator != "" { 52 | return db.Op(ow.cv.Operator, ow.v).Comparison 53 | } 54 | 55 | if ow.v == nil { 56 | return db.Is(nil).Comparison 57 | } 58 | 59 | args, isSlice := toInterfaceArguments(ow.v) 60 | if isSlice { 61 | return db.In(args...).Comparison 62 | } 63 | 64 | return db.Eq(ow.v).Comparison 65 | } 66 | 67 | func (ow *operatorWrapper) preprocess() (string, []interface{}) { 68 | placeholder := "?" 69 | 70 | column, err := ow.cv.Column.Compile(ow.tu.Template) 71 | if err != nil { 72 | panic(fmt.Sprintf("could not compile column: %v", err.Error())) 73 | } 74 | 75 | c := ow.cmp() 76 | 77 | op := ow.tu.comparisonOperatorMapper(c.Operator()) 78 | 79 | var args []interface{} 80 | 81 | switch c.Operator() { 82 | case adapter.ComparisonOperatorNone: 83 | panic("no operator given") 84 | case adapter.ComparisonOperatorCustom: 85 | op = c.CustomOperator() 86 | case adapter.ComparisonOperatorIn, adapter.ComparisonOperatorNotIn: 87 | values := c.Value().([]interface{}) 88 | if len(values) < 1 { 89 | placeholder, args = "(NULL)", []interface{}{} 90 | break 91 | } 92 | placeholder, args = "(?"+strings.Repeat(", ?", len(values)-1)+")", values 93 | case adapter.ComparisonOperatorIs, adapter.ComparisonOperatorIsNot: 94 | switch c.Value() { 95 | case nil: 96 | placeholder, args = "NULL", []interface{}{} 97 | case false: 98 | placeholder, args = "FALSE", []interface{}{} 99 | case true: 100 | placeholder, args = "TRUE", []interface{}{} 101 | } 102 | case adapter.ComparisonOperatorBetween, adapter.ComparisonOperatorNotBetween: 103 | values := c.Value().([]interface{}) 104 | placeholder, args = "? AND ?", []interface{}{values[0], values[1]} 105 | case adapter.ComparisonOperatorEqual: 106 | v := c.Value() 107 | if b, ok := v.([]byte); ok { 108 | v = string(b) 109 | } 110 | args = []interface{}{v} 111 | } 112 | 113 | if args == nil { 114 | args = []interface{}{c.Value()} 115 | } 116 | 117 | if strings.Contains(op, ":column") { 118 | return strings.Replace(op, ":column", column, -1), args 119 | } 120 | 121 | return column + " " + op + " " + placeholder, args 122 | } 123 | -------------------------------------------------------------------------------- /internal/sqlbuilder/custom_types.go: -------------------------------------------------------------------------------- 1 | package sqlbuilder 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | ) 7 | 8 | type ScannerValuer interface { 9 | sql.Scanner 10 | driver.Valuer 11 | } 12 | -------------------------------------------------------------------------------- /internal/sqlbuilder/errors.go: -------------------------------------------------------------------------------- 1 | package sqlbuilder 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Common error messages. 8 | var ( 9 | ErrExpectingPointer = errors.New(`argument must be an address`) 10 | ErrExpectingSlicePointer = errors.New(`argument must be a slice address`) 11 | ErrExpectingSliceMapStruct = errors.New(`argument must be a slice address of maps or structs`) 12 | ErrExpectingMapOrStruct = errors.New(`argument must be either a map or a struct`) 13 | ErrExpectingPointerToEitherMapOrStruct = errors.New(`expecting a pointer to either a map or a struct`) 14 | ) 15 | -------------------------------------------------------------------------------- /internal/sqlbuilder/scanner.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package sqlbuilder 23 | 24 | import ( 25 | "database/sql" 26 | 27 | db "github.com/upper/db/v4" 28 | ) 29 | 30 | type scanner struct { 31 | v db.Unmarshaler 32 | } 33 | 34 | func (u scanner) Scan(v interface{}) error { 35 | return u.v.UnmarshalDB(v) 36 | } 37 | 38 | var _ sql.Scanner = scanner{} 39 | -------------------------------------------------------------------------------- /internal/sqlbuilder/sqlbuilder.go: -------------------------------------------------------------------------------- 1 | package sqlbuilder 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | "github.com/upper/db/v4" 8 | ) 9 | 10 | // Engine represents a SQL database engine. 11 | type Engine interface { 12 | db.Session 13 | 14 | db.SQL 15 | } 16 | 17 | func lookupAdapter(adapterName string) (Adapter, error) { 18 | adapter := db.LookupAdapter(adapterName) 19 | if sqlAdapter, ok := adapter.(Adapter); ok { 20 | return sqlAdapter, nil 21 | } 22 | return nil, fmt.Errorf("%w %q", db.ErrMissingAdapter, adapterName) 23 | } 24 | 25 | func BindTx(adapterName string, tx *sql.Tx) (Tx, error) { 26 | adapter, err := lookupAdapter(adapterName) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return adapter.NewTx(tx) 31 | } 32 | 33 | // Bind creates a binding between an adapter and a *sql.Tx or a *sql.DB. 34 | func BindDB(adapterName string, sess *sql.DB) (db.Session, error) { 35 | adapter, err := lookupAdapter(adapterName) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return adapter.New(sess) 40 | } 41 | -------------------------------------------------------------------------------- /internal/sqlbuilder/wrapper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package sqlbuilder 23 | 24 | import ( 25 | "database/sql" 26 | 27 | db "github.com/upper/db/v4" 28 | ) 29 | 30 | // Tx represents a transaction on a SQL database. A transaction is like a 31 | // regular Session except it has two extra methods: Commit and Rollback. 32 | // 33 | // A transaction needs to be committed (with Commit) to make changes permanent, 34 | // changes can be discarded before committing by rolling back (with Rollback). 35 | // After either committing or rolling back a transaction it can not longer be 36 | // used and it's automatically closed. 37 | type Tx interface { 38 | // All db.Session methods are available on transaction sessions. They will 39 | // run on the same transaction. 40 | db.Session 41 | 42 | Commit() error 43 | 44 | Rollback() error 45 | } 46 | 47 | // Adapter represents a SQL adapter. 48 | type Adapter interface { 49 | // New wraps an active *sql.DB session and returns a SQLBuilder database. The 50 | // adapter needs to be imported to the blank namespace in order for it to be 51 | // used here. 52 | // 53 | // This method is internally used by upper-db to create a builder backed by the 54 | // given database. You may want to use your adapter's New function instead of 55 | // this one. 56 | New(*sql.DB) (db.Session, error) 57 | 58 | // NewTx wraps an active *sql.Tx transation and returns a SQLBuilder 59 | // transaction. The adapter needs to be imported to the blank namespace in 60 | // order for it to be used. 61 | // 62 | // This method is internally used by upper-db to create a builder backed by the 63 | // given transaction. You may want to use your adapter's NewTx function 64 | // instead of this one. 65 | NewTx(*sql.Tx) (Tx, error) 66 | 67 | // Open opens a SQL database. 68 | OpenDSN(db.ConnectionURL) (db.Session, error) 69 | } 70 | 71 | type dbAdapter struct { 72 | Adapter 73 | } 74 | 75 | func (d *dbAdapter) Open(conn db.ConnectionURL) (db.Session, error) { 76 | sess, err := d.Adapter.OpenDSN(conn) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return sess, nil 81 | } 82 | 83 | func NewCompatAdapter(adapter Adapter) db.Adapter { 84 | return &dbAdapter{adapter} 85 | } 86 | -------------------------------------------------------------------------------- /intersection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package db 23 | 24 | import ( 25 | "github.com/upper/db/v4/internal/adapter" 26 | ) 27 | 28 | // AndExpr represents an expression joined by a logical conjuction (AND). 29 | type AndExpr struct { 30 | *adapter.LogicalExprGroup 31 | } 32 | 33 | // And adds more expressions to the group. 34 | func (a *AndExpr) And(andConds ...LogicalExpr) *AndExpr { 35 | var fn func(*[]LogicalExpr) error 36 | if len(andConds) > 0 { 37 | fn = func(in *[]LogicalExpr) error { 38 | *in = append(*in, andConds...) 39 | return nil 40 | } 41 | } 42 | return &AndExpr{a.LogicalExprGroup.Frame(fn)} 43 | } 44 | 45 | // Empty returns false if the expressions has zero conditions. 46 | func (a *AndExpr) Empty() bool { 47 | return a.LogicalExprGroup.Empty() 48 | } 49 | 50 | // And joins conditions under logical conjunction. Conditions can be 51 | // represented by `db.Cond{}`, `db.Or()` or `db.And()`. 52 | // 53 | // Examples: 54 | // 55 | // // name = "Peter" AND last_name = "Parker" 56 | // db.And( 57 | // db.Cond{"name": "Peter"}, 58 | // db.Cond{"last_name": "Parker "}, 59 | // ) 60 | // 61 | // // (name = "Peter" OR name = "Mickey") AND last_name = "Mouse" 62 | // db.And( 63 | // db.Or( 64 | // db.Cond{"name": "Peter"}, 65 | // db.Cond{"name": "Mickey"}, 66 | // ), 67 | // db.Cond{"last_name": "Mouse"}, 68 | // ) 69 | func And(conds ...LogicalExpr) *AndExpr { 70 | return &AndExpr{adapter.NewLogicalExprGroup(adapter.LogicalOperatorAnd, conds...)} 71 | } 72 | 73 | var _ = adapter.LogicalExpr(&AndExpr{}) 74 | -------------------------------------------------------------------------------- /iterator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package db 23 | 24 | // Iterator provides methods for iterating over query results. 25 | type Iterator interface { 26 | // ResultMapper provides methods to retrieve and map results. 27 | ResultMapper 28 | 29 | // Scan dumps the current result into the given pointer variable pointers. 30 | Scan(dest ...interface{}) error 31 | 32 | // NextScan advances the iterator and performs Scan. 33 | NextScan(dest ...interface{}) error 34 | 35 | // ScanOne advances the iterator, performs Scan and closes the iterator. 36 | ScanOne(dest ...interface{}) error 37 | 38 | // Next dumps the current element into the given destination, which could be 39 | // a pointer to either a map or a struct. 40 | Next(dest ...interface{}) bool 41 | 42 | // Err returns the last error produced by the cursor. 43 | Err() error 44 | 45 | // Close closes the iterator and frees up the cursor. 46 | Close() error 47 | } 48 | -------------------------------------------------------------------------------- /marshal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package db 23 | 24 | // Marshaler is the interface implemented by struct fields that can transform 25 | // themselves into values to be stored in a database. 26 | type Marshaler interface { 27 | // MarshalDB returns the internal database representation of the Go value. 28 | MarshalDB() (interface{}, error) 29 | } 30 | 31 | // Unmarshaler is the interface implemented by struct fields that can transform 32 | // themselves from database values into Go values. 33 | type Unmarshaler interface { 34 | // UnmarshalDB receives an internal database representation of a value and 35 | // transforms it into a Go value. 36 | UnmarshalDB(interface{}) error 37 | } 38 | -------------------------------------------------------------------------------- /raw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package db 23 | 24 | import ( 25 | "github.com/upper/db/v4/internal/adapter" 26 | ) 27 | 28 | // RawExpr represents a raw (non-filtered) expression. 29 | type RawExpr = adapter.RawExpr 30 | 31 | // Raw marks chunks of data as protected, so they pass directly to the query 32 | // without any filtering. Use with care. 33 | // 34 | // Example: 35 | // 36 | // // SOUNDEX('Hello') 37 | // Raw("SOUNDEX('Hello')") 38 | func Raw(value string, args ...interface{}) *RawExpr { 39 | return adapter.NewRawExpr(value, args) 40 | } 41 | -------------------------------------------------------------------------------- /record.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | 21 | package db 22 | 23 | // Record is the equivalence between concrete database schemas and Go values. 24 | type Record interface { 25 | Store(sess Session) Store 26 | } 27 | 28 | // HasConstraints is an interface for records that defines a Constraints method 29 | // that returns the record's own constraints. 30 | type HasConstraints interface { 31 | Constraints() Cond 32 | } 33 | 34 | // Validator is an interface for records that defines an (optional) Validate 35 | // method that is called before persisting a record (creating or updating). If 36 | // Validate returns an error the current operation is cancelled and rolled 37 | // back. 38 | type Validator interface { 39 | Validate() error 40 | } 41 | 42 | // BeforeCreateHook is an interface for records that defines an BeforeCreate 43 | // method that is called before creating a record. If BeforeCreate returns an 44 | // error the create process is cancelled and rolled back. 45 | type BeforeCreateHook interface { 46 | BeforeCreate(Session) error 47 | } 48 | 49 | // AfterCreateHook is an interface for records that defines an AfterCreate 50 | // method that is called after creating a record. If AfterCreate returns an 51 | // error the create process is cancelled and rolled back. 52 | type AfterCreateHook interface { 53 | AfterCreate(Session) error 54 | } 55 | 56 | // BeforeUpdateHook is an interface for records that defines a BeforeUpdate 57 | // method that is called before updating a record. If BeforeUpdate returns an 58 | // error the update process is cancelled and rolled back. 59 | type BeforeUpdateHook interface { 60 | BeforeUpdate(Session) error 61 | } 62 | 63 | // AfterUpdateHook is an interface for records that defines an AfterUpdate 64 | // method that is called after updating a record. If AfterUpdate returns an 65 | // error the update process is cancelled and rolled back. 66 | type AfterUpdateHook interface { 67 | AfterUpdate(Session) error 68 | } 69 | 70 | // BeforeDeleteHook is an interface for records that defines a BeforeDelete 71 | // method that is called before removing a record. If BeforeDelete returns an 72 | // error the delete process is cancelled and rolled back. 73 | type BeforeDeleteHook interface { 74 | BeforeDelete(Session) error 75 | } 76 | 77 | // AfterDeleteHook is an interface for records that defines a AfterDelete 78 | // method that is called after removing a record. If AfterDelete returns an 79 | // error the delete process is cancelled and rolled back. 80 | type AfterDeleteHook interface { 81 | AfterDelete(Session) error 82 | } 83 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package db 23 | 24 | // Store represents a data store. 25 | type Store interface { 26 | Collection 27 | } 28 | 29 | // StoreSaver is an interface for data stores that defines a Save method that 30 | // has the task of persisting a record. 31 | type StoreSaver interface { 32 | Save(record Record) error 33 | } 34 | 35 | // StoreCreator is an interface for data stores that defines a Create method 36 | // that has the task of creating a new record. 37 | type StoreCreator interface { 38 | Create(record Record) error 39 | } 40 | 41 | // StoreDeleter is an interface for data stores that defines a Delete method 42 | // that has the task of removing a record. 43 | type StoreDeleter interface { 44 | Delete(record Record) error 45 | } 46 | 47 | // StoreUpdater is an interface for data stores that defines a Update method 48 | // that has the task of updating a record. 49 | type StoreUpdater interface { 50 | Update(record Record) error 51 | } 52 | 53 | // StoreGetter is an interface for data stores that defines a Get method that 54 | // has the task of retrieving a record. 55 | type StoreGetter interface { 56 | Get(record Record, id interface{}) error 57 | } 58 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | TEST_FLAGS ?= 2 | 3 | GODEBUG ?=x509negativeserial=1 4 | 5 | export GODEBUG 6 | 7 | all: test-all 8 | 9 | venv: 10 | $(MAKE) -C ansible venv 11 | 12 | test: venv 13 | go test \ 14 | -failfast \ 15 | -timeout 30m \ 16 | -race \ 17 | -v \ 18 | $(TEST_FLAGS) 19 | 20 | test-all: 21 | TEST_FLAGS="-run ." $(MAKE) test 22 | -------------------------------------------------------------------------------- /tests/ansible/Makefile: -------------------------------------------------------------------------------- 1 | VIRTUAL_ENV ?= $(shell pwd)/.venv 2 | PATH := $(VIRTUAL_ENV)/bin:$(PATH) 3 | PYTHON_BIN ?= $(VIRTUAL_ENV)/bin/python 4 | TARGET ?= none 5 | REQUIREMENTS_FILE ?= requirements.txt 6 | ANSIBLE_CONFIG ?= $(shell pwd)/ansible.cfg 7 | 8 | export PATH 9 | 10 | .venv: 11 | python3 -m venv $(VIRTUAL_ENV) 12 | 13 | venv: .venv pip-install 14 | 15 | pip-freeze: 16 | $(PYTHON_BIN) -m pip freeze > $(REQUIREMENTS_FILE) 17 | 18 | pip-install: 19 | $(PYTHON_BIN) -m pip install -r $(REQUIREMENTS_FILE) 20 | 21 | server-up: 22 | ansible-playbook \ 23 | -l "$(TARGET)" \ 24 | --tags up \ 25 | -v playbook.yml 26 | 27 | server-down: 28 | ansible-playbook \ 29 | -l "$(TARGET)" \ 30 | --tags down \ 31 | -v playbook.yml 32 | -------------------------------------------------------------------------------- /tests/ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = ./inventory.yml 3 | strategy = free 4 | gather_facts = False 5 | -------------------------------------------------------------------------------- /tests/ansible/inventory.yml: -------------------------------------------------------------------------------- 1 | all: 2 | vars: 3 | ansible_connection: local 4 | ansible_python_interpreter: ./.venv/bin/python 5 | docker_public_registry: mirror.gcr.io/ 6 | db_username: upper_db_user 7 | db_password: upp3r//S3cr37 8 | db_name: upper_db 9 | container_bind_ip: "127.0.0.1" 10 | container_bind_port: "{{ lookup('env', 'CONTAINER_BIND_PORT') }}" 11 | container_name_prefix: "upper_db_" 12 | health_check_retries: 30 13 | health_check_delay: 5 14 | hosts: 15 | postgresql-17: 16 | postgresql_version: "17" 17 | postgresql-16: 18 | postgresql_version: "16" 19 | postgresql-15: 20 | postgresql_version: "15" 21 | mysql-latest: 22 | mysql_version: "latest" 23 | mysql-lts: 24 | mysql_version: "lts" 25 | mysql-5: 26 | mysql_version: "5" 27 | cockroachdb-v23: 28 | cockroachdb_version: "v23.2.6" 29 | cockroachdb-v22: 30 | cockroachdb_version: "v22.2.19" 31 | mssql-2022: 32 | mssql_version: "2022-latest" 33 | mssql-2019: 34 | mssql_version: "2019-latest" 35 | mongodb-8: 36 | mongodb_version: "8" 37 | mongodb-7: 38 | mongodb_version: "7" 39 | -------------------------------------------------------------------------------- /tests/ansible/playbook.yml: -------------------------------------------------------------------------------- 1 | - hosts: "postgresql-*" 2 | roles: 3 | - role: postgresql 4 | - hosts: "mysql-*" 5 | roles: 6 | - role: mysql 7 | - hosts: "cockroachdb-*" 8 | roles: 9 | - role: cockroachdb 10 | - hosts: "mssql-*" 11 | roles: 12 | - role: mssql 13 | - hosts: "mongodb-*" 14 | roles: 15 | - role: mongodb 16 | -------------------------------------------------------------------------------- /tests/ansible/requirements.txt: -------------------------------------------------------------------------------- 1 | ansible==11.3.0 2 | ansible-core==2.18.3 3 | certifi==2025.1.31 4 | cffi==1.17.1 5 | charset-normalizer==3.4.1 6 | cryptography==44.0.2 7 | dnspython==2.7.0 8 | idna==3.10 9 | Jinja2==3.1.6 10 | MarkupSafe==3.0.2 11 | packaging==24.2 12 | psycopg2-binary==2.9.10 13 | pycparser==2.22 14 | pymongo==4.11.3 15 | pymssql==2.3.2 16 | PyMySQL==1.1.1 17 | PyYAML==6.0.2 18 | requests==2.32.3 19 | resolvelib==1.0.1 20 | urllib3==2.3.0 21 | -------------------------------------------------------------------------------- /tests/ansible/roles/cockroachdb/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: "Set task facts" 2 | set_fact: 3 | container_name: "{{ container_name_prefix }}cockroachdb_{{ cockroachdb_version }}" 4 | container_image: "{{ docker_public_registry }}cockroachdb/cockroach:{{ cockroachdb_version }}" 5 | container_bind_port: "{{ 26257 if container_bind_port == '' else container_bind_port }}" 6 | tags: 7 | - up 8 | - down 9 | - name: Escape container name 10 | set_fact: 11 | container_name: "{{ container_name | regex_replace('[^a-zA-Z0-9]', '_') }}" 12 | tags: 13 | - up 14 | - down 15 | - name: "Force-stop CockroachDB {{ cockroachdb_version }}" 16 | docker_container: 17 | name: "{{ container_name }}" 18 | state: absent 19 | tags: 20 | - up 21 | - down 22 | - name: "Run CockroachDB {{ cockroachdb_version }}" 23 | docker_container: 24 | name: "{{ container_name }}" 25 | image: "{{ container_image }}" 26 | state: started 27 | ports: 28 | - "{{ container_bind_ip }}:{{ container_bind_port }}:26257" 29 | env: 30 | COCKROACH_USER: "{{ db_username }}" 31 | COCKROACH_DATABASE: "{{ db_name }}" 32 | command: "start-single-node --insecure" 33 | tags: 34 | - up 35 | - name: "CockroachDB health check" 36 | postgresql_query: 37 | db: "{{ db_name }}" 38 | login_host: "{{ container_bind_ip }}" 39 | login_port: "{{ container_bind_port }}" 40 | login_user: "{{ db_username }}" 41 | query: "SELECT 1" 42 | register: cockroachdb_health_check 43 | retries: "{{ health_check_retries }}" 44 | delay: "{{ health_check_delay }}" 45 | until: cockroachdb_health_check is succeeded 46 | tags: 47 | - up 48 | -------------------------------------------------------------------------------- /tests/ansible/roles/mongodb/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: "Set task facts" 2 | set_fact: 3 | container_name: "{{ container_name_prefix }}mongo_{{ mongodb_version }}" 4 | container_image: "{{ docker_public_registry }}mongo:{{ mongodb_version }}" 5 | container_bind_port: "{{ 27017 if container_bind_port == '' else container_bind_port }}" 6 | db_name: "admin" 7 | tags: 8 | - up 9 | - down 10 | - name: Escape container name 11 | set_fact: 12 | container_name: "{{ container_name | regex_replace('[^a-zA-Z0-9]', '_') }}" 13 | tags: 14 | - up 15 | - down 16 | - name: "Force-stop MongoDB {{ mongodb_version }}" 17 | docker_container: 18 | name: "{{ container_name }}" 19 | state: absent 20 | tags: 21 | - up 22 | - down 23 | - name: "Run MongoDB {{ mongodb_version }}" 24 | docker_container: 25 | name: "{{ container_name }}" 26 | image: "{{ container_image }}" 27 | state: started 28 | ports: 29 | - "{{ container_bind_ip }}:{{ container_bind_port }}:27017" 30 | env: 31 | MONGO_INITDB_ROOT_USERNAME: "{{ db_username }}" 32 | MONGO_INITDB_ROOT_PASSWORD: "{{ db_password }}" 33 | MONGO_INITDB_DATABASE: "{{ db_name }}" 34 | tags: 35 | - up 36 | - name: "MongoDB health check" 37 | community.mongodb.mongodb_info: 38 | login_user: "{{ db_username }}" 39 | login_password: "{{ db_password }}" 40 | login_host: "{{ container_bind_ip }}" 41 | login_port: "{{ container_bind_port }}" 42 | login_database: "{{ db_name }}" 43 | filter: "databases" 44 | register: mongodb_health_check 45 | until: mongodb_health_check is not failed 46 | retries: "{{ health_check_retries }}" 47 | delay: "{{ health_check_delay }}" 48 | tags: 49 | - up 50 | -------------------------------------------------------------------------------- /tests/ansible/roles/mssql/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: "Set task facts" 2 | set_fact: 3 | container_name: "{{ container_name_prefix }}mssql_{{ mssql_version }}" 4 | container_image: "mcr.microsoft.com/mssql/server:{{ mssql_version }}" 5 | container_bind_port: "{{ 1433 if container_bind_port == '' else container_bind_port }}" 6 | db_username: "sa" 7 | db_name: "master" 8 | tags: 9 | - up 10 | - down 11 | - name: Escape container name 12 | set_fact: 13 | container_name: "{{ container_name | regex_replace('[^a-zA-Z0-9]', '_') }}" 14 | tags: 15 | - up 16 | - down 17 | - name: "Force-stop MSSQL {{ mssql_version }}" 18 | docker_container: 19 | name: "{{ container_name }}" 20 | state: absent 21 | tags: 22 | - down 23 | - name: "Run MSSQL {{ mssql_version }}" 24 | docker_container: 25 | name: "{{ container_name }}" 26 | image: "{{ container_image }}" 27 | state: started 28 | ports: 29 | - "{{ container_bind_ip }}:{{ container_bind_port }}:1433" 30 | env: 31 | ACCEPT_EULA: "Y" 32 | SA_PASSWORD: "{{ db_password }}" 33 | tags: 34 | - up 35 | - name: "MSSQL health check" 36 | community.general.mssql_script: 37 | login_host: "{{ container_bind_ip }}" 38 | login_port: "{{ container_bind_port }}" 39 | login_user: "{{ db_username }}" 40 | login_password: "{{ db_password }}" 41 | name: "{{ db_name }}" 42 | script: "SELECT 1" 43 | register: mssql_health_check 44 | retries: "{{ health_check_retries }}" 45 | delay: "{{ health_check_delay }}" 46 | until: mssql_health_check is succeeded 47 | tags: 48 | - up 49 | -------------------------------------------------------------------------------- /tests/ansible/roles/mysql/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: "Set task facts" 2 | set_fact: 3 | container_name: "{{ container_name_prefix }}mysql_{{ mysql_version }}" 4 | container_image: "{{ docker_public_registry }}mysql:{{ mysql_version }}" 5 | container_bind_port: "{{ 3306 if container_bind_port == '' else container_bind_port }}" 6 | tags: 7 | - up 8 | - down 9 | - name: Escape container name 10 | set_fact: 11 | container_name: "{{ container_name | regex_replace('[^a-zA-Z0-9]', '_') }}" 12 | tags: 13 | - up 14 | - down 15 | - name: "Force-stop MySQL {{ mysql_version }}" 16 | docker_container: 17 | name: "{{ container_name }}" 18 | state: absent 19 | tags: 20 | - up 21 | - down 22 | - name: "Run MySQL {{ mysql_version }}" 23 | docker_container: 24 | name: "{{ container_name }}" 25 | image: "{{ container_image }}" 26 | state: started 27 | ports: 28 | - "{{ container_bind_ip }}:{{ container_bind_port }}:3306" 29 | ulimits: 30 | - nofile:262144:262144 31 | env: 32 | MYSQL_RANDOM_ROOT_PASSWORD: "yes" 33 | MYSQL_USER: "{{ db_username }}" 34 | MYSQL_PASSWORD: "{{ db_password }}" 35 | MYSQL_DATABASE: "{{ db_name }}" 36 | tags: 37 | - up 38 | - name: "MySQL health check" 39 | mysql_query: 40 | login_host: "{{ container_bind_ip }}" 41 | login_port: "{{ container_bind_port }}" 42 | login_user: "{{ db_username }}" 43 | login_password: "{{ db_password }}" 44 | login_db: "{{ db_name }}" 45 | query: "SELECT 1" 46 | register: mysql_health_check 47 | retries: "{{ health_check_retries }}" 48 | delay: "{{ health_check_delay }}" 49 | until: mysql_health_check is succeeded 50 | tags: 51 | - up 52 | -------------------------------------------------------------------------------- /tests/ansible/roles/postgresql/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: "Set task facts" 2 | set_fact: 3 | container_name: "{{ container_name_prefix }}postgres_{{ postgresql_version }}" 4 | container_image: "{{ docker_public_registry }}postgres:{{ postgresql_version }}" 5 | container_bind_port: "{{ 5432 if container_bind_port == '' else container_bind_port }}" 6 | tags: 7 | - up 8 | - down 9 | - name: Escape container name 10 | set_fact: 11 | container_name: "{{ container_name | regex_replace('[^a-zA-Z0-9]', '_') }}" 12 | tags: 13 | - up 14 | - down 15 | - name: "Force-stop PostgreSQL {{ postgresql_version }}" 16 | docker_container: 17 | name: "{{ container_name }}" 18 | state: absent 19 | tags: 20 | - down 21 | - name: "Run PostgreSQL {{ postgresql_version }}" 22 | docker_container: 23 | name: "{{ container_name }}" 24 | image: "{{ container_image }}" 25 | state: started 26 | ports: 27 | - "{{ container_bind_ip }}:{{ container_bind_port }}:5432" 28 | env: 29 | POSTGRES_USER: "{{ db_username }}" 30 | POSTGRES_PASSWORD: "{{ db_password }}" 31 | POSTGRES_DB: "{{ db_name }}" 32 | tags: 33 | - up 34 | - name: "PostgreSQL health check" 35 | postgresql_query: 36 | db: "{{ db_name }}" 37 | login_host: "{{ container_bind_ip }}" 38 | login_port: "{{ container_bind_port }}" 39 | login_user: "{{ db_username }}" 40 | login_password: "{{ db_password }}" 41 | query: "SELECT 1" 42 | register: postgresql_health_check 43 | retries: "{{ health_check_retries }}" 44 | delay: "{{ health_check_delay }}" 45 | until: postgresql_health_check is succeeded 46 | tags: 47 | - up 48 | -------------------------------------------------------------------------------- /tests/entrypoint_test.go: -------------------------------------------------------------------------------- 1 | package db_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type testHelperConfig struct { 13 | initFn func() (Helper, string, int) 14 | 15 | withoutGeneric bool 16 | withoutRecord bool 17 | withoutSQL bool 18 | } 19 | 20 | func serverUp(t *testing.T, name string, host string, port int) { 21 | cmd := exec.Command("make", "-C", "ansible", "server-up") 22 | 23 | cmd.Env = append( 24 | os.Environ(), 25 | "TARGET="+name, 26 | "CONTAINER_BIND_HOST="+host, 27 | "CONTAINER_BIND_PORT="+fmt.Sprintf("%d", port), 28 | ) 29 | 30 | cmd.Stdout = os.Stdout 31 | cmd.Stderr = os.Stderr 32 | 33 | err := cmd.Run() 34 | if err != nil { 35 | t.Fatalf("could not start server: %v", err) 36 | } 37 | } 38 | 39 | func serverDown(t *testing.T, name string) { 40 | cmd := exec.Command("make", "-C", "ansible", "server-down") 41 | 42 | cmd.Env = append( 43 | os.Environ(), 44 | "TARGET="+name, 45 | ) 46 | cmd.Stdout = os.Stdout 47 | cmd.Stderr = os.Stderr 48 | 49 | err := cmd.Run() 50 | if err != nil { 51 | t.Fatalf("could not stop server: %v", err) 52 | } 53 | } 54 | 55 | func Test(t *testing.T) { 56 | testCfgs := map[string]testHelperConfig{ 57 | "postgresql-17": { 58 | initFn: newPostgreSQLTestHelper(), 59 | }, 60 | "postgresql-16": { 61 | initFn: newPostgreSQLTestHelper(), 62 | }, 63 | "postgresql-15": { 64 | initFn: newPostgreSQLTestHelper(), 65 | }, 66 | "mysql-latest": { 67 | initFn: newMySQLTestHelper(), 68 | }, 69 | "mysql-lts": { 70 | initFn: newMySQLTestHelper(), 71 | }, 72 | "mysql-5": { 73 | initFn: newMySQLTestHelper(), 74 | }, 75 | 76 | "cockroachdb-v23": { 77 | initFn: newCockroachDBTestHelper(), 78 | }, 79 | "cockroachdb-v22": { 80 | initFn: newCockroachDBTestHelper(), 81 | }, 82 | 83 | "mssql-2022": { 84 | initFn: newMSSQLTestHelper(), 85 | withoutRecord: true, 86 | }, 87 | "mssql-2019": { 88 | initFn: newMSSQLTestHelper(), 89 | withoutRecord: true, // TODO: fix MSSQL record tests 90 | }, 91 | 92 | "mongodb-8": { 93 | initFn: newMongoDBTestHelper(), 94 | withoutSQL: true, 95 | withoutRecord: true, 96 | }, 97 | "mongodb-7": { 98 | initFn: newMongoDBTestHelper(), 99 | withoutSQL: true, 100 | withoutRecord: true, 101 | }, 102 | } 103 | 104 | for name, cfg := range testCfgs { 105 | t.Run(name, func(t *testing.T) { 106 | helper, bindAddr, bindPort := cfg.initFn() 107 | 108 | serverUp(t, name, bindAddr, bindPort) 109 | defer serverDown(t, name) 110 | 111 | t.Run("Generic", func(t *testing.T) { 112 | if cfg.withoutGeneric { 113 | t.Skip("Generic tests are disabled for this adapter") 114 | } 115 | 116 | suite.Run(t, &GenericTestSuite{Helper: helper}) 117 | }) 118 | 119 | t.Run("Record", func(t *testing.T) { 120 | if cfg.withoutRecord { 121 | t.Skip("Record tests are disabled for this adapter") 122 | } 123 | 124 | suite.Run(t, &RecordTestSuite{Helper: helper}) 125 | }) 126 | 127 | t.Run("SQL", func(t *testing.T) { 128 | if cfg.withoutSQL { 129 | t.Skip("SQL tests are disabled for this adapter") 130 | } 131 | 132 | suite.Run(t, &SQLTestSuite{Helper: helper}) 133 | }) 134 | }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /tests/mongo/helper.go: -------------------------------------------------------------------------------- 1 | package mongo_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | db "github.com/upper/db/v4" 8 | mongodrv "go.mongodb.org/mongo-driver/mongo" 9 | 10 | "github.com/upper/db/v4/adapter/mongo" 11 | ) 12 | 13 | type Helper struct { 14 | sess db.Session 15 | 16 | connURL mongo.ConnectionURL 17 | } 18 | 19 | func (h *Helper) Session() db.Session { 20 | return h.sess 21 | } 22 | 23 | func (h *Helper) Adapter() string { 24 | return "mongo" 25 | } 26 | 27 | func (h *Helper) TearDown() error { 28 | return h.sess.Close() 29 | } 30 | 31 | func (h *Helper) SetUp() error { 32 | ctx := context.Background() 33 | 34 | var err error 35 | 36 | h.sess, err = mongo.Open(h.connURL) 37 | if err != nil { 38 | return fmt.Errorf("mongo.Open: %w", err) 39 | } 40 | 41 | mgdb, ok := h.sess.Driver().(*mongodrv.Client) 42 | if !ok { 43 | panic("expecting *mongo.Client") 44 | } 45 | 46 | var col *mongodrv.Collection 47 | col = mgdb.Database(h.connURL.Database).Collection("birthdays") 48 | _ = col.Drop(ctx) 49 | 50 | col = mgdb.Database(h.connURL.Database).Collection("fibonacci") 51 | _ = col.Drop(ctx) 52 | 53 | col = mgdb.Database(h.connURL.Database).Collection("is_even") 54 | _ = col.Drop(ctx) 55 | 56 | col = mgdb.Database(h.connURL.Database).Collection("CaSe_TesT") 57 | _ = col.Drop(ctx) 58 | 59 | // Getting a pointer to the "artist" collection. 60 | artist := h.sess.Collection("artist") 61 | 62 | _ = artist.Truncate() 63 | 64 | /* 65 | for i := 0; i < 999; i++ { 66 | _, err = artist.Insert(artistType{ 67 | Name: fmt.Sprintf("artist-%d", i), 68 | }) 69 | if err != nil { 70 | return fmt.Errorf("insert: %w", err) 71 | } 72 | } 73 | */ 74 | 75 | return nil 76 | } 77 | 78 | func NewHelper(connURL mongo.ConnectionURL) *Helper { 79 | return &Helper{ 80 | connURL: connURL, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /union.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-present The upper.io/db authors. All rights reserved. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package db 23 | 24 | import ( 25 | "github.com/upper/db/v4/internal/adapter" 26 | ) 27 | 28 | // OrExpr represents a logical expression joined by logical disjunction (OR). 29 | type OrExpr struct { 30 | *adapter.LogicalExprGroup 31 | } 32 | 33 | // Or adds more expressions to the group. 34 | func (o *OrExpr) Or(orConds ...LogicalExpr) *OrExpr { 35 | var fn func(*[]LogicalExpr) error 36 | if len(orConds) > 0 { 37 | fn = func(in *[]LogicalExpr) error { 38 | *in = append(*in, orConds...) 39 | return nil 40 | } 41 | } 42 | return &OrExpr{o.LogicalExprGroup.Frame(fn)} 43 | } 44 | 45 | // Empty returns false if the expressions has zero conditions. 46 | func (o *OrExpr) Empty() bool { 47 | return o.LogicalExprGroup.Empty() 48 | } 49 | 50 | // Or joins conditions under logical disjunction. Conditions can be represented 51 | // by `db.Cond{}`, `db.Or()` or `db.And()`. 52 | // 53 | // Example: 54 | // 55 | // // year = 2012 OR year = 1987 56 | // db.Or( 57 | // db.Cond{"year": 2012}, 58 | // db.Cond{"year": 1987}, 59 | // ) 60 | func Or(conds ...LogicalExpr) *OrExpr { 61 | return &OrExpr{adapter.NewLogicalExprGroup(adapter.LogicalOperatorOr, defaultJoin(conds...)...)} 62 | } 63 | 64 | var _ = adapter.LogicalExpr(&OrExpr{}) 65 | --------------------------------------------------------------------------------