From 02e2342a97da1adb4ca8807c59a2a9383cebcbba Mon Sep 17 00:00:00 2001 From: August Vi Date: Sun, 18 Oct 2026 22:01:10 -0700 Subject: [PATCH] Move the database tests into a nested dbtest module so sqlx's go.mod requires no other module. --- .github/dependabot.yml | 4 + .github/workflows/ci.yml | 25 +- CHANGELOG.md | 13 + CONTRIBUTING.md | 12 +- Makefile | 13 +- README.md | 4 +- .../conn_preparex_test.go | 28 +- dbtest/go.mod | 14 + go.sum => dbtest/go.sum | 0 dbtest/in_empty_test.go | 35 ++ .../multi_result_set_test.go | 8 +- .../named_cast_test.go | 8 +- .../named_context_test.go | 4 +- named_ptr_test.go => dbtest/named_ptr_test.go | 8 +- dbtest/named_test.go | 219 ++++++++++ .../sqlx_context_test.go | 40 +- sqlx_test.go => dbtest/sqlx_test.go | 375 +----------------- .../stmt_query_row_test.go | 4 +- tx_stmtx_test.go => dbtest/tx_stmtx_test.go | 27 +- go.mod | 8 - in_empty_test.go | 28 -- named_test.go | 212 ---------- sqlx_unit_test.go | 358 +++++++++++++++++ 23 files changed, 772 insertions(+), 675 deletions(-) rename conn_preparex_test.go => dbtest/conn_preparex_test.go (68%) create mode 100644 dbtest/go.mod rename go.sum => dbtest/go.sum (100%) create mode 100644 dbtest/in_empty_test.go rename multi_result_set_test.go => dbtest/multi_result_set_test.go (96%) rename named_cast_test.go => dbtest/named_cast_test.go (93%) rename named_context_test.go => dbtest/named_context_test.go (98%) rename named_ptr_test.go => dbtest/named_ptr_test.go (94%) create mode 100644 dbtest/named_test.go rename sqlx_context_test.go => dbtest/sqlx_context_test.go (97%) rename sqlx_test.go => dbtest/sqlx_test.go (81%) rename stmt_query_row_test.go => dbtest/stmt_query_row_test.go (95%) rename tx_stmtx_test.go => dbtest/tx_stmtx_test.go (66%) create mode 100644 sqlx_unit_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b75efa1..c1c7289 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,6 +4,10 @@ updates: directory: / schedule: interval: monthly +- package-ecosystem: gomod + directory: /dbtest + schedule: + interval: monthly - package-ecosystem: github-actions directory: / schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a861618..d4db090 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,19 +27,31 @@ jobs: - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable + - name: Root module requires no other module + # The database drivers belong to the dbtest module only; a requirement + # here would reach every consumer's module graph. + run: | + out="$(go list -m all)" + if [ "$out" != "$(go list -m)" ]; then echo "go.mod requires:"; echo "$out"; exit 1; fi - name: go vet - run: go vet ./... + run: | + go vet ./... + (cd dbtest && go vet ./...) - name: staticcheck # v0.8.1 cannot read Go 1.27.2 export data ("export data version 5 is greater than maximum # supported version 4"). This master build from 2026-10-09 can; pin a tagged release again # once one is published. - run: go run honnef.co/go/tools/cmd/staticcheck@v0.7.0-0.dev.0.20261009230814-452d5bb86b45 -checks=all ./... + run: | + go run honnef.co/go/tools/cmd/staticcheck@v0.7.0-0.dev.0.20261009230814-452d5bb86b45 -checks=all ./... + (cd dbtest && go run honnef.co/go/tools/cmd/staticcheck@v0.7.0-0.dev.0.20261009230814-452d5bb86b45 -checks=all ./...) - name: goimports run: | out="$(go run golang.org/x/tools/cmd/goimports@v0.52.0 -l -local "$(go list -m)" .)" if [ -n "$out" ]; then echo "goimports would change:"; echo "$out"; exit 1; fi - name: govulncheck - run: go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./... + run: | + go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./... + (cd dbtest && go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...) test: runs-on: ubuntu-latest @@ -85,11 +97,14 @@ jobs: - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: ${{ matrix.go }} - - name: Test with the race detector against Postgres, MySQL and SQLite + - name: Unit tests with the race detector + run: go test -race -count=1 ./... + - name: Database tests with the race detector against Postgres, MySQL and SQLite + working-directory: dbtest env: SQLX_SQLITE_DSN: ${{ runner.temp }}/sqlx.db run: | - go test -race -count=1 -v ./... 2>&1 | tee test.log + go test -race -count=1 -v ./... 2>&1 | tee ../test.log test "${PIPESTATUS[0]}" -eq 0 - name: Fail if a database was skipped # init() prints "Disabling ..." and the schema tests then run zero rounds for that database. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1145e69..f534d4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ Notable changes to this fork, newest first. An entry that imports an upstream pull request names its number and author. Releases up to v1.4.0 are releases of upstream [jmoiron/sqlx](https://github.com/jmoiron/sqlx). +## Unreleased + +### Module and toolchain + +- sqlx's `go.mod` requires no other module. The tests that run against + Postgres, MySQL and SQLite move to the nested module `dbtest/`, which holds + the three drivers, so `go-sql-driver/mysql`, `lib/pq` and `go-sqlite3` no + longer enter the module graph of a program that imports sqlx. Before, Go's + minimum version selection raised a consumer's own MySQL driver to v1.10.1 + even though sqlx only used it in tests (upstream #642, #569, #849). The + checks in three tests that read the unexported `unsafe` field now test its + effect instead: scanning into a struct that lacks some selected columns. + ## v1.4.1 (2026-10-10) The first fork release. It keeps every exported identifier of v1.4.0 (CI runs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0cf572e..94f2306 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,14 +7,18 @@ Bug reports, fixes, tests and documentation are all welcome. Open an issue with the bug report template. The driver and database version, the SQL, the struct or map you bind or scan, and what you expected are what make a report reproducible. A failing test in the style of -`sqlx_test.go` is the best report of all. Questions about how to use sqlx go +`dbtest/sqlx_test.go` is the best report of all. Questions about how to use sqlx go to [Discussions](https://github.com/secondwindoss/sqlx/discussions). ## Making a change 1. Branch from `master`. -2. Add or update a test. The suite always runs against SQLite and also runs - against Postgres and MySQL when their DSN variables are set: +2. Add or update a test. Tests that need no database live next to the code + in the root module and run with `make test`. Tests that talk to a database + live in `dbtest/`, a separate module, so that the drivers never appear in + sqlx's own `go.mod` or in the module graph of anyone who imports it. The + database suite always runs against SQLite and also runs against Postgres + and MySQL when their DSN variables are set: docker run -d --name sqlx-pg -e POSTGRES_PASSWORD=sqlx -p 5432:5432 postgres:16 docker run -d --name sqlx-mysql -e MYSQL_ROOT_PASSWORD=sqlx -e MYSQL_DATABASE=sqlx -p 3306:3306 mysql:8 @@ -31,7 +35,7 @@ to [Discussions](https://github.com/secondwindoss/sqlx/discussions). `go test -run TestGolden -update`, review the diff of `testdata/golden.txt`, and mention the changed lines in the pull request. 4. Open a pull request against `master`. CI runs the lint job and the race - tests on Go 1.23 to stable with all three databases and must be green + tests on Go 1.24 to stable with all three databases and must be green before the merge. Keep the exported API unchanged within v1 unless the change is purely diff --git a/Makefile b/Makefile index ca9a50d..5f09373 100644 --- a/Makefile +++ b/Makefile @@ -15,16 +15,21 @@ has-changes: lint: go vet ./... staticcheck -checks=all ./... + cd dbtest && go vet ./... && staticcheck -checks=all ./... fmt: - go list -f '{{.Dir}}' ./... | xargs -I {} goimports -local $(BASE_PACKAGE) -w {} + goimports -local $(BASE_PACKAGE) -w . vuln-check: govulncheck ./... + cd dbtest && govulncheck ./... -test-race: - go test -v -race -count=1 ./... +test: + go test -race -count=1 ./... + +test-race: test + cd dbtest && go test -v -race -count=1 ./... update-dependencies: - go get -u -t -v ./... go mod tidy + cd dbtest && go get -u -t -v ./... && go mod tidy diff --git a/README.md b/README.md index 0713081..2dab66f 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,9 @@ tests on Go 1.24, 1.25, oldstable and stable. **Databases.** CI tests every change against PostgreSQL 16, MySQL 8 and SQLite. Any `database/sql` driver works; sqlx only needs to know its -placeholder style. +placeholder style. sqlx's `go.mod` requires no other module: the drivers used +by the database tests live in the separate `dbtest/` module, so importing sqlx +never changes which driver version your program builds with. | Placeholder | Drivers | |-------------|---------------------------------------------------------------------------| diff --git a/conn_preparex_test.go b/dbtest/conn_preparex_test.go similarity index 68% rename from conn_preparex_test.go rename to dbtest/conn_preparex_test.go index fd3cc15..dcb9364 100644 --- a/conn_preparex_test.go +++ b/dbtest/conn_preparex_test.go @@ -1,15 +1,18 @@ -package sqlx +package dbtest import ( "context" "strings" "testing" + . "github.com/secondwindoss/sqlx" + "github.com/secondwindoss/sqlx/reflectx" ) // A Stmt prepared on a Conn inherits the Conn's Mapper and unsafe flag -// (upstream #975). +// (upstream #975). The unsafe flag is checked through its effect: scanning +// into a struct that lacks some of the selected columns. func TestConnPreparexKeepsMapperAndUnsafe(t *testing.T) { RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { loadDefaultFixture(db, t) @@ -20,17 +23,11 @@ func TestConnPreparexKeepsMapperAndUnsafe(t *testing.T) { t.Fatal(err) } defer conn.Close() - if !conn.unsafe { - t.Fatal("precondition: conn from db.Unsafe() should be unsafe") - } stmt, err := conn.PreparexContext(ctx, conn.Rebind("SELECT * FROM person WHERE first_name=?")) if err != nil { t.Fatal(err) } defer stmt.Close() - if stmt.unsafe != conn.unsafe { - t.Errorf("unsafe: Stmt.unsafe=%v, want %v (conn.unsafe)", stmt.unsafe, conn.unsafe) - } var partial struct { FirstName string `db:"first_name"` } @@ -38,6 +35,21 @@ func TestConnPreparexKeepsMapperAndUnsafe(t *testing.T) { t.Errorf("unsafe: Get into partial struct through conn-prepared Stmt: %v", err) } + // Control: the same statement on a safe Conn rejects the partial struct. + safeConn, err := db.Connx(ctx) + if err != nil { + t.Fatal(err) + } + defer safeConn.Close() + safeStmt, err := safeConn.PreparexContext(ctx, safeConn.Rebind("SELECT * FROM person WHERE first_name=?")) + if err != nil { + t.Fatal(err) + } + defer safeStmt.Close() + if err := safeStmt.Get(&partial, "Jason"); err == nil { + t.Error("safe: Get into partial struct through a safe Conn's Stmt succeeded, want missing destination error") + } + conn2, err := db.Connx(ctx) if err != nil { t.Fatal(err) diff --git a/dbtest/go.mod b/dbtest/go.mod new file mode 100644 index 0000000..647f28a --- /dev/null +++ b/dbtest/go.mod @@ -0,0 +1,14 @@ +module github.com/secondwindoss/sqlx/dbtest + +go 1.24.0 + +require github.com/secondwindoss/sqlx v1.4.1 + +require ( + filippo.io/edwards25519 v1.2.0 // indirect + github.com/go-sql-driver/mysql v1.10.1 // indirect + github.com/lib/pq v1.12.3 // indirect + github.com/mattn/go-sqlite3 v1.14.52 // indirect +) + +replace github.com/secondwindoss/sqlx => ../ diff --git a/go.sum b/dbtest/go.sum similarity index 100% rename from go.sum rename to dbtest/go.sum diff --git a/dbtest/in_empty_test.go b/dbtest/in_empty_test.go new file mode 100644 index 0000000..83f1196 --- /dev/null +++ b/dbtest/in_empty_test.go @@ -0,0 +1,35 @@ +package dbtest + +import ( + "testing" + + . "github.com/secondwindoss/sqlx" +) + +func TestInEmptySliceAgainstDatabase(t *testing.T) { + RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { + loadDefaultFixture(db, t) + for _, tt := range []struct { + query string + args []interface{} + }{ + {"SELECT * FROM place WHERE telcode IN (?)", []interface{}{[]int{}}}, + {"SELECT * FROM place WHERE country = ? AND telcode IN (?)", []interface{}{"Hong Kong", []int{}}}, + {"SELECT * FROM place WHERE telcode IN (?) AND country = ?", []interface{}{[]int{}, "Hong Kong"}}, + // NOT IN (NULL) matches no row either; this documents the caveat. + {"SELECT * FROM place WHERE telcode NOT IN (?)", []interface{}{[]int{}}}, + } { + q, args, err := In(tt.query, tt.args...) + if err != nil { + t.Fatalf("%s: %v", tt.query, err) + } + var places []Place + if err := db.Select(&places, db.Rebind(q), args...); err != nil { + t.Fatalf("%s: %v", tt.query, err) + } + if len(places) != 0 { + t.Errorf("%s: got %d rows, want none", tt.query, len(places)) + } + } + }) +} diff --git a/multi_result_set_test.go b/dbtest/multi_result_set_test.go similarity index 96% rename from multi_result_set_test.go rename to dbtest/multi_result_set_test.go index efeb31e..27edf4a 100644 --- a/multi_result_set_test.go +++ b/dbtest/multi_result_set_test.go @@ -1,6 +1,10 @@ -package sqlx +package dbtest -import "testing" +import ( + "testing" + + . "github.com/secondwindoss/sqlx" +) // Scanning a second result set with a different column layout must not reuse // the first set's field cache (upstream #380, #857; test from upstream #747). diff --git a/named_cast_test.go b/dbtest/named_cast_test.go similarity index 93% rename from named_cast_test.go rename to dbtest/named_cast_test.go index d37798a..5f13c72 100644 --- a/named_cast_test.go +++ b/dbtest/named_cast_test.go @@ -1,6 +1,10 @@ -package sqlx +package dbtest -import "testing" +import ( + "testing" + + . "github.com/secondwindoss/sqlx" +) // A Postgres cast attached to a named parameter binds the parameter and keeps // the cast (upstream #983); a cast elsewhere in the query is left alone diff --git a/named_context_test.go b/dbtest/named_context_test.go similarity index 98% rename from named_context_test.go rename to dbtest/named_context_test.go index 03f933d..fa620fc 100644 --- a/named_context_test.go +++ b/dbtest/named_context_test.go @@ -1,12 +1,14 @@ //go:build go1.8 // +build go1.8 -package sqlx +package dbtest import ( "context" "database/sql" "testing" + + . "github.com/secondwindoss/sqlx" ) func TestNamedContextQueries(t *testing.T) { diff --git a/named_ptr_test.go b/dbtest/named_ptr_test.go similarity index 94% rename from named_ptr_test.go rename to dbtest/named_ptr_test.go index 49666e0..2ce9a4d 100644 --- a/named_ptr_test.go +++ b/dbtest/named_ptr_test.go @@ -1,6 +1,10 @@ -package sqlx +package dbtest -import "testing" +import ( + "testing" + + . "github.com/secondwindoss/sqlx" +) // Named queries accept a pointer to a slice, to a map and to a slice of maps // (upstream #577, #729). diff --git a/dbtest/named_test.go b/dbtest/named_test.go new file mode 100644 index 0000000..44127a1 --- /dev/null +++ b/dbtest/named_test.go @@ -0,0 +1,219 @@ +package dbtest + +import ( + "database/sql" + "fmt" + "strings" + "testing" + + . "github.com/secondwindoss/sqlx" +) + +type Test struct { + t *testing.T +} + +func (t Test) Error(err error, msg ...interface{}) { + t.t.Helper() + if err != nil { + if len(msg) == 0 { + t.t.Error(err) + } else { + t.t.Error(msg...) + } + } +} + +func (t Test) Errorf(err error, format string, args ...interface{}) { + t.t.Helper() + if err != nil { + t.t.Errorf(format, args...) + } +} + +func TestNamedQueries(t *testing.T) { + RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { + loadDefaultFixture(db, t) + test := Test{t} + var ns *NamedStmt + var err error + + // Check that invalid preparations fail + _, err = db.PrepareNamed("SELECT * FROM person WHERE first_name=:first:name") + if err == nil { + t.Error("Expected an error with invalid prepared statement.") + } + + _, err = db.PrepareNamed("invalid sql") + if err == nil { + t.Error("Expected an error with invalid prepared statement.") + } + + // Check closing works as anticipated + ns, err = db.PrepareNamed("SELECT * FROM person WHERE first_name=:first_name") + test.Error(err) + err = ns.Close() + test.Error(err) + + ns, err = db.PrepareNamed(` + SELECT first_name, last_name, email + FROM person WHERE first_name=:first_name AND email=:email`) + test.Error(err) + + // test Queryx w/ uses Query + p := Person{FirstName: "Jason", LastName: "Moiron", Email: "jmoiron@jmoiron.net"} + + rows, err := ns.Queryx(p) + test.Error(err) + for rows.Next() { + var p2 Person + rows.StructScan(&p2) + if p.FirstName != p2.FirstName { + t.Errorf("got %s, expected %s", p.FirstName, p2.FirstName) + } + if p.LastName != p2.LastName { + t.Errorf("got %s, expected %s", p.LastName, p2.LastName) + } + if p.Email != p2.Email { + t.Errorf("got %s, expected %s", p.Email, p2.Email) + } + } + + // test Select + people := make([]Person, 0, 5) + err = ns.Select(&people, p) + test.Error(err) + + if len(people) != 1 { + t.Errorf("got %d results, expected %d", len(people), 1) + } + if p.FirstName != people[0].FirstName { + t.Errorf("got %s, expected %s", p.FirstName, people[0].FirstName) + } + if p.LastName != people[0].LastName { + t.Errorf("got %s, expected %s", p.LastName, people[0].LastName) + } + if p.Email != people[0].Email { + t.Errorf("got %s, expected %s", p.Email, people[0].Email) + } + + // test struct batch inserts + sls := []Person{ + {FirstName: "Ardie", LastName: "Savea", Email: "asavea@ab.co.nz"}, + {FirstName: "Sonny Bill", LastName: "Williams", Email: "sbw@ab.co.nz"}, + {FirstName: "Ngani", LastName: "Laumape", Email: "nlaumape@ab.co.nz"}, + } + + insert := fmt.Sprintf( + "INSERT INTO person (first_name, last_name, email, added_at) VALUES (:first_name, :last_name, :email, %v)\n", + now, + ) + _, err = db.NamedExec(insert, sls) + test.Error(err) + + // and test ptr batch inserts + _, err = db.NamedExec(insert, &sls) + test.Error(err) + + // test map batch inserts + slsMap := []map[string]interface{}{ + {"first_name": "Ardie", "last_name": "Savea", "email": "asavea@ab.co.nz"}, + {"first_name": "Sonny Bill", "last_name": "Williams", "email": "sbw@ab.co.nz"}, + {"first_name": "Ngani", "last_name": "Laumape", "email": "nlaumape@ab.co.nz"}, + } + + _, err = db.NamedExec(`INSERT INTO person (first_name, last_name, email) + VALUES (:first_name, :last_name, :email) ;--`, slsMap) + test.Error(err) + + type A map[string]interface{} + + typedMap := []A{ + {"first_name": "Ardie", "last_name": "Savea", "email": "asavea@ab.co.nz"}, + {"first_name": "Sonny Bill", "last_name": "Williams", "email": "sbw@ab.co.nz"}, + {"first_name": "Ngani", "last_name": "Laumape", "email": "nlaumape@ab.co.nz"}, + } + + _, err = db.NamedExec(`INSERT INTO person (first_name, last_name, email) + VALUES (:first_name, :last_name, :email) ;--`, typedMap) + test.Error(err) + + for _, p := range sls { + dest := Person{} + err = db.Get(&dest, db.Rebind("SELECT * FROM person WHERE email=?"), p.Email) + test.Error(err) + if dest.Email != p.Email { + t.Errorf("expected %s, got %s", p.Email, dest.Email) + } + } + + // test Exec + ns, err = db.PrepareNamed(` + INSERT INTO person (first_name, last_name, email) + VALUES (:first_name, :last_name, :email)`) + test.Error(err) + + js := Person{ + FirstName: "Julien", + LastName: "Savea", + Email: "jsavea@ab.co.nz", + } + _, err = ns.Exec(js) + test.Error(err) + + // Make sure we can pull him out again + p2 := Person{} + db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), js.Email) + if p2.Email != js.Email { + t.Errorf("expected %s, got %s", js.Email, p2.Email) + } + + // test Txn NamedStmts + tx := db.MustBegin() + txns := tx.NamedStmt(ns) + + // We're going to add Steven in this txn + sl := Person{ + FirstName: "Steven", + LastName: "Luatua", + Email: "sluatua@ab.co.nz", + } + + _, err = txns.Exec(sl) + test.Error(err) + // then rollback... + tx.Rollback() + // looking for Steven after a rollback should fail + err = db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), sl.Email) + if err != sql.ErrNoRows { + t.Errorf("expected no rows error, got %v", err) + } + + // now do the same, but commit + tx = db.MustBegin() + txns = tx.NamedStmt(ns) + _, err = txns.Exec(sl) + test.Error(err) + tx.Commit() + + // looking for Steven after a Commit should succeed + err = db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), sl.Email) + test.Error(err) + if p2.Email != sl.Email { + t.Errorf("expected %s, got %s", sl.Email, p2.Email) + } + + // a colon inside a comment is not a parameter (upstream #394, #894) + q, args, err := db.BindNamed("SELECT * FROM person WHERE id=:id /*TODO: handle accidental colon in named query*/", struct { + ID int `db:"id"` + }{1}) + if err != nil || len(args) != 1 || !strings.Contains(q, "/*TODO: handle accidental colon in named query*/") { + t.Errorf("colon inside a comment with a struct arg: q=%q args=%v err=%v", q, args, err) + } + + q, args, err = db.BindNamed("SELECT * FROM person WHERE id=:id /*TODO: handle accidental colon in named query*/", map[string]interface{}{"id": 1}) + if err != nil || len(args) != 1 || !strings.Contains(q, "/*TODO: handle accidental colon in named query*/") { + t.Errorf("colon inside a comment with a map arg: q=%q args=%v err=%v", q, args, err) + } + }) +} diff --git a/sqlx_context_test.go b/dbtest/sqlx_context_test.go similarity index 97% rename from sqlx_context_test.go rename to dbtest/sqlx_context_test.go index 1e922c9..a164e31 100644 --- a/sqlx_context_test.go +++ b/dbtest/sqlx_context_test.go @@ -10,7 +10,7 @@ // Set any of these variables to 'skip' to skip them. Note that for MySQL, // the string '?parseTime=True' will be appended to the DSN if it's not there // already. -package sqlx +package dbtest import ( "context" @@ -22,6 +22,8 @@ import ( "testing" "time" + . "github.com/secondwindoss/sqlx" + _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" @@ -125,6 +127,7 @@ func TestMissingNamesContextContext(t *testing.T) { rows.Close() // now try various things with unsafe set. + safeDB := db db = db.Unsafe() pps = []PersonPlus{} err = db.SelectContext(ctx, &pps, "SELECT * FROM person") @@ -153,16 +156,14 @@ func TestMissingNamesContextContext(t *testing.T) { rowsx.Close() // test Named stmt - if !isUnsafe(db) { - t.Error("Expected db to be unsafe, but it isn't") - } nstmt, err := db.PrepareNamedContext(ctx, `SELECT * FROM person WHERE first_name != :name`) if err != nil { t.Fatal(err) } // its internal stmt should be marked unsafe - if !nstmt.Stmt.unsafe { - t.Error("expected NamedStmt to be unsafe but its underlying stmt did not inherit safety") + pp = PersonPlus{} + if err := nstmt.Stmt.GetContext(ctx, &pp, "Jason"); err != nil { + t.Errorf("expected NamedStmt to be unsafe but its underlying stmt did not inherit safety: %v", err) } pps = []PersonPlus{} err = nstmt.SelectContext(ctx, &pps, map[string]interface{}{"name": "Jason"}) @@ -174,8 +175,9 @@ func TestMissingNamesContextContext(t *testing.T) { } // test it with a safe db - db.unsafe = false - if isUnsafe(db) { + db = safeDB + pp = PersonPlus{} + if err := db.GetContext(ctx, &pp, "SELECT * FROM person LIMIT 1"); err == nil { t.Error("expected db to be safe but it isn't") } nstmt, err = db.PrepareNamedContext(ctx, `SELECT * FROM person WHERE first_name != :name`) @@ -183,13 +185,12 @@ func TestMissingNamesContextContext(t *testing.T) { t.Fatal(err) } // it should be safe - if isUnsafe(nstmt) { + pps = []PersonPlus{} + if err := nstmt.SelectContext(ctx, &pps, map[string]interface{}{"name": "Jason"}); err == nil { t.Error("NamedStmt did not inherit safety") } + // and the Select below must succeed once it is made unsafe nstmt.Unsafe() - if !isUnsafe(nstmt) { - t.Error("expected newly unsafed NamedStmt to be unsafe") - } pps = []PersonPlus{} err = nstmt.SelectContext(ctx, &pps, map[string]interface{}{"name": "Jason"}) if err != nil { @@ -725,9 +726,6 @@ func TestScanErrorContext(t *testing.T) { }) } -// FIXME: this function is kinda big but it slows things down to be constantly -// loading and reloading the schema.. - func TestUsageContext(t *testing.T) { RunWithSchemaContext(context.Background(), defaultSchema, t, func(ctx context.Context, db *DB, t *testing.T) { loadDefaultFixtureContext(ctx, db, t) @@ -1071,7 +1069,8 @@ func TestUsageContext(t *testing.T) { if err != nil { t.Error(err) } - err = scanAll(rows, &sdest, false) + rows.Close() + err = db.SelectContext(ctx, &sdest, "SELECT email FROM person ORDER BY email ASC;") if err != nil { t.Error(err) } @@ -1135,15 +1134,6 @@ func TestUsageContext(t *testing.T) { }) } -// tests that sqlx will not panic when the wrong driver is passed because -// of an automatic nil dereference in sqlx.Open(), which was fixed. -func TestDoNotPanicOnConnectContext(t *testing.T) { - _, err := ConnectContext(context.Background(), "bogus", "hehe") - if err == nil { - t.Errorf("Should return error when using bogus driverName") - } -} - func TestEmbeddedMapsContext(t *testing.T) { var schema = Schema{ create: ` diff --git a/sqlx_test.go b/dbtest/sqlx_test.go similarity index 81% rename from sqlx_test.go rename to dbtest/sqlx_test.go index 804439d..389fc36 100644 --- a/sqlx_test.go +++ b/dbtest/sqlx_test.go @@ -7,7 +7,7 @@ // Set any of these variables to 'skip' to skip them. Note that for MySQL, // the string '?parseTime=True' will be appended to the DSN if it's not there // already. -package sqlx +package dbtest import ( "database/sql" @@ -21,6 +21,8 @@ import ( "testing" "time" + . "github.com/secondwindoss/sqlx" + _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" @@ -28,18 +30,16 @@ import ( "github.com/secondwindoss/sqlx/reflectx" ) -/* compile time checks that Db, Tx, Stmt (qStmt) implement expected interfaces */ -var _, _ Ext = &DB{}, &Tx{} -var _, _ ColScanner = &Row{}, &Rows{} -var _ Queryer = &qStmt{} -var _ Execer = &qStmt{} - var TestPostgres = true + var TestSqlite = true + var TestMysql = true var sldb *DB + var pgdb *DB + var mysqldb *DB func init() { @@ -300,6 +300,7 @@ func TestMissingNames(t *testing.T) { rows.Close() // now try various things with unsafe set. + safeDB := db db = db.Unsafe() pps = []PersonPlus{} err = db.Select(&pps, "SELECT * FROM person") @@ -328,16 +329,14 @@ func TestMissingNames(t *testing.T) { rowsx.Close() // test Named stmt - if !isUnsafe(db) { - t.Error("Expected db to be unsafe, but it isn't") - } nstmt, err := db.PrepareNamed(`SELECT * FROM person WHERE first_name != :name`) if err != nil { t.Fatal(err) } // its internal stmt should be marked unsafe - if !nstmt.Stmt.unsafe { - t.Error("expected NamedStmt to be unsafe but its underlying stmt did not inherit safety") + pp = PersonPlus{} + if err := nstmt.Stmt.Get(&pp, "Jason"); err != nil { + t.Errorf("expected NamedStmt to be unsafe but its underlying stmt did not inherit safety: %v", err) } pps = []PersonPlus{} err = nstmt.Select(&pps, map[string]interface{}{"name": "Jason"}) @@ -349,8 +348,9 @@ func TestMissingNames(t *testing.T) { } // test it with a safe db - db.unsafe = false - if isUnsafe(db) { + db = safeDB + pp = PersonPlus{} + if err := db.Get(&pp, "SELECT * FROM person LIMIT 1"); err == nil { t.Error("expected db to be safe but it isn't") } nstmt, err = db.PrepareNamed(`SELECT * FROM person WHERE first_name != :name`) @@ -358,13 +358,12 @@ func TestMissingNames(t *testing.T) { t.Fatal(err) } // it should be safe - if isUnsafe(nstmt) { + pps = []PersonPlus{} + if err := nstmt.Select(&pps, map[string]interface{}{"name": "Jason"}); err == nil { t.Error("NamedStmt did not inherit safety") } + // and the Select below must succeed once it is made unsafe nstmt.Unsafe() - if !isUnsafe(nstmt) { - t.Error("expected newly unsafed NamedStmt to be unsafe") - } pps = []PersonPlus{} err = nstmt.Select(&pps, map[string]interface{}{"name": "Jason"}) if err != nil { @@ -908,9 +907,6 @@ func TestMultiInsert(t *testing.T) { }) } -// FIXME: this function is kinda big but it slows things down to be constantly -// loading and reloading the schema.. - func TestUsage(t *testing.T) { RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { loadDefaultFixture(db, t) @@ -1235,7 +1231,8 @@ func TestUsage(t *testing.T) { if err != nil { t.Error(err) } - err = scanAll(rows, &sdest, false) + rows.Close() + err = db.Select(&sdest, "SELECT email FROM person ORDER BY email ASC;") if err != nil { t.Error(err) } @@ -1303,94 +1300,6 @@ type Product struct { ProductID int } -// tests that sqlx will not panic when the wrong driver is passed because -// of an automatic nil dereference in sqlx.Open(), which was fixed. -func TestDoNotPanicOnConnect(t *testing.T) { - db, err := Connect("bogus", "hehe") - if err == nil { - t.Errorf("Should return error when using bogus driverName") - } - if db != nil { - t.Errorf("Should not return the db on a connect failure") - } -} - -func TestRebind(t *testing.T) { - q1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - q2 := `INSERT INTO foo (a, b, c) VALUES (?, ?, "foo"), ("Hi", ?, ?)` - - s1 := Rebind(DOLLAR, q1) - s2 := Rebind(DOLLAR, q2) - - if s1 != `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` { - t.Errorf("q1 failed") - } - - if s2 != `INSERT INTO foo (a, b, c) VALUES ($1, $2, "foo"), ("Hi", $3, $4)` { - t.Errorf("q2 failed") - } - - s1 = Rebind(AT, q1) - s2 = Rebind(AT, q2) - - if s1 != `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10)` { - t.Errorf("q1 failed") - } - - if s2 != `INSERT INTO foo (a, b, c) VALUES (@p1, @p2, "foo"), ("Hi", @p3, @p4)` { - t.Errorf("q2 failed") - } - - s1 = Rebind(NAMED, q1) - s2 = Rebind(NAMED, q2) - - ex1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES ` + - `(:arg1, :arg2, :arg3, :arg4, :arg5, :arg6, :arg7, :arg8, :arg9, :arg10)` - if s1 != ex1 { - t.Error("q1 failed on Named params") - } - - ex2 := `INSERT INTO foo (a, b, c) VALUES (:arg1, :arg2, "foo"), ("Hi", :arg3, :arg4)` - if s2 != ex2 { - t.Error("q2 failed on Named params") - } -} - -func TestBindMap(t *testing.T) { - // Test that it works.. - q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)` - am := map[string]interface{}{ - "name": "Jason Moiron", - "age": 30, - "first": "Jason", - "last": "Moiron", - } - - bq, args, _ := bindMap(QUESTION, q1, am) - expect := `INSERT INTO foo (a, b, c, d) VALUES (?, ?, ?, ?)` - if bq != expect { - t.Errorf("Interpolation of query failed: got `%v`, expected `%v`\n", bq, expect) - } - - if args[0].(string) != "Jason Moiron" { - t.Errorf("Expected `Jason Moiron`, got %v\n", args[0]) - } - - if args[1].(int) != 30 { - t.Errorf("Expected 30, got %v\n", args[1]) - } - - if args[2].(string) != "Jason" { - t.Errorf("Expected Jason, got %v\n", args[2]) - } - - if args[3].(string) != "Moiron" { - t.Errorf("Expected Moiron, got %v\n", args[3]) - } -} - -// Test for #117, embedded nil maps - type Message struct { Text string `db:"string"` Properties PropertyMap `db:"properties"` // Stored as JSON in the database @@ -1626,92 +1535,6 @@ func TestIn(t *testing.T) { }) } -func TestBindStruct(t *testing.T) { - var err error - - q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)` - - type tt struct { - Name string - Age int - First string - Last string - } - - type tt2 struct { - Field1 string `db:"field_1"` - Field2 string `db:"field_2"` - } - - type tt3 struct { - tt2 - Name string - } - - am := tt{"Jason Moiron", 30, "Jason", "Moiron"} - - bq, args, _ := bindStruct(QUESTION, q1, am, mapper()) - expect := `INSERT INTO foo (a, b, c, d) VALUES (?, ?, ?, ?)` - if bq != expect { - t.Errorf("Interpolation of query failed: got `%v`, expected `%v`\n", bq, expect) - } - - if args[0].(string) != "Jason Moiron" { - t.Errorf("Expected `Jason Moiron`, got %v\n", args[0]) - } - - if args[1].(int) != 30 { - t.Errorf("Expected 30, got %v\n", args[1]) - } - - if args[2].(string) != "Jason" { - t.Errorf("Expected Jason, got %v\n", args[2]) - } - - if args[3].(string) != "Moiron" { - t.Errorf("Expected Moiron, got %v\n", args[3]) - } - - am2 := tt2{"Hello", "World"} - bq, args, _ = bindStruct(QUESTION, "INSERT INTO foo (a, b) VALUES (:field_2, :field_1)", am2, mapper()) - expect = `INSERT INTO foo (a, b) VALUES (?, ?)` - if bq != expect { - t.Errorf("Interpolation of query failed: got `%v`, expected `%v`\n", bq, expect) - } - - if args[0].(string) != "World" { - t.Errorf("Expected 'World', got %s\n", args[0].(string)) - } - if args[1].(string) != "Hello" { - t.Errorf("Expected 'Hello', got %s\n", args[1].(string)) - } - - am3 := tt3{Name: "Hello!"} - am3.Field1 = "Hello" - am3.Field2 = "World" - - bq, args, err = bindStruct(QUESTION, "INSERT INTO foo (a, b, c) VALUES (:name, :field_1, :field_2)", am3, mapper()) - - if err != nil { - t.Fatal(err) - } - - expect = `INSERT INTO foo (a, b, c) VALUES (?, ?, ?)` - if bq != expect { - t.Errorf("Interpolation of query failed: got `%v`, expected `%v`\n", bq, expect) - } - - if args[0].(string) != "Hello!" { - t.Errorf("Expected 'Hello!', got %s\n", args[0].(string)) - } - if args[1].(string) != "Hello" { - t.Errorf("Expected 'Hello', got %s\n", args[1].(string)) - } - if args[2].(string) != "World" { - t.Errorf("Expected 'World', got %s\n", args[0].(string)) - } -} - func TestEmbeddedLiterals(t *testing.T) { var schema = Schema{ create: ` @@ -1754,166 +1577,6 @@ func TestEmbeddedLiterals(t *testing.T) { }) } -func BenchmarkBindStruct(b *testing.B) { - b.StopTimer() - q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)` - type t struct { - Name string - Age int - First string - Last string - } - am := t{"Jason Moiron", 30, "Jason", "Moiron"} - b.StartTimer() - for i := 0; i < b.N; i++ { - bindStruct(DOLLAR, q1, am, mapper()) - } -} - -func TestBindNamedMapper(t *testing.T) { - type A map[string]interface{} - m := reflectx.NewMapperFunc("db", NameMapper) - query, args, err := bindNamedMapper(DOLLAR, `select :x`, A{ - "x": "X!", - }, m) - if err != nil { - t.Fatal(err) - } - - got := fmt.Sprintf("%s %s", query, args) - want := `select $1 [X!]` - if got != want { - t.Errorf("\ngot: %q\nwant: %q", got, want) - } - - _, _, err = bindNamedMapper(DOLLAR, `select :x`, map[string]string{ - "x": "X!", - }, m) - if err == nil { - t.Fatal("err is nil") - } - if !strings.Contains(err.Error(), "unsupported map type") { - t.Errorf("wrong error: %s", err) - } -} - -func BenchmarkBindMap(b *testing.B) { - b.StopTimer() - q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)` - am := map[string]interface{}{ - "name": "Jason Moiron", - "age": 30, - "first": "Jason", - "last": "Moiron", - } - b.StartTimer() - for i := 0; i < b.N; i++ { - bindMap(DOLLAR, q1, am) - } -} - -func BenchmarkIn(b *testing.B) { - q := `SELECT * FROM foo WHERE x = ? AND v in (?) AND y = ?` - - for i := 0; i < b.N; i++ { - _, _, _ = In(q, []interface{}{"foo", []int{0, 5, 7, 2, 9}, "bar"}...) - } -} - -func BenchmarkIn1k(b *testing.B) { - q := `SELECT * FROM foo WHERE x = ? AND v in (?) AND y = ?` - - var vals [1000]interface{} - - for i := 0; i < b.N; i++ { - _, _, _ = In(q, []interface{}{"foo", vals[:], "bar"}...) - } -} - -func BenchmarkIn1kInt(b *testing.B) { - q := `SELECT * FROM foo WHERE x = ? AND v in (?) AND y = ?` - - var vals [1000]int - - for i := 0; i < b.N; i++ { - _, _, _ = In(q, []interface{}{"foo", vals[:], "bar"}...) - } -} - -func BenchmarkIn1kString(b *testing.B) { - q := `SELECT * FROM foo WHERE x = ? AND v in (?) AND y = ?` - - var vals [1000]string - - for i := 0; i < b.N; i++ { - _, _, _ = In(q, []interface{}{"foo", vals[:], "bar"}...) - } -} - -func BenchmarkRebind(b *testing.B) { - b.StopTimer() - q1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` - q2 := `INSERT INTO foo (a, b, c) VALUES (?, ?, "foo"), ("Hi", ?, ?)` - b.StartTimer() - - for i := 0; i < b.N; i++ { - Rebind(DOLLAR, q1) - Rebind(DOLLAR, q2) - } -} - -func BenchmarkRebindBuffer(b *testing.B) { - b.StopTimer() - q1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` - q2 := `INSERT INTO foo (a, b, c) VALUES (?, ?, "foo"), ("Hi", ?, ?)` - b.StartTimer() - - for i := 0; i < b.N; i++ { - rebindBuff(DOLLAR, q1) - rebindBuff(DOLLAR, q2) - } -} - -func TestIn130Regression(t *testing.T) { - t.Run("[]interface{}{}", func(t *testing.T) { - q, args, err := In("SELECT * FROM people WHERE name IN (?)", []interface{}{[]string{"gopher"}}...) - if err != nil { - t.Fatal(err) - } - if q != "SELECT * FROM people WHERE name IN (?)" { - t.Errorf("got=%v", q) - } - t.Log(args) - for _, a := range args { - switch a := a.(type) { - case string: - t.Log("ok: string", a) - case *string: - t.Error("ng: string pointer", a, *a) - } - } - }) - - t.Run("[]string{}", func(t *testing.T) { - q, args, err := In("SELECT * FROM people WHERE name IN (?)", []string{"gopher"}) - if err != nil { - t.Fatal(err) - } - if q != "SELECT * FROM people WHERE name IN (?)" { - t.Errorf("got=%v", q) - } - t.Log(args) - for _, a := range args { - switch a := a.(type) { - case string: - t.Log("ok: string", a) - case *string: - t.Error("ng: string pointer", a, *a) - } - } - }) -} - func TestSelectReset(t *testing.T) { RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { loadDefaultFixture(db, t) diff --git a/stmt_query_row_test.go b/dbtest/stmt_query_row_test.go similarity index 95% rename from stmt_query_row_test.go rename to dbtest/stmt_query_row_test.go index df36e4a..c87a0a7 100644 --- a/stmt_query_row_test.go +++ b/dbtest/stmt_query_row_test.go @@ -1,8 +1,10 @@ -package sqlx +package dbtest import ( "context" "testing" + + . "github.com/secondwindoss/sqlx" ) // *Stmt promotes (*sql.Stmt).QueryRowContext(ctx, args...): the first diff --git a/tx_stmtx_test.go b/dbtest/tx_stmtx_test.go similarity index 66% rename from tx_stmtx_test.go rename to dbtest/tx_stmtx_test.go index ed62a18..338df0e 100644 --- a/tx_stmtx_test.go +++ b/dbtest/tx_stmtx_test.go @@ -1,12 +1,16 @@ -package sqlx +package dbtest import ( "context" "testing" + + . "github.com/secondwindoss/sqlx" ) // Tx.Stmtx and Tx.StmtxContext keep the transaction's unsafe flag, and keep -// the wrapped statement's own unsafe flag (upstream #979). +// the wrapped statement's own unsafe flag (upstream #979). The flag is checked +// through its effect: an unsafe Stmt scans into a struct that lacks some of the +// selected columns, a safe one returns a missing destination error. func TestTxStmtxKeepsUnsafe(t *testing.T) { RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { loadDefaultFixture(db, t) @@ -20,26 +24,20 @@ func TestTxStmtxKeepsUnsafe(t *testing.T) { var partial struct { FirstName string `db:"first_name"` } + if err := stmt.Get(&partial, "Jason"); err == nil { + t.Fatal("precondition: Get into partial struct through a safe Stmt succeeded") + } tx, err := db.Unsafe().Beginx() if err != nil { t.Fatal(err) } defer tx.Rollback() - if !tx.unsafe { - t.Fatal("precondition: tx from db.Unsafe() should be unsafe") - } ts := tx.Stmtx(stmt) - if !ts.unsafe { - t.Error("Stmtx: unsafe Tx returned a safe Stmt") - } if err := ts.Get(&partial, "Jason"); err != nil { t.Errorf("Stmtx: Get into partial struct: %v", err) } tsc := tx.StmtxContext(ctx, stmt) - if !tsc.unsafe { - t.Error("StmtxContext: unsafe Tx returned a safe Stmt") - } if err := tsc.GetContext(ctx, &partial, "Jason"); err != nil { t.Errorf("StmtxContext: Get into partial struct: %v", err) } @@ -51,14 +49,11 @@ func TestTxStmtxKeepsUnsafe(t *testing.T) { } defer safeTx.Rollback() us := safeTx.Stmtx(stmt.Unsafe()) - if !us.unsafe { - t.Error("Stmtx: unsafe Stmt wrapped in a safe Tx became safe") - } if err := us.Get(&partial, "Jason"); err != nil { t.Errorf("Stmtx: Get through unsafe Stmt in safe Tx: %v", err) } - if ss := safeTx.StmtxContext(ctx, stmt); ss.unsafe { - t.Error("StmtxContext: safe Stmt in safe Tx became unsafe") + if err := safeTx.StmtxContext(ctx, stmt).GetContext(ctx, &partial, "Jason"); err == nil { + t.Error("StmtxContext: safe Stmt in safe Tx became unsafe (Get into partial struct succeeded)") } }) } diff --git a/go.mod b/go.mod index 5f98319..a7c5b60 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,3 @@ module github.com/secondwindoss/sqlx go 1.24.0 - -require ( - github.com/go-sql-driver/mysql v1.10.1 - github.com/lib/pq v1.12.3 - github.com/mattn/go-sqlite3 v1.14.52 -) - -require filippo.io/edwards25519 v1.2.0 // indirect diff --git a/in_empty_test.go b/in_empty_test.go index 06771da..db6b2da 100644 --- a/in_empty_test.go +++ b/in_empty_test.go @@ -61,31 +61,3 @@ func TestInEmptySlice(t *testing.T) { }) } } - -func TestInEmptySliceAgainstDatabase(t *testing.T) { - RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { - loadDefaultFixture(db, t) - for _, tt := range []struct { - query string - args []interface{} - }{ - {"SELECT * FROM place WHERE telcode IN (?)", []interface{}{[]int{}}}, - {"SELECT * FROM place WHERE country = ? AND telcode IN (?)", []interface{}{"Hong Kong", []int{}}}, - {"SELECT * FROM place WHERE telcode IN (?) AND country = ?", []interface{}{[]int{}, "Hong Kong"}}, - // NOT IN (NULL) matches no row either; this documents the caveat. - {"SELECT * FROM place WHERE telcode NOT IN (?)", []interface{}{[]int{}}}, - } { - q, args, err := In(tt.query, tt.args...) - if err != nil { - t.Fatalf("%s: %v", tt.query, err) - } - var places []Place - if err := db.Select(&places, db.Rebind(q), args...); err != nil { - t.Fatalf("%s: %v", tt.query, err) - } - if len(places) != 0 { - t.Errorf("%s: got %d rows, want none", tt.query, len(places)) - } - } - }) -} diff --git a/named_test.go b/named_test.go index 2504e99..12a24cd 100644 --- a/named_test.go +++ b/named_test.go @@ -1,9 +1,6 @@ package sqlx import ( - "database/sql" - "fmt" - "strings" "testing" ) @@ -99,28 +96,6 @@ func TestCompileQuery(t *testing.T) { } } -type Test struct { - t *testing.T -} - -func (t Test) Error(err error, msg ...interface{}) { - t.t.Helper() - if err != nil { - if len(msg) == 0 { - t.t.Error(err) - } else { - t.t.Error(msg...) - } - } -} - -func (t Test) Errorf(err error, format string, args ...interface{}) { - t.t.Helper() - if err != nil { - t.t.Errorf(format, args...) - } -} - func TestEscapedColons(t *testing.T) { var qs = `SELECT * FROM testtable WHERE timeposted BETWEEN (now() AT TIME ZONE 'utc') AND (now() AT TIME ZONE 'utc') - interval '01:30:00') AND name = '\'this is a test\'' and id = :id` @@ -130,193 +105,6 @@ func TestEscapedColons(t *testing.T) { } } -func TestNamedQueries(t *testing.T) { - RunWithSchema(defaultSchema, t, func(db *DB, t *testing.T, now string) { - loadDefaultFixture(db, t) - test := Test{t} - var ns *NamedStmt - var err error - - // Check that invalid preparations fail - _, err = db.PrepareNamed("SELECT * FROM person WHERE first_name=:first:name") - if err == nil { - t.Error("Expected an error with invalid prepared statement.") - } - - _, err = db.PrepareNamed("invalid sql") - if err == nil { - t.Error("Expected an error with invalid prepared statement.") - } - - // Check closing works as anticipated - ns, err = db.PrepareNamed("SELECT * FROM person WHERE first_name=:first_name") - test.Error(err) - err = ns.Close() - test.Error(err) - - ns, err = db.PrepareNamed(` - SELECT first_name, last_name, email - FROM person WHERE first_name=:first_name AND email=:email`) - test.Error(err) - - // test Queryx w/ uses Query - p := Person{FirstName: "Jason", LastName: "Moiron", Email: "jmoiron@jmoiron.net"} - - rows, err := ns.Queryx(p) - test.Error(err) - for rows.Next() { - var p2 Person - rows.StructScan(&p2) - if p.FirstName != p2.FirstName { - t.Errorf("got %s, expected %s", p.FirstName, p2.FirstName) - } - if p.LastName != p2.LastName { - t.Errorf("got %s, expected %s", p.LastName, p2.LastName) - } - if p.Email != p2.Email { - t.Errorf("got %s, expected %s", p.Email, p2.Email) - } - } - - // test Select - people := make([]Person, 0, 5) - err = ns.Select(&people, p) - test.Error(err) - - if len(people) != 1 { - t.Errorf("got %d results, expected %d", len(people), 1) - } - if p.FirstName != people[0].FirstName { - t.Errorf("got %s, expected %s", p.FirstName, people[0].FirstName) - } - if p.LastName != people[0].LastName { - t.Errorf("got %s, expected %s", p.LastName, people[0].LastName) - } - if p.Email != people[0].Email { - t.Errorf("got %s, expected %s", p.Email, people[0].Email) - } - - // test struct batch inserts - sls := []Person{ - {FirstName: "Ardie", LastName: "Savea", Email: "asavea@ab.co.nz"}, - {FirstName: "Sonny Bill", LastName: "Williams", Email: "sbw@ab.co.nz"}, - {FirstName: "Ngani", LastName: "Laumape", Email: "nlaumape@ab.co.nz"}, - } - - insert := fmt.Sprintf( - "INSERT INTO person (first_name, last_name, email, added_at) VALUES (:first_name, :last_name, :email, %v)\n", - now, - ) - _, err = db.NamedExec(insert, sls) - test.Error(err) - - // and test ptr batch inserts - _, err = db.NamedExec(insert, &sls) - test.Error(err) - - // test map batch inserts - slsMap := []map[string]interface{}{ - {"first_name": "Ardie", "last_name": "Savea", "email": "asavea@ab.co.nz"}, - {"first_name": "Sonny Bill", "last_name": "Williams", "email": "sbw@ab.co.nz"}, - {"first_name": "Ngani", "last_name": "Laumape", "email": "nlaumape@ab.co.nz"}, - } - - _, err = db.NamedExec(`INSERT INTO person (first_name, last_name, email) - VALUES (:first_name, :last_name, :email) ;--`, slsMap) - test.Error(err) - - type A map[string]interface{} - - typedMap := []A{ - {"first_name": "Ardie", "last_name": "Savea", "email": "asavea@ab.co.nz"}, - {"first_name": "Sonny Bill", "last_name": "Williams", "email": "sbw@ab.co.nz"}, - {"first_name": "Ngani", "last_name": "Laumape", "email": "nlaumape@ab.co.nz"}, - } - - _, err = db.NamedExec(`INSERT INTO person (first_name, last_name, email) - VALUES (:first_name, :last_name, :email) ;--`, typedMap) - test.Error(err) - - for _, p := range sls { - dest := Person{} - err = db.Get(&dest, db.Rebind("SELECT * FROM person WHERE email=?"), p.Email) - test.Error(err) - if dest.Email != p.Email { - t.Errorf("expected %s, got %s", p.Email, dest.Email) - } - } - - // test Exec - ns, err = db.PrepareNamed(` - INSERT INTO person (first_name, last_name, email) - VALUES (:first_name, :last_name, :email)`) - test.Error(err) - - js := Person{ - FirstName: "Julien", - LastName: "Savea", - Email: "jsavea@ab.co.nz", - } - _, err = ns.Exec(js) - test.Error(err) - - // Make sure we can pull him out again - p2 := Person{} - db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), js.Email) - if p2.Email != js.Email { - t.Errorf("expected %s, got %s", js.Email, p2.Email) - } - - // test Txn NamedStmts - tx := db.MustBegin() - txns := tx.NamedStmt(ns) - - // We're going to add Steven in this txn - sl := Person{ - FirstName: "Steven", - LastName: "Luatua", - Email: "sluatua@ab.co.nz", - } - - _, err = txns.Exec(sl) - test.Error(err) - // then rollback... - tx.Rollback() - // looking for Steven after a rollback should fail - err = db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), sl.Email) - if err != sql.ErrNoRows { - t.Errorf("expected no rows error, got %v", err) - } - - // now do the same, but commit - tx = db.MustBegin() - txns = tx.NamedStmt(ns) - _, err = txns.Exec(sl) - test.Error(err) - tx.Commit() - - // looking for Steven after a Commit should succeed - err = db.Get(&p2, db.Rebind("SELECT * FROM person WHERE email=?"), sl.Email) - test.Error(err) - if p2.Email != sl.Email { - t.Errorf("expected %s, got %s", sl.Email, p2.Email) - } - - // a colon inside a comment is not a parameter (upstream #394, #894) - q, args, err := db.BindNamed("SELECT * FROM person WHERE id=:id /*TODO: handle accidental colon in named query*/", struct { - ID int `db:"id"` - }{1}) - if err != nil || len(args) != 1 || !strings.Contains(q, "/*TODO: handle accidental colon in named query*/") { - t.Errorf("colon inside a comment with a struct arg: q=%q args=%v err=%v", q, args, err) - } - - q, args, err = db.BindNamed("SELECT * FROM person WHERE id=:id /*TODO: handle accidental colon in named query*/", map[string]interface{}{"id": 1}) - if err != nil || len(args) != 1 || !strings.Contains(q, "/*TODO: handle accidental colon in named query*/") { - t.Errorf("colon inside a comment with a map arg: q=%q args=%v err=%v", q, args, err) - } - }) -} - func TestFixBounds(t *testing.T) { table := []struct { name, query, expect string diff --git a/sqlx_unit_test.go b/sqlx_unit_test.go new file mode 100644 index 0000000..681cd71 --- /dev/null +++ b/sqlx_unit_test.go @@ -0,0 +1,358 @@ +// Unit tests that need no database. The tests that run against Postgres, +// MySQL and SQLite live in the dbtest module, so the drivers stay out of +// this module's go.mod. +package sqlx + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/secondwindoss/sqlx/reflectx" +) + +/* compile time checks that Db, Tx, Stmt (qStmt) implement expected interfaces */ +var _, _ Ext = &DB{}, &Tx{} +var _, _ ColScanner = &Row{}, &Rows{} +var _ Queryer = &qStmt{} +var _ Execer = &qStmt{} + +// tests that sqlx will not panic when the wrong driver is passed because +// of an automatic nil dereference in sqlx.Open(), which was fixed. +func TestDoNotPanicOnConnect(t *testing.T) { + db, err := Connect("bogus", "hehe") + if err == nil { + t.Errorf("Should return error when using bogus driverName") + } + if db != nil { + t.Errorf("Should not return the db on a connect failure") + } +} + +func TestRebind(t *testing.T) { + q1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + q2 := `INSERT INTO foo (a, b, c) VALUES (?, ?, "foo"), ("Hi", ?, ?)` + + s1 := Rebind(DOLLAR, q1) + s2 := Rebind(DOLLAR, q2) + + if s1 != `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` { + t.Errorf("q1 failed") + } + + if s2 != `INSERT INTO foo (a, b, c) VALUES ($1, $2, "foo"), ("Hi", $3, $4)` { + t.Errorf("q2 failed") + } + + s1 = Rebind(AT, q1) + s2 = Rebind(AT, q2) + + if s1 != `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10)` { + t.Errorf("q1 failed") + } + + if s2 != `INSERT INTO foo (a, b, c) VALUES (@p1, @p2, "foo"), ("Hi", @p3, @p4)` { + t.Errorf("q2 failed") + } + + s1 = Rebind(NAMED, q1) + s2 = Rebind(NAMED, q2) + + ex1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES ` + + `(:arg1, :arg2, :arg3, :arg4, :arg5, :arg6, :arg7, :arg8, :arg9, :arg10)` + if s1 != ex1 { + t.Error("q1 failed on Named params") + } + + ex2 := `INSERT INTO foo (a, b, c) VALUES (:arg1, :arg2, "foo"), ("Hi", :arg3, :arg4)` + if s2 != ex2 { + t.Error("q2 failed on Named params") + } +} + +func TestBindMap(t *testing.T) { + // Test that it works.. + q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)` + am := map[string]interface{}{ + "name": "Jason Moiron", + "age": 30, + "first": "Jason", + "last": "Moiron", + } + + bq, args, _ := bindMap(QUESTION, q1, am) + expect := `INSERT INTO foo (a, b, c, d) VALUES (?, ?, ?, ?)` + if bq != expect { + t.Errorf("Interpolation of query failed: got `%v`, expected `%v`\n", bq, expect) + } + + if args[0].(string) != "Jason Moiron" { + t.Errorf("Expected `Jason Moiron`, got %v\n", args[0]) + } + + if args[1].(int) != 30 { + t.Errorf("Expected 30, got %v\n", args[1]) + } + + if args[2].(string) != "Jason" { + t.Errorf("Expected Jason, got %v\n", args[2]) + } + + if args[3].(string) != "Moiron" { + t.Errorf("Expected Moiron, got %v\n", args[3]) + } +} + +func TestBindStruct(t *testing.T) { + var err error + + q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)` + + type tt struct { + Name string + Age int + First string + Last string + } + + type tt2 struct { + Field1 string `db:"field_1"` + Field2 string `db:"field_2"` + } + + type tt3 struct { + tt2 + Name string + } + + am := tt{"Jason Moiron", 30, "Jason", "Moiron"} + + bq, args, _ := bindStruct(QUESTION, q1, am, mapper()) + expect := `INSERT INTO foo (a, b, c, d) VALUES (?, ?, ?, ?)` + if bq != expect { + t.Errorf("Interpolation of query failed: got `%v`, expected `%v`\n", bq, expect) + } + + if args[0].(string) != "Jason Moiron" { + t.Errorf("Expected `Jason Moiron`, got %v\n", args[0]) + } + + if args[1].(int) != 30 { + t.Errorf("Expected 30, got %v\n", args[1]) + } + + if args[2].(string) != "Jason" { + t.Errorf("Expected Jason, got %v\n", args[2]) + } + + if args[3].(string) != "Moiron" { + t.Errorf("Expected Moiron, got %v\n", args[3]) + } + + am2 := tt2{"Hello", "World"} + bq, args, _ = bindStruct(QUESTION, "INSERT INTO foo (a, b) VALUES (:field_2, :field_1)", am2, mapper()) + expect = `INSERT INTO foo (a, b) VALUES (?, ?)` + if bq != expect { + t.Errorf("Interpolation of query failed: got `%v`, expected `%v`\n", bq, expect) + } + + if args[0].(string) != "World" { + t.Errorf("Expected 'World', got %s\n", args[0].(string)) + } + if args[1].(string) != "Hello" { + t.Errorf("Expected 'Hello', got %s\n", args[1].(string)) + } + + am3 := tt3{Name: "Hello!"} + am3.Field1 = "Hello" + am3.Field2 = "World" + + bq, args, err = bindStruct(QUESTION, "INSERT INTO foo (a, b, c) VALUES (:name, :field_1, :field_2)", am3, mapper()) + + if err != nil { + t.Fatal(err) + } + + expect = `INSERT INTO foo (a, b, c) VALUES (?, ?, ?)` + if bq != expect { + t.Errorf("Interpolation of query failed: got `%v`, expected `%v`\n", bq, expect) + } + + if args[0].(string) != "Hello!" { + t.Errorf("Expected 'Hello!', got %s\n", args[0].(string)) + } + if args[1].(string) != "Hello" { + t.Errorf("Expected 'Hello', got %s\n", args[1].(string)) + } + if args[2].(string) != "World" { + t.Errorf("Expected 'World', got %s\n", args[0].(string)) + } +} + +func BenchmarkBindStruct(b *testing.B) { + b.StopTimer() + q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)` + type t struct { + Name string + Age int + First string + Last string + } + am := t{"Jason Moiron", 30, "Jason", "Moiron"} + b.StartTimer() + for i := 0; i < b.N; i++ { + bindStruct(DOLLAR, q1, am, mapper()) + } +} + +func TestBindNamedMapper(t *testing.T) { + type A map[string]interface{} + m := reflectx.NewMapperFunc("db", NameMapper) + query, args, err := bindNamedMapper(DOLLAR, `select :x`, A{ + "x": "X!", + }, m) + if err != nil { + t.Fatal(err) + } + + got := fmt.Sprintf("%s %s", query, args) + want := `select $1 [X!]` + if got != want { + t.Errorf("\ngot: %q\nwant: %q", got, want) + } + + _, _, err = bindNamedMapper(DOLLAR, `select :x`, map[string]string{ + "x": "X!", + }, m) + if err == nil { + t.Fatal("err is nil") + } + if !strings.Contains(err.Error(), "unsupported map type") { + t.Errorf("wrong error: %s", err) + } +} + +func BenchmarkBindMap(b *testing.B) { + b.StopTimer() + q1 := `INSERT INTO foo (a, b, c, d) VALUES (:name, :age, :first, :last)` + am := map[string]interface{}{ + "name": "Jason Moiron", + "age": 30, + "first": "Jason", + "last": "Moiron", + } + b.StartTimer() + for i := 0; i < b.N; i++ { + bindMap(DOLLAR, q1, am) + } +} + +func BenchmarkIn(b *testing.B) { + q := `SELECT * FROM foo WHERE x = ? AND v in (?) AND y = ?` + + for i := 0; i < b.N; i++ { + _, _, _ = In(q, []interface{}{"foo", []int{0, 5, 7, 2, 9}, "bar"}...) + } +} + +func BenchmarkIn1k(b *testing.B) { + q := `SELECT * FROM foo WHERE x = ? AND v in (?) AND y = ?` + + var vals [1000]interface{} + + for i := 0; i < b.N; i++ { + _, _, _ = In(q, []interface{}{"foo", vals[:], "bar"}...) + } +} + +func BenchmarkIn1kInt(b *testing.B) { + q := `SELECT * FROM foo WHERE x = ? AND v in (?) AND y = ?` + + var vals [1000]int + + for i := 0; i < b.N; i++ { + _, _, _ = In(q, []interface{}{"foo", vals[:], "bar"}...) + } +} + +func BenchmarkIn1kString(b *testing.B) { + q := `SELECT * FROM foo WHERE x = ? AND v in (?) AND y = ?` + + var vals [1000]string + + for i := 0; i < b.N; i++ { + _, _, _ = In(q, []interface{}{"foo", vals[:], "bar"}...) + } +} + +func BenchmarkRebind(b *testing.B) { + b.StopTimer() + q1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + q2 := `INSERT INTO foo (a, b, c) VALUES (?, ?, "foo"), ("Hi", ?, ?)` + b.StartTimer() + + for i := 0; i < b.N; i++ { + Rebind(DOLLAR, q1) + Rebind(DOLLAR, q2) + } +} + +func BenchmarkRebindBuffer(b *testing.B) { + b.StopTimer() + q1 := `INSERT INTO foo (a, b, c, d, e, f, g, h, i) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + q2 := `INSERT INTO foo (a, b, c) VALUES (?, ?, "foo"), ("Hi", ?, ?)` + b.StartTimer() + + for i := 0; i < b.N; i++ { + rebindBuff(DOLLAR, q1) + rebindBuff(DOLLAR, q2) + } +} + +func TestIn130Regression(t *testing.T) { + t.Run("[]interface{}{}", func(t *testing.T) { + q, args, err := In("SELECT * FROM people WHERE name IN (?)", []interface{}{[]string{"gopher"}}...) + if err != nil { + t.Fatal(err) + } + if q != "SELECT * FROM people WHERE name IN (?)" { + t.Errorf("got=%v", q) + } + t.Log(args) + for _, a := range args { + switch a := a.(type) { + case string: + t.Log("ok: string", a) + case *string: + t.Error("ng: string pointer", a, *a) + } + } + }) + + t.Run("[]string{}", func(t *testing.T) { + q, args, err := In("SELECT * FROM people WHERE name IN (?)", []string{"gopher"}) + if err != nil { + t.Fatal(err) + } + if q != "SELECT * FROM people WHERE name IN (?)" { + t.Errorf("got=%v", q) + } + t.Log(args) + for _, a := range args { + switch a := a.(type) { + case string: + t.Log("ok: string", a) + case *string: + t.Error("ng: string pointer", a, *a) + } + } + }) +} + +func TestDoNotPanicOnConnectContext(t *testing.T) { + _, err := ConnectContext(context.Background(), "bogus", "hehe") + if err == nil { + t.Errorf("Should return error when using bogus driverName") + } +}