├── .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 |
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 | 
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 |
--------------------------------------------------------------------------------