From cd7b3bdb85d36b7312a104232a09e0a5285f779e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Nieto?= Date: Sun, 14 Jul 2019 20:10:41 +0000 Subject: [PATCH] refactor test suite --- .travis.yml | 4 +- Makefile | 76 +- collection.go | 18 +- comparison.go | 2 +- compound.go | 10 +- cond.go | 2 +- url.go => connection_url.go | 0 constraint.go | 7 +- database.go | 2 +- db.go | 9 +- docker-compose.yml | 40 - errors.go | 2 +- function.go | 2 +- internal/sqladapter/collection.go | 2 +- internal/sqladapter/database.go | 2 +- internal/sqladapter/exql/template.go | 2 +- internal/sqladapter/result.go | 2 +- internal/sqladapter/tx.go | 2 +- lib/sqlbuilder/builder.go | 2 +- lib/sqlbuilder/builder_test.go | 2 +- lib/sqlbuilder/comparison.go | 2 +- lib/sqlbuilder/convert.go | 2 +- lib/sqlbuilder/fetch.go | 2 +- lib/sqlbuilder/paginate.go | 2 +- lib/sqlbuilder/placeholder_test.go | 2 +- lib/sqlbuilder/scanner.go | 2 +- lib/sqlbuilder/select.go | 2 +- lib/sqlbuilder/template.go | 2 +- lib/sqlbuilder/wrapper.go | 2 +- mongo/Makefile | 39 +- mongo/collection.go | 4 +- mongo/database.go | 4 +- mongo/docker-compose.yml | 13 + mongo/generic_test.go | 41 + mongo/helper_test.go | 105 + mongo/{database_test.go => mongo_test.go} | 559 ++--- mongo/result.go | 4 +- mssql/Makefile | 62 +- mssql/collection.go | 2 +- mssql/database.go | 2 +- mssql/docker-compose.yml | 12 + mssql/generic_test.go | 41 + mssql/{adapter_test.go => helper_test.go} | 79 +- mssql/mssql.go | 3 +- mssql/sql_test.go | 41 + mssql/stubs_test.go | 18 - mssql/template_test.go | 2 +- mysql/Makefile | 48 +- mysql/collection.go | 2 +- mysql/database.go | 2 +- mysql/docker-compose.yml | 14 + mysql/generic_test.go | 41 + mysql/helper_test.go | 278 +++ mysql/mysql.go | 3 +- mysql/{adapter_test.go => mysql_test.go} | 409 +--- mysql/sql_test.go | 41 + mysql/stubs_test.go | 18 - mysql/template_test.go | 2 +- postgresql/Makefile | 50 +- postgresql/collection.go | 13 +- postgresql/connection_test.go | 218 +- postgresql/database.go | 2 +- postgresql/docker-compose.yml | 13 + postgresql/generic_test.go | 41 + postgresql/helper_test.go | 305 +++ postgresql/local_test.go | 282 --- postgresql/postgresql.go | 3 +- .../{adapter_test.go => postgresql_test.go} | 728 ++++--- postgresql/sql_test.go | 41 + postgresql/stubs_test.go | 18 - postgresql/template.go | 2 +- postgresql/template_test.go | 2 +- ql/Makefile | 11 +- ql/collection.go | 2 +- ql/database.go | 2 +- ql/generic_test.go | 41 + ql/{adapter_test.go => helper_test.go} | 91 +- operators.go => ql/sql_test.go | 23 +- ql/stubs_test.go | 18 - ql/template.go | 2 +- ql/template_test.go | 2 +- raw.go | 2 +- result.go | 13 +- sqlite/Makefile | 11 +- sqlite/collection.go | 2 +- sqlite/database.go | 2 +- sqlite/generic_test.go | 41 + sqlite/{adapter_test.go => helper_test.go} | 81 +- sqlite/sql_test.go | 41 + sqlite/sqlite.go | 2 +- sqlite/stubs_test.go | 18 - sqlite/template_test.go | 2 +- tests/db_test.go | 1570 -------------- testsuite/generic_suite.go | 909 ++++++++ testsuite/sql_suite.go | 1926 +++++++++++++++++ testsuite/suite.go | 39 + 96 files changed, 5170 insertions(+), 3494 deletions(-) rename url.go => connection_url.go (100%) delete mode 100644 docker-compose.yml create mode 100644 mongo/docker-compose.yml create mode 100644 mongo/generic_test.go create mode 100644 mongo/helper_test.go rename mongo/{database_test.go => mongo_test.go} (59%) create mode 100644 mssql/docker-compose.yml create mode 100644 mssql/generic_test.go rename mssql/{adapter_test.go => helper_test.go} (71%) create mode 100644 mssql/sql_test.go delete mode 100644 mssql/stubs_test.go create mode 100644 mysql/docker-compose.yml create mode 100644 mysql/generic_test.go create mode 100644 mysql/helper_test.go rename mysql/{adapter_test.go => mysql_test.go} (59%) create mode 100644 mysql/sql_test.go delete mode 100644 mysql/stubs_test.go create mode 100644 postgresql/docker-compose.yml create mode 100644 postgresql/generic_test.go create mode 100644 postgresql/helper_test.go delete mode 100644 postgresql/local_test.go rename postgresql/{adapter_test.go => postgresql_test.go} (69%) create mode 100644 postgresql/sql_test.go delete mode 100644 postgresql/stubs_test.go create mode 100644 ql/generic_test.go rename ql/{adapter_test.go => helper_test.go} (70%) rename operators.go => ql/sql_test.go (75%) delete mode 100644 ql/stubs_test.go create mode 100644 sqlite/generic_test.go rename sqlite/{adapter_test.go => helper_test.go} (69%) create mode 100644 sqlite/sql_test.go delete mode 100644 sqlite/stubs_test.go delete mode 100644 tests/db_test.go create mode 100644 testsuite/generic_suite.go create mode 100644 testsuite/sql_suite.go create mode 100644 testsuite/suite.go diff --git a/.travis.yml b/.travis.yml index 158ec610..48bb72ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,7 @@ env: - GOARCH=amd64 - DB_HOST=127.0.0.1 matrix: - - TEST_CMD="make db-up test" POSTGRESQL_VERSION=9 MYSQL_VERSION=5.7 MONGO_VERSION=3.2 MSSQL_VERSION=2017-GA-ubuntu - - TEST_CMD="make db-up test" POSTGRESQL_VERSION=10 MYSQL_VERSION=5.7 MONGO_VERSION=3.6 MSSQL_VERSION=2017-GDR-ubuntu - - TEST_CMD="make db-up test" POSTGRESQL_VERSION=11 MYSQL_VERSION=5 MONGO_VERSION=3 MSSQL_VERSION=latest + - TEST_CMD="make test" - TEST_CMD="make benchmark" notifications: diff --git a/Makefile b/Makefile index 8172210b..0655077f 100644 --- a/Makefile +++ b/Makefile @@ -1,37 +1,13 @@ SHELL := /bin/bash -MONGO_VERSION ?= 3 -MYSQL_VERSION ?= 5 -POSTGRES_VERSION ?= 11 -MSSQL_VERSION ?= 2017-latest-ubuntu - -DB_NAME ?= upperio -DB_USERNAME ?= upperio_user -DB_PASSWORD ?= upperio//s3cr37 - -BIND_HOST ?= 127.0.0.1 - -WRAPPER ?= all -DB_HOST ?= 127.0.0.1 +PARALLEL_FLAGS ?= --halt now,fail=1 --jobs=2 -v -u TEST_FLAGS ?= -PARALLEL_FLAGS ?= --halt 2 -v - -export MONGO_VERSION -export MYSQL_VERSION -export POSTGRES_VERSION -export MSSQL_VERSION - -export DB_USERNAME -export DB_PASSWORD -export DB_NAME -export DB_HOST - -export BIND_HOST -export WRAPPER +export TEST_FLAGS +export PARALLEL_FLAGS -test: reset-db test-libs test-main test-adapters +test: test-libs test-adapters benchmark-lib: go test -v -benchtime=500ms -bench=. ./lib/... @@ -48,43 +24,25 @@ test-internal: go test -v ./internal/... test-libs: - parallel $(PARALLEL_FLAGS) -u \ + parallel $(PARALLEL_FLAGS) \ "$(MAKE) test-{}" ::: \ lib \ internal test-adapters: - parallel $(PARALLEL_FLAGS) -u \ - "$(MAKE) test-adapter-{}" ::: \ - postgresql \ - mysql \ - sqlite \ - mssql \ - ql \ - mongo - -test-main: - go test $(TEST_FLAGS) -v ./tests/... - -reset-db: - parallel $(PARALLEL_FLAGS) \ - "$(MAKE) reset-db-{}" ::: \ - postgresql \ - mysql \ - sqlite \ - mssql \ - ql \ - mongo + for MAKEFILE in $$(grep -Rl test-extended */Makefile | sort -u); do \ + ADAPTER=$$(dirname $$MAKEFILE); \ + ($(MAKE) test-adapter-$$ADAPTER || exit 1); \ + done test-adapter-%: - ($(MAKE) -C $* test || exit 1) - -reset-db-%: - ($(MAKE) -C $* reset-db || exit 1) + ($(MAKE) -C $* test-extended || exit 1) -db-up: - docker-compose -p upper up -d && \ - sleep 15 +test-generic: + export TEST_FLAGS="-run TestGeneric"; \ + $(MAKE) test-adapters -db-down: - docker-compose -p upper down +goimports: + for FILE in $$(find -name "*.go" | grep -v vendor); do \ + goimports -w $$FILE; \ + done diff --git a/collection.go b/collection.go index 20d5b5ac..8cdd6344 100644 --- a/collection.go +++ b/collection.go @@ -24,17 +24,17 @@ package db // Collection is an interface that defines methods useful for handling tables. type Collection interface { // Insert inserts a new item into the collection, it accepts one argument - // that can be either a map or a struct. If the call suceeds, it returns the - // ID of the newly added element as an `interface{}` (the underlying type of - // this ID is unknown and depends on the database adapter). The ID returned - // by Insert() could be passed directly to Find() to retrieve the newly added - // element. + // that can be either a map or a struct. If the call succeeds, it returns the + // ID of the newly added element as an `interface{}` (the actual type of this + // ID depends on both the database adapter and the column that stores this + // ID). The ID returned by Insert() could be passed directly to Find() to + // retrieve the newly added element. Insert(interface{}) (interface{}, error) - // InsertReturning is like Insert() but it updates the passed pointer to map - // or struct with the newly inserted element (and with automatic fields, like - // IDs, timestamps, etc). This is all done atomically within a transaction. - // If the database does not support transactions this method returns + // InsertReturning is like Insert() but it updates the passed map or struct + // with the newly inserted element (and with automatic fields, like IDs, + // timestamps, etc). This is all done atomically within a transaction. If + // the database does not support transactions this method returns // db.ErrUnsupported. InsertReturning(interface{}) error diff --git a/comparison.go b/comparison.go index 947cd269..bf6fa9d3 100644 --- a/comparison.go +++ b/comparison.go @@ -331,4 +331,4 @@ func toInterfaceArray(v interface{}) []interface{} { return []interface{}{v} } -var _ Comparison = &dbComparisonOperator{} +var _ = Comparison(&dbComparisonOperator{}) diff --git a/compound.go b/compound.go index 9cc6ded5..55f4f088 100644 --- a/compound.go +++ b/compound.go @@ -26,8 +26,8 @@ import ( ) // Compound represents an statement that has one or many sentences joined by by -// an operator like "AND" or "OR". This is an exported interface but it's -// rarely used directly, you may want to use the `db.And()` or `db.Or()` +// an operator like "AND" or "OR". This is an exported interface but it was +// designed for internal usage, you may want to use the `db.And()` or `db.Or()` // functions instead. type Compound interface { // Sentences returns child sentences. @@ -125,5 +125,7 @@ func defaultJoin(in ...Compound) []Compound { return in } -var _ = immutable.Immutable(&compound{}) -var _ Compound = Cond{} +var ( + _ = immutable.Immutable(&compound{}) + _ = Compound(Cond{}) +) diff --git a/cond.go b/cond.go index 982a0e77..513a4242 100644 --- a/cond.go +++ b/cond.go @@ -32,7 +32,7 @@ import ( // Each entry of the map represents a condition (a column-value relation bound // by a comparison operator). The comparison operator is optional and can be // specified after the column name, if no comparison operator is provided the -// equality is used. +// equality operator is used as default. // // Examples: // diff --git a/url.go b/connection_url.go similarity index 100% rename from url.go rename to connection_url.go diff --git a/constraint.go b/constraint.go index 7e879a6b..f45ed4f8 100644 --- a/constraint.go +++ b/constraint.go @@ -61,6 +61,7 @@ func NewConstraint(key interface{}, value interface{}) Constraint { return constraint{k: key, v: value} } -var _ Constraints = Cond{} - -var _ Constraint = &constraint{} +var ( + _ = Constraints(Cond{}) + _ = Constraint(&constraint{}) +) diff --git a/database.go b/database.go index 5fe1f3ac..4098deed 100644 --- a/database.go +++ b/database.go @@ -24,7 +24,7 @@ package db // Database is an interface that defines methods that must be satisfied by // all database adapters. type Database interface { - // Driver returns the underlying driver the wrapper uses. + // Driver returns the underlying driver the wrapper uses as an interface{}. // // In order to actually use the driver, the `interface{}` value needs to be // casted into the appropriate type. diff --git a/db.go b/db.go index 03eda1b4..e26a852d 100644 --- a/db.go +++ b/db.go @@ -19,13 +19,10 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// Package db (or upper-db) provides a common interface to work with different -// data sources using adapters that wrap mature database drivers. +// Package db (or upper-db) provides a common interface to work with a variety +// of data sources using adapters that wrap mature database drivers. // -// The main purpose of upper-db is to abstract common database operations and -// encourage users perform advanced operations directly using the underlying -// driver. upper-db supports the MySQL, PostgreSQL, SQLite and QL databases and -// provides partial support (CRUD, no transactions) for MongoDB. +// Install upper-db: // // go get upper.io/db.v3 // diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index e1222e52..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,40 +0,0 @@ -version: '3.3' - -services: - - mongo: - image: mongo:${MONGO_VERSION} - environment: - MONGO_USER: ${DB_USERNAME} - MONGO_PASSWORD: ${DB_PASSWORD} - MONGO_DATABASE: ${DB_NAME} - ports: - - '${BIND_HOST}:27017:27017' - - mysql: - image: mysql:${MYSQL_VERSION} - environment: - MYSQL_USER: ${DB_USERNAME} - MYSQL_PASSWORD: ${DB_PASSWORD} - MYSQL_ALLOW_EMPTY_PASSWORD: 1 - MYSQL_DATABASE: ${DB_NAME} - ports: - - '${BIND_HOST}:3306:3306' - - postgres: - image: postgres:${POSTGRES_VERSION} - environment: - POSTGRES_USER: ${DB_USERNAME} - POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_DB: ${DB_NAME} - ports: - - '${BIND_HOST}:5432:5432' - - mssql: - image: mcr.microsoft.com/mssql/server:${MSSQL_VERSION} - environment: - ACCEPT_EULA: "Y" - SA_PASSWORD: ${DB_PASSWORD} - ports: - - '${BIND_HOST}:1433:1433' - diff --git a/errors.go b/errors.go index e6b16011..0c6f1f44 100644 --- a/errors.go +++ b/errors.go @@ -28,7 +28,7 @@ import ( // Error messages. var ( ErrNoMoreRows = errors.New(`upper: no more rows in this result set`) - ErrNotConnected = errors.New(`upper: you're currently not connected`) + ErrNotConnected = errors.New(`upper: not connected to a database`) ErrMissingDatabaseName = errors.New(`upper: missing database name`) ErrMissingCollectionName = errors.New(`upper: missing collection name`) ErrCollectionDoesNotExist = errors.New(`upper: collection does not exist`) diff --git a/function.go b/function.go index d071d96a..b6945e4d 100644 --- a/function.go +++ b/function.go @@ -77,4 +77,4 @@ func (f *dbFunc) Name() string { return f.name } -var _ Function = &dbFunc{} +var _ = Function(&dbFunc{}) diff --git a/internal/sqladapter/collection.go b/internal/sqladapter/collection.go index 403180d8..ddaafe28 100644 --- a/internal/sqladapter/collection.go +++ b/internal/sqladapter/collection.go @@ -5,7 +5,7 @@ import ( "fmt" "reflect" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter/exql" "upper.io/db.v3/lib/reflectx" ) diff --git a/internal/sqladapter/database.go b/internal/sqladapter/database.go index f9f05d7e..0d3dff09 100644 --- a/internal/sqladapter/database.go +++ b/internal/sqladapter/database.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "time" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/cache" "upper.io/db.v3/internal/sqladapter/compat" "upper.io/db.v3/internal/sqladapter/exql" diff --git a/internal/sqladapter/exql/template.go b/internal/sqladapter/exql/template.go index 4758b49b..bec32bf4 100644 --- a/internal/sqladapter/exql/template.go +++ b/internal/sqladapter/exql/template.go @@ -6,7 +6,7 @@ import ( "sync" "text/template" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/cache" ) diff --git a/internal/sqladapter/result.go b/internal/sqladapter/result.go index 1d0955b7..b35e43f0 100644 --- a/internal/sqladapter/result.go +++ b/internal/sqladapter/result.go @@ -25,7 +25,7 @@ import ( "sync" "sync/atomic" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/immutable" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/internal/sqladapter/tx.go b/internal/sqladapter/tx.go index facb5c02..5897f09d 100644 --- a/internal/sqladapter/tx.go +++ b/internal/sqladapter/tx.go @@ -26,7 +26,7 @@ import ( "database/sql" "sync/atomic" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/lib/sqlbuilder/builder.go b/lib/sqlbuilder/builder.go index ed0a65f4..8a621aab 100644 --- a/lib/sqlbuilder/builder.go +++ b/lib/sqlbuilder/builder.go @@ -34,7 +34,7 @@ import ( "strconv" "strings" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter/compat" "upper.io/db.v3/internal/sqladapter/exql" "upper.io/db.v3/lib/reflectx" diff --git a/lib/sqlbuilder/builder_test.go b/lib/sqlbuilder/builder_test.go index 48d5e400..591add40 100644 --- a/lib/sqlbuilder/builder_test.go +++ b/lib/sqlbuilder/builder_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "upper.io/db.v3" + db "upper.io/db.v3" ) func TestSelect(t *testing.T) { diff --git a/lib/sqlbuilder/comparison.go b/lib/sqlbuilder/comparison.go index dc01fe38..992b248f 100644 --- a/lib/sqlbuilder/comparison.go +++ b/lib/sqlbuilder/comparison.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter/exql" ) diff --git a/lib/sqlbuilder/convert.go b/lib/sqlbuilder/convert.go index 122b2f3c..d0688d73 100644 --- a/lib/sqlbuilder/convert.go +++ b/lib/sqlbuilder/convert.go @@ -5,7 +5,7 @@ import ( "reflect" "strings" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter/exql" ) diff --git a/lib/sqlbuilder/fetch.go b/lib/sqlbuilder/fetch.go index f6bdbff8..1bf355a8 100644 --- a/lib/sqlbuilder/fetch.go +++ b/lib/sqlbuilder/fetch.go @@ -24,7 +24,7 @@ package sqlbuilder import ( "reflect" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/lib/reflectx" ) diff --git a/lib/sqlbuilder/paginate.go b/lib/sqlbuilder/paginate.go index d89b4b92..5e1ab337 100644 --- a/lib/sqlbuilder/paginate.go +++ b/lib/sqlbuilder/paginate.go @@ -7,7 +7,7 @@ import ( "math" "strings" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/immutable" ) diff --git a/lib/sqlbuilder/placeholder_test.go b/lib/sqlbuilder/placeholder_test.go index 37b311c1..907fd3ff 100644 --- a/lib/sqlbuilder/placeholder_test.go +++ b/lib/sqlbuilder/placeholder_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "upper.io/db.v3" + db "upper.io/db.v3" ) func TestPlaceholderSimple(t *testing.T) { diff --git a/lib/sqlbuilder/scanner.go b/lib/sqlbuilder/scanner.go index 84bb7095..076c3822 100644 --- a/lib/sqlbuilder/scanner.go +++ b/lib/sqlbuilder/scanner.go @@ -24,7 +24,7 @@ package sqlbuilder import ( "database/sql" - "upper.io/db.v3" + db "upper.io/db.v3" ) type scanner struct { diff --git a/lib/sqlbuilder/select.go b/lib/sqlbuilder/select.go index fa305c1b..8f2bbc15 100644 --- a/lib/sqlbuilder/select.go +++ b/lib/sqlbuilder/select.go @@ -7,7 +7,7 @@ import ( "fmt" "strings" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/immutable" "upper.io/db.v3/internal/sqladapter/exql" ) diff --git a/lib/sqlbuilder/template.go b/lib/sqlbuilder/template.go index 11ec8c1b..398b5985 100644 --- a/lib/sqlbuilder/template.go +++ b/lib/sqlbuilder/template.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter/exql" ) diff --git a/lib/sqlbuilder/wrapper.go b/lib/sqlbuilder/wrapper.go index 42299c52..cf540436 100644 --- a/lib/sqlbuilder/wrapper.go +++ b/lib/sqlbuilder/wrapper.go @@ -27,7 +27,7 @@ import ( "fmt" "sync" - "upper.io/db.v3" + db "upper.io/db.v3" ) var ( diff --git a/mongo/Makefile b/mongo/Makefile index e9e82315..76aa66e6 100644 --- a/mongo/Makefile +++ b/mongo/Makefile @@ -1,33 +1,40 @@ SHELL := bash +MONGO_VERSION ?= 3 +MONGO_SUPPORTED ?= 4 $(MONGO_VERSION) +PROJECT ?= upper_mongo_$(MONGO_VERSION) + DB_HOST ?= 127.0.0.1 DB_PORT ?= 27017 -DB_NAME ?= upperio +DB_NAME ?= admin DB_USERNAME ?= upperio_user DB_PASSWORD ?= upperio//s3cr37 +TEST_FLAGS ?= +PARALLEL_FLAGS ?= + +export MONGO_VERSION + export DB_HOST -export DB_PORT export DB_NAME export DB_PASSWORD +export DB_PORT export DB_USERNAME -build: - go build && go install +export TEST_FLAGS -require-client: - @if [ -z "$$(which mongo)" ]; then \ - echo 'Missing "mongo" command. Please install the MongoDB client and try again.' && \ - exit 1; \ - fi +test: + go test -v $(TEST_FLAGS) -generate: +server-up: server-down + docker-compose -p $(PROJECT) up -d && \ + sleep 10 -reset-db: require-client - mongo $(DB_NAME) --eval 'db.dropDatabase()' --host $(DB_HOST) --port $(DB_PORT) && \ - mongo $(DB_NAME) --eval 'db.dropUser("$(DB_USERNAME)")' --host $(DB_HOST) --port $(DB_PORT) && \ - mongo $(DB_NAME) --eval 'db.createUser({user: "$(DB_USERNAME)", pwd: "$(DB_PASSWORD)", roles: [{role: "readWrite", db: "$(DB_NAME)"}]})' --host $(DB_HOST) --port $(DB_PORT) +server-down: + docker-compose -p $(PROJECT) down -test: reset-db - go test -v +test-extended: + parallel $(PARALLEL_FLAGS) \ + "MONGO_VERSION={} DB_PORT=\$$((27017+{#})) $(MAKE) server-up test server-down" ::: \ + $(MONGO_SUPPORTED) diff --git a/mongo/collection.go b/mongo/collection.go index fec6c5bd..1b7796c5 100644 --- a/mongo/collection.go +++ b/mongo/collection.go @@ -28,9 +28,9 @@ import ( "reflect" - "gopkg.in/mgo.v2" + mgo "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" - "upper.io/db.v3" + db "upper.io/db.v3" ) // Collection represents a mongodb collection. diff --git a/mongo/database.go b/mongo/database.go index 46861d43..9adf50f5 100644 --- a/mongo/database.go +++ b/mongo/database.go @@ -29,8 +29,8 @@ import ( "sync" "time" - "gopkg.in/mgo.v2" - "upper.io/db.v3" + mgo "gopkg.in/mgo.v2" + db "upper.io/db.v3" ) // Adapter holds the name of the mongodb adapter. diff --git a/mongo/docker-compose.yml b/mongo/docker-compose.yml new file mode 100644 index 00000000..af275258 --- /dev/null +++ b/mongo/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3' + +services: + + server: + image: mongo:${MONGO_VERSION:-3} + environment: + MONGO_INITDB_ROOT_USERNAME: ${DB_USERNAME:-upperio_user} + MONGO_INITDB_ROOT_PASSWORD: ${DB_PASSWORD:-upperio//s3cr37} + MONGO_INITDB_DATABASE: ${DB_NAME:-upperio} + ports: + - '${BIND_HOST:-127.0.0.1}:${DB_PORT:-27017}:27017' + diff --git a/mongo/generic_test.go b/mongo/generic_test.go new file mode 100644 index 00000000..a53f9e14 --- /dev/null +++ b/mongo/generic_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mongo + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type GenericTests struct { + testsuite.GenericTestSuite +} + +func (s *GenericTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestGeneric(t *testing.T) { + suite.Run(t, &GenericTests{}) +} diff --git a/mongo/helper_test.go b/mongo/helper_test.go new file mode 100644 index 00000000..ed390aa5 --- /dev/null +++ b/mongo/helper_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mongo + +import ( + "fmt" + "os" + + mgo "gopkg.in/mgo.v2" + + db "upper.io/db.v3" + "upper.io/db.v3/lib/sqlbuilder" + "upper.io/db.v3/testsuite" +) + +var settings = ConnectionURL{ + Database: os.Getenv("DB_NAME"), + User: os.Getenv("DB_USERNAME"), + Password: os.Getenv("DB_PASSWORD"), + Host: os.Getenv("DB_HOST") + ":" + os.Getenv("DB_PORT"), +} + +type Helper struct { + sess db.Database +} + +func (h *Helper) SQLBuilder() sqlbuilder.Database { + panic("mongo adapter is incompatile with SQLBuilder interface") +} + +func (h *Helper) Session() db.Database { + return h.sess +} + +func (h *Helper) Adapter() string { + return "mongo" +} + +func (h *Helper) TearDown() error { + return h.sess.Close() +} + +func (h *Helper) TearUp() error { + var err error + + h.sess, err = Open(settings) + if err != nil { + return err + } + + mgod, ok := h.sess.Driver().(*mgo.Session) + if !ok { + panic("expecting mgo.Session") + } + + var col *mgo.Collection + col = mgod.DB(settings.Database).C("birthdays") + col.DropCollection() + + col = mgod.DB(settings.Database).C("fibonacci") + col.DropCollection() + + col = mgod.DB(settings.Database).C("is_even") + col.DropCollection() + + col = mgod.DB(settings.Database).C("CaSe_TesT") + col.DropCollection() + + // Getting a pointer to the "artist" collection. + artist := h.sess.Collection("artist") + + _ = artist.Truncate() + + for i := 0; i < 999; i++ { + _, err = artist.Insert(artistType{ + Name: fmt.Sprintf("artist-%d", i), + }) + if err != nil { + return err + } + } + + return nil +} + +var _ testsuite.Helper = &Helper{} diff --git a/mongo/database_test.go b/mongo/mongo_test.go similarity index 59% rename from mongo/database_test.go rename to mongo/mongo_test.go index f8b5ef7d..9a4cedfd 100644 --- a/mongo/database_test.go +++ b/mongo/mongo_test.go @@ -26,15 +26,14 @@ import ( "fmt" "log" "math/rand" - "os" - "reflect" "strings" "testing" "time" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" "gopkg.in/mgo.v2/bson" - "upper.io/db.v3" + db "upper.io/db.v3" + "upper.io/db.v3/testsuite" ) type artistType struct { @@ -42,16 +41,6 @@ type artistType struct { Name string `bson:"name"` } -// Global settings for tests. -var settings = ConnectionURL{ - Database: os.Getenv("DB_NAME"), - User: os.Getenv("DB_USERNAME"), - Password: os.Getenv("DB_PASSWORD"), - Host: os.Getenv("DB_HOST") + ":" + os.Getenv("DB_PORT"), -} - -var host string - // Structure for testing conversions and datatypes. type testValuesStruct struct { Uint uint `bson:"_uint"` @@ -96,8 +85,15 @@ func init() { } } -// Attempts to open an empty datasource. -func TestOpenWithWrongData(t *testing.T) { +type AdapterTests struct { + testsuite.Suite +} + +func (s *AdapterTests) SetupSuite() { + s.Helper = &Helper{} +} + +func (s *AdapterTests) TestOpenWithWrongData() { var err error var rightSettings, wrongSettings ConnectionURL @@ -110,10 +106,8 @@ func TestOpenWithWrongData(t *testing.T) { } // Attempt to open an empty database. - if _, err = Open(rightSettings); err != nil { - // Must fail. - t.Fatal(err) - } + _, err = Open(rightSettings) + s.NoError(err) // Attempt to open with wrong password. wrongSettings = ConnectionURL{ @@ -123,9 +117,8 @@ func TestOpenWithWrongData(t *testing.T) { Password: "fail", } - if _, err = Open(wrongSettings); err == nil { - t.Fatalf("Expecting an error.") - } + _, err = Open(wrongSettings) + s.Error(err) // Attempt to open with wrong database. wrongSettings = ConnectionURL{ @@ -135,9 +128,8 @@ func TestOpenWithWrongData(t *testing.T) { Password: settings.Password, } - if _, err = Open(wrongSettings); err == nil { - t.Fatalf("Expecting an error.") - } + _, err = Open(wrongSettings) + s.Error(err) // Attempt to open with wrong username. wrongSettings = ConnectionURL{ @@ -147,32 +139,21 @@ func TestOpenWithWrongData(t *testing.T) { Password: settings.Password, } - if _, err = Open(wrongSettings); err == nil { - t.Fatalf("Expecting an error.") - } + _, err = Open(wrongSettings) + s.Error(err) } -// Truncates all collections. -func TestTruncate(t *testing.T) { - - var err error - +func (s *AdapterTests) TestTruncate() { // Opening database. sess, err := Open(settings) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) // We should close the database when it's no longer in use. defer sess.Close() // Getting a list of all collections in this database. collections, err := sess.Collections() - - if err != nil { - t.Fatal(err) - } + s.NoError(err) for _, name := range collections { @@ -185,54 +166,34 @@ func TestTruncate(t *testing.T) { if exists == true { // Truncating the structure, if exists. err = col.Truncate() - - if err != nil { - t.Fatal(err) - } + s.NoError(err) } - } } -// This test appends some data into the "artist" table. -func TestInsert(t *testing.T) { - - var err error - var id interface{} - +func (s *AdapterTests) TestInsert() { // Opening database. sess, err := Open(settings) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) // We should close the database when it's no longer in use. defer sess.Close() // Getting a pointer to the "artist" collection. artist := sess.Collection("artist") + _ = artist.Truncate() // Inserting a map. - id, err = artist.Insert(map[string]string{ + id, err := artist.Insert(map[string]string{ "name": "Ozzie", }) + s.NoError(err) + s.NotZero(id) - if err != nil { - t.Fatalf("Insert(): %s", err.Error()) - } - - if id == nil { - t.Fatalf("Expecting an ID.") - } - - if _, ok := id.(bson.ObjectId); ok != true { - t.Fatalf("Expecting a bson.ObjectId.") - } + _, ok := id.(bson.ObjectId) + s.True(ok) - if id.(bson.ObjectId).Valid() != true { - t.Fatalf("Expecting a valid bson.ObjectId.") - } + s.True(id.(bson.ObjectId).Valid()) // Inserting a struct. id, err = artist.Insert(struct { @@ -240,18 +201,12 @@ func TestInsert(t *testing.T) { }{ "Flea", }) + s.NoError(err) + s.NotZero(id) - if id == nil { - t.Fatalf("Expecting an ID.") - } - - if _, ok := id.(bson.ObjectId); ok != true { - t.Fatalf("Expecting a bson.ObjectId.") - } - - if id.(bson.ObjectId).Valid() != true { - t.Fatalf("Expecting a valid bson.ObjectId.") - } + _, ok = id.(bson.ObjectId) + s.True(ok) + s.True(id.(bson.ObjectId).Valid()) // Inserting a struct (using tags to specify the field name). id, err = artist.Insert(struct { @@ -259,18 +214,12 @@ func TestInsert(t *testing.T) { }{ "Slash", }) + s.NoError(err) + s.NotNil(id) - if id == nil { - t.Fatalf("Expecting an ID.") - } - - if _, ok := id.(bson.ObjectId); ok != true { - t.Fatalf("Expecting a bson.ObjectId.") - } - - if id.(bson.ObjectId).Valid() != true { - t.Fatalf("Expecting a valid bson.ObjectId.") - } + _, ok = id.(bson.ObjectId) + s.True(ok) + s.True(id.(bson.ObjectId).Valid()) // Inserting a pointer to a struct id, err = artist.Insert(&struct { @@ -278,54 +227,34 @@ func TestInsert(t *testing.T) { }{ "Metallica", }) + s.NoError(err) + s.NotZero(id) - if id == nil { - t.Fatalf("Expecting an ID.") - } - - if _, ok := id.(bson.ObjectId); ok != true { - t.Fatalf("Expecting a bson.ObjectId.") - } - - if id.(bson.ObjectId).Valid() != true { - t.Fatalf("Expecting a valid bson.ObjectId.") - } + _, ok = id.(bson.ObjectId) + s.True(ok) + s.True(id.(bson.ObjectId).Valid()) // Inserting a pointer to a map id, err = artist.Insert(&map[string]string{ "name": "Freddie", }) + s.NoError(err) + s.NotZero(id) - if id == nil { - t.Fatalf("Expecting an ID.") - } - - if _, ok := id.(bson.ObjectId); ok != true { - t.Fatalf("Expecting a bson.ObjectId.") - } - - if id.(bson.ObjectId).Valid() != true { - t.Fatalf("Expecting a valid bson.ObjectId.") - } - - var total uint64 + _, ok = id.(bson.ObjectId) + s.True(ok) + s.True(id.(bson.ObjectId).Valid()) // Counting elements, must be exactly 6 elements. - if total, err = artist.Find().Count(); err != nil { - t.Fatal(err) - } - - if total != 5 { - t.Fatalf("Expecting exactly 5 rows.") - } + total, err := artist.Find().Count() + s.NoError(err) + s.Equal(uint64(5), total) } -func TestGetNonExistentRow_Issue426(t *testing.T) { +func (s *AdapterTests) TestGetNonExistentRow_Issue426() { // Opening database. sess, err := Open(settings) - if err != nil { - t.Fatal(err) - } + s.NoError(err) defer sess.Close() @@ -334,29 +263,23 @@ func TestGetNonExistentRow_Issue426(t *testing.T) { var one artistType err = artist.Find(db.Cond{"name": "nothing"}).One(&one) - assert.NotZero(t, err) - assert.Equal(t, db.ErrNoMoreRows, err) + s.NotZero(err) + s.Equal(db.ErrNoMoreRows, err) var all []artistType err = artist.Find(db.Cond{"name": "nothing"}).All(&all) - assert.Zero(t, err, "All should not return mgo.ErrNotFound") - assert.Equal(t, 0, len(all)) + s.Zero(err, "All should not return mgo.ErrNotFound") + s.Equal(0, len(all)) } -// This test tries to use an empty filter and count how many elements were -// added into the artist collection. -func TestResultCount(t *testing.T) { - +func (s *AdapterTests) TestResultCount() { var err error var res db.Result // Opening database. sess, err := Open(settings) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) defer sess.Close() @@ -367,26 +290,15 @@ func TestResultCount(t *testing.T) { // Counting all the matching rows. total, err := res.Count() - - if err != nil { - t.Fatal(err) - } - - if total == 0 { - t.Fatalf("Should not be empty, we've just added some rows!") - } - + s.NoError(err) + s.NotZero(total) } -func TestGroup(t *testing.T) { - - var err error - var sess db.Database +func (s *AdapterTests) TestGroup() { var stats db.Collection - if sess, err = Open(settings); err != nil { - t.Fatal(err) - } + sess, err := Open(settings) + s.NoError(err) type statsT struct { Numeric int `db:"numeric" bson:"numeric"` @@ -403,9 +315,8 @@ func TestGroup(t *testing.T) { // Adding row append. for i := 0; i < 1000; i++ { numeric, value := rand.Intn(10), rand.Intn(100) - if _, err = stats.Insert(statsT{numeric, value}); err != nil { - t.Fatal(err) - } + _, err = stats.Insert(statsT{numeric, value}) + s.NoError(err) } // db.statsTest.group({key: {numeric: true}, initial: {sum: 0}, reduce: function(doc, prev) { prev.sum += 1}}); @@ -420,51 +331,25 @@ func TestGroup(t *testing.T) { var results []map[string]interface{} err = res.All(&results) - - // Currently not supported. - if err != db.ErrUnsupported { - t.Fatal(err) - } - - //if len(results) != 10 { - // t.Fatalf(`Expecting exactly 10 results, this could fail, but it's very unlikely to happen.`) - //} - + s.Equal(db.ErrUnsupported, err) } -// Attempts to count all rows in a table that does not exist. -func TestResultNonExistentCount(t *testing.T) { +func (s *AdapterTests) TestResultNonExistentCount() { sess, err := Open(settings) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) defer sess.Close() total, err := sess.Collection("notartist").Find().Count() - - if err != nil { - t.Fatal("MongoDB should not care about a non-existent collecton.", err) - } - - if total != 0 { - t.Fatal("Counter should be zero") - } + s.NoError(err) + s.Zero(total) } -// This test uses and result and tries to fetch items one by one. -func TestResultFetch(t *testing.T) { - - var err error - var res db.Result +func (s *AdapterTests) TestResultFetch() { // Opening database. sess, err := Open(settings) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) // We should close the database when it's no longer in use. defer sess.Close() @@ -472,27 +357,25 @@ func TestResultFetch(t *testing.T) { artist := sess.Collection("artist") // Testing map - res = artist.Find() + res := artist.Find() rowM := map[string]interface{}{} for res.Next(&rowM) { - if rowM["_id"] == nil { - t.Fatalf("Expecting an ID.") - } - if _, ok := rowM["_id"].(bson.ObjectId); ok != true { - t.Fatalf("Expecting a bson.ObjectId.") - } + s.NotZero(rowM["_id"]) - if rowM["_id"].(bson.ObjectId).Valid() != true { - t.Fatalf("Expecting a valid bson.ObjectId.") - } - if name, ok := rowM["name"].(string); !ok || name == "" { - t.Fatalf("Expecting a name.") - } + _, ok := rowM["_id"].(bson.ObjectId) + s.True(ok) + + s.True(rowM["_id"].(bson.ObjectId).Valid()) + + name, ok := rowM["name"].(string) + s.True(ok) + s.NotZero(name) } - res.Close() + err = res.Close() + s.NoError(err) // Testing struct rowS := struct { @@ -503,15 +386,12 @@ func TestResultFetch(t *testing.T) { res = artist.Find() for res.Next(&rowS) { - if rowS.ID.Valid() == false { - t.Fatalf("Expecting a not null ID.") - } - if rowS.Name == "" { - t.Fatalf("Expecting a name.") - } + s.True(rowS.ID.Valid()) + s.NotZero(rowS.Name) } - res.Close() + err = res.Close() + s.NoError(err) // Testing tagged struct rowT := struct { @@ -522,30 +402,22 @@ func TestResultFetch(t *testing.T) { res = artist.Find() for res.Next(&rowT) { - if rowT.Value1.Valid() == false { - t.Fatalf("Expecting a not null ID.") - } - if rowT.Value2 == "" { - t.Fatalf("Expecting a name.") - } + s.True(rowT.Value1.Valid()) + s.NotZero(rowT.Value2) } - res.Close() + err = res.Close() + s.NoError(err) // Testing Result.All() with a slice of maps. res = artist.Find() allRowsM := []map[string]interface{}{} err = res.All(&allRowsM) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) for _, singleRowM := range allRowsM { - if singleRowM["_id"] == nil { - t.Fatalf("Expecting a not null ID.") - } + s.NotZero(singleRowM["_id"]) } // Testing Result.All() with a slice of structs. @@ -556,15 +428,10 @@ func TestResultFetch(t *testing.T) { Name string }{} err = res.All(&allRowsS) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) for _, singleRowS := range allRowsS { - if singleRowS.ID.Valid() == false { - t.Fatalf("Expecting a not null ID.") - } + s.True(singleRowS.ID.Valid()) } // Testing Result.All() with a slice of tagged structs. @@ -575,28 +442,17 @@ func TestResultFetch(t *testing.T) { Value2 string `bson:"name"` }{} err = res.All(&allRowsT) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) for _, singleRowT := range allRowsT { - if singleRowT.Value1.Valid() == false { - t.Fatalf("Expecting a not null ID.") - } + s.True(singleRowT.Value1.Valid()) } } -// This test tries to update some previously added rows. -func TestUpdate(t *testing.T) { - var err error - +func (s *AdapterTests) TestUpdate() { // Opening database. sess, err := Open(settings) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) // We should close the database when it's no longer in use. defer sess.Close() @@ -614,10 +470,7 @@ func TestUpdate(t *testing.T) { res := artist.Find(db.Cond{"_id": db.NotEq(nil)}).Limit(1) err = res.One(&value) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) // Updating with a map rowM := map[string]interface{}{ @@ -625,20 +478,12 @@ func TestUpdate(t *testing.T) { } err = res.Update(rowM) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) err = res.One(&value) + s.NoError(err) - if err != nil { - t.Fatal(err) - } - - if value.Name != rowM["name"] { - t.Fatalf("Expecting a modification.") - } + s.Equal(value.Name, rowM["name"]) // Updating with a struct rowS := struct { @@ -646,20 +491,12 @@ func TestUpdate(t *testing.T) { }{strings.ToLower(value.Name)} err = res.Update(rowS) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) err = res.One(&value) + s.NoError(err) - if err != nil { - t.Fatal(err) - } - - if value.Name != rowS.Name { - t.Fatalf("Expecting a modification.") - } + s.Equal(value.Name, rowS.Name) // Updating with a tagged struct rowT := struct { @@ -667,33 +504,18 @@ func TestUpdate(t *testing.T) { }{strings.Replace(value.Name, "z", "Z", -1)} err = res.Update(rowT) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) err = res.One(&value) + s.NoError(err) - if err != nil { - t.Fatal(err) - } - - if value.Name != rowT.Value1 { - t.Fatalf("Expecting a modification.") - } - + s.Equal(value.Name, rowT.Value1) } -func TestOperators(t *testing.T) { - var err error - var res db.Result - +func (s *AdapterTests) TestOperators() { // Opening database. sess, err := Open(settings) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) // We should close the database when it's no longer in use. defer sess.Close() @@ -706,26 +528,19 @@ func TestOperators(t *testing.T) { Name string }{} - res = artist.Find(db.Cond{"_id": db.NotIn([]int{0, -1})}) + res := artist.Find(db.Cond{"_id": db.NotIn([]int{0, -1})}) - if err = res.One(&rowS); err != nil { - t.Fatalf("One: %q", err) - } + err = res.One(&rowS) + s.NoError(err) - res.Close() + err = res.Close() + s.NoError(err) } -// This test tries to remove some previously added rows. -func TestDelete(t *testing.T) { - - var err error - +func (s *AdapterTests) TestDelete() { // Opening database. sess, err := Open(settings) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) // We should close the database when it's no longer in use. defer sess.Close() @@ -741,33 +556,19 @@ func TestDelete(t *testing.T) { } err = res.One(&first) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) res = artist.Find(db.Cond{"_id": db.Eq(first.ID)}) // Trying to remove the row. err = res.Delete() - - if err != nil { - t.Fatal(err) - } + s.NoError(err) } -// This test tries to add many different datatypes to a single row in a -// collection, then it tries to get the stored datatypes and check if the -// stored and the original values match. -func TestDataTypes(t *testing.T) { - var res db.Result - +func (s *AdapterTests) TestDataTypes() { // Opening database. sess, err := Open(settings) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) // We should close the database when it's no longer in use. defer sess.Close() @@ -777,39 +578,28 @@ func TestDataTypes(t *testing.T) { // Inserting our test subject. id, err := dataTypes.Insert(testValues) - - if err != nil { - t.Fatal(err) - } + s.NoError(err) + s.NotZero(id) // Trying to get the same subject we added. - res = dataTypes.Find(db.Cond{"_id": db.Eq(id)}) + res := dataTypes.Find(db.Cond{"_id": db.Eq(id)}) exists, err := res.Count() - - if err != nil { - t.Fatal(err) - } - - if exists == 0 { - t.Errorf("Expecting an item.") - } + s.NoError(err) + s.NotZero(exists) // Trying to dump the subject into an empty structure of the same type. var item testValuesStruct res.One(&item) // The original value and the test subject must match. - if reflect.DeepEqual(item, testValues) == false { - t.Errorf("Struct is different.") - } + s.Equal(testValues, item) } -func TestPaginator(t *testing.T) { - +func (s *AdapterTests) TestPaginator() { // Opening database. sess, err := Open(settings) - assert.NoError(t, err) + s.NoError(err) // We should close the database when it's no longer in use. defer sess.Close() @@ -818,13 +608,13 @@ func TestPaginator(t *testing.T) { artist := sess.Collection("artist") err = artist.Truncate() - assert.NoError(t, err) + s.NoError(err) for i := 0; i < 999; i++ { _, err = artist.Insert(artistType{ Name: fmt.Sprintf("artist-%d", i), }) - assert.NoError(t, err) + s.NoError(err) } q := sess.Collection("artist").Find().Paginate(15) @@ -832,52 +622,50 @@ func TestPaginator(t *testing.T) { var zerothPage []artistType err = paginator.Page(0).All(&zerothPage) - assert.NoError(t, err) - assert.Equal(t, 13, len(zerothPage)) + s.NoError(err) + s.Equal(13, len(zerothPage)) var secondPage []artistType err = paginator.Page(2).All(&secondPage) - assert.NoError(t, err) - assert.Equal(t, 13, len(secondPage)) + s.NoError(err) + s.Equal(13, len(secondPage)) tp, err := paginator.TotalPages() - assert.NoError(t, err) - assert.NotZero(t, tp) - assert.Equal(t, uint(77), tp) + s.NoError(err) + s.NotZero(tp) + s.Equal(uint(77), tp) ti, err := paginator.TotalEntries() - assert.NoError(t, err) - assert.NotZero(t, ti) - assert.Equal(t, uint64(999), ti) + s.NoError(err) + s.NotZero(ti) + s.Equal(uint64(999), ti) var seventySixthPage []artistType err = paginator.Page(76).All(&seventySixthPage) - assert.NoError(t, err) - assert.Equal(t, 11, len(seventySixthPage)) + s.NoError(err) + s.Equal(11, len(seventySixthPage)) var seventySeventhPage []artistType err = paginator.Page(77).All(&seventySeventhPage) - assert.NoError(t, err) - assert.Equal(t, 0, len(seventySeventhPage)) + s.NoError(err) + s.Equal(0, len(seventySeventhPage)) var hundredthPage []artistType err = paginator.Page(100).All(&hundredthPage) - assert.NoError(t, err) - assert.Equal(t, 0, len(hundredthPage)) + s.NoError(err) + s.Equal(0, len(hundredthPage)) for i := uint(0); i < tp; i++ { current := paginator.Page(i) var items []artistType err := current.All(&items) - if err != nil { - t.Fatal(err) - } + s.NoError(err) if len(items) < 1 { break } for j := 0; j < len(items); j++ { - assert.Equal(t, fmt.Sprintf("artist-%d", int64(13*int(i)+j)), items[j].Name) + s.Equal(fmt.Sprintf("artist-%d", int64(13*int(i)+j)), items[j].Name) } } @@ -887,15 +675,14 @@ func TestPaginator(t *testing.T) { for i := 0; ; i++ { var items []artistType err := current.All(&items) - if err != nil { - t.Fatal(err) - } + s.NoError(err) + if len(items) < 1 { break } for j := 0; j < len(items); j++ { - assert.Equal(t, fmt.Sprintf("artist-%d", int64(13*int(i)+j)), items[j].Name) + s.Equal(fmt.Sprintf("artist-%d", int64(13*int(i)+j)), items[j].Name) } current = current.NextPage(items[len(items)-1].ID) } @@ -908,14 +695,14 @@ func TestPaginator(t *testing.T) { var items []artistType err := current.All(&items) - assert.NoError(t, err) + s.NoError(err) if len(items) < 1 { - assert.Equal(t, 0, len(items)) + s.Equal(0, len(items)) break } for j := 0; j < len(items); j++ { - assert.Equal(t, fmt.Sprintf("artist-%d", 13*int(i)+j), items[j].Name) + s.Equal(fmt.Sprintf("artist-%d", 13*int(i)+j), items[j].Name) } current = current.PrevPage(items[0].ID) @@ -926,15 +713,15 @@ func TestPaginator(t *testing.T) { resultPaginator := sess.Collection("artist").Find().Paginate(15) count, err := resultPaginator.TotalPages() - assert.Equal(t, uint(67), count) - assert.NoError(t, err) + s.Equal(uint(67), count) + s.NoError(err) var items []artistType err = resultPaginator.Page(5).All(&items) - assert.NoError(t, err) + s.NoError(err) for j := 0; j < len(items); j++ { - assert.Equal(t, fmt.Sprintf("artist-%d", 15*5+j), items[j].Name) + s.Equal(fmt.Sprintf("artist-%d", 15*5+j), items[j].Name) } resultPaginator = resultPaginator.Cursor("_id").Page(0) @@ -942,14 +729,14 @@ func TestPaginator(t *testing.T) { var items []artistType err = resultPaginator.All(&items) - assert.NoError(t, err) + s.NoError(err) if len(items) < 1 { break } for j := 0; j < len(items); j++ { - assert.Equal(t, fmt.Sprintf("artist-%d", 15*i+j), items[j].Name) + s.Equal(fmt.Sprintf("artist-%d", 15*i+j), items[j].Name) } resultPaginator = resultPaginator.NextPage(items[len(items)-1].ID) } @@ -959,16 +746,20 @@ func TestPaginator(t *testing.T) { var items []artistType err = resultPaginator.All(&items) - assert.NoError(t, err) + s.NoError(err) if len(items) < 1 { break } for j := 0; j < len(items); j++ { - assert.Equal(t, fmt.Sprintf("artist-%d", 15*i+j), items[j].Name) + s.Equal(fmt.Sprintf("artist-%d", 15*i+j), items[j].Name) } resultPaginator = resultPaginator.PrevPage(items[0].ID) } } } + +func TestAdapter(t *testing.T) { + suite.Run(t, &AdapterTests{}) +} diff --git a/mongo/result.go b/mongo/result.go index c95831b2..61a1d9d3 100644 --- a/mongo/result.go +++ b/mongo/result.go @@ -30,9 +30,9 @@ import ( "encoding/json" - "gopkg.in/mgo.v2" + mgo "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/immutable" ) diff --git a/mssql/Makefile b/mssql/Makefile index 4cd9235e..08a0f8bb 100644 --- a/mssql/Makefile +++ b/mssql/Makefile @@ -1,13 +1,20 @@ -SHELL := bash +SHELL := bash + +MSSQL_VERSION ?= 2017-latest-ubuntu +MSSQL_SUPPORTED ?= $(MSSQL_VERSION) +PROJECT ?= upper_mssql_$(MSSQL_VERSION) DB_HOST ?= 127.0.0.1 DB_PORT ?= 1433 -DB_NAME ?= upperio -DB_USERNAME ?= upperio_user +DB_NAME ?= master +DB_USERNAME ?= sa DB_PASSWORD ?= upperio//s3cr37 -DB_SA_USERNAME ?= sa +TEST_FLAGS ?= +PARALLEL_FLAGS ?= + +export MSSQL_VERSION export DB_HOST export DB_NAME @@ -15,34 +22,19 @@ export DB_PASSWORD export DB_PORT export DB_USERNAME -export DB_SA_USERNAME - -build: - go build && go install - -require-client: - @if [ -z "$$(which tsql)" ]; then \ - echo 'Missing "tsql" command.' && \ - exit 1; \ - fi - -generate: - go generate && \ - go get -d -t -v ./... - -reset-db: require-client - SQL="" && \ - SQL+="USE [master]\nGO\n" && \ - SQL+="IF EXISTS (SELECT name FROM sys.databases WHERE name = '$(DB_NAME)') BEGIN DROP DATABASE [$(DB_NAME)] END\nGO\n" && \ - SQL+="CREATE DATABASE [$(DB_NAME)]\nGO\n" && \ - SQL+="DROP ROLE IF EXISTS [$(DB_USERNAME)]\nGO\n" && \ - SQL+="CREATE ROLE [$(DB_USERNAME)]\nGO\n" && \ - SQL+="USE [$(DB_NAME)]\nGO\n" && \ - SQL+="IF NOT EXISTS (SELECT name FROM sys.server_principals WHERE name = '$(DB_USERNAME)') BEGIN CREATE LOGIN $(DB_USERNAME) WITH PASSWORD = '$(DB_PASSWORD)' END\nGO\n" && \ - SQL+="IF NOT EXISTS (SELECT name FROM sys.database_principals WHERE name = '$(DB_USERNAME)') BEGIN CREATE USER $(DB_USERNAME) END\nGO\n" && \ - SQL+="EXEC sp_addrolemember 'db_owner', N'$(DB_USERNAME)'\nGO\n" && \ - SQL+="EXEC sp_change_users_login 'Update_One', '$(DB_USERNAME)', '$(DB_USERNAME)'\nGO\n" && \ - echo -ne $$SQL | tsql -H $(DB_HOST) -p $(DB_PORT) -U $(DB_SA_USERNAME) -P '$(DB_PASSWORD)' - -test: reset-db generate - go test -tags generated -v +export TEST_FLAGS + +test: + go test -v $(TEST_FLAGS) + +server-up: server-down + docker-compose -p $(PROJECT) up -d && \ + sleep 10 + +server-down: + docker-compose -p $(PROJECT) down + +test-extended: + parallel $(PARALLEL_FLAGS) \ + "MSSQL_VERSION={} DB_PORT=\$$((1433+{#})) $(MAKE) server-up test server-down" ::: \ + $(MSSQL_SUPPORTED) diff --git a/mssql/collection.go b/mssql/collection.go index c14ee1dd..ceb511b4 100644 --- a/mssql/collection.go +++ b/mssql/collection.go @@ -22,7 +22,7 @@ package mssql import ( - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/mssql/database.go b/mssql/database.go index 8c826902..fa337f3d 100644 --- a/mssql/database.go +++ b/mssql/database.go @@ -32,7 +32,7 @@ import ( "database/sql" _ "github.com/denisenkom/go-mssqldb" // MSSQL driver - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/internal/sqladapter/compat" "upper.io/db.v3/internal/sqladapter/exql" diff --git a/mssql/docker-compose.yml b/mssql/docker-compose.yml new file mode 100644 index 00000000..9b18f8ff --- /dev/null +++ b/mssql/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3' + +services: + + server: + image: mcr.microsoft.com/mssql/server:${MSSQL_VERSION:-2017-latest-ubuntu} + environment: + ACCEPT_EULA: "Y" + SA_PASSWORD: ${DB_PASSWORD:-upperio//s3cr37} + ports: + - '${BIND_HOST:-127.0.0.1}:${DB_PORT:-1433}:1433' + diff --git a/mssql/generic_test.go b/mssql/generic_test.go new file mode 100644 index 00000000..f3cd6271 --- /dev/null +++ b/mssql/generic_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mssql + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type GenericTests struct { + testsuite.GenericTestSuite +} + +func (s *GenericTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestGeneric(t *testing.T) { + suite.Run(t, &GenericTests{}) +} diff --git a/mssql/adapter_test.go b/mssql/helper_test.go similarity index 71% rename from mssql/adapter_test.go rename to mssql/helper_test.go index 792874b2..d959232c 100644 --- a/mssql/adapter_test.go +++ b/mssql/helper_test.go @@ -19,18 +19,15 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -//go:generate bash -c "sed s/ADAPTER/mssql/g ../internal/sqladapter/testing/adapter.go.tpl > generated_test.go" package mssql import ( "database/sql" "os" + db "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" -) - -const ( - testTimeZone = "Canada/Eastern" + "upper.io/db.v3/testsuite" ) var settings = ConnectionURL{ @@ -41,9 +38,33 @@ var settings = ConnectionURL{ Options: map[string]string{}, } -func tearUp() error { - sess := mustOpen() - defer sess.Close() +type Helper struct { + sess sqlbuilder.Database +} + +func (h *Helper) Session() db.Database { + return h.sess +} + +func (h *Helper) SQLBuilder() sqlbuilder.Database { + return h.sess +} + +func (h *Helper) Adapter() string { + return "mssql" +} + +func (h *Helper) TearDown() error { + return h.sess.Close() +} + +func (h *Helper) TearUp() error { + var err error + + h.sess, err = Open(settings) + if err != nil { + return err + } batch := []string{ `DROP TABLE IF EXISTS artist`, @@ -62,7 +83,6 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS review`, - `CREATE TABLE review ( id BIGINT PRIMARY KEY NOT NULL IDENTITY(1,1), publication_id BIGINT, @@ -72,7 +92,6 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS data_types`, - `CREATE TABLE data_types ( id BIGINT PRIMARY KEY NOT NULL IDENTITY(1,1), _uint INT DEFAULT 0, @@ -98,7 +117,6 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS stats_test`, - `CREATE TABLE stats_test ( id BIGINT PRIMARY KEY NOT NULL IDENTITY(1,1), [numeric] INT, @@ -106,18 +124,44 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS composite_keys`, - `CREATE TABLE composite_keys ( code VARCHAR(255) default '', user_id VARCHAR(255) default '', some_val VARCHAR(255) default '', PRIMARY KEY (code, user_id) )`, + + `DROP TABLE IF EXISTS [birthdays]`, + `CREATE TABLE [birthdays] ( + id BIGINT IDENTITY(1, 1) PRIMARY KEY NOT NULL, + name NVARCHAR(50), + born DATETIMEOFFSET, + born_ut BIGINT + )`, + + `DROP TABLE IF EXISTS [fibonacci]`, + `CREATE TABLE [fibonacci] ( + id BIGINT PRIMARY KEY NOT NULL IDENTITY(1,1), + input BIGINT NOT NULL, + output BIGINT NOT NULL + )`, + + `DROP TABLE IF EXISTS [is_even]`, + `CREATE TABLE [is_even] ( + input BIGINT NOT NULL, + is_even TINYINT + )`, + + `DROP TABLE IF EXISTS [CaSe_TesT]`, + `CREATE TABLE [CaSe_TesT] ( + id BIGINT PRIMARY KEY NOT NULL IDENTITY(1,1), + case_test NVARCHAR(60) + )`, } - for _, s := range batch { - driver := sess.Driver().(*sql.DB) - if _, err := driver.Exec(s); err != nil { + for _, query := range batch { + driver := h.sess.Driver().(*sql.DB) + if _, err := driver.Exec(query); err != nil { return err } } @@ -125,7 +169,4 @@ func tearUp() error { return nil } -func cleanUpCheck(sess sqlbuilder.Database) (err error) { - // TODO: Check the number of prepared statements. - return nil -} +var _ testsuite.Helper = &Helper{} diff --git a/mssql/mssql.go b/mssql/mssql.go index 2596cd41..04de94d2 100644 --- a/mssql/mssql.go +++ b/mssql/mssql.go @@ -24,8 +24,7 @@ package mssql // import "upper.io/db.v3/mssql" import ( "database/sql" - "upper.io/db.v3" - + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/mssql/sql_test.go b/mssql/sql_test.go new file mode 100644 index 00000000..a7553f0f --- /dev/null +++ b/mssql/sql_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mssql + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type SQLTests struct { + testsuite.SQLTestSuite +} + +func (s *SQLTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestSQL(t *testing.T) { + suite.Run(t, &SQLTests{}) +} diff --git a/mssql/stubs_test.go b/mssql/stubs_test.go deleted file mode 100644 index 02a72c99..00000000 --- a/mssql/stubs_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build !generated - -package mssql - -import ( - "log" - "testing" - - "upper.io/db.v3/lib/sqlbuilder" -) - -func mustOpen() sqlbuilder.Database { - return nil -} - -func TestMain(*testing.M) { - log.Fatal(`Tests use generated code and a custom database, please use "make test".`) -} diff --git a/mssql/template_test.go b/mssql/template_test.go index 6f780806..efdb9a57 100644 --- a/mssql/template_test.go +++ b/mssql/template_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/mysql/Makefile b/mysql/Makefile index 0c89ce25..8e48693e 100644 --- a/mysql/Makefile +++ b/mysql/Makefile @@ -1,5 +1,9 @@ SHELL := bash +MYSQL_VERSION ?= 8 +MYSQL_SUPPORTED ?= $(MYSQL_VERSION) 5.7 +PROJECT ?= upper_mysql_$(MYSQL_VERSION) + DB_HOST ?= 127.0.0.1 DB_PORT ?= 3306 @@ -7,7 +11,10 @@ DB_NAME ?= upperio DB_USERNAME ?= upperio_user DB_PASSWORD ?= upperio//s3cr37 -TEST_FLAGS ?= +TEST_FLAGS ?= +PARALLEL_FLAGS ?= + +export MYSQL_VERSION export DB_HOST export DB_NAME @@ -15,26 +22,19 @@ export DB_PASSWORD export DB_PORT export DB_USERNAME -build: - go build && go install - -require-client: - @if [ -z "$$(which mysql)" ]; then \ - echo 'Missing "mysql" command. Please install the MySQL client and try again.' && \ - exit 1; \ - fi - -generate: - go generate && \ - go get -d -t -v ./... - -reset-db: require-client - SQL="" && \ - SQL+="DROP DATABASE IF EXISTS $(DB_NAME);" && \ - SQL+="CREATE DATABASE $(DB_NAME);" && \ - SQL+="GRANT ALL PRIVILEGES ON $(DB_NAME).* TO $(DB_USERNAME) IDENTIFIED BY '$(DB_PASSWORD)';" && \ - mysql -uroot -h"$(DB_HOST)" -P$(DB_PORT) <<< $$SQL - -test: reset-db generate - #go test -tags generated -v -race # race: limit on 8192 simultaneously alive goroutines is exceeded, dying - go test -tags generated -v $(TEST_FLAGS) +export TEST_FLAGS + +test: + go test -v $(TEST_FLAGS) + +server-up: server-down + docker-compose -p $(PROJECT) up -d && \ + sleep 15 + +server-down: + docker-compose -p $(PROJECT) down + +test-extended: + parallel $(PARALLEL_FLAGS) \ + "MYSQL_VERSION={} DB_PORT=\$$((3306+{#})) $(MAKE) server-up test server-down" ::: \ + $(MYSQL_SUPPORTED) diff --git a/mysql/collection.go b/mysql/collection.go index 325b715f..4eeeefec 100644 --- a/mysql/collection.go +++ b/mysql/collection.go @@ -24,7 +24,7 @@ package mysql import ( "database/sql" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/mysql/database.go b/mysql/database.go index 10f68f94..a88e57ea 100644 --- a/mysql/database.go +++ b/mysql/database.go @@ -35,7 +35,7 @@ import ( "database/sql" _ "github.com/go-sql-driver/mysql" // MySQL driver. - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/internal/sqladapter/compat" "upper.io/db.v3/internal/sqladapter/exql" diff --git a/mysql/docker-compose.yml b/mysql/docker-compose.yml new file mode 100644 index 00000000..18ab3499 --- /dev/null +++ b/mysql/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3' + +services: + + server: + image: mysql:${MYSQL_VERSION:-5} + environment: + MYSQL_USER: ${DB_USERNAME:-upperio_user} + MYSQL_PASSWORD: ${DB_PASSWORD:-upperio//s3cr37} + MYSQL_ALLOW_EMPTY_PASSWORD: 1 + MYSQL_DATABASE: ${DB_NAME:-upperio} + ports: + - '${DB_HOST:-127.0.0.1}:${DB_PORT:-3306}:3306' + diff --git a/mysql/generic_test.go b/mysql/generic_test.go new file mode 100644 index 00000000..bb68e508 --- /dev/null +++ b/mysql/generic_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mysql + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type GenericTests struct { + testsuite.GenericTestSuite +} + +func (s *GenericTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestGeneric(t *testing.T) { + suite.Run(t, &GenericTests{}) +} diff --git a/mysql/helper_test.go b/mysql/helper_test.go new file mode 100644 index 00000000..4cfa6e01 --- /dev/null +++ b/mysql/helper_test.go @@ -0,0 +1,278 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mysql + +import ( + "database/sql" + "fmt" + "os" + "time" + + db "upper.io/db.v3" + "upper.io/db.v3/internal/sqladapter" + "upper.io/db.v3/lib/sqlbuilder" + "upper.io/db.v3/testsuite" +) + +var settings = ConnectionURL{ + Database: os.Getenv("DB_NAME"), + User: os.Getenv("DB_USERNAME"), + Password: os.Getenv("DB_PASSWORD"), + Host: os.Getenv("DB_HOST") + ":" + os.Getenv("DB_PORT"), + Options: map[string]string{ + // See https://github.com/go-sql-driver/mysql/issues/9 + "parseTime": "true", + // Might require you to use mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql + "time_zone": fmt.Sprintf(`'%s'`, testsuite.TimeZone), + "loc": testsuite.TimeZone, + }, +} + +type Helper struct { + sess sqlbuilder.Database +} + +func cleanUp(sess sqlbuilder.Database) error { + stats, err := getStats(sess) + if err != nil { + return err + } + + if activeStatements := sqladapter.NumActiveStatements(); activeStatements > 128 { + return fmt.Errorf("Expecting active statements to be at most 128, got %d", activeStatements) + } + + sess.ClearCache() + + if activeStatements := sqladapter.NumActiveStatements(); activeStatements != 0 { + return fmt.Errorf("Expecting active statements to be 0, got %d", activeStatements) + } + + for i := 0; i < 10; i++ { + stats, err = getStats(sess) + if err != nil { + return err + } + + if stats["Prepared_stmt_count"] != 0 { + time.Sleep(time.Millisecond * 200) // Sometimes it takes a bit to clean prepared statements + err = fmt.Errorf(`Expecting "Prepared_stmt_count" to be 0, got %d`, stats["Prepared_stmt_count"]) + continue + } + break + } + + return err +} + +func getStats(sess sqlbuilder.Database) (map[string]int, error) { + stats := make(map[string]int) + + res, err := sess.Driver().(*sql.DB).Query(`SHOW GLOBAL STATUS LIKE '%stmt%'`) + if err != nil { + return nil, err + } + var result struct { + VariableName string `db:"Variable_name"` + Value int `db:"Value"` + } + + iter := sqlbuilder.NewIterator(res) + for iter.Next(&result) { + stats[result.VariableName] = result.Value + } + + return stats, nil +} + +func (h *Helper) Session() db.Database { + return h.sess +} + +func (h *Helper) SQLBuilder() sqlbuilder.Database { + return h.sess +} + +func (h *Helper) Adapter() string { + return "mysql" +} + +func (h *Helper) TearDown() error { + if err := cleanUp(h.sess); err != nil { + return err + } + + return h.sess.Close() +} + +func (h *Helper) TearUp() error { + var err error + + h.sess, err = Open(settings) + if err != nil { + return err + } + + batch := []string{ + `DROP TABLE IF EXISTS artist`, + `CREATE TABLE artist ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + PRIMARY KEY(id), + name VARCHAR(60) + )`, + + `DROP TABLE IF EXISTS publication`, + `CREATE TABLE publication ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + PRIMARY KEY(id), + title VARCHAR(80), + author_id BIGINT(20) + )`, + + `DROP TABLE IF EXISTS review`, + `CREATE TABLE review ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + PRIMARY KEY(id), + publication_id BIGINT(20), + name VARCHAR(80), + comments TEXT, + created DATETIME NOT NULL + )`, + + `DROP TABLE IF EXISTS data_types`, + `CREATE TABLE data_types ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + PRIMARY KEY(id), + _uint INT(10) UNSIGNED DEFAULT 0, + _uint8 INT(10) UNSIGNED DEFAULT 0, + _uint16 INT(10) UNSIGNED DEFAULT 0, + _uint32 INT(10) UNSIGNED DEFAULT 0, + _uint64 INT(10) UNSIGNED DEFAULT 0, + _int INT(10) DEFAULT 0, + _int8 INT(10) DEFAULT 0, + _int16 INT(10) DEFAULT 0, + _int32 INT(10) DEFAULT 0, + _int64 INT(10) DEFAULT 0, + _float32 DECIMAL(10,6), + _float64 DECIMAL(10,6), + _bool TINYINT(1), + _string text, + _blob blob, + _date TIMESTAMP NULL, + _nildate DATETIME NULL, + _ptrdate DATETIME NULL, + _defaultdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + _time BIGINT UNSIGNED NOT NULL DEFAULT 0 + )`, + + `DROP TABLE IF EXISTS stats_test`, + `CREATE TABLE stats_test ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), + ` + "`numeric`" + ` INT(10), + ` + "`value`" + ` INT(10) + )`, + + `DROP TABLE IF EXISTS composite_keys`, + `CREATE TABLE composite_keys ( + code VARCHAR(255) default '', + user_id VARCHAR(255) default '', + some_val VARCHAR(255) default '', + primary key (code, user_id) + )`, + + `DROP TABLE IF EXISTS admin`, + `CREATE TABLE admin ( + ID int(11) NOT NULL AUTO_INCREMENT, + Accounts varchar(255) DEFAULT '', + LoginPassWord varchar(255) DEFAULT '', + Date TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + PRIMARY KEY (ID,Date) + ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8`, + + `DROP TABLE IF EXISTS my_types`, + + `CREATE TABLE my_types (id int(11) NOT NULL AUTO_INCREMENT, PRIMARY KEY(id) + , json_map JSON + , json_map_ptr JSON + + , auto_json_map JSON + , auto_json_map_string JSON + , auto_json_map_integer JSON + + , json_object JSON + , json_array JSON + + , custom_json_object JSON + , auto_custom_json_object JSON + + , custom_json_object_ptr JSON + , auto_custom_json_object_ptr JSON + + , custom_json_object_array JSON + , auto_custom_json_object_array JSON + , auto_custom_json_object_map JSON + + , integer_compat_value_json_array JSON + , string_compat_value_json_array JSON + , uinteger_compat_value_json_array JSON + + )`, + + `DROP TABLE IF EXISTS ` + "`" + `birthdays` + "`" + ``, + `CREATE TABLE ` + "`" + `birthdays` + "`" + ` ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), + name VARCHAR(50), + born DATE, + born_ut BIGINT(20) SIGNED + ) CHARSET=utf8`, + + `DROP TABLE IF EXISTS ` + "`" + `fibonacci` + "`" + ``, + `CREATE TABLE ` + "`" + `fibonacci` + "`" + ` ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), + input BIGINT(20) UNSIGNED NOT NULL, + output BIGINT(20) UNSIGNED NOT NULL + ) CHARSET=utf8`, + + `DROP TABLE IF EXISTS ` + "`" + `is_even` + "`" + ``, + `CREATE TABLE ` + "`" + `is_even` + "`" + ` ( + input BIGINT(20) UNSIGNED NOT NULL, + is_even TINYINT(1) + ) CHARSET=utf8`, + + `DROP TABLE IF EXISTS ` + "`" + `CaSe_TesT` + "`" + ``, + `CREATE TABLE ` + "`" + `CaSe_TesT` + "`" + ` ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), + case_test VARCHAR(60) + ) CHARSET=utf8`, + } + + for _, query := range batch { + driver := h.sess.Driver().(*sql.DB) + if _, err := driver.Exec(query); err != nil { + return err + } + } + + return nil +} + +var _ testsuite.Helper = &Helper{} diff --git a/mysql/mysql.go b/mysql/mysql.go index 4f375052..be0ff0f9 100644 --- a/mysql/mysql.go +++ b/mysql/mysql.go @@ -24,8 +24,7 @@ package mysql // import "upper.io/db.v3/mysql" import ( "database/sql" - "upper.io/db.v3" - + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/mysql/adapter_test.go b/mysql/mysql_test.go similarity index 59% rename from mysql/adapter_test.go rename to mysql/mysql_test.go index 5ca775c9..3813d6a2 100644 --- a/mysql/adapter_test.go +++ b/mysql/mysql_test.go @@ -19,7 +19,6 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -//go:generate bash -c "sed s/ADAPTER/mysql/g ../internal/sqladapter/testing/adapter.go.tpl > generated_test.go" package mysql import ( @@ -27,179 +26,100 @@ import ( "database/sql/driver" "fmt" "math/rand" - "os" "strconv" "testing" "time" - "github.com/stretchr/testify/assert" - "upper.io/db.v3/internal/sqladapter" + "github.com/stretchr/testify/suite" "upper.io/db.v3/lib/sqlbuilder" + "upper.io/db.v3/testsuite" ) -const ( - testTimeZone = "Canada/Eastern" -) +type int64Compat int64 -var settings = ConnectionURL{ - Database: os.Getenv("DB_NAME"), - User: os.Getenv("DB_USERNAME"), - Password: os.Getenv("DB_PASSWORD"), - Host: os.Getenv("DB_HOST") + ":" + os.Getenv("DB_PORT"), - Options: map[string]string{ - // See https://github.com/go-sql-driver/mysql/issues/9 - "parseTime": "true", - // Might require you to use mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql - "time_zone": fmt.Sprintf(`"%s"`, testTimeZone), - }, -} +type uintCompat uint -func tearUp() error { - sess := mustOpen() - defer sess.Close() - - batch := []string{ - `DROP TABLE IF EXISTS artist`, - - `CREATE TABLE artist ( - id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, - PRIMARY KEY(id), - name VARCHAR(60) - )`, - - `DROP TABLE IF EXISTS publication`, - - `CREATE TABLE publication ( - id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, - PRIMARY KEY(id), - title VARCHAR(80), - author_id BIGINT(20) - )`, - - `DROP TABLE IF EXISTS review`, - - `CREATE TABLE review ( - id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, - PRIMARY KEY(id), - publication_id BIGINT(20), - name VARCHAR(80), - comments TEXT, - created DATETIME NOT NULL - )`, - - `DROP TABLE IF EXISTS data_types`, - - `CREATE TABLE data_types ( - id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, - PRIMARY KEY(id), - _uint INT(10) UNSIGNED DEFAULT 0, - _uint8 INT(10) UNSIGNED DEFAULT 0, - _uint16 INT(10) UNSIGNED DEFAULT 0, - _uint32 INT(10) UNSIGNED DEFAULT 0, - _uint64 INT(10) UNSIGNED DEFAULT 0, - _int INT(10) DEFAULT 0, - _int8 INT(10) DEFAULT 0, - _int16 INT(10) DEFAULT 0, - _int32 INT(10) DEFAULT 0, - _int64 INT(10) DEFAULT 0, - _float32 DECIMAL(10,6), - _float64 DECIMAL(10,6), - _bool TINYINT(1), - _string text, - _blob blob, - _date TIMESTAMP NULL, - _nildate DATETIME NULL, - _ptrdate DATETIME NULL, - _defaultdate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - _time BIGINT UNSIGNED NOT NULL DEFAULT 0 - )`, - - `DROP TABLE IF EXISTS stats_test`, - - `CREATE TABLE stats_test ( - id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), - ` + "`numeric`" + ` INT(10), - ` + "`value`" + ` INT(10) - )`, - - `DROP TABLE IF EXISTS composite_keys`, - - `CREATE TABLE composite_keys ( - code VARCHAR(255) default '', - user_id VARCHAR(255) default '', - some_val VARCHAR(255) default '', - primary key (code, user_id) - )`, - - `CREATE TABLE admin ( - ID int(11) NOT NULL AUTO_INCREMENT, - Accounts varchar(255) DEFAULT '', - LoginPassWord varchar(255) DEFAULT '', - Date TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), - PRIMARY KEY (ID,Date) - ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8`, - - `CREATE TABLE my_types (id int(11) NOT NULL AUTO_INCREMENT, PRIMARY KEY(id) - , json_map JSON - , json_map_ptr JSON - - , auto_json_map JSON - , auto_json_map_string JSON - , auto_json_map_integer JSON - - , json_object JSON - , json_array JSON - - , custom_json_object JSON - , auto_custom_json_object JSON - - , custom_json_object_ptr JSON - , auto_custom_json_object_ptr JSON - - , custom_json_object_array JSON - , auto_custom_json_object_array JSON - , auto_custom_json_object_map JSON - - , integer_compat_value_json_array JSON - , string_compat_value_json_array JSON - , uinteger_compat_value_json_array JSON - - )`, - } +type stringCompat string - for _, s := range batch { - driver := sess.Driver().(*sql.DB) - if _, err := driver.Exec(s); err != nil { - return err +type uint8Compat uint8 + +type int64CompatArray []int64Compat + +type uint8CompatArray []uint8Compat + +type uintCompatArray []uintCompat + +func (u *uint8Compat) Scan(src interface{}) error { + if src != nil { + switch v := src.(type) { + case int64: + *u = uint8Compat((src).(int64)) + case []byte: + i, err := strconv.ParseInt(string(v), 10, 64) + if err != nil { + return err + } + *u = uint8Compat(i) + default: + panic(fmt.Sprintf("expected type %T", src)) } } + return nil +} +func (u *int64Compat) Scan(src interface{}) error { + if src != nil { + switch v := src.(type) { + case int64: + *u = int64Compat((src).(int64)) + case []byte: + i, err := strconv.ParseInt(string(v), 10, 64) + if err != nil { + return err + } + *u = int64Compat(i) + default: + panic(fmt.Sprintf("expected type %T", src)) + } + } return nil } -func getStats(sess sqlbuilder.Database) (map[string]int, error) { - stats := make(map[string]int) +type customJSON struct { + N string `json:"name"` + V float64 `json:"value"` +} - res, err := sess.Driver().(*sql.DB).Query(`SHOW GLOBAL STATUS LIKE '%stmt%'`) - if err != nil { - return nil, err - } - var result struct { - VariableName string `db:"Variable_name"` - Value int `db:"Value"` - } +func (c customJSON) Value() (driver.Value, error) { + return JSONValue(c) +} - iter := sqlbuilder.NewIterator(res) - for iter.Next(&result) { - stats[result.VariableName] = result.Value - } +func (c *customJSON) Scan(src interface{}) error { + return ScanJSON(c, src) +} + +type autoCustomJSON struct { + N string `json:"name"` + V float64 `json:"value"` - return stats, nil + *JSONConverter +} + +var ( + _ = driver.Valuer(&customJSON{}) + _ = sql.Scanner(&customJSON{}) +) + +type AdapterTests struct { + testsuite.Suite +} + +func (s *AdapterTests) SetupSuite() { + s.Helper = &Helper{} } -func TestInsertReturningCompositeKey_Issue383(t *testing.T) { - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) TestInsertReturningCompositeKey_Issue383() { + sess := s.SQLBuilder() type Admin struct { ID int `db:"ID,omitempty"` @@ -218,12 +138,12 @@ func TestInsertReturningCompositeKey_Issue383(t *testing.T) { adminCollection := sess.Collection("admin") err := adminCollection.InsertReturning(&a) - assert.NoError(t, err) + s.NoError(err) - assert.NotZero(t, a.ID) - assert.NotZero(t, a.Date) - assert.Equal(t, "admin", a.Accounts) - assert.Equal(t, "E10ADC3949BA59ABBE56E057F20F883E", a.LoginPassWord) + s.NotZero(a.ID) + s.NotZero(a.Date) + s.Equal("admin", a.Accounts) + s.Equal("E10ADC3949BA59ABBE56E057F20F883E", a.LoginPassWord) b := Admin{ Accounts: "admin2", @@ -232,36 +152,34 @@ func TestInsertReturningCompositeKey_Issue383(t *testing.T) { } err = adminCollection.InsertReturning(&b) - assert.NoError(t, err) + s.NoError(err) - assert.NotZero(t, b.ID) - assert.NotZero(t, b.Date) - assert.Equal(t, "admin2", b.Accounts) - assert.Equal(t, "E10ADC3949BA59ABBE56E057F20F883E", a.LoginPassWord) + s.NotZero(b.ID) + s.NotZero(b.Date) + s.Equal("admin2", b.Accounts) + s.Equal("E10ADC3949BA59ABBE56E057F20F883E", a.LoginPassWord) } -func TestIssue469_BadConnection(t *testing.T) { +func (s *AdapterTests) TestIssue469_BadConnection() { var err error - - sess := mustOpen() - defer sess.Close() + sess := s.SQLBuilder() // Ask the MySQL server to disconnect sessions that remain inactive for more // than 1 second. _, err = sess.Exec(`SET SESSION wait_timeout=1`) - assert.NoError(t, err) + s.NoError(err) // Remain inactive for 2 seconds. time.Sleep(time.Second * 2) // A query should start a new connection, even if the server disconnected us. _, err = sess.Collection("artist").Find().Count() - assert.NoError(t, err) + s.NoError(err) // This is a new session, ask the MySQL server to disconnect sessions that // remain inactive for more than 1 second. _, err = sess.Exec(`SET SESSION wait_timeout=1`) - assert.NoError(t, err) + s.NoError(err) // Remain inactive for 2 seconds. time.Sleep(time.Second * 2) @@ -277,12 +195,12 @@ func TestIssue469_BadConnection(t *testing.T) { } return nil }) - assert.NoError(t, err) + s.NoError(err) // This is a new session, ask the MySQL server to disconnect sessions that // remain inactive for more than 1 second. _, err = sess.Exec(`SET SESSION wait_timeout=1`) - assert.NoError(t, err) + s.NoError(err) err = sess.Tx(nil, func(sess sqlbuilder.Tx) error { var err error @@ -306,12 +224,11 @@ func TestIssue469_BadConnection(t *testing.T) { return nil }) - assert.Error(t, err, "Expecting an error (can't recover from this)") + s.Error(err, "Expecting an error (can't recover from this)") } -func TestMySQLTypes(t *testing.T) { - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) TestMySQLTypes() { + sess := s.SQLBuilder() type MyTypeInline struct { JSONMapPtr *JSONMap `db:"json_map_ptr,omitempty"` @@ -444,38 +361,38 @@ func TestMySQLTypes(t *testing.T) { for i := range myTypeTests { id, err := sess.Collection("my_types").Insert(myTypeTests[i]) - assert.NoError(t, err) + s.NoError(err) var actual MyType err = sess.Collection("my_types").Find(id).One(&actual) - assert.NoError(t, err) + s.NoError(err) expected := myTypeTests[i] expected.ID = id.(int64) - assert.Equal(t, expected, actual) + s.Equal(expected, actual) } for i := range myTypeTests { res, err := sess.InsertInto("my_types").Values(myTypeTests[i]).Exec() - assert.NoError(t, err) + s.NoError(err) id, err := res.LastInsertId() - assert.NoError(t, err) - assert.NotEqual(t, 0, id) + s.NoError(err) + s.NotEqual(0, id) var actual MyType err = sess.Collection("my_types").Find(id).One(&actual) - assert.NoError(t, err) + s.NoError(err) expected := myTypeTests[i] expected.ID = id - assert.Equal(t, expected, actual) + s.Equal(expected, actual) var actual2 MyType err = sess.SelectFrom("my_types").Where("id = ?", id).One(&actual2) - assert.NoError(t, err) - assert.Equal(t, expected, actual2) + s.NoError(err) + s.Equal(expected, actual2) } inserter := sess.InsertInto("my_types") @@ -483,10 +400,10 @@ func TestMySQLTypes(t *testing.T) { inserter = inserter.Values(myTypeTests[i]) } _, err := inserter.Exec() - assert.NoError(t, err) + s.NoError(err) err = sess.Collection("my_types").Truncate() - assert.NoError(t, err) + s.NoError(err) batch := sess.InsertInto("my_types").Batch(50) go func() { @@ -497,126 +414,20 @@ func TestMySQLTypes(t *testing.T) { }() err = batch.Wait() - assert.NoError(t, err) + s.NoError(err) var values []MyType err = sess.SelectFrom("my_types").All(&values) - assert.NoError(t, err) + s.NoError(err) for i := range values { expected := myTypeTests[i] expected.ID = values[i].ID - assert.Equal(t, expected, values[i]) + s.Equal(expected, values[i]) } } } -func cleanUpCheck(sess sqlbuilder.Database) (err error) { - var stats map[string]int - - stats, err = getStats(sess) - if err != nil { - return err - } - - if activeStatements := sqladapter.NumActiveStatements(); activeStatements > 128 { - return fmt.Errorf("Expecting active statements to be at most 128, got %d", activeStatements) - } - - sess.ClearCache() - - if activeStatements := sqladapter.NumActiveStatements(); activeStatements != 0 { - return fmt.Errorf("Expecting active statements to be 0, got %d", activeStatements) - } - - for i := 0; i < 10; i++ { - stats, err = getStats(sess) - if err != nil { - return err - } - - if stats["Prepared_stmt_count"] != 0 { - time.Sleep(time.Millisecond * 200) // Sometimes it takes a bit to clean prepared statements - err = fmt.Errorf(`Expecting "Prepared_stmt_count" to be 0, got %d`, stats["Prepared_stmt_count"]) - continue - } - break - } - - return err +func TestAdapter(t *testing.T) { + suite.Run(t, &AdapterTests{}) } - -type int64Compat int64 - -type uintCompat uint - -type stringCompat string - -type uint8Compat uint8 - -type int64CompatArray []int64Compat - -type uint8CompatArray []uint8Compat - -type uintCompatArray []uintCompat - -func (u *uint8Compat) Scan(src interface{}) error { - if src != nil { - switch v := src.(type) { - case int64: - *u = uint8Compat((src).(int64)) - case []byte: - i, err := strconv.ParseInt(string(v), 10, 64) - if err != nil { - return err - } - *u = uint8Compat(i) - default: - panic(fmt.Sprintf("expected type %T", src)) - } - } - return nil -} - -func (u *int64Compat) Scan(src interface{}) error { - if src != nil { - switch v := src.(type) { - case int64: - *u = int64Compat((src).(int64)) - case []byte: - i, err := strconv.ParseInt(string(v), 10, 64) - if err != nil { - return err - } - *u = int64Compat(i) - default: - panic(fmt.Sprintf("expected type %T", src)) - } - } - return nil -} - -type customJSON struct { - N string `json:"name"` - V float64 `json:"value"` -} - -func (c customJSON) Value() (driver.Value, error) { - return JSONValue(c) -} - -func (c *customJSON) Scan(src interface{}) error { - return ScanJSON(c, src) -} - -type autoCustomJSON struct { - N string `json:"name"` - V float64 `json:"value"` - - *JSONConverter -} - -var ( - _ = driver.Valuer(&customJSON{}) - _ = sql.Scanner(&customJSON{}) -) diff --git a/mysql/sql_test.go b/mysql/sql_test.go new file mode 100644 index 00000000..8e500caf --- /dev/null +++ b/mysql/sql_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package mysql + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type SQLTests struct { + testsuite.SQLTestSuite +} + +func (s *SQLTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestSQL(t *testing.T) { + suite.Run(t, &SQLTests{}) +} diff --git a/mysql/stubs_test.go b/mysql/stubs_test.go deleted file mode 100644 index 6a1eb1ac..00000000 --- a/mysql/stubs_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build !generated - -package mysql - -import ( - "log" - "testing" - - "upper.io/db.v3/lib/sqlbuilder" -) - -func mustOpen() sqlbuilder.Database { - return nil -} - -func TestMain(*testing.M) { - log.Fatal(`Tests use generated code and a custom database, please use "make test".`) -} diff --git a/mysql/template_test.go b/mysql/template_test.go index 9774a7f6..5cdad42c 100644 --- a/mysql/template_test.go +++ b/mysql/template_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/postgresql/Makefile b/postgresql/Makefile index 68c20a63..63f24f2a 100644 --- a/postgresql/Makefile +++ b/postgresql/Makefile @@ -1,5 +1,9 @@ SHELL := bash +POSTGRES_VERSION ?= 11 +POSTGRES_SUPPORTED ?= 12 $(POSTGRES_VERSION) 10 9 +PROJECT ?= upper_postgres_$(POSTGRES_VERSION) + DB_HOST ?= 127.0.0.1 DB_PORT ?= 5432 @@ -7,7 +11,10 @@ DB_NAME ?= upperio DB_USERNAME ?= upperio_user DB_PASSWORD ?= upperio//s3cr37 -TEST_FLAGS ?= +TEST_FLAGS ?= +PARALLEL_FLAGS ?= + +export POSTGRES_VERSION export DB_HOST export DB_NAME @@ -15,28 +22,19 @@ export DB_PASSWORD export DB_PORT export DB_USERNAME -build: - go build && go install - -require-client: - @if [ -z "$$(which psql)" ]; then \ - echo 'Missing "psql" command. Please install the PostgreSQL client and try again.' && \ - exit 1; \ - fi - -generate: - go generate && \ - go get -d -t -v ./... - -reset-db: require-client - SQL="" && \ - SQL+="DROP DATABASE IF EXISTS $(DB_NAME);" && \ - SQL+="DROP ROLE IF EXISTS $(DB_USERNAME);" && \ - SQL+="CREATE USER $(DB_USERNAME) WITH PASSWORD '$(DB_PASSWORD)';" && \ - SQL+="CREATE DATABASE $(DB_NAME) ENCODING 'UTF-8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8' TEMPLATE template0;" && \ - SQL+="GRANT ALL PRIVILEGES ON DATABASE $(DB_NAME) TO $(DB_USERNAME);" && \ - PGPASSWORD="$(DB_PASSWORD)" psql -U$(DB_USERNAME) -h$(DB_HOST) -p$(DB_PORT) template1 <<< $$SQL; - -test: reset-db generate - #go test -tags generated -v -race # race: limit on 8192 simultaneously alive goroutines is exceeded, dying - go test -tags generated -v $(TEST_FLAGS) +export TEST_FLAGS + +test: + go test -v $(TEST_FLAGS) + +server-up: server-down + docker-compose -p $(PROJECT) up -d && \ + sleep 10 + +server-down: + docker-compose -p $(PROJECT) down + +test-extended: + parallel $(PARALLEL_FLAGS) \ + "POSTGRES_VERSION={} DB_PORT=\$$((5432+{#})) $(MAKE) server-up test server-down" ::: \ + $(POSTGRES_SUPPORTED) diff --git a/postgresql/collection.go b/postgresql/collection.go index b07451d8..3659e2d6 100644 --- a/postgresql/collection.go +++ b/postgresql/collection.go @@ -22,9 +22,7 @@ package postgresql import ( - "database/sql" - - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" ) @@ -61,17 +59,14 @@ func (c *collection) Database() sqladapter.Database { // Insert inserts an item (map or struct) into the collection. func (c *collection) Insert(item interface{}) (interface{}, error) { - var err error - pKey := c.BaseCollection.PrimaryKeys() q := c.d.InsertInto(c.Name()).Values(item) if len(pKey) == 0 { // There is no primary key. - var res sql.Result - - if res, err = q.Exec(); err != nil { + res, err := q.Exec() + if err != nil { return nil, err } @@ -88,7 +83,7 @@ func (c *collection) Insert(item interface{}) (interface{}, error) { q = q.Returning(pKey...) var keyMap db.Cond - if err = q.Iterator().One(&keyMap); err != nil { + if err := q.Iterator().One(&keyMap); err != nil { return nil, err } diff --git a/postgresql/connection_test.go b/postgresql/connection_test.go index 2e1ec68b..f1c447f6 100644 --- a/postgresql/connection_test.go +++ b/postgresql/connection_test.go @@ -21,186 +21,106 @@ package postgresql -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestConnectionURL(t *testing.T) { c := ConnectionURL{} // Default connection string is empty. - if c.String() != "" { - t.Fatal(`Expecting default connectiong string to be empty, got:`, c.String()) - } + assert.Equal(t, "", c.String(), "Expecting default connectiong string to be empty") // Adding a host with port. c.Host = "localhost:1234" - - if c.String() != "host=localhost port=1234 sslmode=disable" { - t.Fatal(`Test failed, got:`, c.String()) - } + assert.Equal(t, "host=localhost port=1234 sslmode=disable", c.String()) // Adding a host. c.Host = "localhost" - - if c.String() != "host=localhost sslmode=disable" { - t.Fatal(`Test failed, got:`, c.String()) - } + assert.Equal(t, "host=localhost sslmode=disable", c.String()) // Adding a username. c.User = "Anakin" - - if c.String() != "user=Anakin host=localhost sslmode=disable" { - t.Fatal(`Test failed, got:`, c.String()) - } + assert.Equal(t, "user=Anakin host=localhost sslmode=disable", c.String()) // Adding a password with special characters. c.Password = "Some Sort of ' Password" - - if c.String() != `user=Anakin password=Some\ Sort\ of\ \'\ Password host=localhost sslmode=disable` { - t.Fatal(`Test failed, got:`, c.String()) - } + assert.Equal(t, `user=Anakin password=Some\ Sort\ of\ \'\ Password host=localhost sslmode=disable`, c.String()) // Adding a port. c.Host = "localhost:1234" - - if c.String() != `user=Anakin password=Some\ Sort\ of\ \'\ Password host=localhost port=1234 sslmode=disable` { - t.Fatal(`Test failed, got:`, c.String()) - } + assert.Equal(t, `user=Anakin password=Some\ Sort\ of\ \'\ Password host=localhost port=1234 sslmode=disable`, c.String()) // Adding a database. c.Database = "MyDatabase" - - if c.String() != `user=Anakin password=Some\ Sort\ of\ \'\ Password host=localhost port=1234 dbname=MyDatabase sslmode=disable` { - t.Fatal(`Test failed, got:`, c.String()) - } + assert.Equal(t, `user=Anakin password=Some\ Sort\ of\ \'\ Password host=localhost port=1234 dbname=MyDatabase sslmode=disable`, c.String()) // Adding options. c.Options = map[string]string{ "sslmode": "verify-full", } - - if c.String() != `user=Anakin password=Some\ Sort\ of\ \'\ Password host=localhost port=1234 dbname=MyDatabase sslmode=verify-full` { - t.Fatal(`Test failed, got:`, c.String()) - } + assert.Equal(t, `user=Anakin password=Some\ Sort\ of\ \'\ Password host=localhost port=1234 dbname=MyDatabase sslmode=verify-full`, c.String()) } func TestParseConnectionURL(t *testing.T) { - var u ConnectionURL - var s string - var err error - - s = "postgres://anakin:skywalker@localhost/jedis" - - if u, err = ParseURL(s); err != nil { - t.Fatal(err) - } - - if u.User != "anakin" { - t.Fatal("Failed to parse username.") - } - - if u.Password != "skywalker" { - t.Fatal("Failed to parse password.") - } - - if u.Host != "localhost" { - t.Fatal("Failed to parse hostname.") - } - - if u.Database != "jedis" { - t.Fatal("Failed to parse database.") - } - - if u.Options["sslmode"] != "" { - t.Fatal("Failed to parse SSLMode.") - } - - // case with port - s = "postgres://anakin:skywalker@localhost:1234/jedis" - if u, err = ParseURL(s); err != nil { - t.Fatal(err) - } - - if u.User != "anakin" { - t.Fatal("Failed to parse username.") - } - - if u.Password != "skywalker" { - t.Fatal("Failed to parse password.") - } - - if u.Host != "localhost:1234" { - t.Fatal("Failed to parse hostname.") - } - - if u.Database != "jedis" { - t.Fatal("Failed to parse database.") - } - - if u.Options["sslmode"] != "" { - t.Fatal("Failed to parse SSLMode.") - } - - s = "postgres://anakin:skywalker@localhost/jedis?sslmode=verify-full" - - if u, err = ParseURL(s); err != nil { - t.Fatal(err) - } - - if u.Options["sslmode"] != "verify-full" { - t.Fatal("Failed to parse SSLMode.") - } - - s = "user=anakin password=skywalker host=localhost dbname=jedis" - - if u, err = ParseURL(s); err != nil { - t.Fatal(err) - } - - if u.User != "anakin" { - t.Fatal("Failed to parse username.") - } - - if u.Password != "skywalker" { - t.Fatal("Failed to parse password.") - } - - if u.Host != "localhost" { - t.Fatal("Failed to parse hostname.") - } - - if u.Database != "jedis" { - t.Fatal("Failed to parse database.") - } - - if u.Options["sslmode"] != "" { - t.Fatal("Failed to parse SSLMode.") - } - - s = "user=anakin password=skywalker host=localhost dbname=jedis sslmode=verify-full" - - if u, err = ParseURL(s); err != nil { - t.Fatal(err) - } - - if u.Options["sslmode"] != "verify-full" { - t.Fatal("Failed to parse SSLMode.") - } - - s = "user=anakin password=skywalker host=localhost dbname=jedis sslmode=verify-full timezone=UTC" - - if u, err = ParseURL(s); err != nil { - t.Fatal(err) - } - - if len(u.Options) != 2 { - t.Fatal("Expecting exactly two options.") - } - - if u.Options["sslmode"] != "verify-full" { - t.Fatal("Failed to parse SSLMode.") - } - if u.Options["timezone"] != "UTC" { - t.Fatal("Failed to parse timezone.") + { + s := "postgres://anakin:skywalker@localhost/jedis" + u, err := ParseURL(s) + assert.NoError(t, err) + + assert.Equal(t, "anakin", u.User) + assert.Equal(t, "skywalker", u.Password) + assert.Equal(t, "localhost", u.Host) + assert.Equal(t, "jedis", u.Database) + assert.Zero(t, u.Options["sslmode"], "Failed to parse SSLMode.") + } + + { + // case with port + s := "postgres://anakin:skywalker@localhost:1234/jedis" + u, err := ParseURL(s) + assert.NoError(t, err) + assert.Equal(t, "anakin", u.User) + assert.Equal(t, "skywalker", u.Password) + assert.Equal(t, "jedis", u.Database) + assert.Equal(t, "localhost:1234", u.Host) + assert.Zero(t, u.Options["sslmode"], "Failed to parse SSLMode.") + } + + { + s := "postgres://anakin:skywalker@localhost/jedis?sslmode=verify-full" + u, err := ParseURL(s) + assert.NoError(t, err) + assert.Equal(t, "verify-full", u.Options["sslmode"]) + } + + { + s := "user=anakin password=skywalker host=localhost dbname=jedis" + u, err := ParseURL(s) + assert.NoError(t, err) + assert.Equal(t, "anakin", u.User) + assert.Equal(t, "skywalker", u.Password) + assert.Equal(t, "jedis", u.Database) + assert.Equal(t, "localhost", u.Host) + assert.Zero(t, u.Options["sslmode"], "Failed to parse SSLMode.") + } + + { + s := "user=anakin password=skywalker host=localhost dbname=jedis sslmode=verify-full" + u, err := ParseURL(s) + assert.NoError(t, err) + assert.Equal(t, "verify-full", u.Options["sslmode"]) + } + + { + s := "user=anakin password=skywalker host=localhost dbname=jedis sslmode=verify-full timezone=UTC" + u, err := ParseURL(s) + assert.NoError(t, err) + assert.Equal(t, 2, len(u.Options), "Expecting exactly two options") + assert.Equal(t, "verify-full", u.Options["sslmode"]) + assert.Equal(t, "UTC", u.Options["timezone"]) } } diff --git a/postgresql/database.go b/postgresql/database.go index 5a029ef4..ac7129c1 100644 --- a/postgresql/database.go +++ b/postgresql/database.go @@ -35,7 +35,7 @@ import ( "time" _ "github.com/lib/pq" // PostgreSQL driver. - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/internal/sqladapter/compat" "upper.io/db.v3/internal/sqladapter/exql" diff --git a/postgresql/docker-compose.yml b/postgresql/docker-compose.yml new file mode 100644 index 00000000..4f4884a3 --- /dev/null +++ b/postgresql/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3' + +services: + + server: + image: postgres:${POSTGRES_VERSION:-11} + environment: + POSTGRES_USER: ${DB_USERNAME:-upperio_user} + POSTGRES_PASSWORD: ${DB_PASSWORD:-upperio//s3cr37} + POSTGRES_DB: ${DB_NAME:-upperio} + ports: + - '${DB_HOST:-127.0.0.1}:${DB_PORT:-5432}:5432' + diff --git a/postgresql/generic_test.go b/postgresql/generic_test.go new file mode 100644 index 00000000..4ae62870 --- /dev/null +++ b/postgresql/generic_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package postgresql + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type GenericTests struct { + testsuite.GenericTestSuite +} + +func (s *GenericTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestGeneric(t *testing.T) { + suite.Run(t, &GenericTests{}) +} diff --git a/postgresql/helper_test.go b/postgresql/helper_test.go new file mode 100644 index 00000000..171c930f --- /dev/null +++ b/postgresql/helper_test.go @@ -0,0 +1,305 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package postgresql + +import ( + "database/sql" + "fmt" + "os" + + db "upper.io/db.v3" + "upper.io/db.v3/internal/sqladapter" + "upper.io/db.v3/lib/sqlbuilder" + "upper.io/db.v3/testsuite" +) + +var settings = ConnectionURL{ + Database: os.Getenv("DB_NAME"), + User: os.Getenv("DB_USERNAME"), + Password: os.Getenv("DB_PASSWORD"), + Host: os.Getenv("DB_HOST") + ":" + os.Getenv("DB_PORT"), + Options: map[string]string{ + "timezone": testsuite.TimeZone, + }, +} + +type Helper struct { + sess sqlbuilder.Database +} + +func cleanUp(sess sqlbuilder.Database) error { + stats, err := getStats(sess) + if err != nil { + return err + } + + if activeStatements := sqladapter.NumActiveStatements(); activeStatements > 128 { + return fmt.Errorf("Expecting active statements to be at most 128, got %d", activeStatements) + } + + sess.ClearCache() + + stats, err = getStats(sess) + if err != nil { + return err + } + + if stats["pg_prepared_statements_count"] != 0 { + return fmt.Errorf(`Expecting "Prepared_stmt_count" to be 0, got %d`, stats["Prepared_stmt_count"]) + } + + return nil +} + +func getStats(sess sqlbuilder.Database) (map[string]int, error) { + stats := make(map[string]int) + + row := sess.Driver().(*sql.DB).QueryRow(`SELECT count(1) AS value FROM pg_prepared_statements`) + + var value int + err := row.Scan(&value) + if err != nil { + return nil, err + } + + stats["pg_prepared_statements_count"] = value + + return stats, nil +} + +func (h *Helper) Session() db.Database { + return h.sess +} + +func (h *Helper) SQLBuilder() sqlbuilder.Database { + return h.sess +} + +func (h *Helper) Adapter() string { + return "postgresql" +} + +func (h *Helper) TearDown() error { + if err := cleanUp(h.sess); err != nil { + return err + } + + return h.sess.Close() +} + +func (h *Helper) TearUp() error { + var err error + + h.sess, err = Open(settings) + if err != nil { + return err + } + + batch := []string{ + `DROP TABLE IF EXISTS artist`, + `CREATE TABLE artist ( + id serial primary key, + name varchar(60) + )`, + + `DROP TABLE IF EXISTS publication`, + `CREATE TABLE publication ( + id serial primary key, + title varchar(80), + author_id integer + )`, + + `DROP TABLE IF EXISTS review`, + `CREATE TABLE review ( + id serial primary key, + publication_id integer, + name varchar(80), + comments text, + created timestamp without time zone + )`, + + `DROP TABLE IF EXISTS data_types`, + `CREATE TABLE data_types ( + id serial primary key, + _uint integer, + _uint8 integer, + _uint16 integer, + _uint32 integer, + _uint64 integer, + _int integer, + _int8 integer, + _int16 integer, + _int32 integer, + _int64 integer, + _float32 numeric(10,6), + _float64 numeric(10,6), + _bool boolean, + _string text, + _blob bytea, + _date timestamp with time zone, + _nildate timestamp without time zone null, + _ptrdate timestamp without time zone, + _defaultdate timestamp without time zone DEFAULT now(), + _time bigint + )`, + + `DROP TABLE IF EXISTS stats_test`, + `CREATE TABLE stats_test ( + id serial primary key, + numeric integer, + value integer + )`, + + `DROP TABLE IF EXISTS composite_keys`, + `CREATE TABLE composite_keys ( + code varchar(255) default '', + user_id varchar(255) default '', + some_val varchar(255) default '', + primary key (code, user_id) + )`, + + `DROP TABLE IF EXISTS option_types`, + `CREATE TABLE option_types ( + id serial primary key, + name varchar(255) default '', + tags varchar(64)[], + settings jsonb + )`, + + `DROP TABLE IF EXISTS test_schema.test`, + `DROP SCHEMA IF EXISTS test_schema`, + + `CREATE SCHEMA test_schema`, + `CREATE TABLE test_schema.test (id integer)`, + + `DROP TABLE IF EXISTS pg_types`, + `CREATE TABLE pg_types (id serial primary key + , uint8_value smallint + , uint8_value_array smallint[] + + , int64_value smallint + , int64_value_array smallint[] + + , integer_array integer[] + , string_array text[] + , jsonb_map jsonb + + , integer_array_ptr integer[] + , string_array_ptr text[] + , jsonb_map_ptr jsonb + + , auto_integer_array integer[] + , auto_string_array text[] + , auto_jsonb_map jsonb + , auto_jsonb_map_string jsonb + , auto_jsonb_map_integer jsonb + + , jsonb_object jsonb + , jsonb_array jsonb + + , custom_jsonb_object jsonb + , auto_custom_jsonb_object jsonb + + , custom_jsonb_object_ptr jsonb + , auto_custom_jsonb_object_ptr jsonb + + , custom_jsonb_object_array jsonb + , auto_custom_jsonb_object_array jsonb + , auto_custom_jsonb_object_map jsonb + + , string_value varchar(255) + , integer_value int + , varchar_value varchar(64) + , decimal_value decimal + + , integer_compat_value int + , uinteger_compat_value int + , string_compat_value text + + , integer_compat_value_jsonb_array jsonb + , string_compat_value_jsonb_array jsonb + , uinteger_compat_value_jsonb_array jsonb + + , string_value_ptr varchar(255) + , integer_value_ptr int + , varchar_value_ptr varchar(64) + , decimal_value_ptr decimal + + )`, + + `DROP TABLE IF EXISTS issue_370`, + `CREATE TABLE issue_370 ( + id UUID PRIMARY KEY, + name VARCHAR(25) + )`, + + `DROP TABLE IF EXISTS issue_370_2`, + `CREATE TABLE issue_370_2 ( + id INTEGER[3] PRIMARY KEY, + name VARCHAR(25) + )`, + + `DROP TABLE IF EXISTS varchar_primary_key`, + `CREATE TABLE varchar_primary_key ( + address VARCHAR(42) PRIMARY KEY NOT NULL, + name VARCHAR(25) + )`, + + `DROP TABLE IF EXISTS "birthdays"`, + `CREATE TABLE "birthdays" ( + "id" serial primary key, + "name" CHARACTER VARYING(50), + "born" TIMESTAMP WITH TIME ZONE, + "born_ut" INT + )`, + + `DROP TABLE IF EXISTS "fibonacci"`, + `CREATE TABLE "fibonacci" ( + "id" serial primary key, + "input" NUMERIC, + "output" NUMERIC + )`, + + `DROP TABLE IF EXISTS "is_even"`, + `CREATE TABLE "is_even" ( + "input" NUMERIC, + "is_even" BOOL + )`, + + `DROP TABLE IF EXISTS "CaSe_TesT"`, + `CREATE TABLE "CaSe_TesT" ( + "id" SERIAL PRIMARY KEY, + "case_test" VARCHAR(60) + )`, + } + + for _, query := range batch { + driver := h.sess.Driver().(*sql.DB) + if _, err := driver.Exec(query); err != nil { + return err + } + } + + return nil +} + +var _ testsuite.Helper = &Helper{} diff --git a/postgresql/local_test.go b/postgresql/local_test.go deleted file mode 100644 index d10f2140..00000000 --- a/postgresql/local_test.go +++ /dev/null @@ -1,282 +0,0 @@ -package postgresql - -import ( - "database/sql" - "testing" - - "github.com/stretchr/testify/assert" - "upper.io/db.v3" -) - -func TestStringAndInt64Array(t *testing.T) { - sess := mustOpen() - driver := sess.Driver().(*sql.DB) - - defer func() { - driver.Exec(`DROP TABLE IF EXISTS array_types`) - sess.Close() - }() - - if _, err := driver.Exec(` - CREATE TABLE array_types ( - id serial primary key, - integers bigint[] DEFAULT NULL, - strings varchar(64)[] - )`); err != nil { - assert.NoError(t, err) - } - - arrayTypes := sess.Collection("array_types") - err := arrayTypes.Truncate() - assert.NoError(t, err) - - type arrayType struct { - ID int64 `db:"id,pk"` - Integers Int64Array `db:"integers"` - Strings StringArray `db:"strings"` - } - - tt := []arrayType{ - // Test nil arrays. - arrayType{ - ID: 1, - Integers: nil, - Strings: nil, - }, - - // Test empty arrays. - arrayType{ - ID: 2, - Integers: []int64{}, - Strings: []string{}, - }, - - // Test non-empty arrays. - arrayType{ - ID: 3, - Integers: []int64{1, 2, 3}, - Strings: []string{"1", "2", "3"}, - }, - } - - for _, item := range tt { - id, err := arrayTypes.Insert(item) - assert.NoError(t, err) - - if pk, ok := id.(int64); !ok || pk == 0 { - t.Fatalf("Expecting an ID.") - } - - var itemCheck arrayType - err = arrayTypes.Find(db.Cond{"id": id}).One(&itemCheck) - assert.NoError(t, err) - assert.Len(t, itemCheck.Integers, len(item.Integers)) - assert.Len(t, itemCheck.Strings, len(item.Strings)) - - assert.Equal(t, item, itemCheck) - } -} - -func TestIssue210(t *testing.T) { - list := []string{ - `DROP TABLE IF EXISTS testing123`, - `DROP TABLE IF EXISTS hello`, - `CREATE TABLE IF NOT EXISTS testing123 ( - ID INT PRIMARY KEY NOT NULL, - NAME TEXT NOT NULL - ) - `, - `CREATE TABLE IF NOT EXISTS hello ( - ID INT PRIMARY KEY NOT NULL, - NAME TEXT NOT NULL - )`, - } - - sess := mustOpen() - defer sess.Close() - - tx, err := sess.NewTx(nil) - assert.NoError(t, err) - - for i := range list { - _, err = tx.Exec(list[i]) - assert.NoError(t, err) - } - - err = tx.Commit() - assert.NoError(t, err) - - _, err = sess.Collection("testing123").Find().Count() - assert.NoError(t, err) - - _, err = sess.Collection("hello").Find().Count() - assert.NoError(t, err) -} - -func TestPreparedStatements(t *testing.T) { - sess := mustOpen() - defer sess.Close() - - var val int - - { - stmt, err := sess.Prepare(`SELECT 1`) - assert.NoError(t, err) - assert.NotNil(t, stmt) - - q, err := stmt.Query() - assert.NoError(t, err) - assert.NotNil(t, q) - assert.True(t, q.Next()) - - err = q.Scan(&val) - assert.NoError(t, err) - - err = q.Close() - assert.NoError(t, err) - - assert.Equal(t, 1, val) - - err = stmt.Close() - assert.NoError(t, err) - } - - { - tx, err := sess.NewTx(nil) - assert.NoError(t, err) - - stmt, err := tx.Prepare(`SELECT 2`) - assert.NoError(t, err) - assert.NotNil(t, stmt) - - q, err := stmt.Query() - assert.NoError(t, err) - assert.NotNil(t, q) - assert.True(t, q.Next()) - - err = q.Scan(&val) - assert.NoError(t, err) - - err = q.Close() - assert.NoError(t, err) - - assert.Equal(t, 2, val) - - err = stmt.Close() - assert.NoError(t, err) - - err = tx.Commit() - assert.NoError(t, err) - } - - { - stmt, err := sess.Select(3).Prepare() - assert.NoError(t, err) - assert.NotNil(t, stmt) - - q, err := stmt.Query() - assert.NoError(t, err) - assert.NotNil(t, q) - assert.True(t, q.Next()) - - err = q.Scan(&val) - assert.NoError(t, err) - - err = q.Close() - assert.NoError(t, err) - - assert.Equal(t, 3, val) - - err = stmt.Close() - assert.NoError(t, err) - } -} - -func TestNonTrivialSubqueries(t *testing.T) { - sess := mustOpen() - defer sess.Close() - - { - q, err := sess.Query(`WITH test AS (?) ?`, - sess.Select("id AS foo").From("artist"), - sess.Select("foo").From("test").Where("foo > ?", 0), - ) - - assert.NoError(t, err) - assert.NotNil(t, q) - - assert.True(t, q.Next()) - - var number int - assert.NoError(t, q.Scan(&number)) - - assert.Equal(t, 1, number) - assert.NoError(t, q.Close()) - } - - { - row, err := sess.QueryRow(`WITH test AS (?) ?`, - sess.Select("id AS foo").From("artist"), - sess.Select("foo").From("test").Where("foo > ?", 0), - ) - - assert.NoError(t, err) - assert.NotNil(t, row) - - var number int - assert.NoError(t, row.Scan(&number)) - - assert.Equal(t, 1, number) - } - - { - res, err := sess.Exec(`UPDATE artist a1 SET id = ?`, - sess.Select(db.Raw("id + 1")).From("artist a2").Where("a2.id = a1.id"), - ) - - assert.NoError(t, err) - assert.NotNil(t, res) - } - - { - q, err := sess.Query(db.Raw(`WITH test AS (?) ?`, - sess.Select("id AS foo").From("artist"), - sess.Select("foo").From("test").Where("foo > ?", 0), - )) - - assert.NoError(t, err) - assert.NotNil(t, q) - - assert.True(t, q.Next()) - - var number int - assert.NoError(t, q.Scan(&number)) - - assert.Equal(t, 2, number) - assert.NoError(t, q.Close()) - } - - { - row, err := sess.QueryRow(db.Raw(`WITH test AS (?) ?`, - sess.Select("id AS foo").From("artist"), - sess.Select("foo").From("test").Where("foo > ?", 0), - )) - - assert.NoError(t, err) - assert.NotNil(t, row) - - var number int - assert.NoError(t, row.Scan(&number)) - - assert.Equal(t, 2, number) - } - - { - res, err := sess.Exec(db.Raw(`UPDATE artist a1 SET id = ?`, - sess.Select(db.Raw("id + 1")).From("artist a2").Where("a2.id = a1.id"), - )) - - assert.NoError(t, err) - assert.NotNil(t, res) - } -} diff --git a/postgresql/postgresql.go b/postgresql/postgresql.go index 0a79e2d6..32a8db4b 100644 --- a/postgresql/postgresql.go +++ b/postgresql/postgresql.go @@ -24,8 +24,7 @@ package postgresql // import "upper.io/db.v3/postgresql" import ( "database/sql" - "upper.io/db.v3" - + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/postgresql/adapter_test.go b/postgresql/postgresql_test.go similarity index 69% rename from postgresql/adapter_test.go rename to postgresql/postgresql_test.go index 8b5e50b1..5e714973 100644 --- a/postgresql/adapter_test.go +++ b/postgresql/postgresql_test.go @@ -19,7 +19,6 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -//go:generate bash -c "sed s/ADAPTER/postgresql/g ../internal/sqladapter/testing/adapter.go.tpl > generated_test.go" package postgresql import ( @@ -27,212 +26,20 @@ import ( "database/sql/driver" "fmt" "math/rand" - "os" "strconv" "strings" "sync" "testing" "time" - "github.com/satori/go.uuid" + uuid "github.com/satori/go.uuid" "github.com/stretchr/testify/assert" - "upper.io/db.v3" - "upper.io/db.v3/internal/sqladapter" + "github.com/stretchr/testify/suite" + db "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" + "upper.io/db.v3/testsuite" ) -const ( - testTimeZone = "Canada/Eastern" -) - -var settings = ConnectionURL{ - Database: os.Getenv("DB_NAME"), - User: os.Getenv("DB_USERNAME"), - Password: os.Getenv("DB_PASSWORD"), - Host: os.Getenv("DB_HOST") + ":" + os.Getenv("DB_PORT"), - Options: map[string]string{ - "timezone": testTimeZone, - }, -} - -func tearUp() error { - sess := mustOpen() - defer sess.Close() - - batch := []string{ - `DROP TABLE IF EXISTS artist`, - - `CREATE TABLE artist ( - id serial primary key, - name varchar(60) - )`, - - `DROP TABLE IF EXISTS publication`, - - `CREATE TABLE publication ( - id serial primary key, - title varchar(80), - author_id integer - )`, - - `DROP TABLE IF EXISTS review`, - - `CREATE TABLE review ( - id serial primary key, - publication_id integer, - name varchar(80), - comments text, - created timestamp without time zone - )`, - - `DROP TABLE IF EXISTS data_types`, - - `CREATE TABLE data_types ( - id serial primary key, - _uint integer, - _uint8 integer, - _uint16 integer, - _uint32 integer, - _uint64 integer, - _int integer, - _int8 integer, - _int16 integer, - _int32 integer, - _int64 integer, - _float32 numeric(10,6), - _float64 numeric(10,6), - _bool boolean, - _string text, - _blob bytea, - _date timestamp with time zone, - _nildate timestamp without time zone null, - _ptrdate timestamp without time zone, - _defaultdate timestamp without time zone DEFAULT now(), - _time bigint - )`, - - `DROP TABLE IF EXISTS stats_test`, - - `CREATE TABLE stats_test ( - id serial primary key, - numeric integer, - value integer - )`, - - `DROP TABLE IF EXISTS composite_keys`, - - `CREATE TABLE composite_keys ( - code varchar(255) default '', - user_id varchar(255) default '', - some_val varchar(255) default '', - primary key (code, user_id) - )`, - - `DROP TABLE IF EXISTS option_types`, - - `CREATE TABLE option_types ( - id serial primary key, - name varchar(255) default '', - tags varchar(64)[], - settings jsonb - )`, - - `DROP TABLE IF EXISTS test_schema.test`, - - `DROP SCHEMA IF EXISTS test_schema`, - - `CREATE SCHEMA test_schema`, - - `CREATE TABLE test_schema.test (id integer)`, - - `DROP TABLE IF EXISTS pg_types`, - - `CREATE TABLE pg_types (id serial primary key - , uint8_value smallint - , uint8_value_array smallint[] - - , int64_value smallint - , int64_value_array smallint[] - - , integer_array integer[] - , string_array text[] - , jsonb_map jsonb - - , integer_array_ptr integer[] - , string_array_ptr text[] - , jsonb_map_ptr jsonb - - , auto_integer_array integer[] - , auto_string_array text[] - , auto_jsonb_map jsonb - , auto_jsonb_map_string jsonb - , auto_jsonb_map_integer jsonb - - , jsonb_object jsonb - , jsonb_array jsonb - - , custom_jsonb_object jsonb - , auto_custom_jsonb_object jsonb - - , custom_jsonb_object_ptr jsonb - , auto_custom_jsonb_object_ptr jsonb - - , custom_jsonb_object_array jsonb - , auto_custom_jsonb_object_array jsonb - , auto_custom_jsonb_object_map jsonb - - , string_value varchar(255) - , integer_value int - , varchar_value varchar(64) - , decimal_value decimal - - , integer_compat_value int - , uinteger_compat_value int - , string_compat_value text - - , integer_compat_value_jsonb_array jsonb - , string_compat_value_jsonb_array jsonb - , uinteger_compat_value_jsonb_array jsonb - - , string_value_ptr varchar(255) - , integer_value_ptr int - , varchar_value_ptr varchar(64) - , decimal_value_ptr decimal - - )`, - - `DROP TABLE IF EXISTS issue_370`, - - `CREATE TABLE issue_370 ( - id UUID PRIMARY KEY, - name VARCHAR(25) - )`, - - `DROP TABLE IF EXISTS issue_370_2`, - - `CREATE TABLE issue_370_2 ( - id INTEGER[3] PRIMARY KEY, - name VARCHAR(25) - )`, - - `DROP TABLE IF EXISTS varchar_primary_key`, - - `CREATE TABLE varchar_primary_key ( - address VARCHAR(42) PRIMARY KEY NOT NULL, - name VARCHAR(25) - )`, - } - - for _, s := range batch { - driver := sess.Driver().(*sql.DB) - if _, err := driver.Exec(s); err != nil { - return err - } - } - - return nil -} - type customJSONB struct { N string `json:"name"` V float64 `json:"value"` @@ -316,28 +123,33 @@ func (u int64CompatArray) WrapValue(src interface{}) interface{} { return Array(src) } -func TestIssue469_BadConnection(t *testing.T) { - var err error +type AdapterTests struct { + testsuite.Suite +} - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) SetupSuite() { + s.Helper = &Helper{} +} + +func (s *AdapterTests) Test_Issue469_BadConnection() { + sess := s.SQLBuilder() // Ask the PostgreSQL server to disconnect sessions that remain inactive for more // than 1 second. - _, err = sess.Exec(`SET SESSION idle_in_transaction_session_timeout=1000`) - assert.NoError(t, err) + _, err := sess.Exec(`SET SESSION idle_in_transaction_session_timeout=1000`) + s.NoError(err) // Remain inactive for 2 seconds. time.Sleep(time.Second * 2) // A query should start a new connection, even if the server disconnected us. _, err = sess.Collection("artist").Find().Count() - assert.NoError(t, err) + s.NoError(err) // This is a new session, ask the PostgreSQL server to disconnect sessions that // remain inactive for more than 1 second. _, err = sess.Exec(`SET SESSION idle_in_transaction_session_timeout=1000`) - assert.NoError(t, err) + s.NoError(err) // Remain inactive for 2 seconds. time.Sleep(time.Second * 2) @@ -353,12 +165,12 @@ func TestIssue469_BadConnection(t *testing.T) { } return nil }) - assert.NoError(t, err) + s.NoError(err) // This is a new session, ask the PostgreSQL server to disconnect sessions that // remain inactive for more than 1 second. _, err = sess.Exec(`SET SESSION idle_in_transaction_session_timeout=1000`) - assert.NoError(t, err) + s.NoError(err) err = sess.Tx(nil, func(sess sqlbuilder.Tx) error { var err error @@ -382,11 +194,10 @@ func TestIssue469_BadConnection(t *testing.T) { return nil }) - assert.Error(t, err, "Expecting an error (can't recover from this)") + s.Error(err, "Expecting an error (can't recover from this)") } func testPostgreSQLTypes(t *testing.T, sess sqlbuilder.Database) { - type PGTypeInline struct { IntegerArrayPtr *Int64Array `db:"integer_array_ptr,omitempty"` StringArrayPtr *StringArray `db:"string_array_ptr,omitempty"` @@ -754,13 +565,12 @@ func testPostgreSQLTypes(t *testing.T, sess sqlbuilder.Database) { } } -func TestOptionTypes(t *testing.T) { - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) TestOptionTypes() { + sess := s.SQLBuilder() optionTypes := sess.Collection("option_types") err := optionTypes.Truncate() - assert.NoError(t, err) + s.NoError(err) // TODO: lets do some benchmarking on these auto-wrapped option types.. @@ -783,18 +593,18 @@ func TestOptionTypes(t *testing.T) { } id, err := optionTypes.Insert(item1) - assert.NoError(t, err) + s.NoError(err) if pk, ok := id.(int64); !ok || pk == 0 { - t.Fatalf("Expecting an ID.") + s.T().Fatalf("Expecting an ID.") } var item1Chk optionType err = optionTypes.Find(db.Cond{"id": id}).One(&item1Chk) - assert.NoError(t, err) + s.NoError(err) - assert.Equal(t, float64(1), item1Chk.Settings["a"]) - assert.Equal(t, "toronto", item1Chk.Tags[0]) + s.Equal(float64(1), item1Chk.Settings["a"]) + s.Equal("toronto", item1Chk.Tags[0]) // Item 1 B item1b := &optionType{ @@ -804,18 +614,18 @@ func TestOptionTypes(t *testing.T) { } id, err = optionTypes.Insert(item1b) - assert.NoError(t, err) + s.NoError(err) if pk, ok := id.(int64); !ok || pk == 0 { - t.Fatalf("Expecting an ID.") + s.T().Fatalf("Expecting an ID.") } var item1bChk optionType err = optionTypes.Find(db.Cond{"id": id}).One(&item1bChk) - assert.NoError(t, err) + s.NoError(err) - assert.Equal(t, float64(1), item1bChk.Settings["go"]) - assert.Equal(t, "love", item1bChk.Tags[0]) + s.Equal(float64(1), item1bChk.Settings["go"]) + s.Equal("love", item1bChk.Tags[0]) // Item 1 C item1c := &optionType{ @@ -823,18 +633,18 @@ func TestOptionTypes(t *testing.T) { } id, err = optionTypes.Insert(item1c) - assert.NoError(t, err) + s.NoError(err) if pk, ok := id.(int64); !ok || pk == 0 { - t.Fatalf("Expecting an ID.") + s.T().Fatalf("Expecting an ID.") } var item1cChk optionType err = optionTypes.Find(db.Cond{"id": id}).One(&item1cChk) - assert.NoError(t, err) + s.NoError(err) - assert.Zero(t, len(item1cChk.Tags)) - assert.Zero(t, len(item1cChk.Settings)) + s.Zero(len(item1cChk.Tags)) + s.Zero(len(item1cChk.Settings)) // An option type to pointer jsonb field type optionType2 struct { @@ -849,23 +659,23 @@ func TestOptionTypes(t *testing.T) { } id, err = optionTypes.Insert(item2) - assert.NoError(t, err) + s.NoError(err) if pk, ok := id.(int64); !ok || pk == 0 { - t.Fatalf("Expecting an ID.") + s.T().Fatalf("Expecting an ID.") } var item2Chk optionType2 res := optionTypes.Find(db.Cond{"id": id}) err = res.One(&item2Chk) - assert.NoError(t, err) + s.NoError(err) - assert.Equal(t, id.(int64), item2Chk.ID) + s.Equal(id.(int64), item2Chk.ID) - assert.Equal(t, item2Chk.Name, item2.Name) + s.Equal(item2Chk.Name, item2.Name) - assert.Equal(t, item2Chk.Tags[0], item2.Tags[0]) - assert.Equal(t, len(item2Chk.Tags), len(item2.Tags)) + s.Equal(item2Chk.Tags[0], item2.Tags[0]) + s.Equal(len(item2Chk.Tags), len(item2.Tags)) // Update the value m := JSONBMap{} @@ -873,14 +683,14 @@ func TestOptionTypes(t *testing.T) { m["num"] = 31337 item2.Settings = &m err = res.Update(item2) - assert.NoError(t, err) + s.NoError(err) err = res.One(&item2Chk) - assert.NoError(t, err) + s.NoError(err) - assert.Equal(t, float64(31337), (*item2Chk.Settings)["num"].(float64)) + s.Equal(float64(31337), (*item2Chk.Settings)["num"].(float64)) - assert.Equal(t, "javascript", (*item2Chk.Settings)["lang"]) + s.Equal("javascript", (*item2Chk.Settings)["lang"]) // An option type to pointer string array field type optionType3 struct { @@ -897,15 +707,15 @@ func TestOptionTypes(t *testing.T) { } id, err = optionTypes.Insert(item3) - assert.NoError(t, err) + s.NoError(err) if pk, ok := id.(int64); !ok || pk == 0 { - t.Fatalf("Expecting an ID.") + s.T().Fatalf("Expecting an ID.") } var item3Chk optionType2 err = optionTypes.Find(db.Cond{"id": id}).One(&item3Chk) - assert.NoError(t, err) + s.NoError(err) } type Settings struct { @@ -920,14 +730,13 @@ func (s Settings) Value() (driver.Value, error) { return JSONBValue(s) } -func TestOptionTypeJsonbStruct(t *testing.T) { - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) TestOptionTypeJsonbStruct() { + sess := s.SQLBuilder() optionTypes := sess.Collection("option_types") err := optionTypes.Truncate() - assert.NoError(t, err) + s.NoError(err) type OptionType struct { ID int64 `db:"id,omitempty"` @@ -943,40 +752,38 @@ func TestOptionTypeJsonbStruct(t *testing.T) { } id, err := optionTypes.Insert(item1) - assert.NoError(t, err) + s.NoError(err) if pk, ok := id.(int64); !ok || pk == 0 { - t.Fatalf("Expecting an ID.") + s.T().Fatalf("Expecting an ID.") } var item1Chk OptionType err = optionTypes.Find(db.Cond{"id": id}).One(&item1Chk) - assert.NoError(t, err) + s.NoError(err) - assert.Equal(t, 2, len(item1Chk.Tags)) - assert.Equal(t, "aah", item1Chk.Tags[0]) - assert.Equal(t, "a", item1Chk.Settings.Name) - assert.Equal(t, int64(123), item1Chk.Settings.Num) + s.Equal(2, len(item1Chk.Tags)) + s.Equal("aah", item1Chk.Tags[0]) + s.Equal("a", item1Chk.Settings.Name) + s.Equal(int64(123), item1Chk.Settings.Num) } -func TestSchemaCollection(t *testing.T) { - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) TestSchemaCollection() { + sess := s.SQLBuilder() col := sess.Collection("test_schema.test") _, err := col.Insert(map[string]int{"id": 9}) - assert.Equal(t, nil, err) + s.Equal(nil, err) var dump []map[string]int err = col.Find().All(&dump) - assert.Nil(t, err) - assert.Equal(t, 1, len(dump)) - assert.Equal(t, 9, dump[0]["id"]) + s.Nil(err) + s.Equal(1, len(dump)) + s.Equal(9, dump[0]["id"]) } -func TestMaxOpenConns_Issue340(t *testing.T) { - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) Test_Issue340_MaxOpenConns() { + sess := s.SQLBuilder() sess.SetMaxOpenConns(5) @@ -988,7 +795,7 @@ func TestMaxOpenConns_Issue340(t *testing.T) { _, err := sess.Exec(fmt.Sprintf(`SELECT pg_sleep(1.%d)`, i)) if err != nil { - t.Fatal(err) + s.T().Fatal(err) } }(i) } @@ -998,9 +805,8 @@ func TestMaxOpenConns_Issue340(t *testing.T) { sess.SetMaxOpenConns(0) } -func TestUUIDInsert_Issue370(t *testing.T) { - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) Test_Issue370_InsertUUID() { + sess := s.SQLBuilder() { type itemT struct { @@ -1017,20 +823,20 @@ func TestUUIDInsert_Issue370(t *testing.T) { col := sess.Collection("issue_370") err := col.Truncate() - assert.NoError(t, err) + s.NoError(err) err = col.InsertReturning(&item1) - assert.NoError(t, err) + s.NoError(err) var item2 itemT err = col.Find(item1.ID).One(&item2) - assert.NoError(t, err) - assert.Equal(t, item1.Name, item2.Name) + s.NoError(err) + s.Equal(item1.Name, item2.Name) var item3 itemT err = col.Find(db.Cond{"id": item1.ID}).One(&item3) - assert.NoError(t, err) - assert.Equal(t, item1.Name, item3.Name) + s.NoError(err) + s.Equal(item1.Name, item3.Name) } { @@ -1046,20 +852,20 @@ func TestUUIDInsert_Issue370(t *testing.T) { col := sess.Collection("issue_370") err := col.Truncate() - assert.NoError(t, err) + s.NoError(err) err = col.InsertReturning(&item1) - assert.NoError(t, err) + s.NoError(err) var item2 itemT err = col.Find(item1.ID).One(&item2) - assert.NoError(t, err) - assert.Equal(t, item1.Name, item2.Name) + s.NoError(err) + s.Equal(item1.Name, item2.Name) var item3 itemT err = col.Find(db.Cond{"id": item1.ID}).One(&item3) - assert.NoError(t, err) - assert.Equal(t, item1.Name, item3.Name) + s.NoError(err) + s.Equal(item1.Name, item3.Name) } { @@ -1075,26 +881,25 @@ func TestUUIDInsert_Issue370(t *testing.T) { col := sess.Collection("issue_370_2") err := col.Truncate() - assert.NoError(t, err) + s.NoError(err) err = col.InsertReturning(&item1) - assert.NoError(t, err) + s.NoError(err) var item2 itemT err = col.Find(item1.ID).One(&item2) - assert.NoError(t, err) - assert.Equal(t, item1.Name, item2.Name) + s.NoError(err) + s.Equal(item1.Name, item2.Name) var item3 itemT err = col.Find(db.Cond{"id": item1.ID}).One(&item3) - assert.NoError(t, err) - assert.Equal(t, item1.Name, item3.Name) + s.NoError(err) + s.Equal(item1.Name, item3.Name) } } -func TestInsertVarcharPrimaryKey(t *testing.T) { - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) TestInsertVarcharPrimaryKey() { + sess := s.SQLBuilder() { type itemT struct { @@ -1109,26 +914,25 @@ func TestInsertVarcharPrimaryKey(t *testing.T) { col := sess.Collection("varchar_primary_key") err := col.Truncate() - assert.NoError(t, err) + s.NoError(err) err = col.InsertReturning(&item1) - assert.NoError(t, err) + s.NoError(err) var item2 itemT err = col.Find(db.Cond{"address": item1.Address}).One(&item2) - assert.NoError(t, err) - assert.Equal(t, item1.Name, item2.Name) + s.NoError(err) + s.Equal(item1.Name, item2.Name) var item3 itemT err = col.Find(db.Cond{"address": item1.Address}).One(&item3) - assert.NoError(t, err) - assert.Equal(t, item1.Name, item3.Name) + s.NoError(err) + s.Equal(item1.Name, item3.Name) } } -func TestTxOptions_Issue409(t *testing.T) { - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) Test_Issue409_TxOptions() { + sess := s.SQLBuilder() sess.SetTxOptions(sql.TxOptions{ ReadOnly: true, @@ -1142,102 +946,338 @@ func TestTxOptions_Issue409(t *testing.T) { "author_id": 1, } err := col.InsertReturning(&row) - assert.Error(t, err) + s.Error(err) - assert.True(t, strings.Contains(err.Error(), "read-only transaction")) + s.True(strings.Contains(err.Error(), "read-only transaction")) } } -func TestEscapeQuestionMark(t *testing.T) { - sess := mustOpen() - defer sess.Close() +func (s *AdapterTests) TestEscapeQuestionMark() { + sess := s.SQLBuilder() var val bool { res, err := sess.QueryRow(`SELECT '{"mykey":["val1", "val2"]}'::jsonb->'mykey' ?? ?`, "val2") - assert.NoError(t, err) + s.NoError(err) err = res.Scan(&val) - assert.NoError(t, err) - assert.Equal(t, true, val) + s.NoError(err) + s.Equal(true, val) } { res, err := sess.QueryRow(`SELECT ?::jsonb->'mykey' ?? ?`, `{"mykey":["val1", "val2"]}`, `val2`) - assert.NoError(t, err) + s.NoError(err) err = res.Scan(&val) - assert.NoError(t, err) - assert.Equal(t, true, val) + s.NoError(err) + s.Equal(true, val) } { res, err := sess.QueryRow(`SELECT ?::jsonb->? ?? ?`, `{"mykey":["val1", "val2"]}`, `mykey`, `val2`) - assert.NoError(t, err) + s.NoError(err) err = res.Scan(&val) - assert.NoError(t, err) - assert.Equal(t, true, val) + s.NoError(err) + s.Equal(true, val) } } -func TestTextMode_Issue391(t *testing.T) { - sess := mustOpen() - defer sess.Close() - - testPostgreSQLTypes(t, sess) +func (s *AdapterTests) Test_Issue391_TextMode() { + testPostgreSQLTypes(s.T(), s.SQLBuilder()) } -func TestBinaryMode_Issue391(t *testing.T) { - settingsWithBinaryMode := settings - settingsWithBinaryMode.Options["binary_parameters"] = "yes" +func (s *AdapterTests) Test_Issue391_BinaryMode() { + settingsWithBinaryMode := ConnectionURL{ + Database: settings.Database, + User: settings.User, + Password: settings.Password, + Host: settings.Host, + Options: map[string]string{ + "timezone": testsuite.TimeZone, + "binary_parameters": "yes", + }, + } sess, err := Open(settingsWithBinaryMode) if err != nil { - t.Fatal(err) + s.T().Fatal(err) } defer sess.Close() - testPostgreSQLTypes(t, sess) + testPostgreSQLTypes(s.T(), sess) } -func getStats(sess sqlbuilder.Database) (map[string]int, error) { - stats := make(map[string]int) +func (s *AdapterTests) TestStringAndInt64Array() { + sess := s.SQLBuilder() + driver := sess.Driver().(*sql.DB) - row := sess.Driver().(*sql.DB).QueryRow(`SELECT count(1) AS value FROM pg_prepared_statements`) + defer func() { + driver.Exec(`DROP TABLE IF EXISTS array_types`) + }() - var value int - err := row.Scan(&value) - if err != nil { - return nil, err + if _, err := driver.Exec(` + CREATE TABLE array_types ( + id serial primary key, + integers bigint[] DEFAULT NULL, + strings varchar(64)[] + )`); err != nil { + s.NoError(err) + } + + arrayTypes := sess.Collection("array_types") + err := arrayTypes.Truncate() + s.NoError(err) + + type arrayType struct { + ID int64 `db:"id,pk"` + Integers Int64Array `db:"integers"` + Strings StringArray `db:"strings"` + } + + tt := []arrayType{ + // Test nil arrays. + arrayType{ + ID: 1, + Integers: nil, + Strings: nil, + }, + + // Test empty arrays. + arrayType{ + ID: 2, + Integers: []int64{}, + Strings: []string{}, + }, + + // Test non-empty arrays. + arrayType{ + ID: 3, + Integers: []int64{1, 2, 3}, + Strings: []string{"1", "2", "3"}, + }, } - stats["pg_prepared_statements_count"] = value + for _, item := range tt { + id, err := arrayTypes.Insert(item) + s.NoError(err) + + if pk, ok := id.(int64); !ok || pk == 0 { + s.T().Fatalf("Expecting an ID.") + } + + var itemCheck arrayType + err = arrayTypes.Find(db.Cond{"id": id}).One(&itemCheck) + s.NoError(err) + s.Len(itemCheck.Integers, len(item.Integers)) + s.Len(itemCheck.Strings, len(item.Strings)) - return stats, nil + s.Equal(item, itemCheck) + } } -func cleanUpCheck(sess sqlbuilder.Database) (err error) { - var stats map[string]int - stats, err = getStats(sess) - if err != nil { - return err +func (s *AdapterTests) Test_Issue210() { + list := []string{ + `DROP TABLE IF EXISTS testing123`, + `DROP TABLE IF EXISTS hello`, + `CREATE TABLE IF NOT EXISTS testing123 ( + ID INT PRIMARY KEY NOT NULL, + NAME TEXT NOT NULL + ) + `, + `CREATE TABLE IF NOT EXISTS hello ( + ID INT PRIMARY KEY NOT NULL, + NAME TEXT NOT NULL + )`, } - if activeStatements := sqladapter.NumActiveStatements(); activeStatements > 128 { - return fmt.Errorf("Expecting active statements to be at most 128, got %d", activeStatements) + sess := s.SQLBuilder() + + tx, err := sess.NewTx(nil) + s.NoError(err) + + for i := range list { + _, err = tx.Exec(list[i]) + s.NoError(err) } - sess.ClearCache() + err = tx.Commit() + s.NoError(err) - stats, err = getStats(sess) - if err != nil { - return err + _, err = sess.Collection("testing123").Find().Count() + s.NoError(err) + + _, err = sess.Collection("hello").Find().Count() + s.NoError(err) +} + +func (s *AdapterTests) TestPreparedStatements() { + sess := s.SQLBuilder() + + var val int + + { + stmt, err := sess.Prepare(`SELECT 1`) + s.NoError(err) + s.NotNil(stmt) + + q, err := stmt.Query() + s.NoError(err) + s.NotNil(q) + s.True(q.Next()) + + err = q.Scan(&val) + s.NoError(err) + + err = q.Close() + s.NoError(err) + + s.Equal(1, val) + + err = stmt.Close() + s.NoError(err) + } + + { + tx, err := sess.NewTx(nil) + s.NoError(err) + + stmt, err := tx.Prepare(`SELECT 2`) + s.NoError(err) + s.NotNil(stmt) + + q, err := stmt.Query() + s.NoError(err) + s.NotNil(q) + s.True(q.Next()) + + err = q.Scan(&val) + s.NoError(err) + + err = q.Close() + s.NoError(err) + + s.Equal(2, val) + + err = stmt.Close() + s.NoError(err) + + err = tx.Commit() + s.NoError(err) + } + + { + stmt, err := sess.Select(3).Prepare() + s.NoError(err) + s.NotNil(stmt) + + q, err := stmt.Query() + s.NoError(err) + s.NotNil(q) + s.True(q.Next()) + + err = q.Scan(&val) + s.NoError(err) + + err = q.Close() + s.NoError(err) + + s.Equal(3, val) + + err = stmt.Close() + s.NoError(err) } +} + +func (s *AdapterTests) TestNonTrivialSubqueries() { + sess := s.SQLBuilder() + + // Creating test data + artist := sess.Collection("artist") - if stats["pg_prepared_statements_count"] != 0 { - return fmt.Errorf(`Expecting "Prepared_stmt_count" to be 0, got %d`, stats["Prepared_stmt_count"]) + artistNames := []string{"Ozzie", "Flea", "Slash", "Chrono"} + for _, artistName := range artistNames { + _, err := artist.Insert(map[string]string{ + "name": artistName, + }) + s.NoError(err) } - return nil + + { + q, err := sess.Query(`WITH test AS (?) ?`, + sess.Select("id AS foo").From("artist"), + sess.Select("foo").From("test").Where("foo > ?", 0), + ) + + s.NoError(err) + s.NotNil(q) + + s.True(q.Next()) + + var number int + s.NoError(q.Scan(&number)) + + s.Equal(1, number) + s.NoError(q.Close()) + } + + { + row, err := sess.QueryRow(`WITH test AS (?) ?`, + sess.Select("id AS foo").From("artist"), + sess.Select("foo").From("test").Where("foo > ?", 0), + ) + + s.NoError(err) + s.NotNil(row) + + var number int + s.NoError(row.Scan(&number)) + + s.Equal(1, number) + } + + { + res, err := sess.Exec( + `UPDATE artist a1 SET id = ?`, + sess.Select(db.Raw("id + 5")). + From("artist a2"). + Where("a2.id = a1.id"), + ) + + s.NoError(err) + s.NotNil(res) + } + + { + q, err := sess.Query(db.Raw(`WITH test AS (?) ?`, + sess.Select("id AS foo").From("artist"), + sess.Select("foo").From("test").Where("foo > ?", 0).OrderBy("foo"), + )) + + s.NoError(err) + s.NotNil(q) + + s.True(q.Next()) + + var number int + s.NoError(q.Scan(&number)) + + s.Equal(6, number) + s.NoError(q.Close()) + } + + { + res, err := sess.Exec(db.Raw(`UPDATE artist a1 SET id = ?`, + sess.Select(db.Raw("id + 7")).From("artist a2").Where("a2.id = a1.id"), + )) + + s.NoError(err) + s.NotNil(res) + } +} + +func TestAdapter(t *testing.T) { + suite.Run(t, &AdapterTests{}) } diff --git a/postgresql/sql_test.go b/postgresql/sql_test.go new file mode 100644 index 00000000..f39579fe --- /dev/null +++ b/postgresql/sql_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package postgresql + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type SQLTests struct { + testsuite.SQLTestSuite +} + +func (s *SQLTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestSQL(t *testing.T) { + suite.Run(t, &SQLTests{}) +} diff --git a/postgresql/stubs_test.go b/postgresql/stubs_test.go deleted file mode 100644 index cf1f2834..00000000 --- a/postgresql/stubs_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build !generated - -package postgresql - -import ( - "log" - "testing" - - "upper.io/db.v3/lib/sqlbuilder" -) - -func mustOpen() sqlbuilder.Database { - return nil -} - -func TestMain(*testing.M) { - log.Fatal(`Tests use generated code and a custom database, please use "make test".`) -} diff --git a/postgresql/template.go b/postgresql/template.go index 0a11d3fa..55909f85 100644 --- a/postgresql/template.go +++ b/postgresql/template.go @@ -22,7 +22,7 @@ package postgresql import ( - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/cache" "upper.io/db.v3/internal/sqladapter/exql" ) diff --git a/postgresql/template_test.go b/postgresql/template_test.go index ceb71c18..faa89cc0 100644 --- a/postgresql/template_test.go +++ b/postgresql/template_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/ql/Makefile b/ql/Makefile index 25e3c9a8..b94dce19 100644 --- a/ql/Makefile +++ b/ql/Makefile @@ -13,13 +13,10 @@ require-client: exit 1; \ fi -generate: - go generate && \ - go get -d -t -v ./... - reset-db: require-client rm -f $(DB_NAME) -test: reset-db generate - #go test -tags generated -v -race # race: limit on 8192 simultaneously alive goroutines is exceeded, dying - go test -tags generated -timeout 30m -v +test: reset-db + go test -v + +test-extended: test diff --git a/ql/collection.go b/ql/collection.go index a564cb03..7f94639a 100644 --- a/ql/collection.go +++ b/ql/collection.go @@ -24,7 +24,7 @@ package ql import ( "database/sql" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/ql/database.go b/ql/database.go index 808ca6f0..8942f6f1 100644 --- a/ql/database.go +++ b/ql/database.go @@ -33,7 +33,7 @@ import ( "sync/atomic" _ "modernc.org/ql/driver" // QL driver - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/internal/sqladapter/compat" "upper.io/db.v3/internal/sqladapter/exql" diff --git a/ql/generic_test.go b/ql/generic_test.go new file mode 100644 index 00000000..7405afab --- /dev/null +++ b/ql/generic_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package ql + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type GenericTests struct { + testsuite.GenericTestSuite +} + +func (s *GenericTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestGeneric(t *testing.T) { + suite.Run(t, &GenericTests{}) +} diff --git a/ql/adapter_test.go b/ql/helper_test.go similarity index 70% rename from ql/adapter_test.go rename to ql/helper_test.go index cdfbac05..8a1568eb 100644 --- a/ql/adapter_test.go +++ b/ql/helper_test.go @@ -19,44 +19,62 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -//go:generate bash -c "sed s/ADAPTER/ql/g ../internal/sqladapter/testing/adapter.go.tpl > generated_test.go" package ql import ( "database/sql" "os" + db "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" -) - -const ( - testTimeZone = "Canada/Eastern" + "upper.io/db.v3/testsuite" ) var settings = ConnectionURL{ Database: os.Getenv("DB_NAME"), } -func tearUp() error { - sess := mustOpen() - defer sess.Close() +type Helper struct { + sess sqlbuilder.Database +} + +func (h *Helper) Session() db.Database { + return h.sess +} + +func (h *Helper) SQLBuilder() sqlbuilder.Database { + return h.sess +} + +func (h *Helper) Adapter() string { + return "ql" +} + +func (h *Helper) TearDown() error { + return h.sess.Close() +} + +func (h *Helper) TearUp() error { + var err error + + h.sess, err = Open(settings) + if err != nil { + return err + } batch := []string{ `DROP TABLE IF EXISTS artist`, - `CREATE TABLE artist ( name string )`, `DROP TABLE IF EXISTS publication`, - `CREATE TABLE publication ( title string, author_id int )`, `DROP TABLE IF EXISTS review`, - `CREATE TABLE review ( publication_id int, name string, @@ -65,7 +83,6 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS data_types`, - `CREATE TABLE data_types ( _uint uint, _uint8 uint8, @@ -90,7 +107,6 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS stats_test`, - `CREATE TABLE stats_test ( id uint, numeric int64, @@ -98,7 +114,6 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS composite_keys`, - `-- Composite keys are currently not supported in QL. CREATE TABLE composite_keys ( -- code string, @@ -106,28 +121,46 @@ func tearUp() error { some_val string, -- primary key (code, user_id) )`, - } - driver := sess.Driver().(*sql.DB) - tx, err := driver.Begin() - if err != nil { - return err + `DROP TABLE IF EXISTS birthdays`, + `CREATE TABLE birthdays ( + name string, + born time, + born_ut int + )`, + + `DROP TABLE IF EXISTS fibonacci`, + `CREATE TABLE fibonacci ( + input int, + output int + )`, + + `DROP TABLE IF EXISTS is_even`, + `CREATE TABLE is_even ( + input int, + is_even bool + )`, + + `DROP TABLE IF EXISTS CaSe_TesT`, + `CREATE TABLE CaSe_TesT ( + case_test string + )`, } - for _, s := range batch { - if _, err := tx.Exec(s); err != nil { + for _, query := range batch { + driver := h.sess.Driver().(*sql.DB) + tx, err := driver.Begin() + if err != nil { return err } - } - - if err := tx.Commit(); err != nil { - return err + if _, err := tx.Exec(query); err != nil { + _ = tx.Rollback() + return err + } + tx.Commit() } return nil } -func cleanUpCheck(sess sqlbuilder.Database) (err error) { - // TODO: Check the number of prepared statements. - return nil -} +var _ testsuite.Helper = &Helper{} diff --git a/operators.go b/ql/sql_test.go similarity index 75% rename from operators.go rename to ql/sql_test.go index 0da39a26..e93e3499 100644 --- a/operators.go +++ b/ql/sql_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-present The upper.io/db authors. All rights reserved. +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the @@ -19,4 +19,23 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -package db +package ql + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type SQLTests struct { + testsuite.SQLTestSuite +} + +func (s *SQLTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestSQL(t *testing.T) { + suite.Run(t, &SQLTests{}) +} diff --git a/ql/stubs_test.go b/ql/stubs_test.go deleted file mode 100644 index ba22dd17..00000000 --- a/ql/stubs_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build !generated - -package ql - -import ( - "log" - "testing" - - "upper.io/db.v3/lib/sqlbuilder" -) - -func mustOpen() sqlbuilder.Database { - return nil -} - -func TestMain(*testing.M) { - log.Fatal(`Tests use generated code and a custom database, please use "make test".`) -} diff --git a/ql/template.go b/ql/template.go index bf00758d..ca903a97 100644 --- a/ql/template.go +++ b/ql/template.go @@ -22,7 +22,7 @@ package ql import ( - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/cache" "upper.io/db.v3/internal/sqladapter/exql" ) diff --git a/ql/template_test.go b/ql/template_test.go index 27a18710..eac9e84a 100644 --- a/ql/template_test.go +++ b/ql/template_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/raw.go b/raw.go index ee691ada..09aa0617 100644 --- a/raw.go +++ b/raw.go @@ -92,4 +92,4 @@ func Raw(value string, args ...interface{}) RawValue { return r } -var _ RawValue = &rawValue{} +var _ = RawValue(&rawValue{}) diff --git a/result.go b/result.go index 3ce078b2..e72e7e92 100644 --- a/result.go +++ b/result.go @@ -21,14 +21,14 @@ package db -// Result is an interface that defines methods useful for working with result -// sets. +// Result is an interface that defines methods which are useful for working +// with result sets. type Result interface { // String satisfies fmt.Stringer and returns a SELECT statement for // the result. String() string - // Limit defines the maximum number of results in this set. It only has + // Limit defines the maximum number of results for this set. It only has // effect on `One()`, `All()` and `Next()`. A negative limit cancels any // previous limit settings. Limit(int) Result @@ -38,9 +38,10 @@ type Result interface { // settings. Offset(int) Result - // OrderBy receives field names that define the order in which elements will be - // returned in a query, field names may be prefixed with a minus sign (-) - // indicating descending order, ascending order will be used otherwise. + // OrderBy receives one or more field names that define the order in which + // elements will be returned in a query, field names may be prefixed with a + // minus sign (-) indicating descending order, ascending order will be used + // otherwise. OrderBy(...interface{}) Result // Select defines specific columns to be returned from the elements of the diff --git a/sqlite/Makefile b/sqlite/Makefile index 44045e0c..0ed6db7a 100644 --- a/sqlite/Makefile +++ b/sqlite/Makefile @@ -13,13 +13,10 @@ require-client: exit 1; \ fi -generate: - go generate && \ - go get -d -t -v ./... - reset-db: require-client rm -f $(DB_NAME) -test: reset-db generate - #go test -tags generated -v -race # race: limit on 8192 simultaneously alive goroutines is exceeded, dying - go test -tags generated -v +test: reset-db + go test -v + +test-extended: test diff --git a/sqlite/collection.go b/sqlite/collection.go index 76d90660..d827e578 100644 --- a/sqlite/collection.go +++ b/sqlite/collection.go @@ -24,7 +24,7 @@ package sqlite import ( "database/sql" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/sqlite/database.go b/sqlite/database.go index 043fedf7..3dc4b3d3 100644 --- a/sqlite/database.go +++ b/sqlite/database.go @@ -33,7 +33,7 @@ import ( "sync/atomic" _ "github.com/mattn/go-sqlite3" // SQLite3 driver. - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/internal/sqladapter/compat" "upper.io/db.v3/internal/sqladapter/exql" diff --git a/sqlite/generic_test.go b/sqlite/generic_test.go new file mode 100644 index 00000000..63cbded7 --- /dev/null +++ b/sqlite/generic_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package sqlite + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type GenericTests struct { + testsuite.GenericTestSuite +} + +func (s *GenericTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestGeneric(t *testing.T) { + suite.Run(t, &GenericTests{}) +} diff --git a/sqlite/adapter_test.go b/sqlite/helper_test.go similarity index 69% rename from sqlite/adapter_test.go rename to sqlite/helper_test.go index 435ef59b..311d9a50 100644 --- a/sqlite/adapter_test.go +++ b/sqlite/helper_test.go @@ -19,27 +19,48 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -//go:generate bash -c "sed s/ADAPTER/sqlite/g ../internal/sqladapter/testing/adapter.go.tpl > generated_test.go" package sqlite import ( "database/sql" "os" + db "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" -) - -const ( - testTimeZone = "Canada/Eastern" + "upper.io/db.v3/testsuite" ) var settings = ConnectionURL{ Database: os.Getenv("DB_NAME"), } -func tearUp() error { - sess := mustOpen() - defer sess.Close() +type Helper struct { + sess sqlbuilder.Database +} + +func (h *Helper) Session() db.Database { + return h.sess +} + +func (h *Helper) SQLBuilder() sqlbuilder.Database { + return h.sess +} + +func (h *Helper) Adapter() string { + return "sqlite" +} + +func (h *Helper) TearDown() error { + return h.sess.Close() +} + +func (h *Helper) TearUp() error { + var err error + + h.sess, err = Open(settings) + if err != nil { + return err + } batch := []string{ `PRAGMA foreign_keys=OFF`, @@ -47,14 +68,12 @@ func tearUp() error { `BEGIN TRANSACTION`, `DROP TABLE IF EXISTS artist`, - `CREATE TABLE artist ( id integer primary key, name varchar(60) )`, `DROP TABLE IF EXISTS publication`, - `CREATE TABLE publication ( id integer primary key, title varchar(80), @@ -62,7 +81,6 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS review`, - `CREATE TABLE review ( id integer primary key, publication_id integer, @@ -72,7 +90,6 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS data_types`, - `CREATE TABLE data_types ( id integer primary key, _uint integer, @@ -101,7 +118,6 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS stats_test`, - `CREATE TABLE stats_test ( id integer primary key, numeric integer, @@ -109,7 +125,6 @@ func tearUp() error { )`, `DROP TABLE IF EXISTS composite_keys`, - `CREATE TABLE composite_keys ( code VARCHAR(255) default '', user_id VARCHAR(255) default '', @@ -117,12 +132,39 @@ func tearUp() error { primary key (code, user_id) )`, + `DROP TABLE IF EXISTS "birthdays"`, + `CREATE TABLE "birthdays" ( + "id" INTEGER PRIMARY KEY, + "name" VARCHAR(50) DEFAULT NULL, + "born" DATETIME DEFAULT NULL, + "born_ut" INTEGER + )`, + + `DROP TABLE IF EXISTS "fibonacci"`, + `CREATE TABLE "fibonacci" ( + "id" INTEGER PRIMARY KEY, + "input" INTEGER, + "output" INTEGER + )`, + + `DROP TABLE IF EXISTS "is_even"`, + `CREATE TABLE "is_even" ( + "input" INTEGER, + "is_even" INTEGER + )`, + + `DROP TABLE IF EXISTS "CaSe_TesT"`, + `CREATE TABLE "CaSe_TesT" ( + "id" INTEGER PRIMARY KEY, + "case_test" VARCHAR + )`, + `COMMIT`, } - for _, s := range batch { - driver := sess.Driver().(*sql.DB) - if _, err := driver.Exec(s); err != nil { + for _, query := range batch { + driver := h.sess.Driver().(*sql.DB) + if _, err := driver.Exec(query); err != nil { return err } } @@ -130,7 +172,4 @@ func tearUp() error { return nil } -func cleanUpCheck(sess sqlbuilder.Database) (err error) { - // TODO: Check the number of prepared statements. - return nil -} +var _ testsuite.Helper = &Helper{} diff --git a/sqlite/sql_test.go b/sqlite/sql_test.go new file mode 100644 index 00000000..cd4a5751 --- /dev/null +++ b/sqlite/sql_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2012-today The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package sqlite + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "upper.io/db.v3/testsuite" +) + +type SQLTests struct { + testsuite.SQLTestSuite +} + +func (s *SQLTests) SetupSuite() { + s.Helper = &Helper{} +} + +func TestSQL(t *testing.T) { + suite.Run(t, &SQLTests{}) +} diff --git a/sqlite/sqlite.go b/sqlite/sqlite.go index fde822c5..08cd1c29 100644 --- a/sqlite/sqlite.go +++ b/sqlite/sqlite.go @@ -24,7 +24,7 @@ package sqlite // import "upper.io/db.v3/sqlite" import ( "database/sql" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/internal/sqladapter" "upper.io/db.v3/lib/sqlbuilder" diff --git a/sqlite/stubs_test.go b/sqlite/stubs_test.go deleted file mode 100644 index 7b45ff61..00000000 --- a/sqlite/stubs_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build !generated - -package sqlite - -import ( - "log" - "testing" - - "upper.io/db.v3/lib/sqlbuilder" -) - -func mustOpen() sqlbuilder.Database { - return nil -} - -func TestMain(*testing.M) { - log.Fatal(`Tests use generated code and a custom database, please use "make test".`) -} diff --git a/sqlite/template_test.go b/sqlite/template_test.go index 80737a9a..f2db6387 100644 --- a/sqlite/template_test.go +++ b/sqlite/template_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "upper.io/db.v3" + db "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" ) diff --git a/tests/db_test.go b/tests/db_test.go deleted file mode 100644 index 223a2d9d..00000000 --- a/tests/db_test.go +++ /dev/null @@ -1,1570 +0,0 @@ -// Copyright (c) 2012-present The upper.io/db authors. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -package db_test - -import ( - "database/sql" - "errors" - "fmt" - "log" - "os" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "gopkg.in/mgo.v2" - "gopkg.in/mgo.v2/bson" - "upper.io/db.v3" - "upper.io/db.v3/mongo" - "upper.io/db.v3/mssql" - "upper.io/db.v3/mysql" - "upper.io/db.v3/postgresql" - "upper.io/db.v3/ql" - "upper.io/db.v3/sqlite" -) - -var wrappers = []string{ - mongo.Adapter, - mssql.Adapter, - mysql.Adapter, - postgresql.Adapter, - ql.Adapter, - sqlite.Adapter, -} - -const ( - testAllWrappers = `all` -) - -var ( - errDriverErr = errors.New(`Driver error`) -) - -var settings map[string]db.ConnectionURL - -func init() { - - // Getting settings from the environment. - var host string - if host = os.Getenv("DB_HOST"); host == "" { - host = "localhost" - } - - var wrapper string - if wrapper = os.Getenv("WRAPPER"); wrapper == "" { - wrapper = testAllWrappers - } - - username := os.Getenv("DB_USERNAME") - password := os.Getenv("DB_PASSWORD") - dbname := os.Getenv("DB_NAME") - - log.Printf("Running tests against host %s.\n", host) - - settings = map[string]db.ConnectionURL{ - `sqlite`: &sqlite.ConnectionURL{ - Database: `sqlite3-test.db`, - }, - `mongo`: &mongo.ConnectionURL{ - Database: dbname, - Host: host, - User: username, - Password: password, - }, - `mysql`: &mysql.ConnectionURL{ - Database: dbname, - Host: host, - User: username, - Password: password, - Options: map[string]string{ - "parseTime": "true", - }, - }, - `postgresql`: &postgresql.ConnectionURL{ - Database: dbname, - Host: host, - User: username, - Password: password, - Options: map[string]string{ - "timezone": "UTC", - }, - }, - `mssql`: &mssql.ConnectionURL{ - Database: dbname, - Host: host, - User: username, - Password: password, - }, - `ql`: &ql.ConnectionURL{ - Database: `ql-test.db`, - }, - } - - if wrapper != testAllWrappers { - wrappers = []string{wrapper} - log.Printf("Testing wrapper %s.", wrapper) - } - -} - -var setupFn = map[string]func(driver interface{}) error{ - `mongo`: func(driver interface{}) error { - if mgod, ok := driver.(*mgo.Session); ok { - var col *mgo.Collection - col = mgod.DB("upperio_tests").C("birthdays") - col.DropCollection() - - col = mgod.DB("upperio_tests").C("fibonacci") - col.DropCollection() - - col = mgod.DB("upperio_tests").C("is_even") - col.DropCollection() - - col = mgod.DB("upperio_tests").C("CaSe_TesT") - col.DropCollection() - return nil - } - return errDriverErr - }, - `postgresql`: func(driver interface{}) error { - if sqld, ok := driver.(*sql.DB); ok { - var err error - - _, err = sqld.Exec(`DROP TABLE IF EXISTS "birthdays"`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE "birthdays" ( - "id" serial primary key, - "name" CHARACTER VARYING(50), - "born" TIMESTAMP WITH TIME ZONE, - "born_ut" INT - )`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS "fibonacci"`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE "fibonacci" ( - "id" serial primary key, - "input" NUMERIC, - "output" NUMERIC - )`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS "is_even"`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE "is_even" ( - "input" NUMERIC, - "is_even" BOOL - )`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS "CaSe_TesT"`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE "CaSe_TesT" ( - "id" SERIAL PRIMARY KEY, - "case_test" VARCHAR(60) - )`) - if err != nil { - return err - } - - return nil - } - return fmt.Errorf("Expecting *sql.DB got %T (%#v).", driver, driver) - }, - `mysql`: func(driver interface{}) error { - if sqld, ok := driver.(*sql.DB); ok { - var err error - - _, err = sqld.Exec(`DROP TABLE IF EXISTS ` + "`" + `birthdays` + "`" + ``) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE ` + "`" + `birthdays` + "`" + ` ( - id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), - name VARCHAR(50), - born DATE, - born_ut BIGINT(20) SIGNED - ) CHARSET=utf8`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS ` + "`" + `fibonacci` + "`" + ``) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE ` + "`" + `fibonacci` + "`" + ` ( - id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), - input BIGINT(20) UNSIGNED NOT NULL, - output BIGINT(20) UNSIGNED NOT NULL - ) CHARSET=utf8`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS ` + "`" + `is_even` + "`" + ``) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE ` + "`" + `is_even` + "`" + ` ( - input BIGINT(20) UNSIGNED NOT NULL, - is_even TINYINT(1) - ) CHARSET=utf8`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS ` + "`" + `CaSe_TesT` + "`" + ``) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE ` + "`" + `CaSe_TesT` + "`" + ` ( - id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), - case_test VARCHAR(60) - ) CHARSET=utf8`) - if err != nil { - return err - } - - return nil - } - return fmt.Errorf("Expecting *sql.DB got %T (%#v).", driver, driver) - }, - `mssql`: func(driver interface{}) error { - if sqld, ok := driver.(*sql.DB); ok { - var err error - - _, err = sqld.Exec(`DROP TABLE IF EXISTS [birthdays]`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE [birthdays] ( - id BIGINT IDENTITY(1, 1) PRIMARY KEY NOT NULL, - name NVARCHAR(50), - born DATETIME, - born_ut BIGINT - )`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS [fibonacci]`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE [fibonacci] ( - id BIGINT PRIMARY KEY NOT NULL IDENTITY(1,1), - input BIGINT NOT NULL, - output BIGINT NOT NULL - )`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS [is_even]`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE [is_even] ( - input BIGINT NOT NULL, - is_even TINYINT - )`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS [CaSe_TesT]`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE [CaSe_TesT] ( - id BIGINT PRIMARY KEY NOT NULL IDENTITY(1,1), - case_test NVARCHAR(60) - )`) - if err != nil { - return err - } - - return nil - } - return fmt.Errorf("Expecting *sql.DB got %T (%#v).", driver, driver) - }, - `sqlite`: func(driver interface{}) error { - if sqld, ok := driver.(*sql.DB); ok { - var err error - - _, err = sqld.Exec(`DROP TABLE IF EXISTS "birthdays"`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE "birthdays" ( - "id" INTEGER PRIMARY KEY, - "name" VARCHAR(50) DEFAULT NULL, - "born" DATETIME DEFAULT NULL, - "born_ut" INTEGER - )`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS "fibonacci"`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE "fibonacci" ( - "id" INTEGER PRIMARY KEY, - "input" INTEGER, - "output" INTEGER - )`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS "is_even"`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE "is_even" ( - "input" INTEGER, - "is_even" INTEGER - )`) - if err != nil { - return err - } - - _, err = sqld.Exec(`DROP TABLE IF EXISTS "CaSe_TesT"`) - if err != nil { - return err - } - _, err = sqld.Exec(`CREATE TABLE "CaSe_TesT" ( - "id" INTEGER PRIMARY KEY, - "case_test" VARCHAR - )`) - if err != nil { - return err - } - - return nil - } - return errDriverErr - }, - `ql`: func(driver interface{}) error { - if sqld, ok := driver.(*sql.DB); ok { - var err error - var tx *sql.Tx - - if tx, err = sqld.Begin(); err != nil { - return err - } - - _, err = tx.Exec(`DROP TABLE IF EXISTS birthdays`) - if err != nil { - return err - } - - _, err = tx.Exec(`CREATE TABLE birthdays ( - name string, - born time, - born_ut int - )`) - if err != nil { - return err - } - - _, err = tx.Exec(`DROP TABLE IF EXISTS fibonacci`) - if err != nil { - return err - } - - _, err = tx.Exec(`CREATE TABLE fibonacci ( - input int, - output int - )`) - if err != nil { - return err - } - - _, err = tx.Exec(`DROP TABLE IF EXISTS is_even`) - if err != nil { - return err - } - - _, err = tx.Exec(`CREATE TABLE is_even ( - input int, - is_even bool - )`) - if err != nil { - return err - } - - _, err = tx.Exec(`DROP TABLE IF EXISTS CaSe_TesT`) - if err != nil { - return err - } - - _, err = tx.Exec(`CREATE TABLE CaSe_TesT ( - case_test string - )`) - if err != nil { - return err - } - - if err = tx.Commit(); err != nil { - return err - } - - return nil - } - return errDriverErr - }, -} - -type birthday struct { - Name string `db:"name"` - Born time.Time `db:"born"` - BornUT timeType `db:"born_ut,omitempty"` - OmitMe bool `json:"omit_me" db:"-" bson:"-"` -} - -type fibonacci struct { - Input uint64 `db:"input"` - Output uint64 `db:"output"` - // Test for BSON option. - OmitMe bool `json:"omit_me" db:"omit_me,bson,omitempty" bson:"omit_me,omitempty"` -} - -type oddEven struct { - // Test for JSON option. - Input int `json:"input" db:"input"` - // Test for JSON option. - // The "bson" tag is required by mgo. - IsEven bool `json:"is_even" db:"is_even,json" bson:"is_even"` - OmitMe bool `json:"omit_me" db:"-" bson:"-"` -} - -// Struct that relies on explicit mapping. -type mapE struct { - ID uint `db:"id,omitempty" bson:"-"` - MongoID bson.ObjectId `db:"-" bson:"_id,omitempty"` - CaseTest string `db:"case_test" bson:"case_test"` -} - -// Struct that will fallback to default mapping. -type mapN struct { - ID uint `db:"id,omitempty"` - MongoID bson.ObjectId `db:"-" bson:"_id,omitempty"` - Case_TEST string `db:"case_test"` -} - -// Struct for testing marshalling. -type timeType struct { - // Time is handled internally as time.Time but saved as an (integer) unix - // timestamp. - value time.Time -} - -// time.Time -> unix timestamp -func (u timeType) MarshalDB() (interface{}, error) { - return u.value.Unix(), nil -} - -// unix timestamp -> time.Time -func (u *timeType) UnmarshalDB(v interface{}) error { - var unixTime int64 - - switch t := v.(type) { - case int64: - unixTime = t - case nil: - return nil - default: - return db.ErrUnsupportedValue - } - - t := time.Unix(unixTime, 0).In(time.UTC) - *u = timeType{t} - - return nil -} - -var ( - _ db.Marshaler = timeType{} - _ db.Unmarshaler = &timeType{} -) - -func even(i int) bool { - if i%2 == 0 { - return true - } - return false -} - -func fib(i uint64) uint64 { - if i == 0 { - return 0 - } else if i == 1 { - return 1 - } - return fib(i-1) + fib(i-2) -} - -func TestOpen(t *testing.T) { - var err error - for _, wrapper := range wrappers { - t.Logf("Testing wrapper: %q", wrapper) - - if settings[wrapper] == nil { - t.Fatalf(`No such settings entry for wrapper %s.`, wrapper) - } else { - var sess db.Database - sess, err = db.Open(wrapper, settings[wrapper]) - if err != nil { - t.Fatalf(`Test for wrapper %s failed: %q`, wrapper, err) - } - err = sess.Close() - if err != nil { - t.Fatalf(`Test for wrapper %s failed: %q`, wrapper, err) - } - } - } -} - -func TestSetup(t *testing.T) { - var err error - for _, wrapper := range wrappers { - t.Logf("Testing wrapper: %q", wrapper) - - if settings[wrapper] == nil { - t.Fatalf(`No such settings entry for wrapper %s.`, wrapper) - } else { - var sess db.Database - - sess, err = db.Open(wrapper, settings[wrapper]) - if err != nil { - t.Fatalf(`Test for wrapper %s failed: %q`, wrapper, err) - } - - if setupFn[wrapper] == nil { - t.Fatalf(`Missing setup function for wrapper %s.`, wrapper) - } else { - if err = setupFn[wrapper](sess.Driver()); err != nil { - t.Fatalf(`Failed to setup wrapper %s: %q`, wrapper, err) - } - } - - err = sess.Close() - if err != nil { - t.Fatalf(`Could not close %s: %q`, wrapper, err) - } - - } - } -} - -func TestSimpleCRUD(t *testing.T) { - var err error - - var controlItem birthday - - for _, wrapper := range wrappers { - if settings[wrapper] == nil { - t.Fatalf(`No such settings entry for wrapper %s.`, wrapper) - } else { - - t.Logf("Testing wrapper: %q", wrapper) - - var sess db.Database - - sess, err = db.Open(wrapper, settings[wrapper]) - if err != nil { - t.Fatalf(`Test for wrapper %s failed: %q`, wrapper, err) - } - - defer sess.Close() - - born := time.Date(1941, time.January, 5, 0, 0, 0, 0, time.UTC) - - controlItem = birthday{ - Name: "Hayao Miyazaki", - Born: born, - BornUT: timeType{born}, - } - - col := sess.Collection(`birthdays`) - - var id interface{} - if id, err = col.Insert(controlItem); err != nil { - t.Fatalf(`Could not append item with wrapper %s: %q`, wrapper, err) - } - - var res db.Result - switch wrapper { - case `mongo`: - res = col.Find(db.Cond{"_id": id.(bson.ObjectId)}) - case `ql`: - res = col.Find(db.Cond{"id()": id}) - default: - res = col.Find(db.Cond{"id": id}) - } - - var total uint64 - total, err = res.Count() - - if total != 1 { - t.Fatalf("%s: Expecting one row.", wrapper) - } - - // No support for Marshaler and Unmarshaler is implemeted for QL and - // MongoDB. - if wrapper == `ql` || wrapper == `mongo` { - continue - } - - var testItem birthday - err = res.One(&testItem) - if err != nil { - t.Fatalf("%s One(): %s", wrapper, err) - } - - if wrapper == `sqlite` { - // SQLite does not save time zone info, so you have to do this by hand. - testItem.Born = testItem.Born.In(time.UTC) - } - - if reflect.DeepEqual(testItem, controlItem) == false { - t.Errorf("%s: controlItem (inserted): %v (ts: %v)\n", wrapper, controlItem, controlItem.BornUT.value.Unix()) - t.Fatalf("%s: Structs are different", wrapper) - } - - var testItems []birthday - err = res.All(&testItems) - if err != nil { - t.Fatalf("%s All(): %s", wrapper, err) - } - - if len(testItems) == 0 { - t.Fatalf("%s All(): Expecting at least one row.", wrapper) - } - - for _, testItem = range testItems { - if wrapper == `sqlite` { - // SQLite does not save time zone info, so you have to do this by hand. - testItem.Born = testItem.Born.In(time.UTC) - } - if reflect.DeepEqual(testItem, controlItem) == false { - t.Errorf("%s: testItem: %v\n", wrapper, testItem) - t.Errorf("%s: controlItem: %v\n", wrapper, controlItem) - t.Fatalf("%s: Structs are different", wrapper) - } - } - - controlItem.Name = `宮崎駿` - err = res.Update(controlItem) - - if err != nil { - t.Fatalf(`Could not update with wrapper %s: %q`, wrapper, err) - } - - err = res.One(&testItem) - if err != nil { - t.Fatalf("%s One(): %s", wrapper, err) - } - - if wrapper == `sqlite` { - // SQLite does not save time zone info, so you have to do this by hand. - testItem.Born = testItem.Born.In(time.UTC) - } - - if reflect.DeepEqual(testItem, controlItem) == false { - t.Fatalf("Struct is different with wrapper %s, got: %#v, expecting: %#v.", wrapper, testItem, controlItem) - } - - err = res.Delete() - - if err != nil { - t.Fatalf(`Could not remove with wrapper %s: %q`, wrapper, err) - } - - total, err = res.Count() - - if total != 0 { - t.Fatalf(`Expecting no items %s: %q`, wrapper, err) - } - - err = res.Close() - if err != nil { - t.Errorf("Failed to close result %s: %q.", wrapper, err) - } - - err = sess.Close() - if err != nil { - t.Errorf("Failed to close %s: %q.", wrapper, err) - } - - } - } -} - -func TestFibonacci(t *testing.T) { - var err error - var res db.Result - var total uint64 - - for _, wrapper := range wrappers { - t.Logf("Testing wrapper: %q", wrapper) - - if settings[wrapper] == nil { - t.Fatalf(`No such settings entry for wrapper %s.`, wrapper) - } else { - - var sess db.Database - - sess, err = db.Open(wrapper, settings[wrapper]) - if err != nil { - t.Fatalf(`Test for wrapper %s failed: %q`, wrapper, err) - } - defer sess.Close() - - col := sess.Collection("fibonacci") - - // Adding some items. - var i uint64 - for i = 0; i < 10; i++ { - item := fibonacci{Input: i, Output: fib(i)} - _, err = col.Insert(item) - if err != nil { - t.Fatalf(`Could not append item with wrapper %s: %q`, wrapper, err) - } - } - - // Testing sorting by function. - res = col.Find( - // 5, 6, 7, 3 - db.Or( - db.And( - db.Cond{"input": db.Gte(5)}, - db.Cond{"input": db.Lte(7)}, - ), - db.Cond{"input": db.Eq(3)}, - ), - ) - - // Testing sort by function. - switch wrapper { - case `postgresql`: - res = res.OrderBy(db.Raw(`RANDOM()`)) - case `sqlite`: - res = res.OrderBy(db.Raw(`RANDOM()`)) - case `mysql`: - res = res.OrderBy(db.Raw(`RAND()`)) - case `sqlserver`: - res = res.OrderBy(db.Raw(`NEWID()`)) - } - - total, err = res.Count() - - if err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - if total != 4 { - t.Fatalf("%s: Expecting a count of 4, got %d.", wrapper, total) - } - - // Find() with IN/$in - res = col.Find(db.Cond{"input IN": []int{3, 5, 6, 7}}).OrderBy("input") - - total, err = res.Count() - - if err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - if total != 4 { - t.Fatalf(`Expecting a count of 4.`) - } - - res = res.Offset(1).Limit(2) - - var item fibonacci - for res.Next(&item) { - switch item.Input { - case 5: - case 6: - if fib(item.Input) != item.Output { - t.Fatalf(`Unexpected value in item with wrapper %s.`, wrapper) - } - default: - t.Fatalf(`Unexpected item: %v with wrapper %s.`, item, wrapper) - } - } - if err := res.Err(); err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - // Find() with range - res = col.Find( - // 5, 6, 7, 3 - db.Or( - db.And( - db.Cond{"input >=": 5}, - db.Cond{"input <=": 7}, - ), - db.Cond{"input": 3}, - ), - ).OrderBy("-input") - - if total, err = res.Count(); err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - if total != 4 { - t.Fatalf(`Expecting a count of 4.`) - } - - // Skipping. - res = res.Offset(1).Limit(2) - - var item2 fibonacci - for res.Next(&item2) { - switch item2.Input { - case 5: - case 6: - if fib(item2.Input) != item2.Output { - t.Fatalf(`Unexpected value in item2 with wrapper %s.`, wrapper) - } - default: - t.Fatalf(`Unexpected item2: %v with wrapper %s.`, item2, wrapper) - } - } - if err := res.Err(); err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - if err = res.Delete(); err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - if total, err = res.Count(); err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - if total != 0 { - t.Fatalf(`%s: Unexpected count %d.`, wrapper, total) - } - - // Find() with no arguments. - res = col.Find() - total, err = res.Count() - - if total != 6 { - t.Fatalf(`%s: Unexpected count %d.`, wrapper, total) - } - - // Skipping mongodb as the results of this are not defined there. - if wrapper != `mongo` { - - // Find() with empty db.Cond. - res1 := col.Find(db.Cond{}) - total, err = res1.Count() - - if total != 6 { - t.Fatalf(`%s: Unexpected count %d.`, wrapper, total) - } - - // Find() with empty expression - res1b := col.Find(db.Or(db.And(db.Cond{}, db.Cond{}), db.Or(db.Cond{}))) - total, err = res1b.Count() - - if total != 6 { - t.Fatalf(`%s: Unexpected count %d.`, wrapper, total) - } - - // Find() with explicit IS NULL - res2 := col.Find(db.Cond{"input IS": nil}) - total, err = res2.Count() - - if total != 0 { - t.Fatalf(`%s: Unexpected count %d.`, wrapper, total) - } - - // Find() with implicit IS NULL - res2a := col.Find(db.Cond{"input": nil}) - total, err = res2a.Count() - - if total != 0 { - t.Fatalf(`%s: Unexpected count %d.`, wrapper, total) - } - - // Find() with explicit = NULL - res2b := col.Find(db.Cond{"input =": nil}) - total, err = res2b.Count() - - if total != 0 { - t.Fatalf(`%s: Unexpected count %d.`, wrapper, total) - } - - // Find() with implicit IN - res3 := col.Find(db.Cond{"input": []int{1, 2, 3, 4}}) - total, err = res3.Count() - - if total != 3 { - t.Fatalf(`%s: Unexpected count %d.`, wrapper, total) - } - - // Find() with implicit NOT IN - res3a := col.Find(db.Cond{"input NOT IN": []int{1, 2, 3, 4}}) - total, err = res3a.Count() - - if total != 3 { - t.Fatalf(`%s: Unexpected count %d.`, wrapper, total) - } - } - - var items []fibonacci - err = res.All(&items) - - if err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - if len(items) != 6 { - t.Fatalf(`Waiting for 6 items.`) - } - - for _, item := range items { - switch item.Input { - case 0: - case 1: - case 2: - case 4: - case 8: - case 9: - if fib(item.Input) != item.Output { - t.Fatalf(`Unexpected value in item with wrapper %s.`, wrapper) - } - default: - t.Fatalf(`Unexpected item: %v with wrapper %s.`, item, wrapper) - } - } - - err = res.Close() - if err != nil { - t.Errorf("Failed to close result %s: %q.", wrapper, err) - } - - err = sess.Close() - if err != nil { - t.Errorf("Failed to close %s: %q.", wrapper, err) - } - } - } -} - -func TestEven(t *testing.T) { - var err error - - for _, wrapper := range wrappers { - t.Logf("Testing wrapper: %q", wrapper) - - if settings[wrapper] == nil { - t.Fatalf(`No such settings entry for wrapper %s.`, wrapper) - } else { - var sess db.Database - - sess, err = db.Open(wrapper, settings[wrapper]) - if err != nil { - t.Fatalf(`Test for wrapper %s failed: %q`, wrapper, err) - } - defer sess.Close() - - col := sess.Collection("is_even") - - // Adding some items. - var i int - for i = 1; i < 100; i++ { - item := oddEven{Input: i, IsEven: even(i)} - _, err = col.Insert(item) - if err != nil { - t.Fatalf(`Could not append item with wrapper %s: %q`, wrapper, err) - } - } - - // Retrieving items - res := col.Find(db.Cond{"is_even": true}) - - var item oddEven - for res.Next(&item) { - if item.Input%2 != 0 { - t.Fatalf("Expecting even numbers with wrapper %s. Got: %v\n", wrapper, item) - } - } - if err := res.Err(); err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - if err = res.Delete(); err != nil { - t.Fatalf(`Could not remove with wrapper %s: %q`, wrapper, err) - } - - // Testing named inputs (using tags). - res = col.Find() - - var item2 struct { - Value uint `db:"input" bson:"input"` // The "bson" tag is required by mgo. - } - for res.Next(&item2) { - if item2.Value%2 == 0 { - t.Fatalf("Expecting odd numbers only with wrapper %s. Got: %v\n", wrapper, item2) - } - } - if err := res.Err(); err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - // Testing inline tag. - res = col.Find() - - var item3 struct { - OddEven oddEven `db:",inline" bson:",inline"` - } - for res.Next(&item3) { - if item3.OddEven.Input%2 == 0 { - t.Fatalf("Expecting odd numbers only with wrapper %s. Got: %v\n", wrapper, item3) - } - if item3.OddEven.Input == 0 { - t.Fatal("Expecting a number > 0") - } - } - if err := res.Err(); err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - // Testing inline tag. - type OddEven oddEven - res = col.Find() - - var item31 struct { - OddEven `db:",inline" bson:",inline"` - } - for res.Next(&item31) { - if item31.Input%2 == 0 { - t.Fatalf("Expecting odd numbers only with wrapper %s. Got: %v\n", wrapper, item31) - } - if item31.Input == 0 { - t.Fatal("Expecting a number > 0") - } - } - if err := res.Err(); err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - - // Testing omision tag. - res = col.Find() - - var item4 struct { - Value uint `db:"-"` - } - for res.Next(&item4) { - if item4.Value != 0 { - t.Fatalf("Expecting no data with wrapper %s. Got: %v\n", wrapper, item4) - } - } - if err := res.Err(); err != nil { - t.Fatalf(`%s: %q`, wrapper, err) - } - } - } - -} - -func TestExplicitAndDefaultMapping(t *testing.T) { - var err error - var sess db.Database - var res db.Result - - var testE mapE - var testN mapN - - for _, wrapper := range wrappers { - t.Logf("Testing wrapper: %q", wrapper) - - if settings[wrapper] == nil { - t.Fatalf(`No such settings entry for wrapper %s.`, wrapper) - } else { - - if sess, err = db.Open(wrapper, settings[wrapper]); err != nil { - t.Fatalf(`Test for wrapper %s failed: %q`, wrapper, err) - } - - defer sess.Close() - - col := sess.Collection("CaSe_TesT") - - if err = col.Truncate(); err != nil { - if wrapper == `mongo` { - // Nothing, this is expected. - } else { - t.Fatal(err) - } - } - - // Testing explicit mapping. - testE = mapE{ - CaseTest: "Hello!", - } - - if _, err = col.Insert(testE); err != nil { - t.Fatal(err) - } - - res = col.Find(db.Cond{"case_test": "Hello!"}) - - if wrapper == `ql` { - res = res.Select(`id() as id`, `case_test`) - } - - if err = res.One(&testE); err != nil { - t.Fatal(err) - } - - if wrapper == `mongo` { - if testE.MongoID.Valid() == false { - t.Fatalf("Expecting an ID.") - } - } else { - if testE.ID == 0 { - t.Fatalf("Expecting an ID.") - } - } - - // Testing default mapping. - testN = mapN{ - Case_TEST: "World!", - } - - if _, err = col.Insert(testN); err != nil { - t.Fatal(err) - } - - if wrapper == `mongo` { - res = col.Find(db.Cond{"case_test": "World!"}) - } else { - res = col.Find(db.Cond{"case_test": "World!"}) - } - - if wrapper == `ql` { - res = res.Select(`id() as id`, `case_test`) - } - - if err = res.One(&testN); err != nil { - t.Fatal(err) - } - - if wrapper == `mongo` { - if testN.MongoID.Valid() == false { - t.Fatalf("Expecting an ID.") - } - } else { - if testN.ID == 0 { - t.Fatalf("Expecting an ID.") - } - } - } - } -} - -func TestComparisonOperators(t *testing.T) { - var err error - var sess db.Database - - for _, wrapper := range wrappers { - t.Logf("Testing wrapper: %q", wrapper) - - if settings[wrapper] == nil { - t.Fatalf("No such settings entry for wrapper %s.", wrapper) - } - - if sess, err = db.Open(wrapper, settings[wrapper]); err != nil { - t.Fatalf("Test for wrapper %s failed: %q", wrapper, err) - } - - defer sess.Close() - - birthdays := sess.Collection("birthdays") - err := birthdays.Truncate() - assert.NoError(t, err) - - // Insert data for testing - birthdaysDataset := []birthday{ - { - Name: "Marie Smith", - Born: time.Date(1956, time.August, 5, 0, 0, 0, 0, time.Local), - }, - { - Name: "Peter", - Born: time.Date(1967, time.July, 23, 0, 0, 0, 0, time.Local), - }, - { - Name: "Eve Smith", - Born: time.Date(1911, time.February, 8, 0, 0, 0, 0, time.Local), - }, - { - Name: "Alex López", - Born: time.Date(2001, time.May, 5, 0, 0, 0, 0, time.Local), - }, - { - Name: "Rose Smith", - Born: time.Date(1944, time.December, 9, 0, 0, 0, 0, time.Local), - }, - { - Name: "Daria López", - Born: time.Date(1923, time.March, 23, 0, 0, 0, 0, time.Local), - }, - { - Name: "", - Born: time.Date(1945, time.December, 1, 0, 0, 0, 0, time.Local), - }, - { - Name: "Colin", - Born: time.Date(2010, time.May, 6, 0, 0, 0, 0, time.Local), - }, - } - for _, birthday := range birthdaysDataset { - _, err := birthdays.Insert(birthday) - assert.NoError(t, err) - } - - // Test: equal - { - var item birthday - err := birthdays.Find(db.Cond{ - "name": db.Eq("Colin"), - }).One(&item) - assert.NoError(t, err) - assert.NotNil(t, item) - - assert.Equal(t, "Colin", item.Name) - } - - // Test: not equal - { - var item birthday - err := birthdays.Find(db.Cond{ - "name": db.NotEq("Colin"), - }).One(&item) - assert.NoError(t, err) - assert.NotNil(t, item) - - assert.NotEqual(t, "Colin", item.Name) - } - - // Test: greater than - { - var items []birthday - ref := time.Date(1967, time.July, 23, 0, 0, 0, 0, time.Local) - err := birthdays.Find(db.Cond{ - "born": db.Gt(ref), - }).All(&items) - assert.NoError(t, err) - assert.NotZero(t, len(items)) - assert.Equal(t, 2, len(items)) - for _, item := range items { - assert.True(t, item.Born.After(ref)) - } - } - - // Test: less than - { - var items []birthday - ref := time.Date(1967, time.July, 23, 0, 0, 0, 0, time.Local) - err := birthdays.Find(db.Cond{ - "born": db.Lt(ref), - }).All(&items) - assert.NoError(t, err) - assert.NotZero(t, len(items)) - assert.Equal(t, 5, len(items)) - for _, item := range items { - assert.True(t, item.Born.Before(ref)) - } - } - - // Test: greater than or equal to - { - var items []birthday - ref := time.Date(1967, time.July, 23, 0, 0, 0, 0, time.Local) - err := birthdays.Find(db.Cond{ - "born": db.Gte(ref), - }).All(&items) - assert.NoError(t, err) - assert.NotZero(t, len(items)) - assert.Equal(t, 3, len(items)) - for _, item := range items { - assert.True(t, item.Born.After(ref) || item.Born.Equal(ref)) - } - } - - // Test: less than or equal to - { - var items []birthday - ref := time.Date(1967, time.July, 23, 0, 0, 0, 0, time.Local) - err := birthdays.Find(db.Cond{ - "born": db.Lte(ref), - }).All(&items) - assert.NoError(t, err) - assert.NotZero(t, len(items)) - assert.Equal(t, 6, len(items)) - for _, item := range items { - assert.True(t, item.Born.Before(ref) || item.Born.Equal(ref)) - } - } - - // Test: between - { - var items []birthday - dateA := time.Date(1911, time.February, 8, 0, 0, 0, 0, time.Local) - dateB := time.Date(1967, time.July, 23, 0, 0, 0, 0, time.Local) - err := birthdays.Find(db.Cond{ - "born": db.Between(dateA, dateB), - }).All(&items) - assert.NoError(t, err) - assert.Equal(t, 6, len(items)) - for _, item := range items { - assert.True(t, item.Born.After(dateA) || item.Born.Equal(dateA)) - assert.True(t, item.Born.Before(dateB) || item.Born.Equal(dateB)) - } - } - - // Test: not between - { - var items []birthday - dateA := time.Date(1911, time.February, 8, 0, 0, 0, 0, time.Local) - dateB := time.Date(1967, time.July, 23, 0, 0, 0, 0, time.Local) - err := birthdays.Find(db.Cond{ - "born": db.NotBetween(dateA, dateB), - }).All(&items) - assert.NoError(t, err) - assert.Equal(t, 2, len(items)) - for _, item := range items { - assert.False(t, item.Born.Before(dateA) || item.Born.Equal(dateA)) - assert.False(t, item.Born.Before(dateB) || item.Born.Equal(dateB)) - } - } - - // Test: in - { - var items []birthday - names := []string{"Peter", "Eve Smith", "Daria López", "Alex López"} - err := birthdays.Find(db.Cond{ - "name": db.In(names), - }).All(&items) - assert.NoError(t, err) - assert.Equal(t, 4, len(items)) - for _, item := range items { - inArray := false - for _, name := range names { - if name == item.Name { - inArray = true - } - } - assert.True(t, inArray) - } - } - - // Test: not in - { - var items []birthday - names := []string{"Peter", "Eve Smith", "Daria López", "Alex López"} - err := birthdays.Find(db.Cond{ - "name": db.NotIn(names), - }).All(&items) - assert.NoError(t, err) - assert.Equal(t, 4, len(items)) - for _, item := range items { - inArray := false - for _, name := range names { - if name == item.Name { - inArray = true - } - } - assert.False(t, inArray) - } - } - - // Test: not in - { - var items []birthday - names := []string{"Peter", "Eve Smith", "Daria López", "Alex López"} - err := birthdays.Find(db.Cond{ - "name": db.NotIn(names), - }).All(&items) - assert.NoError(t, err) - assert.Equal(t, 4, len(items)) - for _, item := range items { - inArray := false - for _, name := range names { - if name == item.Name { - inArray = true - } - } - assert.False(t, inArray) - } - } - - // Test: is and is not - { - var items []birthday - err := birthdays.Find(db.And( - db.Cond{"name": db.Is(nil)}, - db.Cond{"name": db.IsNot(nil)}, - )).All(&items) - assert.NoError(t, err) - assert.Equal(t, 0, len(items)) - } - - // Test: is nil - { - var items []birthday - err := birthdays.Find(db.And( - db.Cond{"born_ut": db.IsNull()}, - )).All(&items) - assert.NoError(t, err) - assert.Equal(t, 8, len(items)) - } - - // Test: like and not like - { - var items []birthday - var q db.Result - - switch wrapper { - case "ql", "mongo": - q = birthdays.Find(db.And( - db.Cond{"name": db.Like(".*ari.*")}, - db.Cond{"name": db.NotLike(".*Smith")}, - )) - default: - q = birthdays.Find(db.And( - db.Cond{"name": db.Like("%ari%")}, - db.Cond{"name": db.NotLike("%Smith")}, - )) - } - - err := q.All(&items) - assert.NoError(t, err) - assert.Equal(t, 1, len(items)) - - assert.Equal(t, "Daria López", items[0].Name) - } - - if wrapper != "sqlite" && wrapper != "mssql" { - // Test: regexp - { - var items []birthday - err := birthdays.Find(db.And( - db.Cond{"name": db.RegExp("^[D|C|M]")}, - )).OrderBy("name").All(&items) - assert.NoError(t, err) - assert.Equal(t, 3, len(items)) - - assert.Equal(t, "Colin", items[0].Name) - assert.Equal(t, "Daria López", items[1].Name) - assert.Equal(t, "Marie Smith", items[2].Name) - } - - // Test: not regexp - { - var items []birthday - names := []string{"Daria López", "Colin", "Marie Smith"} - err := birthdays.Find(db.And( - db.Cond{"name": db.NotRegExp("^[D|C|M]")}, - )).OrderBy("name").All(&items) - assert.NoError(t, err) - assert.Equal(t, 5, len(items)) - - for _, item := range items { - for _, name := range names { - assert.NotEqual(t, item.Name, name) - } - } - } - } - - // Test: after - { - ref := time.Date(1944, time.December, 9, 0, 0, 0, 0, time.Local) - var items []birthday - err := birthdays.Find(db.Cond{ - "born": db.After(ref), - }).All(&items) - assert.NoError(t, err) - assert.Equal(t, 5, len(items)) - } - - // Test: on or after - { - ref := time.Date(1944, time.December, 9, 0, 0, 0, 0, time.Local) - var items []birthday - err := birthdays.Find(db.Cond{ - "born": db.OnOrAfter(ref), - }).All(&items) - assert.NoError(t, err) - assert.Equal(t, 6, len(items)) - } - - // Test: before - { - ref := time.Date(1944, time.December, 9, 0, 0, 0, 0, time.Local) - var items []birthday - err := birthdays.Find(db.Cond{ - "born": db.Before(ref), - }).All(&items) - assert.NoError(t, err) - assert.Equal(t, 2, len(items)) - } - - // Test: on or before - { - ref := time.Date(1944, time.December, 9, 0, 0, 0, 0, time.Local) - var items []birthday - err := birthdays.Find(db.Cond{ - "born": db.OnOrBefore(ref), - }).All(&items) - assert.NoError(t, err) - assert.Equal(t, 3, len(items)) - } - } -} diff --git a/testsuite/generic_suite.go b/testsuite/generic_suite.go new file mode 100644 index 00000000..146d19e3 --- /dev/null +++ b/testsuite/generic_suite.go @@ -0,0 +1,909 @@ +// Copyright (c) 2012-present The upper.io/db authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package testsuite + +import ( + "time" + + "github.com/stretchr/testify/suite" + "gopkg.in/mgo.v2/bson" + db "upper.io/db.v3" +) + +type birthday struct { + Name string `db:"name"` + Born time.Time `db:"born"` + BornUT timeType `db:"born_ut,omitempty"` + OmitMe bool `json:"omit_me" db:"-" bson:"-"` +} + +type fibonacci struct { + Input uint64 `db:"input"` + Output uint64 `db:"output"` + // Test for BSON option. + OmitMe bool `json:"omit_me" db:"omit_me,bson,omitempty" bson:"omit_me,omitempty"` +} + +type oddEven struct { + // Test for JSON option. + Input int `json:"input" db:"input"` + // Test for JSON option. + // The "bson" tag is required by mgo. + IsEven bool `json:"is_even" db:"is_even,json" bson:"is_even"` + OmitMe bool `json:"omit_me" db:"-" bson:"-"` +} + +// Struct that relies on explicit mapping. +type mapE struct { + ID uint `db:"id,omitempty" bson:"-"` + MongoID bson.ObjectId `db:"-" bson:"_id,omitempty"` + CaseTest string `db:"case_test" bson:"case_test"` +} + +// Struct that will fallback to default mapping. +type mapN struct { + ID uint `db:"id,omitempty"` + MongoID bson.ObjectId `db:"-" bson:"_id,omitempty"` + Case_TEST string `db:"case_test"` +} + +// Struct for testing marshalling. +type timeType struct { + // Time is handled internally as time.Time but saved as an (integer) unix + // timestamp. + value time.Time +} + +// time.Time -> unix timestamp +func (u timeType) MarshalDB() (interface{}, error) { + return u.value.Unix(), nil +} + +// unix timestamp -> time.Time +func (u *timeType) UnmarshalDB(v interface{}) error { + var unixTime int64 + + switch t := v.(type) { + case int64: + unixTime = t + case nil: + return nil + default: + return db.ErrUnsupportedValue + } + + t := time.Unix(unixTime, 0).In(time.UTC) + *u = timeType{t} + + return nil +} + +var ( + _ db.Marshaler = timeType{} + _ db.Unmarshaler = &timeType{} +) + +func even(i int) bool { + if i%2 == 0 { + return true + } + return false +} + +func fib(i uint64) uint64 { + if i == 0 { + return 0 + } else if i == 1 { + return 1 + } + return fib(i-1) + fib(i-2) +} + +type GenericTestSuite struct { + suite.Suite + + Helper +} + +func (s *GenericTestSuite) AfterTest(suiteName, testName string) { + err := s.TearDown() + s.NoError(err) +} + +func (s *GenericTestSuite) BeforeTest(suiteName, testName string) { + err := s.TearUp() + s.NoError(err) +} + +func (s *GenericTestSuite) TestDatesAndUnicode() { + sess := s.Session() + + born := time.Date(1941, time.January, 5, 0, 0, 0, 0, TimeLocation) + switch s.Adapter() { + case "sqlite", "ql", "mssql": + // Lacks support for storing timezones + born = born.In(time.UTC) + } + + controlItem := birthday{ + Name: "Hayao Miyazaki", + Born: born, + BornUT: timeType{born.UTC()}, + } + + col := sess.Collection(`birthdays`) + + id, err := col.Insert(controlItem) + s.NoError(err) + s.NotZero(id) + + var res db.Result + switch s.Adapter() { + case "mongo": + res = col.Find(db.Cond{"_id": id.(bson.ObjectId)}) + case "ql": + res = col.Find(db.Cond{"id()": id}) + default: + res = col.Find(db.Cond{"id": id}) + } + + var total uint64 + total, err = res.Count() + s.NoError(err) + s.Equal(uint64(1), total) + + switch s.Adapter() { + case "mongo": + s.T().Skip() + } + + var testItem birthday + err = res.One(&testItem) + s.NoError(err) + + switch s.Adapter() { + case "sqlite", "ql", "mssql": + testItem.Born = testItem.Born.In(time.UTC) + } + s.Equal(controlItem, testItem) + + var testItems []birthday + err = res.All(&testItems) + s.NoError(err) + s.NotZero(len(testItems)) + + for _, testItem = range testItems { + switch s.Adapter() { + case "sqlite", "ql", "mssql": + testItem.Born = testItem.Born.In(time.UTC) + } + s.Equal(controlItem, testItem) + } + + controlItem.Name = `宮崎駿` + err = res.Update(controlItem) + s.NoError(err) + + err = res.One(&testItem) + s.NoError(err) + + switch s.Adapter() { + case "sqlite", "ql", "mssql": + testItem.Born = testItem.Born.In(time.UTC) + } + + s.Equal(controlItem, testItem) + + err = res.Delete() + s.NoError(err) + + total, err = res.Count() + s.NoError(err) + s.Zero(total) + + err = res.Close() + s.NoError(err) +} + +func (s *GenericTestSuite) TestFibonacci() { + var err error + var res db.Result + var total uint64 + + sess := s.Session() + + col := sess.Collection("fibonacci") + + // Adding some items. + var i uint64 + for i = 0; i < 10; i++ { + item := fibonacci{Input: i, Output: fib(i)} + _, err = col.Insert(item) + s.NoError(err) + } + + // Testing sorting by function. + res = col.Find( + // 5, 6, 7, 3 + db.Or( + db.And( + db.Cond{"input": db.Gte(5)}, + db.Cond{"input": db.Lte(7)}, + ), + db.Cond{"input": db.Eq(3)}, + ), + ) + + // Testing sort by function. + switch s.Adapter() { + case "postgresql": + res = res.OrderBy(db.Raw("RANDOM()")) + case "sqlite": + res = res.OrderBy(db.Raw("RANDOM()")) + case "mysql": + res = res.OrderBy(db.Raw("RAND()")) + case "mssql": + res = res.OrderBy(db.Raw("NEWID()")) + } + + total, err = res.Count() + s.NoError(err) + s.Equal(uint64(4), total) + + // Find() with IN/$in + res = col.Find(db.Cond{"input IN": []int{3, 5, 6, 7}}).OrderBy("input") + + total, err = res.Count() + s.NoError(err) + s.Equal(uint64(4), total) + + res = res.Offset(1).Limit(2) + + var item fibonacci + for res.Next(&item) { + switch item.Input { + case 5: + case 6: + s.Equal(fib(item.Input), item.Output) + default: + s.T().Fatalf(`Unexpected item: %v.`, item) + } + } + s.NoError(res.Err()) + + // Find() with range + res = col.Find( + // 5, 6, 7, 3 + db.Or( + db.And( + db.Cond{"input >=": 5}, + db.Cond{"input <=": 7}, + ), + db.Cond{"input": 3}, + ), + ).OrderBy("-input") + + total, err = res.Count() + s.NoError(err) + s.Equal(uint64(4), total) + + // Skipping. + res = res.Offset(1).Limit(2) + + var item2 fibonacci + for res.Next(&item2) { + switch item2.Input { + case 5: + case 6: + s.Equal(fib(item2.Input), item2.Output) + default: + s.T().Fatalf(`Unexpected item: %v.`, item2) + } + } + err = res.Err() + s.NoError(err) + + err = res.Delete() + s.NoError(err) + + { + total, err := res.Count() + s.NoError(err) + s.Zero(total) + } + + // Find() with no arguments. + res = col.Find() + { + total, err := res.Count() + s.NoError(err) + s.Equal(uint64(6), total) + } + + // Skipping mongodb as the results of this are not defined there. + if s.Adapter() != `mongo` { + // Find() with empty db.Cond. + { + total, err := col.Find(db.Cond{}).Count() + s.NoError(err) + s.Equal(uint64(6), total) + } + + // Find() with empty expression + { + total, err := col.Find(db.Or(db.And(db.Cond{}, db.Cond{}), db.Or(db.Cond{}))).Count() + s.NoError(err) + s.Equal(uint64(6), total) + } + + // Find() with explicit IS NULL + { + total, err := col.Find(db.Cond{"input IS": nil}).Count() + s.NoError(err) + s.Equal(uint64(0), total) + } + + // Find() with implicit IS NULL + { + total, err := col.Find(db.Cond{"input": nil}).Count() + s.NoError(err) + s.Equal(uint64(0), total) + } + + // Find() with explicit = NULL + { + total, err := col.Find(db.Cond{"input =": nil}).Count() + s.NoError(err) + s.Equal(uint64(0), total) + } + + // Find() with implicit IN + { + total, err := col.Find(db.Cond{"input": []int{1, 2, 3, 4}}).Count() + s.NoError(err) + s.Equal(uint64(3), total) + } + + // Find() with implicit NOT IN + { + total, err := col.Find(db.Cond{"input NOT IN": []int{1, 2, 3, 4}}).Count() + s.NoError(err) + s.Equal(uint64(3), total) + } + } + + var items []fibonacci + err = res.All(&items) + s.NoError(err) + + for _, item := range items { + switch item.Input { + case 0: + case 1: + case 2: + case 4: + case 8: + case 9: + s.Equal(fib(item.Input), item.Output) + default: + s.T().Fatalf(`Unexpected item: %v`, item) + } + } + + err = res.Close() + s.NoError(err) +} + +func (s *GenericTestSuite) TestOddEven() { + sess := s.Session() + + col := sess.Collection("is_even") + + // Adding some items. + var i int + for i = 1; i < 100; i++ { + item := oddEven{Input: i, IsEven: even(i)} + _, err := col.Insert(item) + s.NoError(err) + } + + // Retrieving items + res := col.Find(db.Cond{"is_even": true}) + + var item oddEven + for res.Next(&item) { + s.Zero(item.Input % 2) + } + + err := res.Err() + s.NoError(err) + + err = res.Delete() + s.NoError(err) + + // Testing named inputs (using tags). + res = col.Find() + + var item2 struct { + Value uint `db:"input" bson:"input"` // The "bson" tag is required by mgo. + } + for res.Next(&item2) { + s.NotZero(item2.Value % 2) + } + err = res.Err() + s.NoError(err) + + // Testing inline tag. + res = col.Find() + + var item3 struct { + OddEven oddEven `db:",inline" bson:",inline"` + } + for res.Next(&item3) { + s.NotZero(item3.OddEven.Input % 2) + s.NotZero(item3.OddEven.Input) + } + err = res.Err() + s.NoError(err) + + // Testing inline tag. + type OddEven oddEven + res = col.Find() + + var item31 struct { + OddEven `db:",inline" bson:",inline"` + } + for res.Next(&item31) { + s.NotZero(item31.Input % 2) + s.NotZero(item31.Input) + } + s.NoError(res.Err()) + + // Testing omision tag. + res = col.Find() + + var item4 struct { + Value uint `db:"-"` + } + for res.Next(&item4) { + s.Zero(item4.Value) + } + s.NoError(res.Err()) +} + +func (s *GenericTestSuite) TestExplicitAndDefaultMapping() { + var err error + var res db.Result + + var testE mapE + var testN mapN + + sess := s.Session() + + col := sess.Collection("CaSe_TesT") + + if err = col.Truncate(); err != nil { + if s.Adapter() != "mongo" { + s.NoError(err) + } + } + + // Testing explicit mapping. + testE = mapE{ + CaseTest: "Hello!", + } + + _, err = col.Insert(testE) + s.NoError(err) + + res = col.Find(db.Cond{"case_test": "Hello!"}) + if s.Adapter() == "ql" { + res = res.Select("id() as id", "case_test") + } + + err = res.One(&testE) + s.NoError(err) + + if s.Adapter() == "mongo" { + s.True(testE.MongoID.Valid()) + } else { + s.NotZero(testE.ID) + } + + // Testing default mapping. + testN = mapN{ + Case_TEST: "World!", + } + + _, err = col.Insert(testN) + s.NoError(err) + + if s.Adapter() == `mongo` { + res = col.Find(db.Cond{"case_test": "World!"}) + } else { + res = col.Find(db.Cond{"case_test": "World!"}) + } + + if s.Adapter() == `ql` { + res = res.Select(`id() as id`, `case_test`) + } + + err = res.One(&testN) + s.NoError(err) + + if s.Adapter() == `mongo` { + s.True(testN.MongoID.Valid()) + } else { + s.NotZero(testN.ID) + } +} + +func (s *GenericTestSuite) TestComparisonOperators() { + sess := s.Session() + + birthdays := sess.Collection("birthdays") + err := birthdays.Truncate() + if err != nil { + if s.Adapter() != "mongo" { + s.NoError(err) + } + } + + // Insert data for testing + birthdaysDataset := []birthday{ + { + Name: "Marie Smith", + Born: time.Date(1956, time.August, 5, 0, 0, 0, 0, TimeLocation), + }, + { + Name: "Peter", + Born: time.Date(1967, time.July, 23, 0, 0, 0, 0, TimeLocation), + }, + { + Name: "Eve Smith", + Born: time.Date(1911, time.February, 8, 0, 0, 0, 0, TimeLocation), + }, + { + Name: "Alex López", + Born: time.Date(2001, time.May, 5, 0, 0, 0, 0, TimeLocation), + }, + { + Name: "Rose Smith", + Born: time.Date(1944, time.December, 9, 0, 0, 0, 0, TimeLocation), + }, + { + Name: "Daria López", + Born: time.Date(1923, time.March, 23, 0, 0, 0, 0, TimeLocation), + }, + { + Name: "", + Born: time.Date(1945, time.December, 1, 0, 0, 0, 0, TimeLocation), + }, + { + Name: "Colin", + Born: time.Date(2010, time.May, 6, 0, 0, 0, 0, TimeLocation), + }, + } + for _, birthday := range birthdaysDataset { + _, err := birthdays.Insert(birthday) + s.NoError(err) + } + + // Test: equal + { + var item birthday + err := birthdays.Find(db.Cond{ + "name": db.Eq("Colin"), + }).One(&item) + s.NoError(err) + s.NotNil(item) + + s.Equal("Colin", item.Name) + } + + // Test: not equal + { + var item birthday + err := birthdays.Find(db.Cond{ + "name": db.NotEq("Colin"), + }).One(&item) + s.NoError(err) + s.NotNil(item) + + s.NotEqual("Colin", item.Name) + } + + // Test: greater than + { + var items []birthday + ref := time.Date(1967, time.July, 23, 0, 0, 0, 0, TimeLocation) + err := birthdays.Find(db.Cond{ + "born": db.Gt(ref), + }).All(&items) + s.NoError(err) + s.NotZero(len(items)) + s.Equal(2, len(items)) + for _, item := range items { + s.True(item.Born.After(ref)) + } + } + + // Test: less than + { + var items []birthday + ref := time.Date(1967, time.July, 23, 0, 0, 0, 0, TimeLocation) + err := birthdays.Find(db.Cond{ + "born": db.Lt(ref), + }).All(&items) + s.NoError(err) + s.NotZero(len(items)) + s.Equal(5, len(items)) + for _, item := range items { + s.True(item.Born.Before(ref)) + } + } + + // Test: greater than or equal to + { + var items []birthday + ref := time.Date(1967, time.July, 23, 0, 0, 0, 0, TimeLocation) + err := birthdays.Find(db.Cond{ + "born": db.Gte(ref), + }).All(&items) + s.NoError(err) + s.NotZero(len(items)) + s.Equal(3, len(items)) + for _, item := range items { + s.True(item.Born.After(ref) || item.Born.Equal(ref)) + } + } + + // Test: less than or equal to + { + var items []birthday + ref := time.Date(1967, time.July, 23, 0, 0, 0, 0, TimeLocation) + err := birthdays.Find(db.Cond{ + "born": db.Lte(ref), + }).All(&items) + s.NoError(err) + s.NotZero(len(items)) + s.Equal(6, len(items)) + for _, item := range items { + s.True(item.Born.Before(ref) || item.Born.Equal(ref)) + } + } + + // Test: between + { + var items []birthday + dateA := time.Date(1911, time.February, 8, 0, 0, 0, 0, TimeLocation) + dateB := time.Date(1967, time.July, 23, 0, 0, 0, 0, TimeLocation) + err := birthdays.Find(db.Cond{ + "born": db.Between(dateA, dateB), + }).All(&items) + s.NoError(err) + s.Equal(6, len(items)) + for _, item := range items { + s.True(item.Born.After(dateA) || item.Born.Equal(dateA)) + s.True(item.Born.Before(dateB) || item.Born.Equal(dateB)) + } + } + + // Test: not between + { + var items []birthday + dateA := time.Date(1911, time.February, 8, 0, 0, 0, 0, TimeLocation) + dateB := time.Date(1967, time.July, 23, 0, 0, 0, 0, TimeLocation) + err := birthdays.Find(db.Cond{ + "born": db.NotBetween(dateA, dateB), + }).All(&items) + s.NoError(err) + s.Equal(2, len(items)) + for _, item := range items { + s.False(item.Born.Before(dateA) || item.Born.Equal(dateA)) + s.False(item.Born.Before(dateB) || item.Born.Equal(dateB)) + } + } + + // Test: in + { + var items []birthday + names := []string{"Peter", "Eve Smith", "Daria López", "Alex López"} + err := birthdays.Find(db.Cond{ + "name": db.In(names), + }).All(&items) + s.NoError(err) + s.Equal(4, len(items)) + for _, item := range items { + inArray := false + for _, name := range names { + if name == item.Name { + inArray = true + } + } + s.True(inArray) + } + } + + // Test: not in + { + var items []birthday + names := []string{"Peter", "Eve Smith", "Daria López", "Alex López"} + err := birthdays.Find(db.Cond{ + "name": db.NotIn(names), + }).All(&items) + s.NoError(err) + s.Equal(4, len(items)) + for _, item := range items { + inArray := false + for _, name := range names { + if name == item.Name { + inArray = true + } + } + s.False(inArray) + } + } + + // Test: not in + { + var items []birthday + names := []string{"Peter", "Eve Smith", "Daria López", "Alex López"} + err := birthdays.Find(db.Cond{ + "name": db.NotIn(names), + }).All(&items) + s.NoError(err) + s.Equal(4, len(items)) + for _, item := range items { + inArray := false + for _, name := range names { + if name == item.Name { + inArray = true + } + } + s.False(inArray) + } + } + + // Test: is and is not + { + var items []birthday + err := birthdays.Find(db.And( + db.Cond{"name": db.Is(nil)}, + db.Cond{"name": db.IsNot(nil)}, + )).All(&items) + s.NoError(err) + s.Equal(0, len(items)) + } + + // Test: is nil + { + var items []birthday + err := birthdays.Find(db.And( + db.Cond{"born_ut": db.IsNull()}, + )).All(&items) + s.NoError(err) + s.Equal(8, len(items)) + } + + // Test: like and not like + { + var items []birthday + var q db.Result + + switch s.Adapter() { + case "ql", "mongo": + q = birthdays.Find(db.And( + db.Cond{"name": db.Like(".*ari.*")}, + db.Cond{"name": db.NotLike(".*Smith")}, + )) + default: + q = birthdays.Find(db.And( + db.Cond{"name": db.Like("%ari%")}, + db.Cond{"name": db.NotLike("%Smith")}, + )) + } + + err := q.All(&items) + s.NoError(err) + s.Equal(1, len(items)) + + s.Equal("Daria López", items[0].Name) + } + + if s.Adapter() != "sqlite" && s.Adapter() != "mssql" { + // Test: regexp + { + var items []birthday + err := birthdays.Find(db.And( + db.Cond{"name": db.RegExp("^[D|C|M]")}, + )).OrderBy("name").All(&items) + s.NoError(err) + s.Equal(3, len(items)) + + s.Equal("Colin", items[0].Name) + s.Equal("Daria López", items[1].Name) + s.Equal("Marie Smith", items[2].Name) + } + + // Test: not regexp + { + var items []birthday + names := []string{"Daria López", "Colin", "Marie Smith"} + err := birthdays.Find(db.And( + db.Cond{"name": db.NotRegExp("^[D|C|M]")}, + )).OrderBy("name").All(&items) + s.NoError(err) + s.Equal(5, len(items)) + + for _, item := range items { + for _, name := range names { + s.NotEqual(item.Name, name) + } + } + } + } + + // Test: after + { + ref := time.Date(1944, time.December, 9, 0, 0, 0, 0, TimeLocation) + var items []birthday + err := birthdays.Find(db.Cond{ + "born": db.After(ref), + }).All(&items) + s.NoError(err) + s.Equal(5, len(items)) + } + + // Test: on or after + { + ref := time.Date(1944, time.December, 9, 0, 0, 0, 0, TimeLocation) + var items []birthday + err := birthdays.Find(db.Cond{ + "born": db.OnOrAfter(ref), + }).All(&items) + s.NoError(err) + s.Equal(6, len(items)) + } + + // Test: before + { + ref := time.Date(1944, time.December, 9, 0, 0, 0, 0, TimeLocation) + var items []birthday + err := birthdays.Find(db.Cond{ + "born": db.Before(ref), + }).All(&items) + s.NoError(err) + s.Equal(2, len(items)) + } + + // Test: on or before + { + ref := time.Date(1944, time.December, 9, 0, 0, 0, 0, TimeLocation) + var items []birthday + err := birthdays.Find(db.Cond{ + "born": db.OnOrBefore(ref), + }).All(&items) + s.NoError(err) + s.Equal(3, len(items)) + } +} diff --git a/testsuite/sql_suite.go b/testsuite/sql_suite.go new file mode 100644 index 00000000..fd79ca7b --- /dev/null +++ b/testsuite/sql_suite.go @@ -0,0 +1,1926 @@ +package testsuite + +import ( + "database/sql" + "fmt" + "log" + "math/rand" + "strconv" + "strings" + "sync" + "time" + + "github.com/stretchr/testify/suite" + db "upper.io/db.v3" +) + +type customLogger struct { +} + +func (*customLogger) Log(q *db.QueryStatus) { + switch q.Err { + case nil, db.ErrNoMoreRows: + return // Don't log successful queries. + } + // Alert of any other error. + log.Printf("Expected database error: %v\n%s", q.Err, q.String()) +} + +type artistType struct { + ID int64 `db:"id,omitempty"` + Name string `db:"name"` +} + +type itemWithCompoundKey struct { + Code string `db:"code"` + UserID string `db:"user_id"` + SomeVal string `db:"some_val"` +} + +type customType struct { + Val []byte +} + +type artistWithCustomType struct { + Custom customType `db:"name"` +} + +func (f customType) String() string { + return fmt.Sprintf("foo: %s", string(f.Val)) +} + +func (f customType) MarshalDB() (interface{}, error) { + return f.String(), nil +} + +func (f *customType) UnmarshalDB(in interface{}) error { + switch t := in.(type) { + case []byte: + f.Val = t + case string: + f.Val = []byte(t) + } + return nil +} + +var ( + _ = db.Marshaler(&customType{}) + _ = db.Unmarshaler(&customType{}) +) + +type SQLTestSuite struct { + suite.Suite + + Helper +} + +func (s *SQLTestSuite) AfterTest(suiteName, testName string) { + err := s.TearDown() + s.NoError(err) +} + +func (s *SQLTestSuite) BeforeTest(suiteName, testName string) { + err := s.TearUp() + s.NoError(err) + + sess := s.SQLBuilder() + + // Creating test data + artist := sess.Collection("artist") + + artistNames := []string{"Ozzie", "Flea", "Slash", "Chrono"} + for _, artistName := range artistNames { + _, err := artist.Insert(map[string]string{ + "name": artistName, + }) + s.NoError(err) + } +} + +func (s *SQLTestSuite) TestPreparedStatementsCache() { + sess := s.SQLBuilder() + + sess.SetPreparedStatementCache(true) + defer sess.SetPreparedStatementCache(false) + + var tMu sync.Mutex + tFatal := func(err error) { + tMu.Lock() + defer tMu.Unlock() + + s.T().Fatalf("tmu: %v", err) + } + + // This limit was chosen because, by default, MySQL accepts 16k statements + // and dies. See https://github.com/upper/db/issues/287 + limit := 20000 + var wg sync.WaitGroup + + for i := 0; i < limit; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + + // This query is different on each iteration and generates a new + // prepared statement everytime it's called. + res := sess.Collection("artist"). + Find(). + Select(db.Raw(fmt.Sprintf("count(%d)", i))) + + var count map[string]uint64 + err := res.One(&count) + if err != nil { + tFatal(err) + } + }(i) + } + wg.Wait() + + // Concurrent Insert can open many connections on MySQL / PostgreSQL, this + // sets a limit on them. + sess.SetMaxOpenConns(90) + + switch s.Adapter() { + case "ql": + limit = 1000 + case "sqlite": + // TODO: We'll probably be able to workaround this with a mutex on inserts. + s.T().Skip(`Skipped due to a "database is locked" problem with concurrent transactions. See https://github.com/mattn/go-sqlite3/issues/274`) + } + + for i := 0; i < limit; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + // The same prepared query on every iteration. + _, err := sess.Collection("artist").Insert(artistType{ + Name: fmt.Sprintf("artist-%d", i), + }) + if err != nil { + tFatal(err) + } + }(i) + } + wg.Wait() + + // Insert returning creates a transaction. + for i := 0; i < limit; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + // The same prepared query on every iteration. + artist := artistType{ + Name: fmt.Sprintf("artist-%d", i), + } + err := sess.Collection("artist").InsertReturning(&artist) + if err != nil { + tFatal(err) + } + }(i) + } + wg.Wait() + + // Removing the limit. + sess.SetMaxOpenConns(0) +} + +func (s *SQLTestSuite) TestTruncateAllCollections() { + sess := s.SQLBuilder() + + collections, err := sess.Collections() + s.NoError(err) + s.True(len(collections) > 0) + + for _, name := range collections { + col := sess.Collection(name) + + if col.Exists() { + if err = col.Truncate(); err != nil { + s.NoError(err) + } + } + } +} + +func (s *SQLTestSuite) TestCustomQueryLogger() { + sess := s.SQLBuilder() + + sess.SetLogger(&customLogger{}) + sess.SetLogging(true) + defer func() { + sess.SetLogger(nil) + sess.SetLogging(false) + }() + + _, err := sess.Collection("artist").Find().Count() + s.Equal(nil, err) + + _, err = sess.Collection("artist_x").Find().Count() + s.NotEqual(nil, err) +} + +func (s *SQLTestSuite) TestExpectCursorError() { + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + + res := artist.Find(-1) + c, err := res.Count() + s.Equal(uint64(0), c) + s.NoError(err) + + var item map[string]interface{} + err = res.One(&item) + s.Error(err) +} + +func (s *SQLTestSuite) TestInsertDefault() { + if s.Adapter() == "ql" { + s.T().Skip("Currently not supported.") + } + + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + + err := artist.Truncate() + s.NoError(err) + + id, err := artist.Insert(&artistType{}) + s.NoError(err) + s.NotNil(id) + + err = artist.Truncate() + s.NoError(err) + + id, err = artist.Insert(nil) + s.NoError(err) + s.NotNil(id) +} + +func (s *SQLTestSuite) TestInsertReturning() { + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + + err := artist.Truncate() + s.NoError(err) + + itemMap := map[string]string{ + "name": "Ozzie", + } + s.Zero(itemMap["id"], "Must be zero before inserting") + err = artist.InsertReturning(&itemMap) + s.NoError(err) + s.NotZero(itemMap["id"], "Must not be zero after inserting") + + itemStruct := struct { + ID int `db:"id,omitempty"` + Name string `db:"name"` + }{ + 0, + "Flea", + } + s.Zero(itemStruct.ID, "Must be zero before inserting") + err = artist.InsertReturning(&itemStruct) + s.NoError(err) + s.NotZero(itemStruct.ID, "Must not be zero after inserting") + + count, err := artist.Find().Count() + s.NoError(err) + s.Equal(uint64(2), count, "Expecting 2 elements") + + itemStruct2 := struct { + ID int `db:"id,omitempty"` + Name string `db:"name"` + }{ + 0, + "Slash", + } + s.Zero(itemStruct2.ID, "Must be zero before inserting") + err = artist.InsertReturning(itemStruct2) + s.Error(err, "Should not happen, using a pointer should be enforced") + s.Zero(itemStruct2.ID, "Must still be zero because there was no insertion") + + itemMap2 := map[string]string{ + "name": "Janus", + } + s.Zero(itemMap2["id"], "Must be zero before inserting") + err = artist.InsertReturning(itemMap2) + s.Error(err, "Should not happen, using a pointer should be enforced") + s.Zero(itemMap2["id"], "Must still be zero because there was no insertion") + + // Counting elements, must be exactly 2 elements. + count, err = artist.Find().Count() + s.NoError(err) + s.Equal(uint64(2), count, "Expecting 2 elements") +} + +func (s *SQLTestSuite) TestInsertReturningWithinTransaction() { + sess := s.SQLBuilder() + + err := sess.Collection("artist").Truncate() + s.NoError(err) + + tx, err := sess.NewTx(nil) + s.NoError(err) + defer tx.Close() + + artist := tx.Collection("artist") + + itemMap := map[string]string{ + "name": "Ozzie", + } + s.Zero(itemMap["id"], "Must be zero before inserting") + err = artist.InsertReturning(&itemMap) + s.NoError(err) + s.NotZero(itemMap["id"], "Must not be zero after inserting") + + itemStruct := struct { + ID int `db:"id,omitempty"` + Name string `db:"name"` + }{ + 0, + "Flea", + } + s.Zero(itemStruct.ID, "Must be zero before inserting") + err = artist.InsertReturning(&itemStruct) + s.NoError(err) + s.NotZero(itemStruct.ID, "Must not be zero after inserting") + + count, err := artist.Find().Count() + s.NoError(err) + s.Equal(uint64(2), count, "Expecting 2 elements") + + itemStruct2 := struct { + ID int `db:"id,omitempty"` + Name string `db:"name"` + }{ + 0, + "Slash", + } + s.Zero(itemStruct2.ID, "Must be zero before inserting") + err = artist.InsertReturning(itemStruct2) + s.Error(err, "Should not happen, using a pointer should be enforced") + s.Zero(itemStruct2.ID, "Must still be zero because there was no insertion") + + itemMap2 := map[string]string{ + "name": "Janus", + } + s.Zero(itemMap2["id"], "Must be zero before inserting") + err = artist.InsertReturning(itemMap2) + s.Error(err, "Should not happen, using a pointer should be enforced") + s.Zero(itemMap2["id"], "Must still be zero because there was no insertion") + + // Counting elements, must be exactly 2 elements. + count, err = artist.Find().Count() + s.NoError(err) + s.Equal(uint64(2), count, "Expecting 2 elements") + + // Rolling back everything + err = tx.Rollback() + s.NoError(err) + + // Expecting no elements. + count, err = sess.Collection("artist").Find().Count() + s.NoError(err) + s.Equal(uint64(0), count, "Expecting 0 elements, everything was rolled back!") +} + +func (s *SQLTestSuite) TestInsertIntoArtistsTable() { + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + + err := artist.Truncate() + s.NoError(err) + + itemMap := map[string]string{ + "name": "Ozzie", + } + + id, err := artist.Insert(itemMap) + s.NoError(err) + s.NotNil(id) + + if pk, ok := id.(int64); !ok || pk == 0 { + s.T().Fatalf("Expecting an ID.") + } + + // Attempt to append a struct. + itemStruct := struct { + Name string `db:"name"` + }{ + "Flea", + } + + id, err = artist.Insert(itemStruct) + s.NoError(err) + s.NotNil(id) + + if pk, ok := id.(int64); !ok || pk == 0 { + s.T().Fatalf("Expecting an ID.") + } + + // Attempt to append a tagged struct. + itemStruct2 := struct { + ArtistName string `db:"name"` + }{ + "Slash", + } + + id, err = artist.Insert(itemStruct2) + s.NoError(err) + s.NotNil(id) + + if pk, ok := id.(int64); !ok || pk == 0 { + s.T().Fatalf("Expecting an ID.") + } + + // Attempt to append and update a private key + itemStruct3 := artistType{ + Name: "Janus", + } + + id, err = artist.Insert(&itemStruct3) + s.NoError(err) + if s.Adapter() != "ql" { + s.NotZero(id) // QL always inserts an ID. + } + + // Counting elements, must be exactly 4 elements. + count, err := artist.Find().Count() + s.NoError(err) + s.Equal(uint64(4), count) + + count, err = artist.Find(db.Cond{"name": db.Eq("Ozzie")}).Count() + s.NoError(err) + s.Equal(uint64(1), count) + + count, err = artist.Find("name", "Ozzie").And("name", "Flea").Count() + s.NoError(err) + s.Equal(uint64(0), count) + + count, err = artist.Find(db.Or(db.Cond{"name": "Ozzie"}, db.Cond{"name": "Flea"})).Count() + s.NoError(err) + s.Equal(uint64(2), count) + + count, err = artist.Find(db.And(db.Cond{"name": "Ozzie"}, db.Cond{"name": "Flea"})).Count() + s.NoError(err) + s.Equal(uint64(0), count) + + count, err = artist.Find(db.Cond{"name": "Ozzie"}).And(db.Cond{"name": "Flea"}).Count() + s.NoError(err) + s.Equal(uint64(0), count) +} + +func (s *SQLTestSuite) TestQueryNonExistentCollection() { + sess := s.SQLBuilder() + + count, err := sess.Collection("doesnotexist").Find().Count() + s.Error(err) + s.Zero(count) +} + +func (s *SQLTestSuite) TestGetOneResult() { + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + + for i := 0; i < 5; i++ { + _, err := artist.Insert(map[string]string{ + "name": fmt.Sprintf("Artist %d", i), + }) + s.NoError(err) + } + + // Fetching one struct. + var someArtist artistType + err := artist.Find().Limit(1).One(&someArtist) + s.NoError(err) + + s.NotZero(someArtist.Name) + if s.Adapter() != "ql" { + s.NotZero(someArtist.ID) + } + + // Fetching a pointer to a pointer. + var someArtistObj *artistType + err = artist.Find().Limit(1).One(&someArtistObj) + s.NoError(err) + s.NotZero(someArtist.Name) + if s.Adapter() != "ql" { + s.NotZero(someArtist.ID) + } +} + +func (s *SQLTestSuite) TestGetWithOffset() { + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + + // Fetching one struct. + var artists []artistType + err := artist.Find().Offset(1).All(&artists) + s.NoError(err) + + s.Equal(3, len(artists)) +} + +func (s *SQLTestSuite) TestGetResultsOneByOne() { + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + + rowMap := map[string]interface{}{} + + res := artist.Find() + + if s.Adapter() == "ql" { + res = res.Select("id() as id", "name") + } + + err := res.Err() + s.NoError(err) + + for res.Next(&rowMap) { + s.NotZero(rowMap["id"]) + s.NotZero(rowMap["name"]) + } + err = res.Err() + s.NoError(err) + + err = res.Close() + s.NoError(err) + + // Dumping into a tagged struct. + rowStruct2 := struct { + Value1 int64 `db:"id"` + Value2 string `db:"name"` + }{} + + res = artist.Find() + + if s.Adapter() == "ql" { + res = res.Select("id() as id", "name") + } + + for res.Next(&rowStruct2) { + s.NotZero(rowStruct2.Value1) + s.NotZero(rowStruct2.Value2) + } + err = res.Err() + s.NoError(err) + + err = res.Close() + s.NoError(err) + + // Dumping into a slice of maps. + allRowsMap := []map[string]interface{}{} + + res = artist.Find() + if s.Adapter() == "ql" { + res.Select("id() as id") + } + + err = res.All(&allRowsMap) + s.NoError(err) + s.Equal(4, len(allRowsMap)) + + for _, singleRowMap := range allRowsMap { + if fmt.Sprintf("%d", singleRowMap["id"]) == "0" { + s.T().Fatalf("Expecting a not null ID.") + } + } + + // Dumping into a slice of structs. + allRowsStruct := []struct { + ID int64 `db:"id,omitempty"` + Name string `db:"name"` + }{} + + res = artist.Find() + if s.Adapter() == "ql" { + res.Select("id() as id") + } + + if err = res.All(&allRowsStruct); err != nil { + s.T().Fatal(err) + } + + s.Equal(4, len(allRowsStruct)) + + for _, singleRowStruct := range allRowsStruct { + s.NotZero(singleRowStruct.ID) + } + + // Dumping into a slice of tagged structs. + allRowsStruct2 := []struct { + Value1 int64 `db:"id"` + Value2 string `db:"name"` + }{} + + res = artist.Find() + if s.Adapter() == "ql" { + res.Select("id() as id", "name") + } + + err = res.All(&allRowsStruct2) + s.NoError(err) + + s.Equal(4, len(allRowsStruct2)) + + for _, singleRowStruct := range allRowsStruct2 { + s.NotZero(singleRowStruct.Value1) + } +} + +func (s *SQLTestSuite) TestGetAllResults() { + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + + total, err := artist.Find().Count() + s.NoError(err) + s.NotZero(total) + + // Fetching all artists into struct + artists := []artistType{} + + res := artist.Find() + if s.Adapter() == "ql" { + res.Select("id() as id", "name") + } + + err = res.All(&artists) + s.NoError(err) + s.Equal(len(artists), int(total)) + + s.NotZero(artists[0].Name) + s.NotZero(artists[0].ID) + + // Fetching all artists into struct pointers + artistObjs := []*artistType{} + res = artist.Find() + if s.Adapter() == "ql" { + res.Select("id() as id", "name") + } + err = res.All(&artistObjs) + s.NoError(err) + s.Equal(len(artistObjs), int(total)) + + s.NotZero(artistObjs[0].Name) + s.NotZero(artistObjs[0].ID) +} + +func (s *SQLTestSuite) TestInlineStructs() { + type reviewTypeDetails struct { + Name string `db:"name"` + Comments string `db:"comments"` + Created time.Time `db:"created"` + } + + type reviewType struct { + ID int64 `db:"id,omitempty"` + PublicationID int64 `db:"publication_id"` + Details reviewTypeDetails `db:",inline"` + } + + sess := s.SQLBuilder() + + review := sess.Collection("review") + + err := review.Truncate() + s.NoError(err) + + rec := reviewType{ + PublicationID: 123, + Details: reviewTypeDetails{ + Name: "..name..", + Comments: "..comments..", + }, + } + + var createdAt time.Time + + switch s.Adapter() { + case "postgresql": + createdAt = time.Date(2016, time.January, 1, 2, 3, 4, 0, time.FixedZone("", 0)) + case "mysql": + // MySQL uses a global time zone + createdAt = time.Date(2016, time.January, 1, 2, 3, 4, 0, TimeLocation) + default: + createdAt = time.Date(2016, time.January, 1, 2, 3, 4, 0, time.UTC) + } + rec.Details.Created = createdAt + + id, err := review.Insert(rec) + s.NoError(err) + s.NotZero(id.(int64)) + + rec.ID = id.(int64) + + var recChk reviewType + res := review.Find() + if s.Adapter() == "ql" { + res.Select("id() as id", "publication_id", "comments", "name", "created") + } + err = res.One(&recChk) + s.NoError(err) + + s.Equal(rec, recChk) +} + +func (s *SQLTestSuite) TestUpdate() { + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + + _, err := artist.Insert(map[string]string{ + "name": "Ozzie", + }) + s.NoError(err) + + // Defining destination struct + value := struct { + ID int64 `db:"id,omitempty"` + Name string `db:"name"` + }{} + + // Getting the first artist. + cond := db.Cond{"id !=": db.NotEq(0)} + if s.Adapter() == "ql" { + cond = db.Cond{"id() !=": 0} + } + res := artist.Find(cond).Limit(1) + + err = res.One(&value) + s.NoError(err) + + res = artist.Find(value.ID) + + // Updating set with a map + rowMap := map[string]interface{}{ + "name": strings.ToUpper(value.Name), + } + + err = res.Update(rowMap) + s.NoError(err) + + // Pulling it again. + err = res.One(&value) + s.NoError(err) + + // Verifying. + s.Equal(value.Name, rowMap["name"]) + + if s.Adapter() != "ql" { + + // Updating using raw + if err = res.Update(map[string]interface{}{"name": db.Raw("LOWER(name)")}); err != nil { + s.T().Fatal(err) + } + + // Pulling it again. + err = res.One(&value) + s.NoError(err) + + // Verifying. + s.Equal(value.Name, strings.ToLower(rowMap["name"].(string))) + + // Updating using raw + if err = res.Update(struct { + Name db.RawValue `db:"name"` + }{db.Raw(`UPPER(name)`)}); err != nil { + s.T().Fatal(err) + } + + // Pulling it again. + err = res.One(&value) + s.NoError(err) + + // Verifying. + s.Equal(value.Name, strings.ToUpper(rowMap["name"].(string))) + + // Updating using raw + if err = res.Update(struct { + Name db.Function `db:"name"` + }{db.Func("LOWER", db.Raw("name"))}); err != nil { + s.T().Fatal(err) + } + + // Pulling it again. + err = res.One(&value) + s.NoError(err) + + // Verifying. + s.Equal(value.Name, strings.ToLower(rowMap["name"].(string))) + } + + // Updating set with a struct + rowStruct := struct { + Name string `db:"name"` + }{strings.ToLower(value.Name)} + + err = res.Update(rowStruct) + s.NoError(err) + + // Pulling it again. + err = res.One(&value) + s.NoError(err) + + // Verifying + s.Equal(value.Name, rowStruct.Name) + + // Updating set with a tagged struct + rowStruct2 := struct { + Value1 string `db:"name"` + }{"john"} + + err = res.Update(rowStruct2) + s.NoError(err) + + // Pulling it again. + err = res.One(&value) + s.NoError(err) + + // Verifying + s.Equal(value.Name, rowStruct2.Value1) + + // Updating set with a tagged object + rowStruct3 := &struct { + Value1 string `db:"name"` + }{"anderson"} + + err = res.Update(rowStruct3) + s.NoError(err) + + // Pulling it again. + err = res.One(&value) + s.NoError(err) + + // Verifying + s.Equal(value.Name, rowStruct3.Value1) +} + +func (s *SQLTestSuite) TestFunction() { + sess := s.SQLBuilder() + + rowStruct := struct { + ID int64 + Name string + }{} + + artist := sess.Collection("artist") + + cond := db.Cond{"id NOT IN": []int{0, -1}} + if s.Adapter() == "ql" { + cond = db.Cond{"id() NOT IN": []int{0, -1}} + } + res := artist.Find(cond) + + err := res.One(&rowStruct) + s.NoError(err) + + total, err := res.Count() + s.NoError(err) + s.Equal(uint64(4), total) + + // Testing conditions + cond = db.Cond{"id NOT IN": []interface{}{0, -1}} + if s.Adapter() == "ql" { + cond = db.Cond{"id() NOT IN": []interface{}{0, -1}} + } + res = artist.Find(cond) + + err = res.One(&rowStruct) + s.NoError(err) + + total, err = res.Count() + s.NoError(err) + s.Equal(uint64(4), total) + + res = artist.Find().Select("name") + + var rowMap map[string]interface{} + err = res.One(&rowMap) + s.NoError(err) + + total, err = res.Count() + s.NoError(err) + s.Equal(uint64(4), total) + + res = artist.Find().Select("name") + + err = res.One(&rowMap) + s.NoError(err) + + total, err = res.Count() + s.NoError(err) + s.Equal(uint64(4), total) +} + +func (s *SQLTestSuite) TestNullableFields() { + sess := s.SQLBuilder() + + type testType struct { + ID int64 `db:"id,omitempty"` + NullStringTest sql.NullString `db:"_string"` + NullInt64Test sql.NullInt64 `db:"_int64"` + NullFloat64Test sql.NullFloat64 `db:"_float64"` + NullBoolTest sql.NullBool `db:"_bool"` + } + + col := sess.Collection(`data_types`) + + err := col.Truncate() + s.NoError(err) + + // Testing insertion of invalid nulls. + test := testType{ + NullStringTest: sql.NullString{"", false}, + NullInt64Test: sql.NullInt64{0, false}, + NullFloat64Test: sql.NullFloat64{0.0, false}, + NullBoolTest: sql.NullBool{false, false}, + } + + id, err := col.Insert(testType{}) + s.NoError(err) + + // Testing fetching of invalid nulls. + err = col.Find(id).One(&test) + s.NoError(err) + + s.False(test.NullInt64Test.Valid) + s.False(test.NullFloat64Test.Valid) + s.False(test.NullBoolTest.Valid) + + // Testing insertion of valid nulls. + test = testType{ + NullStringTest: sql.NullString{"", true}, + NullInt64Test: sql.NullInt64{0, true}, + NullFloat64Test: sql.NullFloat64{0.0, true}, + NullBoolTest: sql.NullBool{false, true}, + } + + id, err = col.Insert(test) + s.NoError(err) + + // Testing fetching of valid nulls. + err = col.Find(id).One(&test) + s.NoError(err) + + s.True(test.NullInt64Test.Valid) + s.True(test.NullBoolTest.Valid) + s.True(test.NullStringTest.Valid) +} + +func (s *SQLTestSuite) TestGroup() { + sess := s.SQLBuilder() + + type statsType struct { + Numeric int `db:"numeric"` + Value int `db:"value"` + } + + stats := sess.Collection("stats_test") + + err := stats.Truncate() + s.NoError(err) + + // Adding row append. + for i := 0; i < 100; i++ { + numeric, value := rand.Intn(5), rand.Intn(100) + _, err := stats.Insert(statsType{numeric, value}) + s.NoError(err) + } + + // Testing GROUP BY + res := stats.Find().Select( + "numeric", + db.Raw("count(1) AS counter"), + db.Raw("sum(value) AS total"), + ).Group("numeric") + + var results []map[string]interface{} + + err = res.All(&results) + s.NoError(err) + + s.Equal(5, len(results)) +} + +func (s *SQLTestSuite) TestInsertAndDelete() { + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + res := artist.Find() + + total, err := res.Count() + s.NoError(err) + + err = res.Delete() + s.NoError(err) + + total, err = res.Count() + s.NoError(err) + s.Equal(uint64(0), total) +} + +func (s *SQLTestSuite) TestCompositeKeys() { + if s.Adapter() == "ql" { + s.T().Skip("Currently not supported.") + } + + sess := s.SQLBuilder() + + compositeKeys := sess.Collection("composite_keys") + + { + n := rand.Intn(100000) + + item := itemWithCompoundKey{ + "ABCDEF", + strconv.Itoa(n), + "Some value", + } + + id, err := compositeKeys.Insert(&item) + s.NoError(err) + s.NotZero(id) + + var item2 itemWithCompoundKey + s.NotEqual(item2.SomeVal, item.SomeVal) + + // Finding by ID + err = compositeKeys.Find(id).One(&item2) + s.NoError(err) + + s.Equal(item2.SomeVal, item.SomeVal) + } + + { + n := rand.Intn(100000) + + item := itemWithCompoundKey{ + "ABCDEF", + strconv.Itoa(n), + "Some value", + } + + err := compositeKeys.InsertReturning(&item) + s.NoError(err) + } +} + +// Attempts to test database transactions. +func (s *SQLTestSuite) TestTransactionsAndRollback() { + if s.Adapter() == "ql" { + s.T().Skip("Currently not supported.") + } + + sess := s.SQLBuilder() + + // Simple transaction that should not fail. + tx, err := sess.NewTx(nil) + s.NoError(err) + + artist := tx.Collection("artist") + err = artist.Truncate() + s.NoError(err) + + _, err = artist.Insert(artistType{1, "First"}) + s.NoError(err) + + err = tx.Commit() + s.NoError(err) + + // An attempt to use the same transaction must fail. + err = tx.Commit() + s.Error(err) + + err = tx.Close() + s.NoError(err) + + err = tx.Close() + s.NoError(err) + + // Use another transaction. + tx, err = sess.NewTx(nil) + s.NoError(err) + + artist = tx.Collection("artist") + + _, err = artist.Insert(artistType{2, "Second"}) + s.NoError(err) + + // Won't fail. + _, err = artist.Insert(artistType{3, "Third"}) + s.NoError(err) + + // Will fail. + _, err = artist.Insert(artistType{1, "Duplicated"}) + s.Error(err) + + err = tx.Rollback() + s.NoError(err) + + err = tx.Commit() + s.Error(err, "Already rolled back.") + + // Let's verify we still have one element. + artist = sess.Collection("artist") + + count, err := artist.Find().Count() + s.NoError(err) + s.Equal(uint64(1), count) + + err = tx.Close() + s.NoError(err) + + // Attempt to add some rows. + tx, err = sess.NewTx(nil) + s.NoError(err) + + artist = tx.Collection("artist") + + // Won't fail. + _, err = artist.Insert(artistType{2, "Second"}) + s.NoError(err) + + // Won't fail. + _, err = artist.Insert(artistType{3, "Third"}) + s.NoError(err) + + // Then rollback for no reason. + err = tx.Rollback() + s.NoError(err) + + err = tx.Commit() + s.Error(err, "Already rolled back.") + + // Let's verify we still have one element. + artist = sess.Collection("artist") + + count, err = artist.Find().Count() + s.NoError(err) + s.Equal(uint64(1), count) + + err = tx.Close() + s.NoError(err) + + // Attempt to add some rows. + tx, err = sess.NewTx(nil) + s.NoError(err) + + artist = tx.Collection("artist") + + // Won't fail. + _, err = artist.Insert(artistType{2, "Second"}) + s.NoError(err) + + // Won't fail. + _, err = artist.Insert(artistType{3, "Third"}) + s.NoError(err) + + err = tx.Commit() + s.NoError(err) + + err = tx.Rollback() + s.Error(err, "Already committed") + + // Let's verify we have 3 rows. + artist = sess.Collection("artist") + + count, err = artist.Find().Count() + s.NoError(err) + s.Equal(uint64(3), count) +} + +func (s *SQLTestSuite) TestDataTypes() { + if s.Adapter() == "ql" { + s.T().Skip("Currently not supported.") + } + + type testValuesStruct struct { + Uint uint `db:"_uint"` + Uint8 uint8 `db:"_uint8"` + Uint16 uint16 `db:"_uint16"` + Uint32 uint32 `db:"_uint32"` + Uint64 uint64 `db:"_uint64"` + + Int int `db:"_int"` + Int8 int8 `db:"_int8"` + Int16 int16 `db:"_int16"` + Int32 int32 `db:"_int32"` + Int64 int64 `db:"_int64"` + + Float32 float32 `db:"_float32"` + Float64 float64 `db:"_float64"` + + Bool bool `db:"_bool"` + String string `db:"_string"` + Blob []byte `db:"_blob"` + + Date time.Time `db:"_date"` + DateN *time.Time `db:"_nildate"` + DateP *time.Time `db:"_ptrdate"` + DateD *time.Time `db:"_defaultdate,omitempty"` + Time int64 `db:"_time"` + } + + sess := s.SQLBuilder() + + // Getting a pointer to the "data_types" collection. + dataTypes := sess.Collection("data_types") + + // Removing all data. + err := dataTypes.Truncate() + s.NoError(err) + + // Inserting our test subject. + loc, err := time.LoadLocation(TimeZone) + s.NoError(err) + + ts := time.Date(2011, 7, 28, 1, 2, 3, 0, loc) // timestamp with time zone + + var tnz time.Time + switch s.Adapter() { + case "postgresql": + tnz = time.Date(2012, 7, 28, 1, 2, 3, 0, time.FixedZone("", 0)) // timestamp without time zone + case "mysql": + // MySQL uses a global timezone + tnz = time.Date(2012, 7, 28, 1, 2, 3, 0, TimeLocation) + default: + tnz = time.Date(2012, 7, 28, 1, 2, 3, 0, time.UTC) // timestamp without time zone + } + + testValues := testValuesStruct{ + 1, 1, 1, 1, 1, + -1, -1, -1, -1, -1, + + 1.337, 1.337, + + true, + "Hello world!", + []byte("Hello world!"), + + ts, + nil, + &tnz, + nil, + int64(time.Second * time.Duration(7331)), + } + id, err := dataTypes.Insert(testValues) + s.NoError(err) + s.NotNil(id) + + // Defining our set. + cond := db.Cond{"id": id} + if s.Adapter() == "ql" { + cond = db.Cond{"id()": id} + } + res := dataTypes.Find(cond) + + count, err := res.Count() + s.NoError(err) + s.NotZero(count) + + // Trying to dump the subject into an empty structure of the same type. + var item testValuesStruct + + err = res.One(&item) + s.NoError(err) + s.NotNil(item.DateD) + + // Copy the default date (this value is set by the database) + testValues.DateD = item.DateD + item.Date = item.Date.In(loc) + + // The original value and the test subject must match. + s.Equal(testValues, item) +} + +func (s *SQLTestSuite) TestUpdateWithNullColumn() { + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + err := artist.Truncate() + s.NoError(err) + + type Artist struct { + ID int64 `db:"id,omitempty"` + Name *string `db:"name"` + } + + name := "José" + id, err := artist.Insert(Artist{0, &name}) + s.NoError(err) + + var item Artist + err = artist.Find(id).One(&item) + s.NoError(err) + + s.NotEqual(nil, item.Name) + s.Equal(name, *item.Name) + + artist.Find(id).Update(Artist{Name: nil}) + s.NoError(err) + + var item2 Artist + err = artist.Find(id).One(&item2) + s.NoError(err) + + s.Equal((*string)(nil), item2.Name) +} + +func (s *SQLTestSuite) TestBatchInsert() { + sess := s.SQLBuilder() + + for batchSize := 0; batchSize < 17; batchSize++ { + err := sess.Collection("artist").Truncate() + s.NoError(err) + + q := sess.InsertInto("artist").Columns("name") + + if s.Adapter() == "postgresql" { + q = q.Amend(func(query string) string { + return query + ` ON CONFLICT DO NOTHING` + }) + } + + batch := q.Batch(batchSize) + + totalItems := int(rand.Int31n(21)) + + go func() { + defer batch.Done() + for i := 0; i < totalItems; i++ { + batch.Values(fmt.Sprintf("artist-%d", i)) + } + }() + + err = batch.Wait() + s.NoError(err) + s.NoError(batch.Err()) + + c, err := sess.Collection("artist").Find().Count() + s.NoError(err) + s.Equal(uint64(totalItems), c) + + for i := 0; i < totalItems; i++ { + c, err := sess.Collection("artist").Find(db.Cond{"name": fmt.Sprintf("artist-%d", i)}).Count() + s.NoError(err) + s.Equal(uint64(1), c) + } + } +} + +func (s *SQLTestSuite) TestBatchInsertNoColumns() { + sess := s.SQLBuilder() + + for batchSize := 0; batchSize < 17; batchSize++ { + err := sess.Collection("artist").Truncate() + s.NoError(err) + + batch := sess.InsertInto("artist").Batch(batchSize) + + totalItems := int(rand.Int31n(21)) + + go func() { + defer batch.Done() + for i := 0; i < totalItems; i++ { + value := struct { + Name string `db:"name"` + }{fmt.Sprintf("artist-%d", i)} + batch.Values(value) + } + }() + + err = batch.Wait() + s.NoError(err) + s.NoError(batch.Err()) + + c, err := sess.Collection("artist").Find().Count() + s.NoError(err) + s.Equal(uint64(totalItems), c) + + for i := 0; i < totalItems; i++ { + c, err := sess.Collection("artist").Find(db.Cond{"name": fmt.Sprintf("artist-%d", i)}).Count() + s.NoError(err) + s.Equal(uint64(1), c) + } + } +} + +func (s *SQLTestSuite) TestBatchInsertReturningKeys() { + if s.Adapter() != "postgresql" { + s.T().Skip("Currently not supported.") + } + + sess := s.SQLBuilder() + + err := sess.Collection("artist").Truncate() + s.NoError(err) + + batchSize, totalItems := 7, 12 + + batch := sess.InsertInto("artist").Columns("name").Returning("id").Batch(batchSize) + + go func() { + defer batch.Done() + for i := 0; i < totalItems; i++ { + batch.Values(fmt.Sprintf("artist-%d", i)) + } + }() + + var keyMap []struct { + ID int `db:"id"` + } + for batch.NextResult(&keyMap) { + // Each insertion must produce new keys. + s.True(len(keyMap) > 0) + s.True(len(keyMap) <= batchSize) + + // Find the elements we've just inserted + keys := make([]int, 0, len(keyMap)) + for i := range keyMap { + keys = append(keys, keyMap[i].ID) + } + + // Make sure count matches. + c, err := sess.Collection("artist").Find(db.Cond{"id": keys}).Count() + s.NoError(err) + s.Equal(uint64(len(keyMap)), c) + } + s.NoError(batch.Err()) + + // Count all new elements + c, err := sess.Collection("artist").Find().Count() + s.NoError(err) + s.Equal(uint64(totalItems), c) +} + +func (s *SQLTestSuite) TestPaginator() { + sess := s.SQLBuilder() + + err := sess.Collection("artist").Truncate() + s.NoError(err) + + batch := sess.InsertInto("artist").Batch(100) + + go func() { + defer batch.Done() + for i := 0; i < 999; i++ { + value := struct { + Name string `db:"name"` + }{fmt.Sprintf("artist-%d", i)} + batch.Values(value) + } + }() + + err = batch.Wait() + s.NoError(err) + s.NoError(batch.Err()) + + q := sess.SelectFrom("artist") + if s.Adapter() == "ql" { + q = sess.SelectFrom(sess.Select("id() AS id", "name").From("artist")) + } + + const pageSize = 13 + cursorColumn := "id" + + paginator := q.Paginate(pageSize) + + var zerothPage []artistType + err = paginator.Page(0).All(&zerothPage) + s.NoError(err) + s.Equal(pageSize, len(zerothPage)) + + var firstPage []artistType + err = paginator.Page(1).All(&firstPage) + s.NoError(err) + s.Equal(pageSize, len(firstPage)) + + s.Equal(zerothPage, firstPage) + + var secondPage []artistType + err = paginator.Page(2).All(&secondPage) + s.NoError(err) + s.Equal(pageSize, len(secondPage)) + + totalPages, err := paginator.TotalPages() + s.NoError(err) + s.NotZero(totalPages) + s.Equal(uint(77), totalPages) + + totalEntries, err := paginator.TotalEntries() + s.NoError(err) + s.NotZero(totalEntries) + s.Equal(uint64(999), totalEntries) + + var lastPage []artistType + err = paginator.Page(totalPages).All(&lastPage) + s.NoError(err) + s.Equal(11, len(lastPage)) + + var beyondLastPage []artistType + err = paginator.Page(totalPages + 1).All(&beyondLastPage) + s.NoError(err) + s.Equal(0, len(beyondLastPage)) + + var hundredthPage []artistType + err = paginator.Page(100).All(&hundredthPage) + s.NoError(err) + s.Equal(0, len(hundredthPage)) + + for i := uint(0); i < totalPages; i++ { + current := paginator.Page(i + 1) + + var items []artistType + err := current.All(&items) + if err != nil { + s.T().Fatal(err) + } + if len(items) < 1 { + s.Equal(totalPages+1, i) + break + } + for j := 0; j < len(items); j++ { + s.Equal(fmt.Sprintf("artist-%d", int64(pageSize*int(i)+j)), items[j].Name) + } + } + + paginator = paginator.Cursor(cursorColumn) + { + current := paginator.Page(1) + for i := 0; ; i++ { + var items []artistType + err := current.All(&items) + if err != nil { + s.T().Fatal(err) + } + if len(items) < 1 { + s.Equal(int(totalPages), i) + break + } + + for j := 0; j < len(items); j++ { + s.Equal(fmt.Sprintf("artist-%d", int64(pageSize*int(i)+j)), items[j].Name) + } + current = current.NextPage(items[len(items)-1].ID) + } + } + + { + current := paginator.Page(totalPages) + for i := totalPages; ; i-- { + var items []artistType + + err := current.All(&items) + s.NoError(err) + + if len(items) < 1 { + s.Equal(uint(0), i) + break + } + for j := 0; j < len(items); j++ { + s.Equal(fmt.Sprintf("artist-%d", pageSize*int(i-1)+j), items[j].Name) + } + + current = current.PrevPage(items[0].ID) + } + } + + if s.Adapter() == "ql" { + s.T().Skip("Unsupported, see https://github.com/cznic/ql/issues/182") + } + + { + result := sess.Collection("artist").Find() + if s.Adapter() == "ql" { + result = result.Select("id() AS id", "name") + } + fifteenResults := 15 + resultPaginator := result.Paginate(uint(fifteenResults)) + + count, err := resultPaginator.TotalPages() + s.Equal(uint(67), count) + s.NoError(err) + + var items []artistType + fifthPage := 5 + err = resultPaginator.Page(uint(fifthPage)).All(&items) + s.NoError(err) + + for j := 0; j < len(items); j++ { + s.Equal(fmt.Sprintf("artist-%d", int(fifteenResults)*(fifthPage-1)+j), items[j].Name) + } + + resultPaginator = resultPaginator.Cursor(cursorColumn).Page(1) + for i := 0; ; i++ { + var items []artistType + + err = resultPaginator.All(&items) + s.NoError(err) + + if len(items) < 1 { + break + } + + for j := 0; j < len(items); j++ { + s.Equal(fmt.Sprintf("artist-%d", fifteenResults*i+j), items[j].Name) + } + resultPaginator = resultPaginator.NextPage(items[len(items)-1].ID) + } + + resultPaginator = resultPaginator.Cursor(cursorColumn).Page(count) + for i := count; ; i-- { + var items []artistType + + err = resultPaginator.All(&items) + s.NoError(err) + + if len(items) < 1 { + s.Equal(uint(0), i) + break + } + + for j := 0; j < len(items); j++ { + s.Equal(fmt.Sprintf("artist-%d", fifteenResults*(int(i)-1)+j), items[j].Name) + } + resultPaginator = resultPaginator.PrevPage(items[0].ID) + } + } + + { + // Testing page size 0. + paginator := q.Paginate(0) + + totalPages, err := paginator.TotalPages() + s.NoError(err) + s.Equal(uint(1), totalPages) + + totalEntries, err := paginator.TotalEntries() + s.NoError(err) + s.Equal(uint64(999), totalEntries) + + var allItems []artistType + err = paginator.Page(0).All(&allItems) + s.NoError(err) + s.Equal(totalEntries, uint64(len(allItems))) + + } +} + +func (s *SQLTestSuite) TestSQLBuilder() { + sess := s.SQLBuilder() + + var all []map[string]interface{} + + err := sess.Collection("artist").Truncate() + s.NoError(err) + + _, err = sess.InsertInto("artist").Values(struct { + Name string `db:"name"` + }{"Rinko Kikuchi"}).Exec() + s.NoError(err) + + // Using explicit iterator. + iter := sess.SelectFrom("artist").Iterator() + err = iter.All(&all) + + s.NoError(err) + s.NotZero(all) + + // Using explicit iterator to fetch one item. + var item map[string]interface{} + iter = sess.SelectFrom("artist").Iterator() + err = iter.One(&item) + + s.NoError(err) + s.NotZero(item) + + // Using explicit iterator and NextScan. + iter = sess.SelectFrom("artist").Iterator() + var id int + var name string + + if s.Adapter() == "ql" { + err = iter.NextScan(&name) + id = 1 + } else { + err = iter.NextScan(&id, &name) + } + + s.NoError(err) + s.NotZero(id) + s.NotEmpty(name) + s.NoError(iter.Close()) + + err = iter.NextScan(&id, &name) + s.Error(err) + + // Using explicit iterator and ScanOne. + iter = sess.SelectFrom("artist").Iterator() + id, name = 0, "" + if s.Adapter() == "ql" { + err = iter.ScanOne(&name) + id = 1 + } else { + err = iter.ScanOne(&id, &name) + } + + s.NoError(err) + s.NotZero(id) + s.NotEmpty(name) + + err = iter.ScanOne(&id, &name) + s.Error(err) + + // Using explicit iterator and Next. + iter = sess.SelectFrom("artist").Iterator() + + var artist map[string]interface{} + for iter.Next(&artist) { + if s.Adapter() != "ql" { + s.NotZero(artist["id"]) + } + s.NotEmpty(artist["name"]) + } + // We should not have any error after finishing successfully exiting a Next() loop. + s.Empty(iter.Err()) + + for i := 0; i < 5; i++ { + // But we'll get errors if we attempt to continue using Next(). + s.False(iter.Next(&artist)) + s.Error(iter.Err()) + } + + // Using implicit iterator. + q := sess.SelectFrom("artist") + err = q.All(&all) + + s.NoError(err) + s.NotZero(all) + + tx, err := sess.NewTx(nil) + s.NoError(err) + s.NotZero(tx) + defer tx.Close() + + q = tx.SelectFrom("artist") + s.NotZero(iter) + + err = q.All(&all) + s.NoError(err) + s.NotZero(all) + + s.NoError(tx.Commit()) +} + +func (s *SQLTestSuite) TestExhaustConnectionPool() { + if s.Adapter() == "ql" { + s.T().Skip("Currently not supported.") + } + + var tMu sync.Mutex + + tFatal := func(err error) { + tMu.Lock() + defer tMu.Unlock() + + s.T().Fatal(err) + } + + tLogf := func(format string, args ...interface{}) { + tMu.Lock() + defer tMu.Unlock() + + s.T().Logf(format, args...) + } + + sess := s.SQLBuilder() + + sess.SetLogging(true) + defer func() { + sess.SetLogging(false) + }() + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + tLogf("Tx %d: Pending", i) + + wg.Add(1) + go func(wg *sync.WaitGroup, i int) { + defer wg.Done() + + // Requesting a new transaction session. + start := time.Now() + tLogf("Tx: %d: NewTx", i) + tx, err := sess.NewTx(nil) + if err != nil { + tFatal(err) + } + tLogf("Tx %d: OK (time to connect: %v)", i, time.Now().Sub(start)) + + if !sess.LoggingEnabled() { + tLogf("Expecting logging to be enabled") + } + + if !tx.LoggingEnabled() { + tLogf("Expecting logging to be enabled (enabled by parent session)") + } + + // Let's suppose that we do a bunch of complex stuff and that the + // transaction lasts 3 seconds. + time.Sleep(time.Second * 3) + + switch i % 7 { + case 0: + var account map[string]interface{} + if err := tx.Collection("artist").Find().One(&account); err != nil { + tFatal(err) + } + if err := tx.Commit(); err != nil { + tFatal(err) + } + tLogf("Tx %d: Committed", i) + case 1: + if _, err := tx.DeleteFrom("artist").Exec(); err != nil { + tFatal(err) + } + if err := tx.Rollback(); err != nil { + tFatal(err) + } + tLogf("Tx %d: Rolled back", i) + case 2: + if err := tx.Close(); err != nil { + tFatal(err) + } + tLogf("Tx %d: Closed", i) + case 3: + var account map[string]interface{} + if err := tx.Collection("artist").Find().One(&account); err != nil { + tFatal(err) + } + if err := tx.Commit(); err != nil { + tFatal(err) + } + if err := tx.Close(); err != nil { + tFatal(err) + } + tLogf("Tx %d: Committed and closed", i) + case 4: + if err := tx.Rollback(); err != nil { + tFatal(err) + } + if err := tx.Close(); err != nil { + tFatal(err) + } + tLogf("Tx %d: Rolled back and closed", i) + case 5: + if err := tx.Close(); err != nil { + tFatal(err) + } + if err := tx.Commit(); err == nil { + tFatal(fmt.Errorf("Error expected")) + } + tLogf("Tx %d: Closed and committed", i) + case 6: + if err := tx.Close(); err != nil { + tFatal(err) + } + if err := tx.Rollback(); err == nil { + tFatal(fmt.Errorf("Error expected")) + } + tLogf("Tx %d: Closed and rolled back", i) + } + }(&wg, i) + } + + wg.Wait() +} + +func (s *SQLTestSuite) TestCustomType() { + // See https://github.com/upper/db/issues/332 + sess := s.SQLBuilder() + + artist := sess.Collection("artist") + + err := artist.Truncate() + s.NoError(err) + + id, err := artist.Insert(artistWithCustomType{ + Custom: customType{Val: []byte("some name")}, + }) + s.NoError(err) + s.NotNil(id) + + var bar artistWithCustomType + err = artist.Find(id).One(&bar) + s.NoError(err) + + s.Equal("foo: some name", string(bar.Custom.Val)) +} diff --git a/testsuite/suite.go b/testsuite/suite.go new file mode 100644 index 00000000..98f275b9 --- /dev/null +++ b/testsuite/suite.go @@ -0,0 +1,39 @@ +package testsuite + +import ( + "time" + + "github.com/stretchr/testify/suite" + db "upper.io/db.v3" + "upper.io/db.v3/lib/sqlbuilder" +) + +const TimeZone = "Canada/Eastern" + +var TimeLocation, _ = time.LoadLocation(TimeZone) + +type Helper interface { + SQLBuilder() sqlbuilder.Database + Session() db.Database + + Adapter() string + + TearUp() error + TearDown() error +} + +type Suite struct { + suite.Suite + + Helper +} + +func (s *Suite) AfterTest(suiteName, testName string) { + err := s.TearDown() + s.NoError(err) +} + +func (s *Suite) BeforeTest(suiteName, testName string) { + err := s.TearUp() + s.NoError(err) +}