From ce5be947b4061e5c037d169c11674eaae77045bc Mon Sep 17 00:00:00 2001 From: jtojnar Date: Sun, 16 Feb 2020 21:42:04 +0000 Subject: [PATCH] Add postgres support (#135) Revert fixture fixes for postgres Use postgres connection string with spaces instead of url Fix label order Make postgres tests in ci less verbose Add sequence update script Skip resets in postgres Remove option to skip resets in postgres Make postgres tests in ci verboseq Update test fixtures database Fix file tests on postgres Add postgres options to sample config Make sure tests init test fixtures before running the actual tests Fix issues with IDs too big to fit in an int Fix duplicate auto incremented IDs Refactor / Fix team tests Refactor team member tests Fix team member create Fix label test Fix getting labels Fix test fixtures for postgresql Fix connection string params Disable ssl mode on postgres integration tests Disable ssl mode on postgres tests Use sprintf to create the connection string for postgresql fixup! Add postgres support Add postgres support Added generate as a make dependency for make build Clarify docs on building Co-authored-by: kolaente Co-authored-by: Jan Tojnar Reviewed-on: https://kolaente.dev/vikunja/api/pulls/135 --- .drone1.yml | 52 +- Makefile | 6 +- config.yml.sample | 7 +- docs/content/doc/setup/backups.md | 18 + docs/content/doc/setup/config.md | 7 +- docs/content/doc/setup/install-backend.md | 1 + go.mod | 5 +- go.sum | 15 +- pkg/config/config.go | 2 + pkg/db/db.go | 48 + pkg/db/test_fixtures.go | 73 +- pkg/files/files_test.go | 4 +- pkg/migration/migration.go | 10 + pkg/models/label_task.go | 5 +- pkg/models/label_test.go | 1 - pkg/models/models.go | 1 + pkg/models/namespace_test.go | 2 +- pkg/models/task_attachment_test.go | 17 +- pkg/models/team_members_test.go | 107 +- pkg/models/teams.go | 12 +- pkg/models/teams_test.go | 166 +- .../testfixtures/v3}/.editorconfig | 0 .../testfixtures/v3}/.gitattributes | 0 .../testfixtures/v3}/.gitignore | 4 +- .../testfixtures/v3/.goreleaser.yml | 41 + .../testfixtures/v3}/.sample.env | 1 - .../testfixtures/v3/CHANGELOG.md | 83 + .../testfixtures/v3/Dockerfile | 9 + .../go-testfixtures/testfixtures/v3}/LICENSE | 0 .../go-testfixtures/testfixtures/v3/README.md | 464 ++++ .../testfixtures/v3/Taskfile.yml | 59 + .../testfixtures/v3/docker-compose.yml | 37 + .../go-testfixtures/testfixtures/v3/dump.go | 155 ++ .../go-testfixtures/testfixtures/v3/go.mod | 14 + .../go-testfixtures/testfixtures/v3/go.sum | 26 + .../testfixtures/v3}/helper.go | 14 +- .../go-testfixtures/testfixtures/v3}/json.go | 0 .../go-testfixtures/testfixtures/v3}/mysql.go | 21 +- .../testfixtures/v3}/postgresql.go | 57 +- .../testfixtures/v3}/sqlite.go | 11 +- .../testfixtures/v3}/sqlserver.go | 64 +- .../testfixtures/v3/testfixtures.go | 558 +++++ .../go-testfixtures/testfixtures/v3/time.go | 43 + vendor/github.com/hashicorp/hcl/go.mod | 2 - vendor/github.com/lib/pq/.gitignore | 4 + vendor/github.com/lib/pq/.travis.sh | 73 + vendor/github.com/lib/pq/.travis.yml | 44 + vendor/github.com/lib/pq/CONTRIBUTING.md | 29 + vendor/github.com/lib/pq/LICENSE.md | 8 + vendor/github.com/lib/pq/README.md | 95 + vendor/github.com/lib/pq/TESTS.md | 33 + vendor/github.com/lib/pq/array.go | 756 +++++++ vendor/github.com/lib/pq/buf.go | 91 + vendor/github.com/lib/pq/conn.go | 1923 +++++++++++++++++ vendor/github.com/lib/pq/conn_go18.go | 149 ++ vendor/github.com/lib/pq/connector.go | 110 + vendor/github.com/lib/pq/copy.go | 282 +++ vendor/github.com/lib/pq/doc.go | 245 +++ vendor/github.com/lib/pq/encode.go | 602 ++++++ vendor/github.com/lib/pq/error.go | 515 +++++ vendor/github.com/lib/pq/go.mod | 1 + vendor/github.com/lib/pq/notify.go | 797 +++++++ vendor/github.com/lib/pq/oid/doc.go | 6 + vendor/github.com/lib/pq/oid/types.go | 343 +++ vendor/github.com/lib/pq/rows.go | 93 + vendor/github.com/lib/pq/scram/scram.go | 264 +++ vendor/github.com/lib/pq/ssl.go | 175 ++ vendor/github.com/lib/pq/ssl_permissions.go | 20 + vendor/github.com/lib/pq/ssl_windows.go | 9 + vendor/github.com/lib/pq/url.go | 76 + vendor/github.com/lib/pq/user_posix.go | 24 + vendor/github.com/lib/pq/user_windows.go | 27 + vendor/github.com/lib/pq/uuid.go | 23 + vendor/github.com/spf13/afero/go.mod | 2 - vendor/github.com/spf13/pflag/.travis.yml | 7 +- vendor/github.com/spf13/pflag/README.md | 4 +- vendor/github.com/spf13/pflag/bool_slice.go | 38 + vendor/github.com/spf13/pflag/count.go | 4 +- .../github.com/spf13/pflag/duration_slice.go | 38 + vendor/github.com/spf13/pflag/flag.go | 16 +- .../github.com/spf13/pflag/float32_slice.go | 174 ++ .../github.com/spf13/pflag/float64_slice.go | 166 ++ vendor/github.com/spf13/pflag/go.mod | 3 + vendor/github.com/spf13/pflag/go.sum | 0 vendor/github.com/spf13/pflag/int32_slice.go | 174 ++ vendor/github.com/spf13/pflag/int64_slice.go | 166 ++ vendor/github.com/spf13/pflag/int_slice.go | 30 + vendor/github.com/spf13/pflag/ip_slice.go | 40 +- vendor/github.com/spf13/pflag/string_array.go | 26 + vendor/github.com/spf13/pflag/string_slice.go | 22 +- .../github.com/spf13/pflag/string_to_int64.go | 149 ++ vendor/github.com/spf13/pflag/uint_slice.go | 42 + vendor/github.com/spf13/viper/go.mod | 2 - vendor/gopkg.in/testfixtures.v2/.travis.yml | 26 - vendor/gopkg.in/testfixtures.v2/README.md | 388 ---- vendor/gopkg.in/testfixtures.v2/Taskfile.yml | 64 - vendor/gopkg.in/testfixtures.v2/appveyor.yml | 51 - vendor/gopkg.in/testfixtures.v2/deprecated.go | 75 - vendor/gopkg.in/testfixtures.v2/errors.go | 41 - vendor/gopkg.in/testfixtures.v2/generate.go | 110 - vendor/gopkg.in/testfixtures.v2/go.mod | 17 - vendor/gopkg.in/testfixtures.v2/go.sum | 27 - vendor/gopkg.in/testfixtures.v2/options.go | 19 - vendor/gopkg.in/testfixtures.v2/oracle.go | 171 -- .../gopkg.in/testfixtures.v2/testfixtures.go | 306 --- vendor/gopkg.in/testfixtures.v2/time.go | 34 - vendor/modules.txt | 10 +- 107 files changed, 9886 insertions(+), 1595 deletions(-) rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/.editorconfig (100%) rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/.gitattributes (100%) rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/.gitignore (93%) create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/.goreleaser.yml rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/.sample.env (83%) create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/CHANGELOG.md create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/Dockerfile rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/LICENSE (100%) create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/README.md create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/Taskfile.yml create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/docker-compose.yml create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/dump.go create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/go.mod create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/go.sum rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/helper.go (87%) rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/json.go (100%) rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/mysql.go (78%) rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/postgresql.go (79%) rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/sqlite.go (79%) rename vendor/{gopkg.in/testfixtures.v2 => github.com/go-testfixtures/testfixtures/v3}/sqlserver.go (57%) create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/testfixtures.go create mode 100644 vendor/github.com/go-testfixtures/testfixtures/v3/time.go create mode 100644 vendor/github.com/lib/pq/.gitignore create mode 100644 vendor/github.com/lib/pq/.travis.sh create mode 100644 vendor/github.com/lib/pq/.travis.yml create mode 100644 vendor/github.com/lib/pq/CONTRIBUTING.md create mode 100644 vendor/github.com/lib/pq/LICENSE.md create mode 100644 vendor/github.com/lib/pq/README.md create mode 100644 vendor/github.com/lib/pq/TESTS.md create mode 100644 vendor/github.com/lib/pq/array.go create mode 100644 vendor/github.com/lib/pq/buf.go create mode 100644 vendor/github.com/lib/pq/conn.go create mode 100644 vendor/github.com/lib/pq/conn_go18.go create mode 100644 vendor/github.com/lib/pq/connector.go create mode 100644 vendor/github.com/lib/pq/copy.go create mode 100644 vendor/github.com/lib/pq/doc.go create mode 100644 vendor/github.com/lib/pq/encode.go create mode 100644 vendor/github.com/lib/pq/error.go create mode 100644 vendor/github.com/lib/pq/go.mod create mode 100644 vendor/github.com/lib/pq/notify.go create mode 100644 vendor/github.com/lib/pq/oid/doc.go create mode 100644 vendor/github.com/lib/pq/oid/types.go create mode 100644 vendor/github.com/lib/pq/rows.go create mode 100644 vendor/github.com/lib/pq/scram/scram.go create mode 100644 vendor/github.com/lib/pq/ssl.go create mode 100644 vendor/github.com/lib/pq/ssl_permissions.go create mode 100644 vendor/github.com/lib/pq/ssl_windows.go create mode 100644 vendor/github.com/lib/pq/url.go create mode 100644 vendor/github.com/lib/pq/user_posix.go create mode 100644 vendor/github.com/lib/pq/user_windows.go create mode 100644 vendor/github.com/lib/pq/uuid.go create mode 100644 vendor/github.com/spf13/pflag/float32_slice.go create mode 100644 vendor/github.com/spf13/pflag/float64_slice.go create mode 100644 vendor/github.com/spf13/pflag/go.mod create mode 100644 vendor/github.com/spf13/pflag/go.sum create mode 100644 vendor/github.com/spf13/pflag/int32_slice.go create mode 100644 vendor/github.com/spf13/pflag/int64_slice.go create mode 100644 vendor/github.com/spf13/pflag/string_to_int64.go delete mode 100644 vendor/gopkg.in/testfixtures.v2/.travis.yml delete mode 100644 vendor/gopkg.in/testfixtures.v2/README.md delete mode 100644 vendor/gopkg.in/testfixtures.v2/Taskfile.yml delete mode 100644 vendor/gopkg.in/testfixtures.v2/appveyor.yml delete mode 100644 vendor/gopkg.in/testfixtures.v2/deprecated.go delete mode 100644 vendor/gopkg.in/testfixtures.v2/errors.go delete mode 100644 vendor/gopkg.in/testfixtures.v2/generate.go delete mode 100644 vendor/gopkg.in/testfixtures.v2/go.mod delete mode 100644 vendor/gopkg.in/testfixtures.v2/go.sum delete mode 100644 vendor/gopkg.in/testfixtures.v2/options.go delete mode 100644 vendor/gopkg.in/testfixtures.v2/oracle.go delete mode 100644 vendor/gopkg.in/testfixtures.v2/testfixtures.go delete mode 100644 vendor/gopkg.in/testfixtures.v2/time.go diff --git a/.drone1.yml b/.drone1.yml index 9fc09bc5d24..031b9ad4974 100644 --- a/.drone1.yml +++ b/.drone1.yml @@ -6,16 +6,26 @@ workspace: path: src/code.vikunja.io/api services: - - name: test-db-unit + - name: test-mysql-unit image: mariadb:10 environment: MYSQL_ROOT_PASSWORD: vikunjatest MYSQL_DATABASE: vikunjatest - - name: test-db-integration + - name: test-mysql-integration image: mariadb:10 environment: MYSQL_ROOT_PASSWORD: vikunjatest MYSQL_DATABASE: vikunjatest + - name: test-postgres-unit + image: postgres:12 + environment: + POSTGRES_PASSWORD: vikunjatest + POSTGRES_DB: vikunjatest + - name: test-postgres-integration + image: postgres:12 + environment: + POSTGRES_PASSWORD: vikunjatest + POSTGRES_DB: vikunjatest trigger: branch: @@ -78,7 +88,7 @@ steps: environment: VIKUNJA_TESTS_USE_CONFIG: 1 VIKUNJA_DATABASE_TYPE: mysql - VIKUNJA_DATABASE_HOST: test-db-unit + VIKUNJA_DATABASE_HOST: test-mysql-unit VIKUNJA_DATABASE_USER: root VIKUNJA_DATABASE_PASSWORD: vikunjatest VIKUNJA_DATABASE_DATABASE: vikunjatest @@ -88,6 +98,23 @@ steps: when: event: [ push, tag, pull_request ] + - name: test-postgres + image: vikunja/golang-build:latest + pull: true + environment: + VIKUNJA_TESTS_USE_CONFIG: 1 + VIKUNJA_DATABASE_TYPE: postgres + VIKUNJA_DATABASE_HOST: test-postgres-unit + VIKUNJA_DATABASE_USER: postgres + VIKUNJA_DATABASE_PASSWORD: vikunjatest + VIKUNJA_DATABASE_DATABASE: vikunjatest + VIKUNJA_DATABASE_SSLMODE: disable + commands: + - make test + depends_on: [ build ] + when: + event: [ push, tag, pull_request ] + - name: integration-test image: vikunja/golang-build:latest pull: true @@ -115,7 +142,7 @@ steps: environment: VIKUNJA_TESTS_USE_CONFIG: 1 VIKUNJA_DATABASE_TYPE: mysql - VIKUNJA_DATABASE_HOST: test-db-integration + VIKUNJA_DATABASE_HOST: test-mysql-integration VIKUNJA_DATABASE_USER: root VIKUNJA_DATABASE_PASSWORD: vikunjatest VIKUNJA_DATABASE_DATABASE: vikunjatest @@ -125,6 +152,23 @@ steps: when: event: [ push, tag, pull_request ] + - name: integration-test-postgres + image: vikunja/golang-build:latest + pull: true + environment: + VIKUNJA_TESTS_USE_CONFIG: 1 + VIKUNJA_DATABASE_TYPE: postgres + VIKUNJA_DATABASE_HOST: test-postgres-integration + VIKUNJA_DATABASE_USER: postgres + VIKUNJA_DATABASE_PASSWORD: vikunjatest + VIKUNJA_DATABASE_DATABASE: vikunjatest + VIKUNJA_DATABASE_SSLMODE: disable + commands: + - make integration-test + depends_on: [ build ] + when: + event: [ push, tag, pull_request ] + --- ######## # Build a release when pushing to master diff --git a/Makefile b/Makefile index 9d53e7ff227..3ad16b28935 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,8 @@ clean: .PHONY: test test: - VIKUNJA_SERVICE_ROOTPATH=$(shell pwd) go test $(GOFLAGS) -cover -coverprofile cover.out $(PACKAGES) + # We run everything sequentially and not in parallel to prevent issues with real test databases + VIKUNJA_SERVICE_ROOTPATH=$(shell pwd) go test $(GOFLAGS) -p 1 -cover -coverprofile cover.out $(PACKAGES) .PHONY: test-coverage test-coverage: test @@ -72,7 +73,8 @@ test-coverage: test .PHONY: integration-test integration-test: - VIKUNJA_SERVICE_ROOTPATH=$(shell pwd) go test $(GOFLAGS) code.vikunja.io/api/pkg/integrations + # We run everything sequentially and not in parallel to prevent issues with real test databases + VIKUNJA_SERVICE_ROOTPATH=$(shell pwd) go test $(GOFLAGS) -p 1 code.vikunja.io/api/pkg/integrations .PHONY: lint lint: diff --git a/config.yml.sample b/config.yml.sample index 7905cb257dc..e2a32cd2032 100644 --- a/config.yml.sample +++ b/config.yml.sample @@ -30,7 +30,7 @@ service: timezone: GMT database: - # Database type to use. Supported types are mysql and sqlite. + # Database type to use. Supported types are mysql, postgres and sqlite. type: "sqlite" # Database user which is used to connect to the database. user: "vikunja" @@ -42,12 +42,15 @@ database: database: "vikunja" # When using sqlite, this is the path where to store the data Path: "./vikunja.db" - # Sets the max open connections to the database. Only used when using mysql. + # Sets the max open connections to the database. Only used when using mysql and postgres. maxopenconnections: 100 # Sets the maximum number of idle connections to the db. maxidleconnections: 50 # The maximum lifetime of a single db connection in miliseconds. maxconnectionlifetime: 10000 + # Secure connection mode. Only used with postgres. + # (see https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection_String_Parameters) + sslmode: disable cache: # If cache is enabled or not diff --git a/docs/content/doc/setup/backups.md b/docs/content/doc/setup/backups.md index c587fee5cc9..0dbf1cfbbae 100644 --- a/docs/content/doc/setup/backups.md +++ b/docs/content/doc/setup/backups.md @@ -29,6 +29,24 @@ To restore it, simply pipe it back into the `mysql` command: mysql -u -p -h < vkunja-backup.sql {{< /highlight >}} +## PostgreSQL + +To create a backup from PostgreSQL use the `pg_dump` command: + +{{< highlight bash >}} +pg_dump -U -h > vikunja-backup.sql +{{< /highlight >}} + +You might be prompted for the password of the database user. + +To restore it, simply pipe it back into the `psql` command: + +{{< highlight bash >}} +psql -U -h < vikunja-backup.sql +{{< /highlight >}} + +For more information, please visit the [relevant PostgreSQL documentation](https://www.postgresql.org/docs/12/backup-dump.html). + ## SQLite To backup sqllite databases, it is enough to copy the database elsewhere. diff --git a/docs/content/doc/setup/config.md b/docs/content/doc/setup/config.md index b165d899905..18e8f59bc23 100644 --- a/docs/content/doc/setup/config.md +++ b/docs/content/doc/setup/config.md @@ -73,7 +73,7 @@ service: timezone: GMT database: - # Database type to use. Supported types are mysql and sqlite. + # Database type to use. Supported types are mysql, postgres and sqlite. type: "sqlite" # Database user which is used to connect to the database. user: "vikunja" @@ -85,12 +85,15 @@ database: database: "vikunja" # When using sqlite, this is the path where to store the data Path: "./vikunja.db" - # Sets the max open connections to the database. Only used when using mysql. + # Sets the max open connections to the database. Only used when using mysql and postgres. maxopenconnections: 100 # Sets the maximum number of idle connections to the db. maxidleconnections: 50 # The maximum lifetime of a single db connection in miliseconds. maxconnectionlifetime: 10000 + # Secure connection mode. Only used with postgres. + # (see https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection_String_Parameters) + sslmode: disable cache: # If cache is enabled or not diff --git a/docs/content/doc/setup/install-backend.md b/docs/content/doc/setup/install-backend.md index 08245ea47d8..a60e2407cc3 100644 --- a/docs/content/doc/setup/install-backend.md +++ b/docs/content/doc/setup/install-backend.md @@ -55,6 +55,7 @@ After=network.target # Depending on how you configured Vikunja, you may want to uncomment these: #Requires=mysql.service #Requires=mariadb.service +#Requires=postgresql.service #Requires=redis.service [Service] diff --git a/go.mod b/go.mod index e1fa9a38e23..6a430518541 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/go-openapi/spec v0.19.4 // indirect github.com/go-redis/redis v6.15.2+incompatible github.com/go-sql-driver/mysql v1.5.0 + github.com/go-testfixtures/testfixtures/v3 v3.1.1 github.com/go-xorm/core v0.6.2 // indirect github.com/go-xorm/xorm v0.7.9 // indirect github.com/golang/protobuf v1.3.2 // indirect @@ -45,9 +46,9 @@ require ( github.com/labstack/echo/v4 v4.1.14 github.com/labstack/gommon v0.3.0 github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef + github.com/lib/pq v1.3.0 github.com/mailru/easyjson v0.7.0 // indirect github.com/mattn/go-isatty v0.0.12 // indirect - github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6 // indirect github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 @@ -74,8 +75,6 @@ require ( gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/d4l3k/messagediff.v1 v1.2.1 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df - gopkg.in/testfixtures.v2 v2.5.3 - gopkg.in/yaml.v2 v2.2.7 // indirect honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a src.techknowlogick.com/xgo v0.0.0-20190507142556-a5b29ecb0ff4 src.techknowlogick.com/xormigrate v1.1.0 diff --git a/go.sum b/go.sum index 3792efd8625..5e46bcd53e9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,4 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= @@ -54,10 +53,9 @@ github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f h1:WH0w/R4Yoey+04HhFxqZ6VX6I0d7RMyw5aXQ9UTvQPs= -github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4 h1:YcpmyvADGYw5LqMnHqSkyIELsHCGF6PkrmM31V8rF7o= github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= +github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e h1:LzwWXEScfcTu7vUZNlDDWDARoSGEtvlDKK2BYHowNeE= github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= @@ -110,6 +108,8 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-testfixtures/testfixtures/v3 v3.1.1 h1:SBIfzULODQQ7JV6AD931MvAz8pnkk6QCfMIcoOBDaXQ= +github.com/go-testfixtures/testfixtures/v3 v3.1.1/go.mod h1:RZctY24ixituGC73XlAV1gkCwYMVwiSwPm26MNlQIhE= github.com/go-xorm/core v0.6.2 h1:EJLcSxf336POJr670wKB55Mah9f93xzvGYzNRgnT8/Y= github.com/go-xorm/core v0.6.2/go.mod h1:bwPIfLdm/FzWgVUH8WPVlr+uJhscvNGFcaZKXsI3n2c= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= @@ -211,13 +211,11 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-oci8 v0.0.0-20181115070430-6eefff3c767c/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8= -github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6 h1:gheNi9lnffYyVyqQzJqY7lo+M3bCDVw5fLU/jSuCMhc= -github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -290,6 +288,8 @@ github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmq github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -330,7 +330,6 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -454,8 +453,6 @@ gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8 gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= -gopkg.in/testfixtures.v2 v2.5.3 h1:P8gDACSLJGxutzBqbzvfiXYgmQ2s00LIr4uAvWBCPAg= -gopkg.in/testfixtures.v2 v2.5.3/go.mod h1:rGPtsOtPcZhs7AsHYf1WmufW1hEsM6DXdLrYz60nrQQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= diff --git a/pkg/config/config.go b/pkg/config/config.go index db9dff9d262..c24d3a8a38b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -55,6 +55,7 @@ const ( DatabaseMaxOpenConnections Key = `database.maxopenconnections` DatabaseMaxIdleConnections Key = `database.maxidleconnections` DatabaseMaxConnectionLifetime Key = `database.maxconnectionlifetime` + DatabaseSslMode Key = `database.sslmode` CacheEnabled Key = `cache.enabled` CacheType Key = `cache.type` @@ -181,6 +182,7 @@ func InitDefaultConfig() { DatabaseMaxOpenConnections.setDefault(100) DatabaseMaxIdleConnections.setDefault(50) DatabaseMaxConnectionLifetime.setDefault(10000) + DatabaseSslMode.setDefault("disable") // Cacher CacheEnabled.setDefault(false) diff --git a/pkg/db/db.go b/pkg/db/db.go index ec26ef4b71c..aee4a8dfc9a 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -21,7 +21,9 @@ import ( "code.vikunja.io/api/pkg/log" "encoding/gob" "fmt" + "net/url" "strconv" + "strings" "time" "xorm.io/core" "xorm.io/xorm" @@ -29,6 +31,7 @@ import ( xrc "gitea.com/xorm/xorm-redis-cache" _ "github.com/go-sql-driver/mysql" // Because. + _ "github.com/lib/pq" // Because. _ "github.com/mattn/go-sqlite3" // Because. ) @@ -53,6 +56,11 @@ func CreateDBEngine() (engine *xorm.Engine, err error) { if err != nil { return } + } else if config.DatabaseType.GetString() == "postgres" { + engine, err = initPostgresEngine() + if err != nil { + return + } } else { // Otherwise use sqlite engine, err = initSqliteEngine() @@ -113,6 +121,46 @@ func initMysqlEngine() (engine *xorm.Engine, err error) { return } +// parsePostgreSQLHostPort parses given input in various forms defined in +// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING +// and returns proper host and port number. +func parsePostgreSQLHostPort(info string) (string, string) { + host, port := "127.0.0.1", "5432" + if strings.Contains(info, ":") && !strings.HasSuffix(info, "]") { + idx := strings.LastIndex(info, ":") + host = info[:idx] + port = info[idx+1:] + } else if len(info) > 0 { + host = info + } + return host, port +} + +func initPostgresEngine() (engine *xorm.Engine, err error) { + host, port := parsePostgreSQLHostPort(config.DatabaseHost.GetString()) + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", + host, + port, + url.PathEscape(config.DatabaseUser.GetString()), + url.PathEscape(config.DatabasePassword.GetString()), + config.DatabaseDatabase.GetString(), + config.DatabaseSslMode.GetString(), + ) + + engine, err = xorm.NewEngine("postgres", connStr) + if err != nil { + return + } + engine.SetMaxOpenConns(config.DatabaseMaxOpenConnections.GetInt()) + engine.SetMaxIdleConns(config.DatabaseMaxIdleConnections.GetInt()) + max, err := time.ParseDuration(strconv.Itoa(config.DatabaseMaxConnectionLifetime.GetInt()) + `ms`) + if err != nil { + return + } + engine.SetConnMaxLifetime(max) + return +} + func initSqliteEngine() (engine *xorm.Engine, err error) { path := config.DatabasePath.GetString() if path == "" { diff --git a/pkg/db/test_fixtures.go b/pkg/db/test_fixtures.go index 16d75a2b3f7..90c6c4d5bd6 100644 --- a/pkg/db/test_fixtures.go +++ b/pkg/db/test_fixtures.go @@ -19,41 +19,92 @@ package db import ( "code.vikunja.io/api/pkg/config" + "fmt" + "github.com/go-testfixtures/testfixtures/v3" "github.com/stretchr/testify/assert" - "gopkg.in/testfixtures.v2" "path/filepath" "testing" ) -var fixtures *testfixtures.Context +var fixtures *testfixtures.Loader // InitFixtures initialize test fixtures for a test database func InitFixtures(tablenames ...string) (err error) { - var helper testfixtures.Helper = &testfixtures.SQLite{} - if config.DatabaseType.GetString() == "mysql" { - helper = &testfixtures.MySQL{} - } + var testfiles func(loader *testfixtures.Loader) error dir := filepath.Join(config.ServiceRootpath.GetString(), "pkg", "db", "fixtures") - testfixtures.SkipDatabaseNameCheck(true) - // If fixture table names are specified, load them // Otherwise, load all fixtures if len(tablenames) > 0 { for i, name := range tablenames { tablenames[i] = filepath.Join(dir, name+".yml") } - fixtures, err = testfixtures.NewFiles(x.DB().DB, helper, tablenames...) + testfiles = testfixtures.Files(tablenames...) } else { - fixtures, err = testfixtures.NewFolder(x.DB().DB, helper, dir) + testfiles = testfixtures.Directory(dir) } + + loaderOptions := []func(loader *testfixtures.Loader) error{ + testfixtures.Database(x.DB().DB), + testfixtures.Dialect(config.DatabaseType.GetString()), + testfixtures.DangerousSkipTestDatabaseCheck(), + testfiles, + } + + if config.DatabaseType.GetString() == "postgres" { + loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences()) + } + + fixtures, err = testfixtures.New(loaderOptions...) + if err != nil { + return err + } + return err } // LoadFixtures load fixtures for a test database func LoadFixtures() error { - return fixtures.Load() + err := fixtures.Load() + if err != nil { + return err + } + + // Copied from https://github.com/go-gitea/gitea/blob/master/models/test_fixtures.go#L39 + // Now if we're running postgres we need to tell it to update the sequences + if x.Dialect().DriverName() == "postgres" { + results, err := x.QueryString(`SELECT 'SELECT SETVAL(' || + quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) || + ', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' || + quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';' + FROM pg_class AS S, + pg_depend AS D, + pg_class AS T, + pg_attribute AS C, + pg_tables AS PGT + WHERE S.relkind = 'S' + AND S.oid = D.objid + AND D.refobjid = T.oid + AND D.refobjid = C.attrelid + AND D.refobjsubid = C.attnum + AND T.relname = PGT.tablename + ORDER BY S.relname;`) + if err != nil { + fmt.Printf("Failed to generate sequence update: %v\n", err) + return err + } + for _, r := range results { + for _, value := range r { + _, err = x.Exec(value) + if err != nil { + fmt.Printf("Failed to update sequence: %s Error: %v\n", value, err) + return err + } + } + } + } + return err } // LoadAndAssertFixtures loads all fixtures defined before and asserts they are correctly loaded diff --git a/pkg/files/files_test.go b/pkg/files/files_test.go index 53e27270028..51adaa3d1a4 100644 --- a/pkg/files/files_test.go +++ b/pkg/files/files_test.go @@ -57,11 +57,11 @@ func TestCreate(t *testing.T) { content: []byte("testfile"), } ta := &testauth{id: 1} - _, err := Create(tf, "testfile", 100, ta) + createdFile, err := Create(tf, "testfile", 100, ta) assert.NoError(t, err) // Check the file was created correctly - file := &File{ID: 2} + file := &File{ID: createdFile.ID} err = file.LoadFileMetaByID() assert.NoError(t, err) assert.Equal(t, int64(1), file.CreatedByID) diff --git a/pkg/migration/migration.go b/pkg/migration/migration.go index 062691af81d..ed3f5ecfe26 100644 --- a/pkg/migration/migration.go +++ b/pkg/migration/migration.go @@ -114,6 +114,11 @@ func dropTableColum(x *xorm.Engine, tableName, col string) error { if err != nil { return err } + case "postgres": + _, err := x.Exec("ALTER TABLE " + tableName + " DROP COLUMN " + col) + if err != nil { + return err + } default: log.Fatal("Unknown db.") } @@ -130,6 +135,11 @@ func modifyColumn(x *xorm.Engine, tableName, col, newDefinition string) error { if err != nil { return err } + case "postgres": + _, err := x.Exec("ALTER TABLE " + tableName + " ALTER COLUMN " + col + " " + newDefinition) + if err != nil { + return err + } default: log.Fatal("Unknown db.") } diff --git a/pkg/models/label_task.go b/pkg/models/label_task.go index 43cfb863b28..c9705a45a3a 100644 --- a/pkg/models/label_task.go +++ b/pkg/models/label_task.go @@ -154,8 +154,10 @@ func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, res // Because of this whole thing, we need this extra switch here to only group by Task IDs if needed. // Probably not the most ideal solution. var groupBy = "labels.id,label_task.task_id" + var selectStmt = "labels.*, label_task.task_id" if opts.GroupByLabelIDsOnly { groupBy = "labels.id" + selectStmt = "labels.*" } // Get all labels associated with these tasks @@ -166,11 +168,12 @@ func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, res } err = x.Table("labels"). - Select("labels.*, label_task.task_id"). + Select(selectStmt). Join("LEFT", "label_task", "label_task.label_id = labels.id"). Where(cond). And("labels.title LIKE ?", "%"+opts.Search+"%"). GroupBy(groupBy). + OrderBy("labels.id ASC"). Limit(getLimitFromPageIndex(opts.Page, opts.PerPage)). Find(&labels) if err != nil { diff --git a/pkg/models/label_test.go b/pkg/models/label_test.go index b86dbf4f422..321ab036e4a 100644 --- a/pkg/models/label_test.go +++ b/pkg/models/label_test.go @@ -83,7 +83,6 @@ func TestLabel_ReadAll(t *testing.T) { }, }, { - TaskID: 1, Label: Label{ ID: 4, Title: "Label #4 - visible via other task", diff --git a/pkg/models/models.go b/pkg/models/models.go index acef5f8a8f0..bfa3f1331ef 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -21,6 +21,7 @@ import ( "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/log" _ "github.com/go-sql-driver/mysql" // Because. + _ "github.com/lib/pq" // Because. "xorm.io/xorm" _ "github.com/mattn/go-sqlite3" // Because. diff --git a/pkg/models/namespace_test.go b/pkg/models/namespace_test.go index 64ead1c1317..91a2ac740f2 100644 --- a/pkg/models/namespace_test.go +++ b/pkg/models/namespace_test.go @@ -86,7 +86,7 @@ func TestNamespace_Create(t *testing.T) { assert.Equal(t, "Dolor sit amet.", dummynamespace.Description) // Try updating one with a nonexistant owner - dummynamespace.Owner.ID = 94829838572 + dummynamespace.Owner.ID = 999999 err = dummynamespace.Update() assert.Error(t, err) assert.True(t, user.IsErrUserDoesNotExist(err)) diff --git a/pkg/models/task_attachment_test.go b/pkg/models/task_attachment_test.go index 292405caf6e..c1306a794d5 100644 --- a/pkg/models/task_attachment_test.go +++ b/pkg/models/task_attachment_test.go @@ -19,6 +19,7 @@ package models import ( "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/user" "github.com/stretchr/testify/assert" @@ -30,6 +31,7 @@ import ( func TestTaskAttachment_ReadOne(t *testing.T) { t.Run("Normal File", func(t *testing.T) { + db.LoadAndAssertFixtures(t) files.InitTestFileFixtures(t) ta := &TaskAttachment{ ID: 1, @@ -50,6 +52,7 @@ func TestTaskAttachment_ReadOne(t *testing.T) { assert.Equal(t, []byte("testfile1"), content) }) t.Run("Nonexisting Attachment", func(t *testing.T) { + db.LoadAndAssertFixtures(t) files.InitTestFileFixtures(t) ta := &TaskAttachment{ ID: 9999, @@ -59,6 +62,7 @@ func TestTaskAttachment_ReadOne(t *testing.T) { assert.True(t, IsErrTaskAttachmentDoesNotExist(err)) }) t.Run("Existing Attachment, Nonexisting File", func(t *testing.T) { + db.LoadAndAssertFixtures(t) files.InitTestFileFixtures(t) ta := &TaskAttachment{ ID: 2, @@ -88,6 +92,7 @@ func (t *testfile) Close() error { } func TestTaskAttachment_NewAttachment(t *testing.T) { + db.LoadAndAssertFixtures(t) files.InitTestFileFixtures(t) // Assert the file is being stored correctly ta := TaskAttachment{ @@ -118,16 +123,18 @@ func TestTaskAttachment_NewAttachment(t *testing.T) { } func TestTaskAttachment_ReadAll(t *testing.T) { + db.LoadAndAssertFixtures(t) files.InitTestFileFixtures(t) ta := &TaskAttachment{TaskID: 1} as, _, _, err := ta.ReadAll(&user.User{ID: 1}, "", 0, 50) attachments, _ := as.([]*TaskAttachment) assert.NoError(t, err) - assert.Len(t, attachments, 3) + assert.Len(t, attachments, 2) assert.Equal(t, "test", attachments[0].File.Name) } func TestTaskAttachment_Delete(t *testing.T) { + db.LoadAndAssertFixtures(t) files.InitTestFileFixtures(t) t.Run("Normal", func(t *testing.T) { ta := &TaskAttachment{ID: 1} @@ -156,12 +163,14 @@ func TestTaskAttachment_Rights(t *testing.T) { u := &user.User{ID: 1} t.Run("Can Read", func(t *testing.T) { t.Run("Allowed", func(t *testing.T) { + db.LoadAndAssertFixtures(t) ta := &TaskAttachment{TaskID: 1} can, err := ta.CanRead(u) assert.NoError(t, err) assert.True(t, can) }) t.Run("Forbidden", func(t *testing.T) { + db.LoadAndAssertFixtures(t) ta := &TaskAttachment{TaskID: 14} can, err := ta.CanRead(u) assert.NoError(t, err) @@ -170,18 +179,21 @@ func TestTaskAttachment_Rights(t *testing.T) { }) t.Run("Can Delete", func(t *testing.T) { t.Run("Allowed", func(t *testing.T) { + db.LoadAndAssertFixtures(t) ta := &TaskAttachment{TaskID: 1} can, err := ta.CanDelete(u) assert.NoError(t, err) assert.True(t, can) }) t.Run("Forbidden, no access", func(t *testing.T) { + db.LoadAndAssertFixtures(t) ta := &TaskAttachment{TaskID: 14} can, err := ta.CanDelete(u) assert.NoError(t, err) assert.False(t, can) }) t.Run("Forbidden, shared read only", func(t *testing.T) { + db.LoadAndAssertFixtures(t) ta := &TaskAttachment{TaskID: 15} can, err := ta.CanDelete(u) assert.NoError(t, err) @@ -190,18 +202,21 @@ func TestTaskAttachment_Rights(t *testing.T) { }) t.Run("Can Create", func(t *testing.T) { t.Run("Allowed", func(t *testing.T) { + db.LoadAndAssertFixtures(t) ta := &TaskAttachment{TaskID: 1} can, err := ta.CanCreate(u) assert.NoError(t, err) assert.True(t, can) }) t.Run("Forbidden, no access", func(t *testing.T) { + db.LoadAndAssertFixtures(t) ta := &TaskAttachment{TaskID: 14} can, err := ta.CanCreate(u) assert.NoError(t, err) assert.False(t, can) }) t.Run("Forbidden, shared read only", func(t *testing.T) { + db.LoadAndAssertFixtures(t) ta := &TaskAttachment{TaskID: 15} can, err := ta.CanCreate(u) assert.NoError(t, err) diff --git a/pkg/models/team_members_test.go b/pkg/models/team_members_test.go index 198ccb7f334..1222d10570e 100644 --- a/pkg/models/team_members_test.go +++ b/pkg/models/team_members_test.go @@ -24,61 +24,60 @@ import ( ) func TestTeamMember_Create(t *testing.T) { - db.LoadAndAssertFixtures(t) - // Dummy team member - dummyteammember := TeamMember{ - TeamID: 1, - Username: "user3", + doer := &user.User{ + ID: 1, } - // Doer - doer, err := user.GetUserByID(1) - assert.NoError(t, err) - - // Insert a new team member - allowed, _ := dummyteammember.CanCreate(doer) - assert.True(t, allowed) - err = dummyteammember.Create(doer) - assert.NoError(t, err) - - // Check he's in there - team := Team{ID: 1} - err = team.ReadOne() - assert.NoError(t, err) - assert.Equal(t, 3, len(team.Members)) - - // Try inserting a user twice - err = dummyteammember.Create(doer) - assert.Error(t, err) - assert.True(t, IsErrUserIsMemberOfTeam(err)) - - // Delete it - allowed, _ = dummyteammember.CanDelete(doer) - assert.True(t, allowed) - err = dummyteammember.Delete() - assert.NoError(t, err) - - // Delete the other one - tm := TeamMember{TeamID: 1, Username: "user2"} - err = tm.Delete() - assert.NoError(t, err) - - // Try deleting the last one - tm = TeamMember{TeamID: 1, Username: "user1"} - err = tm.Delete() - assert.Error(t, err) - assert.True(t, IsErrCannotDeleteLastTeamMember(err)) - - // Try inserting a user which does not exist - dummyteammember.Username = "user9484" - err = dummyteammember.Create(doer) - assert.Error(t, err) - assert.True(t, user.IsErrUserDoesNotExist(err)) - - // Try adding a user to a team which does not exist - tm = TeamMember{TeamID: 94824, Username: "user1"} - err = tm.Create(doer) - assert.Error(t, err) - assert.True(t, IsErrTeamDoesNotExist(err)) + t.Run("normal", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + tm := &TeamMember{ + TeamID: 1, + Username: "user3", + } + err := tm.Create(doer) + assert.NoError(t, err) + }) + t.Run("already existing", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + tm := &TeamMember{ + TeamID: 1, + Username: "user1", + } + err := tm.Create(doer) + assert.Error(t, err) + assert.True(t, IsErrUserIsMemberOfTeam(err)) + }) + t.Run("nonexisting user", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + tm := &TeamMember{ + TeamID: 1, + Username: "nonexistinguser", + } + err := tm.Create(doer) + assert.Error(t, err) + assert.True(t, user.IsErrUserDoesNotExist(err)) + }) + t.Run("nonexisting team", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + tm := &TeamMember{ + TeamID: 9999999, + Username: "user1", + } + err := tm.Create(doer) + assert.Error(t, err) + assert.True(t, IsErrTeamDoesNotExist(err)) + }) +} + +func TestTeamMember_Delete(t *testing.T) { + t.Run("normal", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + tm := &TeamMember{ + TeamID: 1, + Username: "user1", + } + err := tm.Delete() + assert.NoError(t, err) + }) } diff --git a/pkg/models/teams.go b/pkg/models/teams.go index a8c2a583dc6..4c515c50228 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -68,7 +68,7 @@ type TeamMember struct { // Used under the hood to manage team members UserID int64 `xorm:"int(11) not null INDEX" json:"-"` // Whether or not the member is an admin of the team. See the docs for more about what a team admin can do - Admin bool `xorm:"tinyint(1) INDEX null" json:"admin"` + Admin bool `xorm:"null" json:"admin"` // A timestamp when this relation was created. You cannot change this value. Created timeutil.TimeStamp `xorm:"created not null" json:"created"` @@ -131,7 +131,7 @@ func addMoreInfoToTeams(teams []*Team) (err error) { } // Get all owners and team members - users := []*TeamUser{} + users := make(map[int64]*TeamUser) err = x.Select("*"). Table("users"). Join("LEFT", "team_members", "team_members.user_id = users.id"). @@ -153,9 +153,15 @@ func addMoreInfoToTeams(teams []*Team) (err error) { continue } u.Email = "" - teamMap[u.TeamID].CreatedBy = &u.User teamMap[u.TeamID].Members = append(teamMap[u.TeamID].Members, u) } + + // We need to do this in a second loop as owners might not be the last ones in the list + for _, team := range teamMap { + if teamUser, has := users[team.CreatedByID]; has { + team.CreatedBy = &teamUser.User + } + } return } diff --git a/pkg/models/teams_test.go b/pkg/models/teams_test.go index 14e0e5d3058..57c9001da77 100644 --- a/pkg/models/teams_test.go +++ b/pkg/models/teams_test.go @@ -17,6 +17,7 @@ package models import ( + "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/user" "github.com/stretchr/testify/assert" "reflect" @@ -24,79 +25,108 @@ import ( ) func TestTeam_Create(t *testing.T) { - //Dummyteam - dummyteam := Team{ - Name: "Testteam293", - Description: "Lorem Ispum", + doer := &user.User{ + ID: 1, + Username: "user1", } + t.Run("normal", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + team := &Team{ + Name: "Testteam293", + Description: "Lorem Ispum", + } + err := team.Create(doer) + assert.NoError(t, err) + }) + t.Run("empty name", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + team := &Team{} + err := team.Create(doer) + assert.Error(t, err) + assert.True(t, IsErrTeamNameCannotBeEmpty(err)) + }) +} - // Doer - doer, err := user.GetUserByID(1) - assert.NoError(t, err) +func TestTeam_ReadOne(t *testing.T) { + t.Run("normal", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + team := &Team{ID: 1} + err := team.ReadOne() + assert.NoError(t, err) + assert.Equal(t, "testteam1", team.Name) + assert.Equal(t, "Lorem Ipsum", team.Description) + assert.Equal(t, int64(1), team.CreatedBy.ID) + assert.Equal(t, int64(1), team.CreatedByID) + }) + t.Run("invalid id", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + team := &Team{ID: -1} + err := team.ReadOne() + assert.Error(t, err) + assert.True(t, IsErrTeamDoesNotExist(err)) + }) + t.Run("nonexisting", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + team := &Team{ID: 99999} + err := team.ReadOne() + assert.Error(t, err) + assert.True(t, IsErrTeamDoesNotExist(err)) + }) +} - // Insert it - allowed, _ := dummyteam.CanCreate(doer) - assert.True(t, allowed) - err = dummyteam.Create(doer) - assert.NoError(t, err) +func TestTeam_ReadAll(t *testing.T) { + doer := &user.User{ID: 1} + t.Run("normal", func(t *testing.T) { + team := &Team{} + ts, _, _, err := team.ReadAll(doer, "", 1, 50) + assert.NoError(t, err) + assert.Equal(t, reflect.TypeOf(ts).Kind(), reflect.Slice) + s := reflect.ValueOf(ts) + assert.Equal(t, 8, s.Len()) + }) +} - // Check if it was inserted and we're admin - tm := Team{ID: dummyteam.ID} - err = tm.ReadOne() - assert.NoError(t, err) - assert.Equal(t, 1, len(tm.Members)) - assert.Equal(t, doer.ID, tm.Members[0].User.ID) - assert.True(t, tm.Members[0].Admin) - allowed, _ = dummyteam.CanRead(doer) - assert.True(t, allowed) +func TestTeam_Update(t *testing.T) { + t.Run("normal", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + team := &Team{ + ID: 1, + Name: "SomethingNew", + } + err := team.Update() + assert.NoError(t, err) + }) + t.Run("empty name", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + team := &Team{ + ID: 1, + Name: "", + } + err := team.Update() + assert.Error(t, err) + assert.True(t, IsErrTeamNameCannotBeEmpty(err)) + }) + t.Run("nonexisting", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + team := &Team{ + ID: 9999, + Name: "SomethingNew", + } + err := team.Update() + assert.Error(t, err) + assert.True(t, IsErrTeamDoesNotExist(err)) + }) +} - // Try getting a team with an ID < 0 - _, err = GetTeamByID(-1) - assert.Error(t, err) - assert.True(t, IsErrTeamDoesNotExist(err)) - - // Get all teams the user is part of - ts, _, _, err := tm.ReadAll(doer, "", 1, 50) - assert.NoError(t, err) - assert.Equal(t, reflect.TypeOf(ts).Kind(), reflect.Slice) - s := reflect.ValueOf(ts) - assert.Equal(t, 9, s.Len()) - - // Check inserting it with an empty name - dummyteam.Name = "" - err = dummyteam.Create(doer) - assert.Error(t, err) - assert.True(t, IsErrTeamNameCannotBeEmpty(err)) - - // update it (still no name, should fail) - allowed, _ = dummyteam.CanUpdate(doer) - assert.True(t, allowed) - err = dummyteam.Update() - assert.Error(t, err) - assert.True(t, IsErrTeamNameCannotBeEmpty(err)) - - // Update it, this time with a name - dummyteam.Name = "Lorem" - err = dummyteam.Update() - assert.NoError(t, err) - - // Delete it - allowed, err = dummyteam.CanDelete(doer) - assert.NoError(t, err) - assert.True(t, allowed) - err = dummyteam.Delete() - assert.NoError(t, err) - - // Try deleting a (now) nonexistant team - allowed, err = dummyteam.CanDelete(doer) - assert.False(t, allowed) - assert.Error(t, err) - assert.True(t, IsErrTeamDoesNotExist(err)) - - // Try updating the (now) nonexistant team - err = dummyteam.Update() - assert.Error(t, err) - assert.True(t, IsErrTeamDoesNotExist(err)) +func TestTeam_Delete(t *testing.T) { + t.Run("normal", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + team := &Team{ + ID: 1, + } + err := team.Delete() + assert.NoError(t, err) + }) } func TestIsErrInvalidRight(t *testing.T) { diff --git a/vendor/gopkg.in/testfixtures.v2/.editorconfig b/vendor/github.com/go-testfixtures/testfixtures/v3/.editorconfig similarity index 100% rename from vendor/gopkg.in/testfixtures.v2/.editorconfig rename to vendor/github.com/go-testfixtures/testfixtures/v3/.editorconfig diff --git a/vendor/gopkg.in/testfixtures.v2/.gitattributes b/vendor/github.com/go-testfixtures/testfixtures/v3/.gitattributes similarity index 100% rename from vendor/gopkg.in/testfixtures.v2/.gitattributes rename to vendor/github.com/go-testfixtures/testfixtures/v3/.gitattributes diff --git a/vendor/gopkg.in/testfixtures.v2/.gitignore b/vendor/github.com/go-testfixtures/testfixtures/v3/.gitignore similarity index 93% rename from vendor/gopkg.in/testfixtures.v2/.gitignore rename to vendor/github.com/go-testfixtures/testfixtures/v3/.gitignore index a8ea01dbb40..8772d9ba0cb 100644 --- a/vendor/gopkg.in/testfixtures.v2/.gitignore +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/.gitignore @@ -23,7 +23,7 @@ _testmain.go *.test *.prof -# SQLite databases *.sqlite3 - .env +/testfixtures +/dist diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/.goreleaser.yml b/vendor/github.com/go-testfixtures/testfixtures/v3/.goreleaser.yml new file mode 100644 index 00000000000..cab247177d0 --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/.goreleaser.yml @@ -0,0 +1,41 @@ +build: + binary: testfixtures + main: ./cmd/testfixtures + goos: + - windows + - darwin + - linux + goarch: + - 386 + - amd64 + ignore: + - goos: darwin + goarch: 386 + flags: + - -tags=sqlite + +archives: + - name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}" + format_overrides: + - goos: windows + format: zip + +release: + draft: true + +snapshot: + name_template: "{{.Tag}}" + +checksum: + name_template: "testfixtures_checksums.txt" + +nfpms: + - vendor: testfixtures + homepage: https://github.com/go-testfixtures/testfixtures + maintainer: Andrey Nering + description: Ruby on Rails like test fixtures for Go. + license: MIT + formats: + - deb + - rpm + file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}" diff --git a/vendor/gopkg.in/testfixtures.v2/.sample.env b/vendor/github.com/go-testfixtures/testfixtures/v3/.sample.env similarity index 83% rename from vendor/gopkg.in/testfixtures.v2/.sample.env rename to vendor/github.com/go-testfixtures/testfixtures/v3/.sample.env index ac950abb7b2..fcc7cd0c74f 100644 --- a/vendor/gopkg.in/testfixtures.v2/.sample.env +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/.sample.env @@ -2,4 +2,3 @@ PG_CONN_STRING="user=postgres dbname=testfixtures_test sslmode=disable" MYSQL_CONN_STRING="root:@/testfixtures_test?multiStatements=true" SQLITE_CONN_STRING="testdb.sqlite3" SQLSERVER_CONN_STRING="server=localhost\SQLExpress;database=testfixtures_test;user id=sa;password=sqlserver;encrypt=disable" -ORACLE_CONN_STRING="testfixtures/testfixtures@localhost/XE" diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/CHANGELOG.md b/vendor/github.com/go-testfixtures/testfixtures/v3/CHANGELOG.md new file mode 100644 index 00000000000..e60b193319e --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/CHANGELOG.md @@ -0,0 +1,83 @@ +# Changelog + +## v3.1.1 - 2019-01-11 + +- testfixtures now work with both `mssql` and `sqlserver` drivers. + Note that [the `mssql` one is deprecated](https://github.com/denisenkom/go-mssqldb#deprecated), + though. So try to migrate to `sqlserver` once possible. + +## v3.1.0 - 2019-01-09 + +- Using `sqlserver` driver instead of the deprecated `mssql` + ([#58](https://github.com/go-testfixtures/testfixtures/pull/58)). + +## v3.0.0 - 2019-12-26 + +### Breaking changes + +- The import path changed from `gopkg.in/testfixtures.v2` to + `github.com/go-testfixtures/testfixtures/v3`. +- This package no longer support Oracle databases. This decision was + taken because too few people actually used this package with Oracle and it + was the most difficult to test (we didn't run on CI due the lack of an + official Docker image, etc). +- The public API was totally rewritten to be more flexible and ideomatic. + It now uses functional options. It differs from v2, but should be easy + enough to upgrade. +- Some deprecated APIs from v2 were removed as well. +- This now requires Go >= 1.13. + +### New features + +- We now have a CLI so you can easily use testfixtures to load a sample + database from fixtures if you want. +- Templating via [text/template](https://golang.org/pkg/text/template/) + is now available. This allows some fancier use cases like generating data + or specific columns dynamically. +- It's now possible to choose which time zone to use when parsing timestamps + from fixtures. The default is the same as before, whatever is set on + `time.Local`. +- Errors now use the new `%w` verb only available on Go >= 1.13. + +### MISC + +- Travis and AppVeyor are gone. We're using GitHub Actions exclusively now. + The whole suite is ran inside Docker (with help of Docker Compose), so it's + easy to run tests locally as well. + +Check the new README for some examples! + +## v2.6.0 - 2019-10-24 + +- Add support for TimescaleDB + ([#53](https://github.com/go-testfixtures/testfixtures/pull/53)). + +## v2.5.3 - 2018-12-15 + +- Fixes related to use of foreign key pragmas on MySQL (#43). + +## v2.5.2 - 2018-11-25 + +- This library now supports [Go Modules](https://github.com/golang/go/wiki/Modules); +- Also allow `.yaml` (as an alternative to `.yml`) as the file extension (#42). + +## v2.5.1 - 2018-11-04 + +- Allowing disabling reset of PostgreSQL sequences (#38). + +## v2.5.0 - 2018-09-07 + +- Add public function DetectTestDatabase (#35, #36). + +## v2.4.5 - 2018-07-07 + +- Fix for MySQL/MariaDB: ignoring views on operations that should be run only on tables (#33). + +## v2.4.4 - 2018-07-02 + +- Fix for multiple schemas on Microsoft SQL Server (#29 and #30); +- Configuring AppVeyor CI to also test for Microsoft SQL Server. + +--- + +Sorry, we don't have changelog for older releases 😢. diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/Dockerfile b/vendor/github.com/go-testfixtures/testfixtures/v3/Dockerfile new file mode 100644 index 00000000000..12fd21990a6 --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.13.5-alpine + +RUN apk update +RUN apk add alpine-sdk + +WORKDIR /testfixtures +COPY . . + +RUN go mod download diff --git a/vendor/gopkg.in/testfixtures.v2/LICENSE b/vendor/github.com/go-testfixtures/testfixtures/v3/LICENSE similarity index 100% rename from vendor/gopkg.in/testfixtures.v2/LICENSE rename to vendor/github.com/go-testfixtures/testfixtures/v3/LICENSE diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/README.md b/vendor/github.com/go-testfixtures/testfixtures/v3/README.md new file mode 100644 index 00000000000..8c30e47817f --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/README.md @@ -0,0 +1,464 @@ +# testfixtures + +[![GoDoc](https://godoc.org/github.com/go-testfixtures/testfixtures?status.svg)][doc] + +> ***Warning***: this package will wipe the database data before loading the +fixtures! It is supposed to be used on a test database. Please, double check +if you are running it against the correct database. + +> **TIP**: There are options not described in this README page. It's +> recommended that you also check [the documentation][doc]. + +Writing tests is hard, even more when you have to deal with an SQL database. +This package aims to make writing functional tests for web apps written in +Go easier. + +Basically this package mimics the ["Ruby on Rails' way"][railstests] of writing tests +for database applications, where sample data is kept in fixtures files. Before +the execution of every test, the test database is cleaned and the fixture data +is loaded into the database. + +The idea is running tests against a real database, instead of relying in mocks, +which is boring to setup and may lead to production bugs not being caught in +the tests. + +## Installation + +First, import it like this: + +```go +import ( + "github.com/go-testfixtures/testfixtures/v3" +) +``` + +## Usage + +Create a folder for the fixture files. Each file should contain data for a +single table and have the name `.yml`: + +``` +myapp/ + myapp.go + myapp_test.go + ... + fixtures/ + posts.yml + comments.yml + tags.yml + posts_tags.yml + ... +``` + +The file would look like this (it can have as many record you want): + +```yml +# comments.yml +- id: 1 + post_id: 1 + content: A comment... + author_name: John Doe + author_email: john@doe.com + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 + +- id: 2 + post_id: 2 + content: Another comment... + author_name: John Doe + author_email: john@doe.com + created_at: 2020-12-31 23:59:59 + updated_at: 2020-12-31 23:59:59 + +# ... +``` + +An YAML object or array will be converted to JSON. It will be stored on a native +JSON type like JSONB on PostgreSQL or as a TEXT or VARCHAR column on other +databases. + +```yml +- id: 1 + post_attributes: + author: John Due + author_email: john@due.com + title: "..." + tags: + - programming + - go + - testing + post: "..." +``` + +If you need to write raw SQL, probably to call a function, prefix the value +of the column with `RAW=`: + +```yml +- id: 1 + uuid_column: RAW=uuid_generate_v4() + postgis_type_column: RAW=ST_GeomFromText('params...') + created_at: RAW=NOW() + updated_at: RAW=NOW() +``` + +Your tests would look like this: + +```go +package myapp + +import ( + "database/sql" + + _ "github.com/lib/pq" + "github.com/go-testfixtures/testfixtures/v3" +) + +var ( + db *sql.DB + fixtures *testfixtures.Loader +) + +func TestMain(m *testing.M) { + var err error + + // Open connection to the test database. + // Do NOT import fixtures in a production database! + // Existing data would be deleted. + db, err = sql.Open("postgres", "dbname=myapp_test") + if err != nil { + ... + } + + fixtures, err := testfixtures.New( + testfixtures.Database(db), // You database connection + testfixtures.Dialect("postgres"), // Available: "postgresql", "timescaledb", "mysql", "mariadb", "sqlite" and "sqlserver" + testfixtures.Directory("testdata/fixtures"), // the directory containing the YAML files + ) + if err != nil { + ... + } + + os.Exit(m.Run()) +} + +func prepareTestDatabase() { + if err := fixtures.Load(); err != nil { + ... + } +} + +func TestX(t *testing.T) { + prepareTestDatabase() + + // Your test here ... +} + +func TestY(t *testing.T) { + prepareTestDatabase() + + // Your test here ... +} + +func TestZ(t *testing.T) { + prepareTestDatabase() + + // Your test here ... +} +``` + +Alternatively, you can use the `Files` option, to specify which +files you want to load into the database: + +```go +fixtures, err := testfixtures.New( + testfixtures.Database(db), + testfixtures.Dialect("postgres"), + testfixtures.Files( + "fixtures/orders.yml", + "fixtures/customers.yml", + ), +) +if err != nil { + ... +} + +fixtures, err := testfixtures.NewFiles(db, &testfixtures.PostgreSQL{}, + "fixtures/orders.yml", + "fixtures/customers.yml", + // add as many files you want +) +if err != nil { + ... +} +``` + +## Security check + +In order to prevent you from accidentally wiping the wrong database, this +package will refuse to load fixtures if the database name (or database +filename for SQLite) doesn't contains "test". If you want to disable this +check, use: + +```go +testfixtures.New( + ... + testfixtures.DangerousSkipTestDatabaseCheck(), +) +``` + +## Sequences + +For PostgreSQL, this package also resets all sequences to a high +number to prevent duplicated primary keys while running the tests. +The default is 10000, but you can change that with: + +```go +testfixtures.New( + ... + testfixtures.ResetSequencesTo(10000), +) +``` + +Or, if you want to skip the reset of sequences entirely: + +```go +testfixtures.New( + ... + testfixtures.SkipResetSequences(), +) +``` + +## Compatible databases + +### PostgreSQL / TimescaleDB + +This package has two approaches to disable foreign keys while importing fixtures +for PostgreSQL databases: + +#### With `DISABLE TRIGGER` + +This is the default approach. For that use: + +```go +testfixtures.New( + ... + testfixtures.Dialect("postgres"), // or "timescaledb" +) +``` + +With the above snippet this package will use `DISABLE TRIGGER` to temporarily +disabling foreign key constraints while loading fixtures. This work with any +version of PostgreSQL, but it is **required** to be connected in the database +as a SUPERUSER. You can make a PostgreSQL user a SUPERUSER with: + +```sql +ALTER USER your_user SUPERUSER; +``` + +#### With `ALTER CONSTRAINT` + +This approach don't require to be connected as a SUPERUSER, but only work with +PostgreSQL versions >= 9.4. Try this if you are getting foreign key violation +errors with the previous approach. It is as simple as using: + +```go +testfixtures.New( + ... + testfixtures.Dialect("postgres"), + testfixtures.UseAlterConstraint(), +) +``` + +Tested using the [github.com/lib/pq](https://github.com/lib/pq) driver. + +### MySQL / MariaDB + +Just make sure the connection string have +[the multistatement parameter](https://github.com/go-sql-driver/mysql#multistatements) +set to true, and use: + +```go +testfixtures.New( + ... + testfixtures.Dialect("mysql"), // or "mariadb" +) +``` + +Tested using the [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) driver. + +### SQLite + +SQLite is also supported. It is recommended to create foreign keys as +`DEFERRABLE` (the default) to prevent problems. See more +[on the SQLite documentation](https://www.sqlite.org/foreignkeys.html#fk_deferred). +(Foreign key constraints are no-op by default on SQLite, but enabling it is +recommended). + +```go +testfixtures.New( + ... + testfixtures.Dialect("sqlite"), +) +``` + +Tested using the [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) driver. + +### Microsoft SQL Server + +SQL Server support requires SQL Server >= 2008. Inserting on `IDENTITY` columns +are handled as well. Just make sure you are logged in with a user with +`ALTER TABLE` permission. + +```go +testfixtures.New( + ... + testfixtures.Dialect("sqlserver"), +) +``` + +Tested using the `mssql` and `sqlserver` drivers from the +[github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb) lib. + +## Templating + +Testfixtures supports templating, but it's disabled by default. Most people +won't need it, but it may be useful to dynamically generate data. + +Enable it by doing: + +```go +testfixtures.New( + ... + testfixtures.Template(), + + // the above options are optional + TemplateFuncs(...), + TemplateDelims("{{", "}}"), + TemplateOptions("missingkey=zero"), + TemplateData(...), +) +``` + +The YAML file could look like this: + +```yaml +# It's possible generate values... +- id: {{sha256 "my-awesome-post}} + title: My Awesome Post + text: {{randomText}} + +# ... or records +{{range $post := $.Posts}} +- id: {{$post.Id}} + title: {{$post.Title}} + text: {{$post.Text}} +{{end}} +``` + +## Generating fixtures for a existing database + +The following code will generate a YAML file for each table of the database +into a given folder. It may be useful to boostrap a test scenario from a sample +database of your app. + +```go +dumper, err := testfixtures.NewDumper( + testfixtures.DumpDatabase(db), + testfixtures.DumpDialect("postgres"), // or your database of choice + testfixtures.DumpDirectory("tmp/fixtures"), + textfixtures.DumpTables( // optional, will dump all table if not given + "posts", + "comments", + "tags", + ) +) +if err != nil { + ... +} +if err := dumper.Dump(); err != nil { + ... +} +``` + +> This was intended to run in small sample databases. It will likely break +if run in a production/big database. + +## Gotchas + +### Parallel testing + +This library doesn't yet support running tests in parallel! Running tests +in parallel can result in random data being present in the database, which +will likely cause tests to randomly/intermittently fail. + +This is specially tricky since it's not immediately clear that `go test ./...` +run tests for each package in parallel. If more than one package use this +library, you can face this issue. Please, use `go test -p 1 ./...` or run tests +for each package in separated commands to fix this issue. + +If you're looking into being able to run tests in parallel you can try using +testfixtures together with the [txdb][gotxdb] package, which allows wrapping +each test run in a transaction. + +## CLI + +We also have a CLI to load fixtures in a given database. +Grab it from the [releases page](https://github.com/go-testfixtures/testfixtures/releases) +and use it like: + +```bash +testfixtures -d postgres -c "postgres://user:password@localhost/database" -D testdata/fixtures +``` + +The connection string changes for each database driver. + +Use `--help` for all flags. + +## Contributing + +We recommend you to [install Task](https://taskfile.dev/#/installation) and +Docker before contributing to this package, since some stuff is automated +using these tools. + +It's recommended to use Docker Compose to run tests, since it runs tests for +all supported databases once. To do that you just need to run: + +```bash +task docker +``` + +But if you want to run tests locally, copy the `.sample.env` file as `.env` +and edit it according to your database setup. You'll need to create a database +(likely names `testfixtures_test`) before continuing. Then run the command +for the database you want to run tests against: + +```bash +task test:pg # PostgreSQL +task test:mysql # MySQL +task test:sqlite # SQLite +task test:sqlserver # Microsoft SQL Server +``` + +GitHub Actions (CI) runs the same Docker setup available locally. + +## Alternatives + +If you don't think using fixtures is a good idea, you can try one of these +packages instead: + +- [factory-go][factorygo]: Factory for Go. Inspired by Python's Factory Boy +and Ruby's Factory Girl +- [go-txdb (Single transaction SQL driver for Go)][gotxdb]: Use a single +database transaction for each functional test, so you can rollback to +previous state between tests to have the same database state in all tests +- [go-sqlmock][gosqlmock]: A mock for the sql.DB interface. This allow you to +unit test database code without having to connect to a real database +- [dbcleaner][dbcleaner] - Clean database for testing, inspired by +database_cleaner for Ruby + +[doc]: https://pkg.go.dev/github.com/go-testfixtures/testfixtures/v3?tab=doc +[railstests]: http://guides.rubyonrails.org/testing.html#the-test-database +[gotxdb]: https://github.com/DATA-DOG/go-txdb +[gosqlmock]: https://github.com/DATA-DOG/go-sqlmock +[factorygo]: https://github.com/bluele/factory-go +[dbcleaner]: https://github.com/khaiql/dbcleaner diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/Taskfile.yml b/vendor/github.com/go-testfixtures/testfixtures/v3/Taskfile.yml new file mode 100644 index 00000000000..a4ae1c899e6 --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/Taskfile.yml @@ -0,0 +1,59 @@ +# https://taskfile.org + +version: '2' + +tasks: + build: + cmds: + - go build -v -tags sqlite -o ./testfixtures{{exeExt}} ./cmd/testfixtures + + test-cli: + cmds: + - ./testfixtures -d sqlite -c testdb.sqlite3 -D testdata/fixtures + + test:pg: + desc: Test PostgreSQL + cmds: + - task: test-db + vars: {DATABASE: postgresql} + + test:mysql: + desc: Test MySQL + cmds: + - task: test:db + vars: {DATABASE: mysql} + + test:sqlite: + desc: Test SQLite + cmds: + - task: test-db + vars: {DATABASE: sqlite} + + test:sqlserver: + desc: Test SQLServer + cmds: + - task: test-db + vars: {DATABASE: sqlserver} + + test-db: + cmds: + - go test -v -tags {{.DATABASE}} + + goreleaser:test: + desc: Tests release process without publishing + cmds: + - goreleaser --snapshot --rm-dist + + docker: + cmds: + - task: docker:build + - task: docker:test + + docker:build: + cmds: + - docker build -t testfixtures . + + docker:test: + cmds: + - docker-compose down -v + - docker-compose run testfixtures go test -v -tags 'postgresql sqlite mysql sqlserver' diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/docker-compose.yml b/vendor/github.com/go-testfixtures/testfixtures/v3/docker-compose.yml new file mode 100644 index 00000000000..44afd56e1c4 --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3' + +services: + testfixtures: + image: testfixtures + depends_on: + - postgresql + - mysql + - sqlserver + environment: + PGPASSWORD: postgres + PG_CONN_STRING: host=postgresql user=postgres dbname=testfixtures_test port=5432 sslmode=disable + + MYSQL_CONN_STRING: root:mysql@tcp(mysql)/testfixtures_test?multiStatements=true + + SQLITE_CONN_STRING: testfixtures_test.sqlite3 + + SQLSERVER_CONN_STRING: server=sqlserver;database=master;user id=sa;password=SQL@1server;encrypt=disable + + postgresql: + image: postgres:12.1-alpine + environment: + POSTGRES_DB: testfixtures_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + + mysql: + image: mysql:8.0 + environment: + MYSQL_DATABASE: testfixtures_test + MYSQL_ROOT_PASSWORD: mysql + + sqlserver: + image: mcr.microsoft.com/mssql/server:2019-latest + environment: + ACCEPT_EULA: 'Y' + SA_PASSWORD: SQL@1server diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/dump.go b/vendor/github.com/go-testfixtures/testfixtures/v3/dump.go new file mode 100644 index 00000000000..eaf634cb65b --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/dump.go @@ -0,0 +1,155 @@ +package testfixtures + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "unicode/utf8" + + "gopkg.in/yaml.v2" +) + +// Dumper is resposible for dumping fixtures from the database into a +// directory. +type Dumper struct { + db *sql.DB + helper helper + dir string + + tables []string +} + +// NewDumper creates a new dumper with the given options. +// +// The "DumpDatabase", "DumpDialect" and "DumpDirectory" options are required. +func NewDumper(options ...func(*Dumper) error) (*Dumper, error) { + d := &Dumper{} + + for _, option := range options { + if err := option(d); err != nil { + return nil, err + } + } + + return d, nil +} + +// DumpDatabase sets the database to be dumped. +func DumpDatabase(db *sql.DB) func(*Dumper) error { + return func(d *Dumper) error { + d.db = db + return nil + } +} + +// DumpDialect informs Loader about which database dialect you're using. +// +// Possible options are "postgresql", "timescaledb", "mysql", "mariadb", +// "sqlite" and "sqlserver". +func DumpDialect(dialect string) func(*Dumper) error { + return func(d *Dumper) error { + h, err := helperForDialect(dialect) + if err != nil { + return err + } + d.helper = h + return nil + } +} + +// DumpDirectory sets the directory where the fixtures files will be created. +func DumpDirectory(dir string) func(*Dumper) error { + return func(d *Dumper) error { + d.dir = dir + return nil + } +} + +// DumpTables allows you to choose which tables you want to dump. +// +// If not informed, Dumper will dump all tables by default. +func DumpTables(tables ...string) func(*Dumper) error { + return func(d *Dumper) error { + d.tables = tables + return nil + } +} + +// Dump dumps the databases as YAML fixtures. +func (d *Dumper) Dump() error { + tables := d.tables + if len(tables) == 0 { + var err error + tables, err = d.helper.tableNames(d.db) + if err != nil { + return err + } + } + + for _, table := range tables { + if err := d.dumpTable(table); err != nil { + return err + } + } + return nil +} + +func (d *Dumper) dumpTable(table string) error { + query := fmt.Sprintf("SELECT * FROM %s", d.helper.quoteKeyword(table)) + rows, err := d.db.Query(query) + if err != nil { + return err + } + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + return err + } + + fixtures := make([]interface{}, 0, 10) + for rows.Next() { + entries := make([]interface{}, len(columns)) + entryPtrs := make([]interface{}, len(entries)) + for i := range entries { + entryPtrs[i] = &entries[i] + } + if err := rows.Scan(entryPtrs...); err != nil { + return err + } + + entryMap := make(map[string]interface{}, len(entries)) + for i, column := range columns { + entryMap[column] = convertValue(entries[i]) + } + fixtures = append(fixtures, entryMap) + } + if err = rows.Err(); err != nil { + return err + } + + filePath := filepath.Join(d.dir, table+".yml") + f, err := os.Create(filePath) + if err != nil { + return err + } + defer f.Close() + + data, err := yaml.Marshal(fixtures) + if err != nil { + return err + } + _, err = f.Write(data) + return err +} + +func convertValue(value interface{}) interface{} { + switch v := value.(type) { + case []byte: + if utf8.Valid(v) { + return string(v) + } + } + return value +} diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/go.mod b/vendor/github.com/go-testfixtures/testfixtures/v3/go.mod new file mode 100644 index 00000000000..b5f0ae0eea7 --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/go.mod @@ -0,0 +1,14 @@ +module github.com/go-testfixtures/testfixtures/v3 + +require ( + github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 + github.com/go-sql-driver/mysql v1.4.1 + github.com/joho/godotenv v1.3.0 + github.com/lib/pq v1.3.0 + github.com/mattn/go-sqlite3 v2.0.2+incompatible + github.com/spf13/pflag v1.0.5 + google.golang.org/appengine v1.3.0 // indirect + gopkg.in/yaml.v2 v2.2.7 +) + +go 1.13 diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/go.sum b/vendor/github.com/go-testfixtures/testfixtures/v3/go.sum new file mode 100644 index 00000000000..38616db8092 --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/go.sum @@ -0,0 +1,26 @@ +github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 h1:OGNva6WhsKst5OZf7eZOklDztV3hwtTHovdrLHV+MsA= +github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U= +github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/gopkg.in/testfixtures.v2/helper.go b/vendor/github.com/go-testfixtures/testfixtures/v3/helper.go similarity index 87% rename from vendor/gopkg.in/testfixtures.v2/helper.go rename to vendor/github.com/go-testfixtures/testfixtures/v3/helper.go index bd1ebba62ef..06ba5b24e3e 100644 --- a/vendor/gopkg.in/testfixtures.v2/helper.go +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/helper.go @@ -8,13 +8,12 @@ import ( const ( paramTypeDollar = iota + 1 paramTypeQuestion - paramTypeColon + paramTypeAtSign ) type loadFunction func(tx *sql.Tx) error -// Helper is the generic interface for the database helper -type Helper interface { +type helper interface { init(*sql.DB) error disableReferentialIntegrity(*sql.DB, loadFunction) error paramType() int @@ -43,11 +42,10 @@ type batchSplitter interface { } var ( - _ Helper = &MySQL{} - _ Helper = &PostgreSQL{} - _ Helper = &SQLite{} - _ Helper = &Oracle{} - _ Helper = &SQLServer{} + _ helper = &mySQL{} + _ helper = &postgreSQL{} + _ helper = &sqlite{} + _ helper = &sqlserver{} ) type baseHelper struct{} diff --git a/vendor/gopkg.in/testfixtures.v2/json.go b/vendor/github.com/go-testfixtures/testfixtures/v3/json.go similarity index 100% rename from vendor/gopkg.in/testfixtures.v2/json.go rename to vendor/github.com/go-testfixtures/testfixtures/v3/json.go diff --git a/vendor/gopkg.in/testfixtures.v2/mysql.go b/vendor/github.com/go-testfixtures/testfixtures/v3/mysql.go similarity index 78% rename from vendor/gopkg.in/testfixtures.v2/mysql.go rename to vendor/github.com/go-testfixtures/testfixtures/v3/mysql.go index a104c446092..2d1d8902070 100644 --- a/vendor/gopkg.in/testfixtures.v2/mysql.go +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/mysql.go @@ -5,14 +5,13 @@ import ( "fmt" ) -// MySQL is the MySQL helper for this package -type MySQL struct { +type mySQL struct { baseHelper tables []string tablesChecksum map[string]int64 } -func (h *MySQL) init(db *sql.DB) error { +func (h *mySQL) init(db *sql.DB) error { var err error h.tables, err = h.tableNames(db) if err != nil { @@ -22,21 +21,21 @@ func (h *MySQL) init(db *sql.DB) error { return nil } -func (*MySQL) paramType() int { +func (*mySQL) paramType() int { return paramTypeQuestion } -func (*MySQL) quoteKeyword(str string) string { +func (*mySQL) quoteKeyword(str string) string { return fmt.Sprintf("`%s`", str) } -func (*MySQL) databaseName(q queryable) (string, error) { +func (*mySQL) databaseName(q queryable) (string, error) { var dbName string err := q.QueryRow("SELECT DATABASE()").Scan(&dbName) return dbName, err } -func (h *MySQL) tableNames(q queryable) ([]string, error) { +func (h *mySQL) tableNames(q queryable) ([]string, error) { query := ` SELECT table_name FROM information_schema.tables @@ -69,7 +68,7 @@ func (h *MySQL) tableNames(q queryable) ([]string, error) { } -func (h *MySQL) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { +func (h *mySQL) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { tx, err := db.Begin() if err != nil { return err @@ -92,7 +91,7 @@ func (h *MySQL) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (er return tx.Commit() } -func (h *MySQL) isTableModified(q queryable, tableName string) (bool, error) { +func (h *mySQL) isTableModified(q queryable, tableName string) (bool, error) { checksum, err := h.getChecksum(q, tableName) if err != nil { return true, err @@ -103,7 +102,7 @@ func (h *MySQL) isTableModified(q queryable, tableName string) (bool, error) { return oldChecksum == 0 || checksum != oldChecksum, nil } -func (h *MySQL) afterLoad(q queryable) error { +func (h *mySQL) afterLoad(q queryable) error { if h.tablesChecksum != nil { return nil } @@ -119,7 +118,7 @@ func (h *MySQL) afterLoad(q queryable) error { return nil } -func (h *MySQL) getChecksum(q queryable, tableName string) (int64, error) { +func (h *mySQL) getChecksum(q queryable, tableName string) (int64, error) { sql := fmt.Sprintf("CHECKSUM TABLE %s", h.quoteKeyword(tableName)) var ( table string diff --git a/vendor/gopkg.in/testfixtures.v2/postgresql.go b/vendor/github.com/go-testfixtures/testfixtures/v3/postgresql.go similarity index 79% rename from vendor/gopkg.in/testfixtures.v2/postgresql.go rename to vendor/github.com/go-testfixtures/testfixtures/v3/postgresql.go index 69e5588084f..a8ccd1d9498 100644 --- a/vendor/gopkg.in/testfixtures.v2/postgresql.go +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/postgresql.go @@ -6,19 +6,12 @@ import ( "strings" ) -// PostgreSQL is the PG helper for this package -type PostgreSQL struct { +type postgreSQL struct { baseHelper - // UseAlterConstraint If true, the contraint disabling will do - // using ALTER CONTRAINT sintax, only allowed in PG >= 9.4. - // If false, the constraint disabling will use DISABLE TRIGGER ALL, - // which requires SUPERUSER privileges. - UseAlterConstraint bool - - // SkipResetSequences prevents the reset of the databases - // sequences after load fixtures time - SkipResetSequences bool + useAlterConstraint bool + skipResetSequences bool + resetSequencesTo int64 tables []string sequences []string @@ -31,7 +24,7 @@ type pgConstraint struct { constraintName string } -func (h *PostgreSQL) init(db *sql.DB) error { +func (h *postgreSQL) init(db *sql.DB) error { var err error h.tables, err = h.tableNames(db) @@ -52,17 +45,17 @@ func (h *PostgreSQL) init(db *sql.DB) error { return nil } -func (*PostgreSQL) paramType() int { +func (*postgreSQL) paramType() int { return paramTypeDollar } -func (*PostgreSQL) databaseName(q queryable) (string, error) { +func (*postgreSQL) databaseName(q queryable) (string, error) { var dbName string err := q.QueryRow("SELECT current_database()").Scan(&dbName) return dbName, err } -func (h *PostgreSQL) tableNames(q queryable) ([]string, error) { +func (h *postgreSQL) tableNames(q queryable) ([]string, error) { var tables []string sql := ` @@ -71,7 +64,8 @@ func (h *PostgreSQL) tableNames(q queryable) ([]string, error) { INNER JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace WHERE pg_class.relkind = 'r' AND pg_namespace.nspname NOT IN ('pg_catalog', 'information_schema') - AND pg_namespace.nspname NOT LIKE 'pg_toast%'; + AND pg_namespace.nspname NOT LIKE 'pg_toast%' + AND pg_namespace.nspname NOT LIKE '\_timescaledb%'; ` rows, err := q.Query(sql) if err != nil { @@ -92,12 +86,13 @@ func (h *PostgreSQL) tableNames(q queryable) ([]string, error) { return tables, nil } -func (h *PostgreSQL) getSequences(q queryable) ([]string, error) { +func (h *postgreSQL) getSequences(q queryable) ([]string, error) { const sql = ` SELECT pg_namespace.nspname || '.' || pg_class.relname AS sequence_name FROM pg_class INNER JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace WHERE pg_class.relkind = 'S' + AND pg_namespace.nspname NOT LIKE '\_timescaledb%' ` rows, err := q.Query(sql) @@ -120,7 +115,7 @@ func (h *PostgreSQL) getSequences(q queryable) ([]string, error) { return sequences, nil } -func (*PostgreSQL) getNonDeferrableConstraints(q queryable) ([]pgConstraint, error) { +func (*postgreSQL) getNonDeferrableConstraints(q queryable) ([]pgConstraint, error) { var constraints []pgConstraint sql := ` @@ -128,6 +123,7 @@ func (*PostgreSQL) getNonDeferrableConstraints(q queryable) ([]pgConstraint, err FROM information_schema.table_constraints WHERE constraint_type = 'FOREIGN KEY' AND is_deferrable = 'NO' + AND table_schema NOT LIKE '\_timescaledb%' ` rows, err := q.Query(sql) if err != nil { @@ -148,7 +144,7 @@ func (*PostgreSQL) getNonDeferrableConstraints(q queryable) ([]pgConstraint, err return constraints, nil } -func (h *PostgreSQL) disableTriggers(db *sql.DB, loadFn loadFunction) (err error) { +func (h *postgreSQL) disableTriggers(db *sql.DB, loadFn loadFunction) (err error) { defer func() { // re-enable triggers after load var sql string @@ -181,7 +177,7 @@ func (h *PostgreSQL) disableTriggers(db *sql.DB, loadFn loadFunction) (err error return tx.Commit() } -func (h *PostgreSQL) makeConstraintsDeferrable(db *sql.DB, loadFn loadFunction) (err error) { +func (h *postgreSQL) makeConstraintsDeferrable(db *sql.DB, loadFn loadFunction) (err error) { defer func() { // ensure constraint being not deferrable again after load var sql string @@ -218,9 +214,9 @@ func (h *PostgreSQL) makeConstraintsDeferrable(db *sql.DB, loadFn loadFunction) return tx.Commit() } -func (h *PostgreSQL) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { +func (h *postgreSQL) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { // ensure sequences being reset after load - if !h.SkipResetSequences { + if !h.skipResetSequences { defer func() { if err2 := h.resetSequences(db); err2 != nil && err == nil { err = err2 @@ -228,13 +224,18 @@ func (h *PostgreSQL) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction }() } - if h.UseAlterConstraint { + if h.useAlterConstraint { return h.makeConstraintsDeferrable(db, loadFn) } return h.disableTriggers(db, loadFn) } -func (h *PostgreSQL) resetSequences(db *sql.DB) error { +func (h *postgreSQL) resetSequences(db *sql.DB) error { + resetSequencesTo := h.resetSequencesTo + if resetSequencesTo == 0 { + resetSequencesTo = 10000 + } + for _, sequence := range h.sequences { _, err := db.Exec(fmt.Sprintf("SELECT SETVAL('%s', %d)", sequence, resetSequencesTo)) if err != nil { @@ -244,7 +245,7 @@ func (h *PostgreSQL) resetSequences(db *sql.DB) error { return nil } -func (h *PostgreSQL) isTableModified(q queryable, tableName string) (bool, error) { +func (h *postgreSQL) isTableModified(q queryable, tableName string) (bool, error) { checksum, err := h.getChecksum(q, tableName) if err != nil { return false, err @@ -255,7 +256,7 @@ func (h *PostgreSQL) isTableModified(q queryable, tableName string) (bool, error return oldChecksum == "" || checksum != oldChecksum, nil } -func (h *PostgreSQL) afterLoad(q queryable) error { +func (h *postgreSQL) afterLoad(q queryable) error { if h.tablesChecksum != nil { return nil } @@ -271,7 +272,7 @@ func (h *PostgreSQL) afterLoad(q queryable) error { return nil } -func (h *PostgreSQL) getChecksum(q queryable, tableName string) (string, error) { +func (h *postgreSQL) getChecksum(q queryable, tableName string) (string, error) { sqlStr := fmt.Sprintf(` SELECT md5(CAST((array_agg(t.*)) AS TEXT)) FROM %s AS t @@ -286,7 +287,7 @@ func (h *PostgreSQL) getChecksum(q queryable, tableName string) (string, error) return checksum.String, nil } -func (*PostgreSQL) quoteKeyword(s string) string { +func (*postgreSQL) quoteKeyword(s string) string { parts := strings.Split(s, ".") for i, p := range parts { parts[i] = fmt.Sprintf(`"%s"`, p) diff --git a/vendor/gopkg.in/testfixtures.v2/sqlite.go b/vendor/github.com/go-testfixtures/testfixtures/v3/sqlite.go similarity index 79% rename from vendor/gopkg.in/testfixtures.v2/sqlite.go rename to vendor/github.com/go-testfixtures/testfixtures/v3/sqlite.go index 150f0141083..d3d31534154 100644 --- a/vendor/gopkg.in/testfixtures.v2/sqlite.go +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/sqlite.go @@ -5,16 +5,15 @@ import ( "path/filepath" ) -// SQLite is the SQLite Helper for this package -type SQLite struct { +type sqlite struct { baseHelper } -func (*SQLite) paramType() int { +func (*sqlite) paramType() int { return paramTypeQuestion } -func (*SQLite) databaseName(q queryable) (string, error) { +func (*sqlite) databaseName(q queryable) (string, error) { var seq int var main, dbName string err := q.QueryRow("PRAGMA database_list").Scan(&seq, &main, &dbName) @@ -25,7 +24,7 @@ func (*SQLite) databaseName(q queryable) (string, error) { return dbName, nil } -func (*SQLite) tableNames(q queryable) ([]string, error) { +func (*sqlite) tableNames(q queryable) ([]string, error) { query := ` SELECT name FROM sqlite_master @@ -51,7 +50,7 @@ func (*SQLite) tableNames(q queryable) ([]string, error) { return tables, nil } -func (*SQLite) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { +func (*sqlite) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { defer func() { if _, err2 := db.Exec("PRAGMA defer_foreign_keys = OFF"); err2 != nil && err == nil { err = err2 diff --git a/vendor/gopkg.in/testfixtures.v2/sqlserver.go b/vendor/github.com/go-testfixtures/testfixtures/v3/sqlserver.go similarity index 57% rename from vendor/gopkg.in/testfixtures.v2/sqlserver.go rename to vendor/github.com/go-testfixtures/testfixtures/v3/sqlserver.go index d2fc854797c..c862a3b62bf 100644 --- a/vendor/gopkg.in/testfixtures.v2/sqlserver.go +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/sqlserver.go @@ -6,17 +6,29 @@ import ( "strings" ) -// SQLServer is the helper for SQL Server for this package. -// SQL Server >= 2008 is required. -type SQLServer struct { +type sqlserver struct { baseHelper - tables []string + paramTypeCache int + tables []string } -func (h *SQLServer) init(db *sql.DB) error { +func (h *sqlserver) init(db *sql.DB) error { var err error + // NOTE(@andreynering): The SQL Server lib (github.com/denisenkom/go-mssqldb) + // supports both the "?" style (when using the deprecated "mssql" driver) + // and the "@p1" style (when using the new "sqlserver" driver). + // + // Since we don't have a way to know which driver it's been used, + // this is a small hack to detect the allowed param style. + var v int + if err := db.QueryRow("SELECT ?", 1).Scan(&v); err == nil && v == 1 { + h.paramTypeCache = paramTypeQuestion + } else { + h.paramTypeCache = paramTypeAtSign + } + h.tables, err = h.tableNames(db) if err != nil { return err @@ -25,11 +37,11 @@ func (h *SQLServer) init(db *sql.DB) error { return nil } -func (*SQLServer) paramType() int { - return paramTypeQuestion +func (h *sqlserver) paramType() int { + return h.paramTypeCache } -func (*SQLServer) quoteKeyword(s string) string { +func (*sqlserver) quoteKeyword(s string) string { parts := strings.Split(s, ".") for i, p := range parts { parts[i] = fmt.Sprintf(`[%s]`, p) @@ -37,14 +49,14 @@ func (*SQLServer) quoteKeyword(s string) string { return strings.Join(parts, ".") } -func (*SQLServer) databaseName(q queryable) (string, error) { +func (*sqlserver) databaseName(q queryable) (string, error) { var dbName string err := q.QueryRow("SELECT DB_NAME()").Scan(&dbName) return dbName, err } -func (*SQLServer) tableNames(q queryable) ([]string, error) { - rows, err := q.Query("SELECT table_schema + '.' + table_name FROM information_schema.tables") +func (*sqlserver) tableNames(q queryable) ([]string, error) { + rows, err := q.Query("SELECT table_schema + '.' + table_name FROM information_schema.tables WHERE table_name <> 'spt_values'") if err != nil { return nil, err } @@ -64,36 +76,42 @@ func (*SQLServer) tableNames(q queryable) ([]string, error) { return tables, nil } -func (h *SQLServer) tableHasIdentityColumn(q queryable, tableName string) bool { - sql := ` +func (h *sqlserver) tableHasIdentityColumn(q queryable, tableName string) (bool, error) { + sql := fmt.Sprintf(` SELECT COUNT(*) FROM SYS.IDENTITY_COLUMNS - WHERE OBJECT_ID = OBJECT_ID(?) - ` + WHERE OBJECT_ID = OBJECT_ID('%s') + `, tableName) var count int - q.QueryRow(sql, h.quoteKeyword(tableName)).Scan(&count) - return count > 0 + if err := q.QueryRow(sql).Scan(&count); err != nil { + return false, err + } + return count > 0, nil } -func (h *SQLServer) whileInsertOnTable(tx *sql.Tx, tableName string, fn func() error) (err error) { - if h.tableHasIdentityColumn(tx, tableName) { +func (h *sqlserver) whileInsertOnTable(tx *sql.Tx, tableName string, fn func() error) (err error) { + hasIdentityColumn, err := h.tableHasIdentityColumn(tx, tableName) + if err != nil { + return err + } + if hasIdentityColumn { defer func() { _, err2 := tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s OFF", h.quoteKeyword(tableName))) if err2 != nil && err == nil { - err = err2 + err = fmt.Errorf("testfixtures: could not disable identity insert: %w", err2) } }() _, err := tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s ON", h.quoteKeyword(tableName))) if err != nil { - return err + return fmt.Errorf("testfixtures: could not enable identity insert: %w", err) } } return fn() } -func (h *SQLServer) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { +func (h *sqlserver) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { // ensure the triggers are re-enable after all defer func() { var sql string @@ -130,6 +148,6 @@ func (h *SQLServer) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) // SQL Server because commands like a `CREATE SCHEMA...` and a `CREATE TABLE...` // could not be executed in the same batch. // See https://docs.microsoft.com/en-us/previous-versions/sql/sql-server-2008-r2/ms175502(v=sql.105)#rules-for-using-batches -func (*SQLServer) splitter() []byte { +func (*sqlserver) splitter() []byte { return []byte("GO\n") } diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/testfixtures.go b/vendor/github.com/go-testfixtures/testfixtures/v3/testfixtures.go new file mode 100644 index 00000000000..3b7ef8c79d7 --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/testfixtures.go @@ -0,0 +1,558 @@ +package testfixtures // import "github.com/go-testfixtures/testfixtures/v3" + +import ( + "bytes" + "database/sql" + "fmt" + "io/ioutil" + "path" + "path/filepath" + "regexp" + "strings" + "text/template" + "time" + + "gopkg.in/yaml.v2" +) + +// Loader is the responsible to loading fixtures. +type Loader struct { + db *sql.DB + helper helper + fixturesFiles []*fixtureFile + + skipTestDatabaseCheck bool + location *time.Location + + template bool + templateFuncs template.FuncMap + templateLeftDelim string + templateRightDelim string + templateOptions []string + templateData interface{} +} + +type fixtureFile struct { + path string + fileName string + content []byte + insertSQLs []insertSQL +} + +type insertSQL struct { + sql string + params []interface{} +} + +var ( + testDatabaseRegexp = regexp.MustCompile("(?i)test") + + errDatabaseIsRequired = fmt.Errorf("testfixtures: database is required") + errDialectIsRequired = fmt.Errorf("testfixtures: dialect is required") +) + +// New instantiates a new Loader instance. The "Database" and "Driver" +// options are required. +func New(options ...func(*Loader) error) (*Loader, error) { + l := &Loader{ + templateLeftDelim: "{{", + templateRightDelim: "}}", + templateOptions: []string{"missingkey=zero"}, + } + + for _, option := range options { + if err := option(l); err != nil { + return nil, err + } + } + + if l.db == nil { + return nil, errDatabaseIsRequired + } + if l.helper == nil { + return nil, errDialectIsRequired + } + + if err := l.helper.init(l.db); err != nil { + return nil, err + } + if err := l.buildInsertSQLs(); err != nil { + return nil, err + } + + return l, nil +} + +// Database sets an existing sql.DB instant to Loader. +func Database(db *sql.DB) func(*Loader) error { + return func(l *Loader) error { + l.db = db + return nil + } +} + +// Dialect informs Loader about which database dialect you're using. +// +// Possible options are "postgresql", "timescaledb", "mysql", "mariadb", +// "sqlite" and "sqlserver". +func Dialect(dialect string) func(*Loader) error { + return func(l *Loader) error { + h, err := helperForDialect(dialect) + if err != nil { + return err + } + l.helper = h + return nil + } +} + +func helperForDialect(dialect string) (helper, error) { + switch dialect { + case "postgres", "postgresql", "timescaledb": + return &postgreSQL{}, nil + case "mysql", "mariadb": + return &mySQL{}, nil + case "sqlite", "sqlite3": + return &sqlite{}, nil + case "mssql", "sqlserver": + return &sqlserver{}, nil + default: + return nil, fmt.Errorf(`testfixtures: unrecognized dialect "%s"`, dialect) + } +} + +// UseAlterConstraint If true, the contraint disabling will do +// using ALTER CONTRAINT sintax, only allowed in PG >= 9.4. +// If false, the constraint disabling will use DISABLE TRIGGER ALL, +// which requires SUPERUSER privileges. +// +// Only valid for PostgreSQL. Returns an error otherwise. +func UseAlterConstraint() func(*Loader) error { + return func(l *Loader) error { + pgHelper, ok := l.helper.(*postgreSQL) + if !ok { + return fmt.Errorf("testfixtures: UseAlterConstraint is only valid for PostgreSQL databases") + } + pgHelper.useAlterConstraint = true + return nil + } +} + +// SkipResetSequences prevents Loader from reseting sequences after loading +// fixtures. +// +// Only valid for PostgreSQL. Returns an error otherwise. +func SkipResetSequences() func(*Loader) error { + return func(l *Loader) error { + pgHelper, ok := l.helper.(*postgreSQL) + if !ok { + return fmt.Errorf("testfixtures: SkipResetSequences is only valid for PostgreSQL databases") + } + pgHelper.skipResetSequences = true + return nil + } +} + +// ResetSequencesTo sets the value the sequences will be reset to. +// +// Defaults to 10000. +// +// Only valid for PostgreSQL. Returns an error otherwise. +func ResetSequencesTo(value int64) func(*Loader) error { + return func(l *Loader) error { + pgHelper, ok := l.helper.(*postgreSQL) + if !ok { + return fmt.Errorf("testfixtures: ResetSequencesTo is only valid for PostgreSQL databases") + } + pgHelper.resetSequencesTo = value + return nil + } +} + +// DangerousSkipTestDatabaseCheck will make Loader not check if the database +// name contains "test". Use with caution! +func DangerousSkipTestDatabaseCheck() func(*Loader) error { + return func(l *Loader) error { + l.skipTestDatabaseCheck = true + return nil + } +} + +// Directory informs Loader to load YAML files from a given directory. +func Directory(dir string) func(*Loader) error { + return func(l *Loader) error { + fixtures, err := l.fixturesFromDir(dir) + if err != nil { + return err + } + l.fixturesFiles = fixtures + return nil + } +} + +// Files informs Loader to load a given set of YAML files. +func Files(files ...string) func(*Loader) error { + return func(l *Loader) error { + fixtures, err := l.fixturesFromFiles(files...) + if err != nil { + return err + } + l.fixturesFiles = fixtures + return nil + } +} + +// Location makes Loader use the given location by default when parsing +// dates. If not given, by default it uses the value of time.Local. +func Location(location *time.Location) func(*Loader) error { + return func(l *Loader) error { + l.location = location + return nil + } +} + +// Template makes loader process each YAML file as an template using the +// text/template package. +// +// For more information on how templates work in Go please read: +// https://golang.org/pkg/text/template/ +// +// If not given the YAML files are parsed as is. +func Template() func(*Loader) error { + return func(l *Loader) error { + l.template = true + return nil + } +} + +// TemplateFuncs allow choosing which functions will be available +// when processing templates. +// +// For more information see: https://golang.org/pkg/text/template/#Template.Funcs +func TemplateFuncs(funcs template.FuncMap) func(*Loader) error { + return func(l *Loader) error { + if !l.template { + return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateFuns() option`) + } + + l.templateFuncs = funcs + return nil + } +} + +// TemplateDelims allow choosing which delimiters will be used for templating. +// This defaults to "{{" and "}}". +// +// For more information see https://golang.org/pkg/text/template/#Template.Delims +func TemplateDelims(left, right string) func(*Loader) error { + return func(l *Loader) error { + if !l.template { + return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateDelims() option`) + } + + l.templateLeftDelim = left + l.templateRightDelim = right + return nil + } +} + +// TemplateOptions allows you to specific which text/template options will +// be enabled when processing templates. +// +// This defaults to "missingkey=zero". Check the available options here: +// https://golang.org/pkg/text/template/#Template.Option +func TemplateOptions(options ...string) func(*Loader) error { + return func(l *Loader) error { + if !l.template { + return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateOptions() option`) + } + + l.templateOptions = options + return nil + } +} + +// TemplateData allows you to specify which data will be available +// when processing templates. Data is accesible by prefixing it with a "." +// like {{.MyKey}}. +func TemplateData(data interface{}) func(*Loader) error { + return func(l *Loader) error { + if !l.template { + return fmt.Errorf(`testfixtures: the Template() options is required in order to use the TemplateData() option`) + } + + l.templateData = data + return nil + } +} + +// EnsureTestDatabase returns an error if the database name does not contains +// "test". +func (l *Loader) EnsureTestDatabase() error { + dbName, err := l.helper.databaseName(l.db) + if err != nil { + return err + } + if !testDatabaseRegexp.MatchString(dbName) { + return fmt.Errorf(`testfixtures: database "%s" does not appear to be a test database`, dbName) + } + return nil +} + +// Load wipes and after load all fixtures in the database. +// if err := fixtures.Load(); err != nil { +// ... +// } +func (l *Loader) Load() error { + if !l.skipTestDatabaseCheck { + if err := l.EnsureTestDatabase(); err != nil { + return err + } + } + + err := l.helper.disableReferentialIntegrity(l.db, func(tx *sql.Tx) error { + for _, file := range l.fixturesFiles { + modified, err := l.helper.isTableModified(tx, file.fileNameWithoutExtension()) + if err != nil { + return err + } + if !modified { + continue + } + if err := file.delete(tx, l.helper); err != nil { + return err + } + + err = l.helper.whileInsertOnTable(tx, file.fileNameWithoutExtension(), func() error { + for j, i := range file.insertSQLs { + if _, err := tx.Exec(i.sql, i.params...); err != nil { + return &InsertError{ + Err: err, + File: file.fileName, + Index: j, + SQL: i.sql, + Params: i.params, + } + } + } + return nil + }) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + return l.helper.afterLoad(l.db) +} + +// InsertError will be returned if any error happens on database while +// inserting the record. +type InsertError struct { + Err error + File string + Index int + SQL string + Params []interface{} +} + +func (e *InsertError) Error() string { + return fmt.Sprintf( + "testfixtures: error inserting record: %v, on file: %s, index: %d, sql: %s, params: %v", + e.Err, + e.File, + e.Index, + e.SQL, + e.Params, + ) +} + +func (l *Loader) buildInsertSQLs() error { + for _, f := range l.fixturesFiles { + var records interface{} + if err := yaml.Unmarshal(f.content, &records); err != nil { + return fmt.Errorf("testfixtures: could not unmarshal YAML: %w", err) + } + + switch records := records.(type) { + case []interface{}: + f.insertSQLs = make([]insertSQL, 0, len(records)) + + for _, record := range records { + recordMap, ok := record.(map[interface{}]interface{}) + if !ok { + return fmt.Errorf("testfixtures: could not cast record: not a map[interface{}]interface{}") + } + + sql, values, err := l.buildInsertSQL(f, recordMap) + if err != nil { + return err + } + + f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values}) + } + case map[interface{}]interface{}: + f.insertSQLs = make([]insertSQL, 0, len(records)) + + for _, record := range records { + recordMap, ok := record.(map[interface{}]interface{}) + if !ok { + return fmt.Errorf("testfixtures: could not cast record: not a map[interface{}]interface{}") + } + + sql, values, err := l.buildInsertSQL(f, recordMap) + if err != nil { + return err + } + + f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values}) + } + default: + return fmt.Errorf("testfixtures: fixture is not a slice or map") + } + } + + return nil +} + +func (f *fixtureFile) fileNameWithoutExtension() string { + return strings.Replace(f.fileName, filepath.Ext(f.fileName), "", 1) +} + +func (f *fixtureFile) delete(tx *sql.Tx, h helper) error { + if _, err := tx.Exec(fmt.Sprintf("DELETE FROM %s", h.quoteKeyword(f.fileNameWithoutExtension()))); err != nil { + return fmt.Errorf(`testfixtures: could not clean table "%s": %w`, f.fileNameWithoutExtension(), err) + } + return nil +} + +func (l *Loader) buildInsertSQL(f *fixtureFile, record map[interface{}]interface{}) (sqlStr string, values []interface{}, err error) { + var ( + sqlColumns = make([]string, 0, len(record)) + sqlValues = make([]string, 0, len(record)) + i = 1 + ) + for key, value := range record { + keyStr, ok := key.(string) + if !ok { + err = fmt.Errorf("testfixtures: record map key is not a string") + return + } + + sqlColumns = append(sqlColumns, l.helper.quoteKeyword(keyStr)) + + // if string, try convert to SQL or time + // if map or array, convert to json + switch v := value.(type) { + case string: + if strings.HasPrefix(v, "RAW=") { + sqlValues = append(sqlValues, strings.TrimPrefix(v, "RAW=")) + continue + } + + if t, err := l.tryStrToDate(v); err == nil { + value = t + } + case []interface{}, map[interface{}]interface{}: + value = recursiveToJSON(v) + } + + switch l.helper.paramType() { + case paramTypeDollar: + sqlValues = append(sqlValues, fmt.Sprintf("$%d", i)) + case paramTypeQuestion: + sqlValues = append(sqlValues, "?") + case paramTypeAtSign: + sqlValues = append(sqlValues, fmt.Sprintf("@p%d", i)) + } + + values = append(values, value) + i++ + } + + sqlStr = fmt.Sprintf( + "INSERT INTO %s (%s) VALUES (%s)", + l.helper.quoteKeyword(f.fileNameWithoutExtension()), + strings.Join(sqlColumns, ", "), + strings.Join(sqlValues, ", "), + ) + return +} + +func (l *Loader) fixturesFromDir(dir string) ([]*fixtureFile, error) { + fileinfos, err := ioutil.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf(`testfixtures: could not stat directory "%s": %w`, dir, err) + } + + files := make([]*fixtureFile, 0, len(fileinfos)) + + for _, fileinfo := range fileinfos { + fileExt := filepath.Ext(fileinfo.Name()) + if !fileinfo.IsDir() && (fileExt == ".yml" || fileExt == ".yaml") { + fixture := &fixtureFile{ + path: path.Join(dir, fileinfo.Name()), + fileName: fileinfo.Name(), + } + fixture.content, err = ioutil.ReadFile(fixture.path) + if err != nil { + return nil, fmt.Errorf(`testfixtures: could not read file "%s": %w`, fixture.path, err) + } + if err := l.processFileTemplate(fixture); err != nil { + return nil, err + } + files = append(files, fixture) + } + } + return files, nil +} + +func (l *Loader) fixturesFromFiles(fileNames ...string) ([]*fixtureFile, error) { + var ( + fixtureFiles = make([]*fixtureFile, 0, len(fileNames)) + err error + ) + + for _, f := range fileNames { + fixture := &fixtureFile{ + path: f, + fileName: filepath.Base(f), + } + fixture.content, err = ioutil.ReadFile(fixture.path) + if err != nil { + return nil, fmt.Errorf(`testfixtures: could not read file "%s": %w`, fixture.path, err) + } + if err := l.processFileTemplate(fixture); err != nil { + return nil, err + } + fixtureFiles = append(fixtureFiles, fixture) + } + + return fixtureFiles, nil +} + +func (l *Loader) processFileTemplate(f *fixtureFile) error { + if !l.template { + return nil + } + + t := template.New(""). + Funcs(l.templateFuncs). + Delims(l.templateLeftDelim, l.templateRightDelim). + Option(l.templateOptions...) + t, err := t.Parse(string(f.content)) + if err != nil { + return fmt.Errorf(`textfixtures: error on parsing template in %s: %w`, f.fileName, err) + } + + var buffer bytes.Buffer + if err := t.Execute(&buffer, l.templateData); err != nil { + return fmt.Errorf(`textfixtures: error on executing template in %s: %w`, f.fileName, err) + } + + f.content = buffer.Bytes() + return nil +} diff --git a/vendor/github.com/go-testfixtures/testfixtures/v3/time.go b/vendor/github.com/go-testfixtures/testfixtures/v3/time.go new file mode 100644 index 00000000000..3d50a98cc99 --- /dev/null +++ b/vendor/github.com/go-testfixtures/testfixtures/v3/time.go @@ -0,0 +1,43 @@ +package testfixtures + +import ( + "fmt" + "time" +) + +var timeFormats = [...]string{ + "2006-01-02", + "2006-01-02 15:04", + "2006-01-02 15:04:05", + "20060102", + "20060102 15:04", + "20060102 15:04:05", + "02/01/2006", + "02/01/2006 15:04", + "02/01/2006 15:04:05", + "2006-01-02T15:04-07:00", + "2006-01-02T15:04:05-07:00", + "2006-01-02T15:04:05Z07:00", + "2006-01-02 15:04:05Z07:00", + "2006-01-02T15:04:05Z0700", + "2006-01-02 15:04:05Z0700", + "2006-01-02T15:04:05Z07", + "2006-01-02 15:04:05Z07", + "2006-01-02 15:04:05 MST", +} + +func (l *Loader) tryStrToDate(s string) (time.Time, error) { + loc := l.location + if loc == nil { + loc = time.Local + } + + for _, f := range timeFormats { + t, err := time.ParseInLocation(f, s, loc) + if err != nil { + continue + } + return t, nil + } + return time.Time{}, fmt.Errorf(`testfixtures: could not convert string "%s" to time`, s) +} diff --git a/vendor/github.com/hashicorp/hcl/go.mod b/vendor/github.com/hashicorp/hcl/go.mod index 3fdd02e2beb..4debbbe3580 100644 --- a/vendor/github.com/hashicorp/hcl/go.mod +++ b/vendor/github.com/hashicorp/hcl/go.mod @@ -1,5 +1,3 @@ module github.com/hashicorp/hcl require github.com/davecgh/go-spew v1.1.1 - -go 1.13 diff --git a/vendor/github.com/lib/pq/.gitignore b/vendor/github.com/lib/pq/.gitignore new file mode 100644 index 00000000000..0f1d00e1196 --- /dev/null +++ b/vendor/github.com/lib/pq/.gitignore @@ -0,0 +1,4 @@ +.db +*.test +*~ +*.swp diff --git a/vendor/github.com/lib/pq/.travis.sh b/vendor/github.com/lib/pq/.travis.sh new file mode 100644 index 00000000000..ebf447030be --- /dev/null +++ b/vendor/github.com/lib/pq/.travis.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -eu + +client_configure() { + sudo chmod 600 $PQSSLCERTTEST_PATH/postgresql.key +} + +pgdg_repository() { + local sourcelist='sources.list.d/postgresql.list' + + curl -sS 'https://www.postgresql.org/media/keys/ACCC4CF8.asc' | sudo apt-key add - + echo deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main $PGVERSION | sudo tee "/etc/apt/$sourcelist" + sudo apt-get -o Dir::Etc::sourcelist="$sourcelist" -o Dir::Etc::sourceparts='-' -o APT::Get::List-Cleanup='0' update +} + +postgresql_configure() { + sudo tee /etc/postgresql/$PGVERSION/main/pg_hba.conf > /dev/null <<-config + local all all trust + hostnossl all pqgossltest 127.0.0.1/32 reject + hostnossl all pqgosslcert 127.0.0.1/32 reject + hostssl all pqgossltest 127.0.0.1/32 trust + hostssl all pqgosslcert 127.0.0.1/32 cert + host all all 127.0.0.1/32 trust + hostnossl all pqgossltest ::1/128 reject + hostnossl all pqgosslcert ::1/128 reject + hostssl all pqgossltest ::1/128 trust + hostssl all pqgosslcert ::1/128 cert + host all all ::1/128 trust + config + + xargs sudo install -o postgres -g postgres -m 600 -t /var/lib/postgresql/$PGVERSION/main/ <<-certificates + certs/root.crt + certs/server.crt + certs/server.key + certificates + + sort -VCu <<-versions || + $PGVERSION + 9.2 + versions + sudo tee -a /etc/postgresql/$PGVERSION/main/postgresql.conf > /dev/null <<-config + ssl_ca_file = 'root.crt' + ssl_cert_file = 'server.crt' + ssl_key_file = 'server.key' + config + + echo 127.0.0.1 postgres | sudo tee -a /etc/hosts > /dev/null + + sudo service postgresql restart +} + +postgresql_install() { + xargs sudo apt-get -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confnew' install <<-packages + postgresql-$PGVERSION + postgresql-server-dev-$PGVERSION + postgresql-contrib-$PGVERSION + packages +} + +postgresql_uninstall() { + sudo service postgresql stop + xargs sudo apt-get -y --purge remove <<-packages + libpq-dev + libpq5 + postgresql + postgresql-client-common + postgresql-common + packages + sudo rm -rf /var/lib/postgresql +} + +$1 diff --git a/vendor/github.com/lib/pq/.travis.yml b/vendor/github.com/lib/pq/.travis.yml new file mode 100644 index 00000000000..8396f5d9d47 --- /dev/null +++ b/vendor/github.com/lib/pq/.travis.yml @@ -0,0 +1,44 @@ +language: go + +go: + - 1.11.x + - 1.12.x + - master + +sudo: true + +env: + global: + - PGUSER=postgres + - PQGOSSLTESTS=1 + - PQSSLCERTTEST_PATH=$PWD/certs + - PGHOST=127.0.0.1 + matrix: + - PGVERSION=10 + - PGVERSION=9.6 + - PGVERSION=9.5 + - PGVERSION=9.4 + +before_install: + - ./.travis.sh postgresql_uninstall + - ./.travis.sh pgdg_repository + - ./.travis.sh postgresql_install + - ./.travis.sh postgresql_configure + - ./.travis.sh client_configure + - go get golang.org/x/tools/cmd/goimports + - go get golang.org/x/lint/golint + - GO111MODULE=on go get honnef.co/go/tools/cmd/staticcheck@2019.2.1 + +before_script: + - createdb pqgotest + - createuser -DRS pqgossltest + - createuser -DRS pqgosslcert + +script: + - > + goimports -d -e $(find -name '*.go') | awk '{ print } END { exit NR == 0 ? 0 : 1 }' + - go vet ./... + - staticcheck -go 1.11 ./... + - golint ./... + - PQTEST_BINARY_PARAMETERS=no go test -race -v ./... + - PQTEST_BINARY_PARAMETERS=yes go test -race -v ./... diff --git a/vendor/github.com/lib/pq/CONTRIBUTING.md b/vendor/github.com/lib/pq/CONTRIBUTING.md new file mode 100644 index 00000000000..84c937f1561 --- /dev/null +++ b/vendor/github.com/lib/pq/CONTRIBUTING.md @@ -0,0 +1,29 @@ +## Contributing to pq + +`pq` has a backlog of pull requests, but contributions are still very +much welcome. You can help with patch review, submitting bug reports, +or adding new functionality. There is no formal style guide, but +please conform to the style of existing code and general Go formatting +conventions when submitting patches. + +### Patch review + +Help review existing open pull requests by commenting on the code or +proposed functionality. + +### Bug reports + +We appreciate any bug reports, but especially ones with self-contained +(doesn't depend on code outside of pq), minimal (can't be simplified +further) test cases. It's especially helpful if you can submit a pull +request with just the failing test case (you'll probably want to +pattern it after the tests in +[conn_test.go](https://github.com/lib/pq/blob/master/conn_test.go). + +### New functionality + +There are a number of pending patches for new functionality, so +additional feature patches will take a while to merge. Still, patches +are generally reviewed based on usefulness and complexity in addition +to time-in-queue, so if you have a knockout idea, take a shot. Feel +free to open an issue discussion your proposed patch beforehand. diff --git a/vendor/github.com/lib/pq/LICENSE.md b/vendor/github.com/lib/pq/LICENSE.md new file mode 100644 index 00000000000..5773904a30e --- /dev/null +++ b/vendor/github.com/lib/pq/LICENSE.md @@ -0,0 +1,8 @@ +Copyright (c) 2011-2013, 'pq' Contributors +Portions Copyright (C) 2011 Blake Mizerany + +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. diff --git a/vendor/github.com/lib/pq/README.md b/vendor/github.com/lib/pq/README.md new file mode 100644 index 00000000000..385fe73508e --- /dev/null +++ b/vendor/github.com/lib/pq/README.md @@ -0,0 +1,95 @@ +# pq - A pure Go postgres driver for Go's database/sql package + +[![GoDoc](https://godoc.org/github.com/lib/pq?status.svg)](https://godoc.org/github.com/lib/pq) +[![Build Status](https://travis-ci.org/lib/pq.svg?branch=master)](https://travis-ci.org/lib/pq) + +## Install + + go get github.com/lib/pq + +## Docs + +For detailed documentation and basic usage examples, please see the package +documentation at . + +## Tests + +`go test` is used for testing. See [TESTS.md](TESTS.md) for more details. + +## Features + +* SSL +* Handles bad connections for `database/sql` +* Scan `time.Time` correctly (i.e. `timestamp[tz]`, `time[tz]`, `date`) +* Scan binary blobs correctly (i.e. `bytea`) +* Package for `hstore` support +* COPY FROM support +* pq.ParseURL for converting urls to connection strings for sql.Open. +* Many libpq compatible environment variables +* Unix socket support +* Notifications: `LISTEN`/`NOTIFY` +* pgpass support + +## Future / Things you can help with + +* Better COPY FROM / COPY TO (see discussion in #181) + +## Thank you (alphabetical) + +Some of these contributors are from the original library `bmizerany/pq.go` whose +code still exists in here. + +* Andy Balholm (andybalholm) +* Ben Berkert (benburkert) +* Benjamin Heatwole (bheatwole) +* Bill Mill (llimllib) +* Bjørn Madsen (aeons) +* Blake Gentry (bgentry) +* Brad Fitzpatrick (bradfitz) +* Charlie Melbye (cmelbye) +* Chris Bandy (cbandy) +* Chris Gilling (cgilling) +* Chris Walsh (cwds) +* Dan Sosedoff (sosedoff) +* Daniel Farina (fdr) +* Eric Chlebek (echlebek) +* Eric Garrido (minusnine) +* Eric Urban (hydrogen18) +* Everyone at The Go Team +* Evan Shaw (edsrzf) +* Ewan Chou (coocood) +* Fazal Majid (fazalmajid) +* Federico Romero (federomero) +* Fumin (fumin) +* Gary Burd (garyburd) +* Heroku (heroku) +* James Pozdena (jpoz) +* Jason McVetta (jmcvetta) +* Jeremy Jay (pbnjay) +* Joakim Sernbrant (serbaut) +* John Gallagher (jgallagher) +* Jonathan Rudenberg (titanous) +* Joël Stemmer (jstemmer) +* Kamil Kisiel (kisielk) +* Kelly Dunn (kellydunn) +* Keith Rarick (kr) +* Kir Shatrov (kirs) +* Lann Martin (lann) +* Maciek Sakrejda (uhoh-itsmaciek) +* Marc Brinkmann (mbr) +* Marko Tiikkaja (johto) +* Matt Newberry (MattNewberry) +* Matt Robenolt (mattrobenolt) +* Martin Olsen (martinolsen) +* Mike Lewis (mikelikespie) +* Nicolas Patry (Narsil) +* Oliver Tonnhofer (olt) +* Patrick Hayes (phayes) +* Paul Hammond (paulhammond) +* Ryan Smith (ryandotsmith) +* Samuel Stauffer (samuel) +* Timothée Peignier (cyberdelia) +* Travis Cline (tmc) +* TruongSinh Tran-Nguyen (truongsinh) +* Yaismel Miranda (ympons) +* notedit (notedit) diff --git a/vendor/github.com/lib/pq/TESTS.md b/vendor/github.com/lib/pq/TESTS.md new file mode 100644 index 00000000000..f05021115be --- /dev/null +++ b/vendor/github.com/lib/pq/TESTS.md @@ -0,0 +1,33 @@ +# Tests + +## Running Tests + +`go test` is used for testing. A running PostgreSQL +server is required, with the ability to log in. The +database to connect to test with is "pqgotest," on +"localhost" but these can be overridden using [environment +variables](https://www.postgresql.org/docs/9.3/static/libpq-envars.html). + +Example: + + PGHOST=/run/postgresql go test + +## Benchmarks + +A benchmark suite can be run as part of the tests: + + go test -bench . + +## Example setup (Docker) + +Run a postgres container: + +``` +docker run --expose 5432:5432 postgres +``` + +Run tests: + +``` +PGHOST=localhost PGPORT=5432 PGUSER=postgres PGSSLMODE=disable PGDATABASE=postgres go test +``` diff --git a/vendor/github.com/lib/pq/array.go b/vendor/github.com/lib/pq/array.go new file mode 100644 index 00000000000..e4933e22764 --- /dev/null +++ b/vendor/github.com/lib/pq/array.go @@ -0,0 +1,756 @@ +package pq + +import ( + "bytes" + "database/sql" + "database/sql/driver" + "encoding/hex" + "fmt" + "reflect" + "strconv" + "strings" +) + +var typeByteSlice = reflect.TypeOf([]byte{}) +var typeDriverValuer = reflect.TypeOf((*driver.Valuer)(nil)).Elem() +var typeSQLScanner = reflect.TypeOf((*sql.Scanner)(nil)).Elem() + +// Array returns the optimal driver.Valuer and sql.Scanner for an array or +// slice of any dimension. +// +// For example: +// db.Query(`SELECT * FROM t WHERE id = ANY($1)`, pq.Array([]int{235, 401})) +// +// var x []sql.NullInt64 +// db.QueryRow('SELECT ARRAY[235, 401]').Scan(pq.Array(&x)) +// +// Scanning multi-dimensional arrays is not supported. Arrays where the lower +// bound is not one (such as `[0:0]={1}') are not supported. +func Array(a interface{}) interface { + driver.Valuer + sql.Scanner +} { + switch a := a.(type) { + case []bool: + return (*BoolArray)(&a) + case []float64: + return (*Float64Array)(&a) + case []int64: + return (*Int64Array)(&a) + case []string: + return (*StringArray)(&a) + + case *[]bool: + return (*BoolArray)(a) + case *[]float64: + return (*Float64Array)(a) + case *[]int64: + return (*Int64Array)(a) + case *[]string: + return (*StringArray)(a) + } + + return GenericArray{a} +} + +// ArrayDelimiter may be optionally implemented by driver.Valuer or sql.Scanner +// to override the array delimiter used by GenericArray. +type ArrayDelimiter interface { + // ArrayDelimiter returns the delimiter character(s) for this element's type. + ArrayDelimiter() string +} + +// BoolArray represents a one-dimensional array of the PostgreSQL boolean type. +type BoolArray []bool + +// Scan implements the sql.Scanner interface. +func (a *BoolArray) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to BoolArray", src) +} + +func (a *BoolArray) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "BoolArray") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(BoolArray, len(elems)) + for i, v := range elems { + if len(v) != 1 { + return fmt.Errorf("pq: could not parse boolean array index %d: invalid boolean %q", i, v) + } + switch v[0] { + case 't': + b[i] = true + case 'f': + b[i] = false + default: + return fmt.Errorf("pq: could not parse boolean array index %d: invalid boolean %q", i, v) + } + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. +func (a BoolArray) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be exactly two curly brackets, N bytes of values, + // and N-1 bytes of delimiters. + b := make([]byte, 1+2*n) + + for i := 0; i < n; i++ { + b[2*i] = ',' + if a[i] { + b[1+2*i] = 't' + } else { + b[1+2*i] = 'f' + } + } + + b[0] = '{' + b[2*n] = '}' + + return string(b), nil + } + + return "{}", nil +} + +// ByteaArray represents a one-dimensional array of the PostgreSQL bytea type. +type ByteaArray [][]byte + +// Scan implements the sql.Scanner interface. +func (a *ByteaArray) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to ByteaArray", src) +} + +func (a *ByteaArray) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "ByteaArray") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(ByteaArray, len(elems)) + for i, v := range elems { + b[i], err = parseBytea(v) + if err != nil { + return fmt.Errorf("could not parse bytea array index %d: %s", i, err.Error()) + } + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. It uses the "hex" format which +// is only supported on PostgreSQL 9.0 or newer. +func (a ByteaArray) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be at least two curly brackets, 2*N bytes of quotes, + // 3*N bytes of hex formatting, and N-1 bytes of delimiters. + size := 1 + 6*n + for _, x := range a { + size += hex.EncodedLen(len(x)) + } + + b := make([]byte, size) + + for i, s := 0, b; i < n; i++ { + o := copy(s, `,"\\x`) + o += hex.Encode(s[o:], a[i]) + s[o] = '"' + s = s[o+1:] + } + + b[0] = '{' + b[size-1] = '}' + + return string(b), nil + } + + return "{}", nil +} + +// Float64Array represents a one-dimensional array of the PostgreSQL double +// precision type. +type Float64Array []float64 + +// Scan implements the sql.Scanner interface. +func (a *Float64Array) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to Float64Array", src) +} + +func (a *Float64Array) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "Float64Array") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(Float64Array, len(elems)) + for i, v := range elems { + if b[i], err = strconv.ParseFloat(string(v), 64); err != nil { + return fmt.Errorf("pq: parsing array element index %d: %v", i, err) + } + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. +func (a Float64Array) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be at least two curly brackets, N bytes of values, + // and N-1 bytes of delimiters. + b := make([]byte, 1, 1+2*n) + b[0] = '{' + + b = strconv.AppendFloat(b, a[0], 'f', -1, 64) + for i := 1; i < n; i++ { + b = append(b, ',') + b = strconv.AppendFloat(b, a[i], 'f', -1, 64) + } + + return string(append(b, '}')), nil + } + + return "{}", nil +} + +// GenericArray implements the driver.Valuer and sql.Scanner interfaces for +// an array or slice of any dimension. +type GenericArray struct{ A interface{} } + +func (GenericArray) evaluateDestination(rt reflect.Type) (reflect.Type, func([]byte, reflect.Value) error, string) { + var assign func([]byte, reflect.Value) error + var del = "," + + // TODO calculate the assign function for other types + // TODO repeat this section on the element type of arrays or slices (multidimensional) + { + if reflect.PtrTo(rt).Implements(typeSQLScanner) { + // dest is always addressable because it is an element of a slice. + assign = func(src []byte, dest reflect.Value) (err error) { + ss := dest.Addr().Interface().(sql.Scanner) + if src == nil { + err = ss.Scan(nil) + } else { + err = ss.Scan(src) + } + return + } + goto FoundType + } + + assign = func([]byte, reflect.Value) error { + return fmt.Errorf("pq: scanning to %s is not implemented; only sql.Scanner", rt) + } + } + +FoundType: + + if ad, ok := reflect.Zero(rt).Interface().(ArrayDelimiter); ok { + del = ad.ArrayDelimiter() + } + + return rt, assign, del +} + +// Scan implements the sql.Scanner interface. +func (a GenericArray) Scan(src interface{}) error { + dpv := reflect.ValueOf(a.A) + switch { + case dpv.Kind() != reflect.Ptr: + return fmt.Errorf("pq: destination %T is not a pointer to array or slice", a.A) + case dpv.IsNil(): + return fmt.Errorf("pq: destination %T is nil", a.A) + } + + dv := dpv.Elem() + switch dv.Kind() { + case reflect.Slice: + case reflect.Array: + default: + return fmt.Errorf("pq: destination %T is not a pointer to array or slice", a.A) + } + + switch src := src.(type) { + case []byte: + return a.scanBytes(src, dv) + case string: + return a.scanBytes([]byte(src), dv) + case nil: + if dv.Kind() == reflect.Slice { + dv.Set(reflect.Zero(dv.Type())) + return nil + } + } + + return fmt.Errorf("pq: cannot convert %T to %s", src, dv.Type()) +} + +func (a GenericArray) scanBytes(src []byte, dv reflect.Value) error { + dtype, assign, del := a.evaluateDestination(dv.Type().Elem()) + dims, elems, err := parseArray(src, []byte(del)) + if err != nil { + return err + } + + // TODO allow multidimensional + + if len(dims) > 1 { + return fmt.Errorf("pq: scanning from multidimensional ARRAY%s is not implemented", + strings.Replace(fmt.Sprint(dims), " ", "][", -1)) + } + + // Treat a zero-dimensional array like an array with a single dimension of zero. + if len(dims) == 0 { + dims = append(dims, 0) + } + + for i, rt := 0, dv.Type(); i < len(dims); i, rt = i+1, rt.Elem() { + switch rt.Kind() { + case reflect.Slice: + case reflect.Array: + if rt.Len() != dims[i] { + return fmt.Errorf("pq: cannot convert ARRAY%s to %s", + strings.Replace(fmt.Sprint(dims), " ", "][", -1), dv.Type()) + } + default: + // TODO handle multidimensional + } + } + + values := reflect.MakeSlice(reflect.SliceOf(dtype), len(elems), len(elems)) + for i, e := range elems { + if err := assign(e, values.Index(i)); err != nil { + return fmt.Errorf("pq: parsing array element index %d: %v", i, err) + } + } + + // TODO handle multidimensional + + switch dv.Kind() { + case reflect.Slice: + dv.Set(values.Slice(0, dims[0])) + case reflect.Array: + for i := 0; i < dims[0]; i++ { + dv.Index(i).Set(values.Index(i)) + } + } + + return nil +} + +// Value implements the driver.Valuer interface. +func (a GenericArray) Value() (driver.Value, error) { + if a.A == nil { + return nil, nil + } + + rv := reflect.ValueOf(a.A) + + switch rv.Kind() { + case reflect.Slice: + if rv.IsNil() { + return nil, nil + } + case reflect.Array: + default: + return nil, fmt.Errorf("pq: Unable to convert %T to array", a.A) + } + + if n := rv.Len(); n > 0 { + // There will be at least two curly brackets, N bytes of values, + // and N-1 bytes of delimiters. + b := make([]byte, 0, 1+2*n) + + b, _, err := appendArray(b, rv, n) + return string(b), err + } + + return "{}", nil +} + +// Int64Array represents a one-dimensional array of the PostgreSQL integer types. +type Int64Array []int64 + +// Scan implements the sql.Scanner interface. +func (a *Int64Array) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to Int64Array", src) +} + +func (a *Int64Array) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "Int64Array") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(Int64Array, len(elems)) + for i, v := range elems { + if b[i], err = strconv.ParseInt(string(v), 10, 64); err != nil { + return fmt.Errorf("pq: parsing array element index %d: %v", i, err) + } + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. +func (a Int64Array) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be at least two curly brackets, N bytes of values, + // and N-1 bytes of delimiters. + b := make([]byte, 1, 1+2*n) + b[0] = '{' + + b = strconv.AppendInt(b, a[0], 10) + for i := 1; i < n; i++ { + b = append(b, ',') + b = strconv.AppendInt(b, a[i], 10) + } + + return string(append(b, '}')), nil + } + + return "{}", nil +} + +// StringArray represents a one-dimensional array of the PostgreSQL character types. +type StringArray []string + +// Scan implements the sql.Scanner interface. +func (a *StringArray) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to StringArray", src) +} + +func (a *StringArray) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "StringArray") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(StringArray, len(elems)) + for i, v := range elems { + if b[i] = string(v); v == nil { + return fmt.Errorf("pq: parsing array element index %d: cannot convert nil to string", i) + } + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. +func (a StringArray) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be at least two curly brackets, 2*N bytes of quotes, + // and N-1 bytes of delimiters. + b := make([]byte, 1, 1+3*n) + b[0] = '{' + + b = appendArrayQuotedBytes(b, []byte(a[0])) + for i := 1; i < n; i++ { + b = append(b, ',') + b = appendArrayQuotedBytes(b, []byte(a[i])) + } + + return string(append(b, '}')), nil + } + + return "{}", nil +} + +// appendArray appends rv to the buffer, returning the extended buffer and +// the delimiter used between elements. +// +// It panics when n <= 0 or rv's Kind is not reflect.Array nor reflect.Slice. +func appendArray(b []byte, rv reflect.Value, n int) ([]byte, string, error) { + var del string + var err error + + b = append(b, '{') + + if b, del, err = appendArrayElement(b, rv.Index(0)); err != nil { + return b, del, err + } + + for i := 1; i < n; i++ { + b = append(b, del...) + if b, del, err = appendArrayElement(b, rv.Index(i)); err != nil { + return b, del, err + } + } + + return append(b, '}'), del, nil +} + +// appendArrayElement appends rv to the buffer, returning the extended buffer +// and the delimiter to use before the next element. +// +// When rv's Kind is neither reflect.Array nor reflect.Slice, it is converted +// using driver.DefaultParameterConverter and the resulting []byte or string +// is double-quoted. +// +// See http://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-IO +func appendArrayElement(b []byte, rv reflect.Value) ([]byte, string, error) { + if k := rv.Kind(); k == reflect.Array || k == reflect.Slice { + if t := rv.Type(); t != typeByteSlice && !t.Implements(typeDriverValuer) { + if n := rv.Len(); n > 0 { + return appendArray(b, rv, n) + } + + return b, "", nil + } + } + + var del = "," + var err error + var iv interface{} = rv.Interface() + + if ad, ok := iv.(ArrayDelimiter); ok { + del = ad.ArrayDelimiter() + } + + if iv, err = driver.DefaultParameterConverter.ConvertValue(iv); err != nil { + return b, del, err + } + + switch v := iv.(type) { + case nil: + return append(b, "NULL"...), del, nil + case []byte: + return appendArrayQuotedBytes(b, v), del, nil + case string: + return appendArrayQuotedBytes(b, []byte(v)), del, nil + } + + b, err = appendValue(b, iv) + return b, del, err +} + +func appendArrayQuotedBytes(b, v []byte) []byte { + b = append(b, '"') + for { + i := bytes.IndexAny(v, `"\`) + if i < 0 { + b = append(b, v...) + break + } + if i > 0 { + b = append(b, v[:i]...) + } + b = append(b, '\\', v[i]) + v = v[i+1:] + } + return append(b, '"') +} + +func appendValue(b []byte, v driver.Value) ([]byte, error) { + return append(b, encode(nil, v, 0)...), nil +} + +// parseArray extracts the dimensions and elements of an array represented in +// text format. Only representations emitted by the backend are supported. +// Notably, whitespace around brackets and delimiters is significant, and NULL +// is case-sensitive. +// +// See http://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-IO +func parseArray(src, del []byte) (dims []int, elems [][]byte, err error) { + var depth, i int + + if len(src) < 1 || src[0] != '{' { + return nil, nil, fmt.Errorf("pq: unable to parse array; expected %q at offset %d", '{', 0) + } + +Open: + for i < len(src) { + switch src[i] { + case '{': + depth++ + i++ + case '}': + elems = make([][]byte, 0) + goto Close + default: + break Open + } + } + dims = make([]int, i) + +Element: + for i < len(src) { + switch src[i] { + case '{': + if depth == len(dims) { + break Element + } + depth++ + dims[depth-1] = 0 + i++ + case '"': + var elem = []byte{} + var escape bool + for i++; i < len(src); i++ { + if escape { + elem = append(elem, src[i]) + escape = false + } else { + switch src[i] { + default: + elem = append(elem, src[i]) + case '\\': + escape = true + case '"': + elems = append(elems, elem) + i++ + break Element + } + } + } + default: + for start := i; i < len(src); i++ { + if bytes.HasPrefix(src[i:], del) || src[i] == '}' { + elem := src[start:i] + if len(elem) == 0 { + return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i) + } + if bytes.Equal(elem, []byte("NULL")) { + elem = nil + } + elems = append(elems, elem) + break Element + } + } + } + } + + for i < len(src) { + if bytes.HasPrefix(src[i:], del) && depth > 0 { + dims[depth-1]++ + i += len(del) + goto Element + } else if src[i] == '}' && depth > 0 { + dims[depth-1]++ + depth-- + i++ + } else { + return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i) + } + } + +Close: + for i < len(src) { + if src[i] == '}' && depth > 0 { + depth-- + i++ + } else { + return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i) + } + } + if depth > 0 { + err = fmt.Errorf("pq: unable to parse array; expected %q at offset %d", '}', i) + } + if err == nil { + for _, d := range dims { + if (len(elems) % d) != 0 { + err = fmt.Errorf("pq: multidimensional arrays must have elements with matching dimensions") + } + } + } + return +} + +func scanLinearArray(src, del []byte, typ string) (elems [][]byte, err error) { + dims, elems, err := parseArray(src, del) + if err != nil { + return nil, err + } + if len(dims) > 1 { + return nil, fmt.Errorf("pq: cannot convert ARRAY%s to %s", strings.Replace(fmt.Sprint(dims), " ", "][", -1), typ) + } + return elems, err +} diff --git a/vendor/github.com/lib/pq/buf.go b/vendor/github.com/lib/pq/buf.go new file mode 100644 index 00000000000..4b0a0a8f7e9 --- /dev/null +++ b/vendor/github.com/lib/pq/buf.go @@ -0,0 +1,91 @@ +package pq + +import ( + "bytes" + "encoding/binary" + + "github.com/lib/pq/oid" +) + +type readBuf []byte + +func (b *readBuf) int32() (n int) { + n = int(int32(binary.BigEndian.Uint32(*b))) + *b = (*b)[4:] + return +} + +func (b *readBuf) oid() (n oid.Oid) { + n = oid.Oid(binary.BigEndian.Uint32(*b)) + *b = (*b)[4:] + return +} + +// N.B: this is actually an unsigned 16-bit integer, unlike int32 +func (b *readBuf) int16() (n int) { + n = int(binary.BigEndian.Uint16(*b)) + *b = (*b)[2:] + return +} + +func (b *readBuf) string() string { + i := bytes.IndexByte(*b, 0) + if i < 0 { + errorf("invalid message format; expected string terminator") + } + s := (*b)[:i] + *b = (*b)[i+1:] + return string(s) +} + +func (b *readBuf) next(n int) (v []byte) { + v = (*b)[:n] + *b = (*b)[n:] + return +} + +func (b *readBuf) byte() byte { + return b.next(1)[0] +} + +type writeBuf struct { + buf []byte + pos int +} + +func (b *writeBuf) int32(n int) { + x := make([]byte, 4) + binary.BigEndian.PutUint32(x, uint32(n)) + b.buf = append(b.buf, x...) +} + +func (b *writeBuf) int16(n int) { + x := make([]byte, 2) + binary.BigEndian.PutUint16(x, uint16(n)) + b.buf = append(b.buf, x...) +} + +func (b *writeBuf) string(s string) { + b.buf = append(append(b.buf, s...), '\000') +} + +func (b *writeBuf) byte(c byte) { + b.buf = append(b.buf, c) +} + +func (b *writeBuf) bytes(v []byte) { + b.buf = append(b.buf, v...) +} + +func (b *writeBuf) wrap() []byte { + p := b.buf[b.pos:] + binary.BigEndian.PutUint32(p, uint32(len(p))) + return b.buf +} + +func (b *writeBuf) next(c byte) { + p := b.buf[b.pos:] + binary.BigEndian.PutUint32(p, uint32(len(p))) + b.pos = len(b.buf) + 1 + b.buf = append(b.buf, c, 0, 0, 0, 0) +} diff --git a/vendor/github.com/lib/pq/conn.go b/vendor/github.com/lib/pq/conn.go new file mode 100644 index 00000000000..55152b1242c --- /dev/null +++ b/vendor/github.com/lib/pq/conn.go @@ -0,0 +1,1923 @@ +package pq + +import ( + "bufio" + "context" + "crypto/md5" + "crypto/sha256" + "database/sql" + "database/sql/driver" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "os" + "os/user" + "path" + "path/filepath" + "strconv" + "strings" + "time" + "unicode" + + "github.com/lib/pq/oid" + "github.com/lib/pq/scram" +) + +// Common error types +var ( + ErrNotSupported = errors.New("pq: Unsupported command") + ErrInFailedTransaction = errors.New("pq: Could not complete operation in a failed transaction") + ErrSSLNotSupported = errors.New("pq: SSL is not enabled on the server") + ErrSSLKeyHasWorldPermissions = errors.New("pq: Private key file has group or world access. Permissions should be u=rw (0600) or less") + ErrCouldNotDetectUsername = errors.New("pq: Could not detect default username. Please provide one explicitly") + + errUnexpectedReady = errors.New("unexpected ReadyForQuery") + errNoRowsAffected = errors.New("no RowsAffected available after the empty statement") + errNoLastInsertID = errors.New("no LastInsertId available after the empty statement") +) + +// Driver is the Postgres database driver. +type Driver struct{} + +// Open opens a new connection to the database. name is a connection string. +// Most users should only use it through database/sql package from the standard +// library. +func (d *Driver) Open(name string) (driver.Conn, error) { + return Open(name) +} + +func init() { + sql.Register("postgres", &Driver{}) +} + +type parameterStatus struct { + // server version in the same format as server_version_num, or 0 if + // unavailable + serverVersion int + + // the current location based on the TimeZone value of the session, if + // available + currentLocation *time.Location +} + +type transactionStatus byte + +const ( + txnStatusIdle transactionStatus = 'I' + txnStatusIdleInTransaction transactionStatus = 'T' + txnStatusInFailedTransaction transactionStatus = 'E' +) + +func (s transactionStatus) String() string { + switch s { + case txnStatusIdle: + return "idle" + case txnStatusIdleInTransaction: + return "idle in transaction" + case txnStatusInFailedTransaction: + return "in a failed transaction" + default: + errorf("unknown transactionStatus %d", s) + } + + panic("not reached") +} + +// Dialer is the dialer interface. It can be used to obtain more control over +// how pq creates network connections. +type Dialer interface { + Dial(network, address string) (net.Conn, error) + DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) +} + +// DialerContext is the context-aware dialer interface. +type DialerContext interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + +type defaultDialer struct { + d net.Dialer +} + +func (d defaultDialer) Dial(network, address string) (net.Conn, error) { + return d.d.Dial(network, address) +} +func (d defaultDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return d.DialContext(ctx, network, address) +} +func (d defaultDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + return d.d.DialContext(ctx, network, address) +} + +type conn struct { + c net.Conn + buf *bufio.Reader + namei int + scratch [512]byte + txnStatus transactionStatus + txnFinish func() + + // Save connection arguments to use during CancelRequest. + dialer Dialer + opts values + + // Cancellation key data for use with CancelRequest messages. + processID int + secretKey int + + parameterStatus parameterStatus + + saveMessageType byte + saveMessageBuffer []byte + + // If true, this connection is bad and all public-facing functions should + // return ErrBadConn. + bad bool + + // If set, this connection should never use the binary format when + // receiving query results from prepared statements. Only provided for + // debugging. + disablePreparedBinaryResult bool + + // Whether to always send []byte parameters over as binary. Enables single + // round-trip mode for non-prepared Query calls. + binaryParameters bool + + // If true this connection is in the middle of a COPY + inCopy bool +} + +// Handle driver-side settings in parsed connection string. +func (cn *conn) handleDriverSettings(o values) (err error) { + boolSetting := func(key string, val *bool) error { + if value, ok := o[key]; ok { + if value == "yes" { + *val = true + } else if value == "no" { + *val = false + } else { + return fmt.Errorf("unrecognized value %q for %s", value, key) + } + } + return nil + } + + err = boolSetting("disable_prepared_binary_result", &cn.disablePreparedBinaryResult) + if err != nil { + return err + } + return boolSetting("binary_parameters", &cn.binaryParameters) +} + +func (cn *conn) handlePgpass(o values) { + // if a password was supplied, do not process .pgpass + if _, ok := o["password"]; ok { + return + } + filename := os.Getenv("PGPASSFILE") + if filename == "" { + // XXX this code doesn't work on Windows where the default filename is + // XXX %APPDATA%\postgresql\pgpass.conf + // Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470 + userHome := os.Getenv("HOME") + if userHome == "" { + user, err := user.Current() + if err != nil { + return + } + userHome = user.HomeDir + } + filename = filepath.Join(userHome, ".pgpass") + } + fileinfo, err := os.Stat(filename) + if err != nil { + return + } + mode := fileinfo.Mode() + if mode&(0x77) != 0 { + // XXX should warn about incorrect .pgpass permissions as psql does + return + } + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + scanner := bufio.NewScanner(io.Reader(file)) + hostname := o["host"] + ntw, _ := network(o) + port := o["port"] + db := o["dbname"] + username := o["user"] + // From: https://github.com/tg/pgpass/blob/master/reader.go + getFields := func(s string) []string { + fs := make([]string, 0, 5) + f := make([]rune, 0, len(s)) + + var esc bool + for _, c := range s { + switch { + case esc: + f = append(f, c) + esc = false + case c == '\\': + esc = true + case c == ':': + fs = append(fs, string(f)) + f = f[:0] + default: + f = append(f, c) + } + } + return append(fs, string(f)) + } + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 || line[0] == '#' { + continue + } + split := getFields(line) + if len(split) != 5 { + continue + } + if (split[0] == "*" || split[0] == hostname || (split[0] == "localhost" && (hostname == "" || ntw == "unix"))) && (split[1] == "*" || split[1] == port) && (split[2] == "*" || split[2] == db) && (split[3] == "*" || split[3] == username) { + o["password"] = split[4] + return + } + } +} + +func (cn *conn) writeBuf(b byte) *writeBuf { + cn.scratch[0] = b + return &writeBuf{ + buf: cn.scratch[:5], + pos: 1, + } +} + +// Open opens a new connection to the database. dsn is a connection string. +// Most users should only use it through database/sql package from the standard +// library. +func Open(dsn string) (_ driver.Conn, err error) { + return DialOpen(defaultDialer{}, dsn) +} + +// DialOpen opens a new connection to the database using a dialer. +func DialOpen(d Dialer, dsn string) (_ driver.Conn, err error) { + c, err := NewConnector(dsn) + if err != nil { + return nil, err + } + c.dialer = d + return c.open(context.Background()) +} + +func (c *Connector) open(ctx context.Context) (cn *conn, err error) { + // Handle any panics during connection initialization. Note that we + // specifically do *not* want to use errRecover(), as that would turn any + // connection errors into ErrBadConns, hiding the real error message from + // the user. + defer errRecoverNoErrBadConn(&err) + + o := c.opts + + cn = &conn{ + opts: o, + dialer: c.dialer, + } + err = cn.handleDriverSettings(o) + if err != nil { + return nil, err + } + cn.handlePgpass(o) + + cn.c, err = dial(ctx, c.dialer, o) + if err != nil { + return nil, err + } + + err = cn.ssl(o) + if err != nil { + if cn.c != nil { + cn.c.Close() + } + return nil, err + } + + // cn.startup panics on error. Make sure we don't leak cn.c. + panicking := true + defer func() { + if panicking { + cn.c.Close() + } + }() + + cn.buf = bufio.NewReader(cn.c) + cn.startup(o) + + // reset the deadline, in case one was set (see dial) + if timeout, ok := o["connect_timeout"]; ok && timeout != "0" { + err = cn.c.SetDeadline(time.Time{}) + } + panicking = false + return cn, err +} + +func dial(ctx context.Context, d Dialer, o values) (net.Conn, error) { + network, address := network(o) + // SSL is not necessary or supported over UNIX domain sockets + if network == "unix" { + o["sslmode"] = "disable" + } + + // Zero or not specified means wait indefinitely. + if timeout, ok := o["connect_timeout"]; ok && timeout != "0" { + seconds, err := strconv.ParseInt(timeout, 10, 0) + if err != nil { + return nil, fmt.Errorf("invalid value for parameter connect_timeout: %s", err) + } + duration := time.Duration(seconds) * time.Second + + // connect_timeout should apply to the entire connection establishment + // procedure, so we both use a timeout for the TCP connection + // establishment and set a deadline for doing the initial handshake. + // The deadline is then reset after startup() is done. + deadline := time.Now().Add(duration) + var conn net.Conn + if dctx, ok := d.(DialerContext); ok { + ctx, cancel := context.WithTimeout(ctx, duration) + defer cancel() + conn, err = dctx.DialContext(ctx, network, address) + } else { + conn, err = d.DialTimeout(network, address, duration) + } + if err != nil { + return nil, err + } + err = conn.SetDeadline(deadline) + return conn, err + } + if dctx, ok := d.(DialerContext); ok { + return dctx.DialContext(ctx, network, address) + } + return d.Dial(network, address) +} + +func network(o values) (string, string) { + host := o["host"] + + if strings.HasPrefix(host, "/") { + sockPath := path.Join(host, ".s.PGSQL."+o["port"]) + return "unix", sockPath + } + + return "tcp", net.JoinHostPort(host, o["port"]) +} + +type values map[string]string + +// scanner implements a tokenizer for libpq-style option strings. +type scanner struct { + s []rune + i int +} + +// newScanner returns a new scanner initialized with the option string s. +func newScanner(s string) *scanner { + return &scanner{[]rune(s), 0} +} + +// Next returns the next rune. +// It returns 0, false if the end of the text has been reached. +func (s *scanner) Next() (rune, bool) { + if s.i >= len(s.s) { + return 0, false + } + r := s.s[s.i] + s.i++ + return r, true +} + +// SkipSpaces returns the next non-whitespace rune. +// It returns 0, false if the end of the text has been reached. +func (s *scanner) SkipSpaces() (rune, bool) { + r, ok := s.Next() + for unicode.IsSpace(r) && ok { + r, ok = s.Next() + } + return r, ok +} + +// parseOpts parses the options from name and adds them to the values. +// +// The parsing code is based on conninfo_parse from libpq's fe-connect.c +func parseOpts(name string, o values) error { + s := newScanner(name) + + for { + var ( + keyRunes, valRunes []rune + r rune + ok bool + ) + + if r, ok = s.SkipSpaces(); !ok { + break + } + + // Scan the key + for !unicode.IsSpace(r) && r != '=' { + keyRunes = append(keyRunes, r) + if r, ok = s.Next(); !ok { + break + } + } + + // Skip any whitespace if we're not at the = yet + if r != '=' { + r, ok = s.SkipSpaces() + } + + // The current character should be = + if r != '=' || !ok { + return fmt.Errorf(`missing "=" after %q in connection info string"`, string(keyRunes)) + } + + // Skip any whitespace after the = + if r, ok = s.SkipSpaces(); !ok { + // If we reach the end here, the last value is just an empty string as per libpq. + o[string(keyRunes)] = "" + break + } + + if r != '\'' { + for !unicode.IsSpace(r) { + if r == '\\' { + if r, ok = s.Next(); !ok { + return fmt.Errorf(`missing character after backslash`) + } + } + valRunes = append(valRunes, r) + + if r, ok = s.Next(); !ok { + break + } + } + } else { + quote: + for { + if r, ok = s.Next(); !ok { + return fmt.Errorf(`unterminated quoted string literal in connection string`) + } + switch r { + case '\'': + break quote + case '\\': + r, _ = s.Next() + fallthrough + default: + valRunes = append(valRunes, r) + } + } + } + + o[string(keyRunes)] = string(valRunes) + } + + return nil +} + +func (cn *conn) isInTransaction() bool { + return cn.txnStatus == txnStatusIdleInTransaction || + cn.txnStatus == txnStatusInFailedTransaction +} + +func (cn *conn) checkIsInTransaction(intxn bool) { + if cn.isInTransaction() != intxn { + cn.bad = true + errorf("unexpected transaction status %v", cn.txnStatus) + } +} + +func (cn *conn) Begin() (_ driver.Tx, err error) { + return cn.begin("") +} + +func (cn *conn) begin(mode string) (_ driver.Tx, err error) { + if cn.bad { + return nil, driver.ErrBadConn + } + defer cn.errRecover(&err) + + cn.checkIsInTransaction(false) + _, commandTag, err := cn.simpleExec("BEGIN" + mode) + if err != nil { + return nil, err + } + if commandTag != "BEGIN" { + cn.bad = true + return nil, fmt.Errorf("unexpected command tag %s", commandTag) + } + if cn.txnStatus != txnStatusIdleInTransaction { + cn.bad = true + return nil, fmt.Errorf("unexpected transaction status %v", cn.txnStatus) + } + return cn, nil +} + +func (cn *conn) closeTxn() { + if finish := cn.txnFinish; finish != nil { + finish() + } +} + +func (cn *conn) Commit() (err error) { + defer cn.closeTxn() + if cn.bad { + return driver.ErrBadConn + } + defer cn.errRecover(&err) + + cn.checkIsInTransaction(true) + // We don't want the client to think that everything is okay if it tries + // to commit a failed transaction. However, no matter what we return, + // database/sql will release this connection back into the free connection + // pool so we have to abort the current transaction here. Note that you + // would get the same behaviour if you issued a COMMIT in a failed + // transaction, so it's also the least surprising thing to do here. + if cn.txnStatus == txnStatusInFailedTransaction { + if err := cn.rollback(); err != nil { + return err + } + return ErrInFailedTransaction + } + + _, commandTag, err := cn.simpleExec("COMMIT") + if err != nil { + if cn.isInTransaction() { + cn.bad = true + } + return err + } + if commandTag != "COMMIT" { + cn.bad = true + return fmt.Errorf("unexpected command tag %s", commandTag) + } + cn.checkIsInTransaction(false) + return nil +} + +func (cn *conn) Rollback() (err error) { + defer cn.closeTxn() + if cn.bad { + return driver.ErrBadConn + } + defer cn.errRecover(&err) + return cn.rollback() +} + +func (cn *conn) rollback() (err error) { + cn.checkIsInTransaction(true) + _, commandTag, err := cn.simpleExec("ROLLBACK") + if err != nil { + if cn.isInTransaction() { + cn.bad = true + } + return err + } + if commandTag != "ROLLBACK" { + return fmt.Errorf("unexpected command tag %s", commandTag) + } + cn.checkIsInTransaction(false) + return nil +} + +func (cn *conn) gname() string { + cn.namei++ + return strconv.FormatInt(int64(cn.namei), 10) +} + +func (cn *conn) simpleExec(q string) (res driver.Result, commandTag string, err error) { + b := cn.writeBuf('Q') + b.string(q) + cn.send(b) + + for { + t, r := cn.recv1() + switch t { + case 'C': + res, commandTag = cn.parseComplete(r.string()) + case 'Z': + cn.processReadyForQuery(r) + if res == nil && err == nil { + err = errUnexpectedReady + } + // done + return + case 'E': + err = parseError(r) + case 'I': + res = emptyRows + case 'T', 'D': + // ignore any results + default: + cn.bad = true + errorf("unknown response for simple query: %q", t) + } + } +} + +func (cn *conn) simpleQuery(q string) (res *rows, err error) { + defer cn.errRecover(&err) + + b := cn.writeBuf('Q') + b.string(q) + cn.send(b) + + for { + t, r := cn.recv1() + switch t { + case 'C', 'I': + // We allow queries which don't return any results through Query as + // well as Exec. We still have to give database/sql a rows object + // the user can close, though, to avoid connections from being + // leaked. A "rows" with done=true works fine for that purpose. + if err != nil { + cn.bad = true + errorf("unexpected message %q in simple query execution", t) + } + if res == nil { + res = &rows{ + cn: cn, + } + } + // Set the result and tag to the last command complete if there wasn't a + // query already run. Although queries usually return from here and cede + // control to Next, a query with zero results does not. + if t == 'C' && res.colNames == nil { + res.result, res.tag = cn.parseComplete(r.string()) + } + res.done = true + case 'Z': + cn.processReadyForQuery(r) + // done + return + case 'E': + res = nil + err = parseError(r) + case 'D': + if res == nil { + cn.bad = true + errorf("unexpected DataRow in simple query execution") + } + // the query didn't fail; kick off to Next + cn.saveMessage(t, r) + return + case 'T': + // res might be non-nil here if we received a previous + // CommandComplete, but that's fine; just overwrite it + res = &rows{cn: cn} + res.rowsHeader = parsePortalRowDescribe(r) + + // To work around a bug in QueryRow in Go 1.2 and earlier, wait + // until the first DataRow has been received. + default: + cn.bad = true + errorf("unknown response for simple query: %q", t) + } + } +} + +type noRows struct{} + +var emptyRows noRows + +var _ driver.Result = noRows{} + +func (noRows) LastInsertId() (int64, error) { + return 0, errNoLastInsertID +} + +func (noRows) RowsAffected() (int64, error) { + return 0, errNoRowsAffected +} + +// Decides which column formats to use for a prepared statement. The input is +// an array of type oids, one element per result column. +func decideColumnFormats(colTyps []fieldDesc, forceText bool) (colFmts []format, colFmtData []byte) { + if len(colTyps) == 0 { + return nil, colFmtDataAllText + } + + colFmts = make([]format, len(colTyps)) + if forceText { + return colFmts, colFmtDataAllText + } + + allBinary := true + allText := true + for i, t := range colTyps { + switch t.OID { + // This is the list of types to use binary mode for when receiving them + // through a prepared statement. If a type appears in this list, it + // must also be implemented in binaryDecode in encode.go. + case oid.T_bytea: + fallthrough + case oid.T_int8: + fallthrough + case oid.T_int4: + fallthrough + case oid.T_int2: + fallthrough + case oid.T_uuid: + colFmts[i] = formatBinary + allText = false + + default: + allBinary = false + } + } + + if allBinary { + return colFmts, colFmtDataAllBinary + } else if allText { + return colFmts, colFmtDataAllText + } else { + colFmtData = make([]byte, 2+len(colFmts)*2) + binary.BigEndian.PutUint16(colFmtData, uint16(len(colFmts))) + for i, v := range colFmts { + binary.BigEndian.PutUint16(colFmtData[2+i*2:], uint16(v)) + } + return colFmts, colFmtData + } +} + +func (cn *conn) prepareTo(q, stmtName string) *stmt { + st := &stmt{cn: cn, name: stmtName} + + b := cn.writeBuf('P') + b.string(st.name) + b.string(q) + b.int16(0) + + b.next('D') + b.byte('S') + b.string(st.name) + + b.next('S') + cn.send(b) + + cn.readParseResponse() + st.paramTyps, st.colNames, st.colTyps = cn.readStatementDescribeResponse() + st.colFmts, st.colFmtData = decideColumnFormats(st.colTyps, cn.disablePreparedBinaryResult) + cn.readReadyForQuery() + return st +} + +func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) { + if cn.bad { + return nil, driver.ErrBadConn + } + defer cn.errRecover(&err) + + if len(q) >= 4 && strings.EqualFold(q[:4], "COPY") { + s, err := cn.prepareCopyIn(q) + if err == nil { + cn.inCopy = true + } + return s, err + } + return cn.prepareTo(q, cn.gname()), nil +} + +func (cn *conn) Close() (err error) { + // Skip cn.bad return here because we always want to close a connection. + defer cn.errRecover(&err) + + // Ensure that cn.c.Close is always run. Since error handling is done with + // panics and cn.errRecover, the Close must be in a defer. + defer func() { + cerr := cn.c.Close() + if err == nil { + err = cerr + } + }() + + // Don't go through send(); ListenerConn relies on us not scribbling on the + // scratch buffer of this connection. + return cn.sendSimpleMessage('X') +} + +// Implement the "Queryer" interface +func (cn *conn) Query(query string, args []driver.Value) (driver.Rows, error) { + return cn.query(query, args) +} + +func (cn *conn) query(query string, args []driver.Value) (_ *rows, err error) { + if cn.bad { + return nil, driver.ErrBadConn + } + if cn.inCopy { + return nil, errCopyInProgress + } + defer cn.errRecover(&err) + + // Check to see if we can use the "simpleQuery" interface, which is + // *much* faster than going through prepare/exec + if len(args) == 0 { + return cn.simpleQuery(query) + } + + if cn.binaryParameters { + cn.sendBinaryModeQuery(query, args) + + cn.readParseResponse() + cn.readBindResponse() + rows := &rows{cn: cn} + rows.rowsHeader = cn.readPortalDescribeResponse() + cn.postExecuteWorkaround() + return rows, nil + } + st := cn.prepareTo(query, "") + st.exec(args) + return &rows{ + cn: cn, + rowsHeader: st.rowsHeader, + }, nil +} + +// Implement the optional "Execer" interface for one-shot queries +func (cn *conn) Exec(query string, args []driver.Value) (res driver.Result, err error) { + if cn.bad { + return nil, driver.ErrBadConn + } + defer cn.errRecover(&err) + + // Check to see if we can use the "simpleExec" interface, which is + // *much* faster than going through prepare/exec + if len(args) == 0 { + // ignore commandTag, our caller doesn't care + r, _, err := cn.simpleExec(query) + return r, err + } + + if cn.binaryParameters { + cn.sendBinaryModeQuery(query, args) + + cn.readParseResponse() + cn.readBindResponse() + cn.readPortalDescribeResponse() + cn.postExecuteWorkaround() + res, _, err = cn.readExecuteResponse("Execute") + return res, err + } + // Use the unnamed statement to defer planning until bind + // time, or else value-based selectivity estimates cannot be + // used. + st := cn.prepareTo(query, "") + r, err := st.Exec(args) + if err != nil { + panic(err) + } + return r, err +} + +func (cn *conn) send(m *writeBuf) { + _, err := cn.c.Write(m.wrap()) + if err != nil { + panic(err) + } +} + +func (cn *conn) sendStartupPacket(m *writeBuf) error { + _, err := cn.c.Write((m.wrap())[1:]) + return err +} + +// Send a message of type typ to the server on the other end of cn. The +// message should have no payload. This method does not use the scratch +// buffer. +func (cn *conn) sendSimpleMessage(typ byte) (err error) { + _, err = cn.c.Write([]byte{typ, '\x00', '\x00', '\x00', '\x04'}) + return err +} + +// saveMessage memorizes a message and its buffer in the conn struct. +// recvMessage will then return these values on the next call to it. This +// method is useful in cases where you have to see what the next message is +// going to be (e.g. to see whether it's an error or not) but you can't handle +// the message yourself. +func (cn *conn) saveMessage(typ byte, buf *readBuf) { + if cn.saveMessageType != 0 { + cn.bad = true + errorf("unexpected saveMessageType %d", cn.saveMessageType) + } + cn.saveMessageType = typ + cn.saveMessageBuffer = *buf +} + +// recvMessage receives any message from the backend, or returns an error if +// a problem occurred while reading the message. +func (cn *conn) recvMessage(r *readBuf) (byte, error) { + // workaround for a QueryRow bug, see exec + if cn.saveMessageType != 0 { + t := cn.saveMessageType + *r = cn.saveMessageBuffer + cn.saveMessageType = 0 + cn.saveMessageBuffer = nil + return t, nil + } + + x := cn.scratch[:5] + _, err := io.ReadFull(cn.buf, x) + if err != nil { + return 0, err + } + + // read the type and length of the message that follows + t := x[0] + n := int(binary.BigEndian.Uint32(x[1:])) - 4 + var y []byte + if n <= len(cn.scratch) { + y = cn.scratch[:n] + } else { + y = make([]byte, n) + } + _, err = io.ReadFull(cn.buf, y) + if err != nil { + return 0, err + } + *r = y + return t, nil +} + +// recv receives a message from the backend, but if an error happened while +// reading the message or the received message was an ErrorResponse, it panics. +// NoticeResponses are ignored. This function should generally be used only +// during the startup sequence. +func (cn *conn) recv() (t byte, r *readBuf) { + for { + var err error + r = &readBuf{} + t, err = cn.recvMessage(r) + if err != nil { + panic(err) + } + switch t { + case 'E': + panic(parseError(r)) + case 'N': + // ignore + default: + return + } + } +} + +// recv1Buf is exactly equivalent to recv1, except it uses a buffer supplied by +// the caller to avoid an allocation. +func (cn *conn) recv1Buf(r *readBuf) byte { + for { + t, err := cn.recvMessage(r) + if err != nil { + panic(err) + } + + switch t { + case 'A', 'N': + // ignore + case 'S': + cn.processParameterStatus(r) + default: + return t + } + } +} + +// recv1 receives a message from the backend, panicking if an error occurs +// while attempting to read it. All asynchronous messages are ignored, with +// the exception of ErrorResponse. +func (cn *conn) recv1() (t byte, r *readBuf) { + r = &readBuf{} + t = cn.recv1Buf(r) + return t, r +} + +func (cn *conn) ssl(o values) error { + upgrade, err := ssl(o) + if err != nil { + return err + } + + if upgrade == nil { + // Nothing to do + return nil + } + + w := cn.writeBuf(0) + w.int32(80877103) + if err = cn.sendStartupPacket(w); err != nil { + return err + } + + b := cn.scratch[:1] + _, err = io.ReadFull(cn.c, b) + if err != nil { + return err + } + + if b[0] != 'S' { + return ErrSSLNotSupported + } + + cn.c, err = upgrade(cn.c) + return err +} + +// isDriverSetting returns true iff a setting is purely for configuring the +// driver's options and should not be sent to the server in the connection +// startup packet. +func isDriverSetting(key string) bool { + switch key { + case "host", "port": + return true + case "password": + return true + case "sslmode", "sslcert", "sslkey", "sslrootcert": + return true + case "fallback_application_name": + return true + case "connect_timeout": + return true + case "disable_prepared_binary_result": + return true + case "binary_parameters": + return true + + default: + return false + } +} + +func (cn *conn) startup(o values) { + w := cn.writeBuf(0) + w.int32(196608) + // Send the backend the name of the database we want to connect to, and the + // user we want to connect as. Additionally, we send over any run-time + // parameters potentially included in the connection string. If the server + // doesn't recognize any of them, it will reply with an error. + for k, v := range o { + if isDriverSetting(k) { + // skip options which can't be run-time parameters + continue + } + // The protocol requires us to supply the database name as "database" + // instead of "dbname". + if k == "dbname" { + k = "database" + } + w.string(k) + w.string(v) + } + w.string("") + if err := cn.sendStartupPacket(w); err != nil { + panic(err) + } + + for { + t, r := cn.recv() + switch t { + case 'K': + cn.processBackendKeyData(r) + case 'S': + cn.processParameterStatus(r) + case 'R': + cn.auth(r, o) + case 'Z': + cn.processReadyForQuery(r) + return + default: + errorf("unknown response for startup: %q", t) + } + } +} + +func (cn *conn) auth(r *readBuf, o values) { + switch code := r.int32(); code { + case 0: + // OK + case 3: + w := cn.writeBuf('p') + w.string(o["password"]) + cn.send(w) + + t, r := cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 0 { + errorf("unexpected authentication response: %q", t) + } + case 5: + s := string(r.next(4)) + w := cn.writeBuf('p') + w.string("md5" + md5s(md5s(o["password"]+o["user"])+s)) + cn.send(w) + + t, r := cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 0 { + errorf("unexpected authentication response: %q", t) + } + case 10: + sc := scram.NewClient(sha256.New, o["user"], o["password"]) + sc.Step(nil) + if sc.Err() != nil { + errorf("SCRAM-SHA-256 error: %s", sc.Err().Error()) + } + scOut := sc.Out() + + w := cn.writeBuf('p') + w.string("SCRAM-SHA-256") + w.int32(len(scOut)) + w.bytes(scOut) + cn.send(w) + + t, r := cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 11 { + errorf("unexpected authentication response: %q", t) + } + + nextStep := r.next(len(*r)) + sc.Step(nextStep) + if sc.Err() != nil { + errorf("SCRAM-SHA-256 error: %s", sc.Err().Error()) + } + + scOut = sc.Out() + w = cn.writeBuf('p') + w.bytes(scOut) + cn.send(w) + + t, r = cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 12 { + errorf("unexpected authentication response: %q", t) + } + + nextStep = r.next(len(*r)) + sc.Step(nextStep) + if sc.Err() != nil { + errorf("SCRAM-SHA-256 error: %s", sc.Err().Error()) + } + + default: + errorf("unknown authentication response: %d", code) + } +} + +type format int + +const formatText format = 0 +const formatBinary format = 1 + +// One result-column format code with the value 1 (i.e. all binary). +var colFmtDataAllBinary = []byte{0, 1, 0, 1} + +// No result-column format codes (i.e. all text). +var colFmtDataAllText = []byte{0, 0} + +type stmt struct { + cn *conn + name string + rowsHeader + colFmtData []byte + paramTyps []oid.Oid + closed bool +} + +func (st *stmt) Close() (err error) { + if st.closed { + return nil + } + if st.cn.bad { + return driver.ErrBadConn + } + defer st.cn.errRecover(&err) + + w := st.cn.writeBuf('C') + w.byte('S') + w.string(st.name) + st.cn.send(w) + + st.cn.send(st.cn.writeBuf('S')) + + t, _ := st.cn.recv1() + if t != '3' { + st.cn.bad = true + errorf("unexpected close response: %q", t) + } + st.closed = true + + t, r := st.cn.recv1() + if t != 'Z' { + st.cn.bad = true + errorf("expected ready for query, but got: %q", t) + } + st.cn.processReadyForQuery(r) + + return nil +} + +func (st *stmt) Query(v []driver.Value) (r driver.Rows, err error) { + if st.cn.bad { + return nil, driver.ErrBadConn + } + defer st.cn.errRecover(&err) + + st.exec(v) + return &rows{ + cn: st.cn, + rowsHeader: st.rowsHeader, + }, nil +} + +func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) { + if st.cn.bad { + return nil, driver.ErrBadConn + } + defer st.cn.errRecover(&err) + + st.exec(v) + res, _, err = st.cn.readExecuteResponse("simple query") + return res, err +} + +func (st *stmt) exec(v []driver.Value) { + if len(v) >= 65536 { + errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(v)) + } + if len(v) != len(st.paramTyps) { + errorf("got %d parameters but the statement requires %d", len(v), len(st.paramTyps)) + } + + cn := st.cn + w := cn.writeBuf('B') + w.byte(0) // unnamed portal + w.string(st.name) + + if cn.binaryParameters { + cn.sendBinaryParameters(w, v) + } else { + w.int16(0) + w.int16(len(v)) + for i, x := range v { + if x == nil { + w.int32(-1) + } else { + b := encode(&cn.parameterStatus, x, st.paramTyps[i]) + w.int32(len(b)) + w.bytes(b) + } + } + } + w.bytes(st.colFmtData) + + w.next('E') + w.byte(0) + w.int32(0) + + w.next('S') + cn.send(w) + + cn.readBindResponse() + cn.postExecuteWorkaround() + +} + +func (st *stmt) NumInput() int { + return len(st.paramTyps) +} + +// parseComplete parses the "command tag" from a CommandComplete message, and +// returns the number of rows affected (if applicable) and a string +// identifying only the command that was executed, e.g. "ALTER TABLE". If the +// command tag could not be parsed, parseComplete panics. +func (cn *conn) parseComplete(commandTag string) (driver.Result, string) { + commandsWithAffectedRows := []string{ + "SELECT ", + // INSERT is handled below + "UPDATE ", + "DELETE ", + "FETCH ", + "MOVE ", + "COPY ", + } + + var affectedRows *string + for _, tag := range commandsWithAffectedRows { + if strings.HasPrefix(commandTag, tag) { + t := commandTag[len(tag):] + affectedRows = &t + commandTag = tag[:len(tag)-1] + break + } + } + // INSERT also includes the oid of the inserted row in its command tag. + // Oids in user tables are deprecated, and the oid is only returned when + // exactly one row is inserted, so it's unlikely to be of value to any + // real-world application and we can ignore it. + if affectedRows == nil && strings.HasPrefix(commandTag, "INSERT ") { + parts := strings.Split(commandTag, " ") + if len(parts) != 3 { + cn.bad = true + errorf("unexpected INSERT command tag %s", commandTag) + } + affectedRows = &parts[len(parts)-1] + commandTag = "INSERT" + } + // There should be no affected rows attached to the tag, just return it + if affectedRows == nil { + return driver.RowsAffected(0), commandTag + } + n, err := strconv.ParseInt(*affectedRows, 10, 64) + if err != nil { + cn.bad = true + errorf("could not parse commandTag: %s", err) + } + return driver.RowsAffected(n), commandTag +} + +type rowsHeader struct { + colNames []string + colTyps []fieldDesc + colFmts []format +} + +type rows struct { + cn *conn + finish func() + rowsHeader + done bool + rb readBuf + result driver.Result + tag string + + next *rowsHeader +} + +func (rs *rows) Close() error { + if finish := rs.finish; finish != nil { + defer finish() + } + // no need to look at cn.bad as Next() will + for { + err := rs.Next(nil) + switch err { + case nil: + case io.EOF: + // rs.Next can return io.EOF on both 'Z' (ready for query) and 'T' (row + // description, used with HasNextResultSet). We need to fetch messages until + // we hit a 'Z', which is done by waiting for done to be set. + if rs.done { + return nil + } + default: + return err + } + } +} + +func (rs *rows) Columns() []string { + return rs.colNames +} + +func (rs *rows) Result() driver.Result { + if rs.result == nil { + return emptyRows + } + return rs.result +} + +func (rs *rows) Tag() string { + return rs.tag +} + +func (rs *rows) Next(dest []driver.Value) (err error) { + if rs.done { + return io.EOF + } + + conn := rs.cn + if conn.bad { + return driver.ErrBadConn + } + defer conn.errRecover(&err) + + for { + t := conn.recv1Buf(&rs.rb) + switch t { + case 'E': + err = parseError(&rs.rb) + case 'C', 'I': + if t == 'C' { + rs.result, rs.tag = conn.parseComplete(rs.rb.string()) + } + continue + case 'Z': + conn.processReadyForQuery(&rs.rb) + rs.done = true + if err != nil { + return err + } + return io.EOF + case 'D': + n := rs.rb.int16() + if err != nil { + conn.bad = true + errorf("unexpected DataRow after error %s", err) + } + if n < len(dest) { + dest = dest[:n] + } + for i := range dest { + l := rs.rb.int32() + if l == -1 { + dest[i] = nil + continue + } + dest[i] = decode(&conn.parameterStatus, rs.rb.next(l), rs.colTyps[i].OID, rs.colFmts[i]) + } + return + case 'T': + next := parsePortalRowDescribe(&rs.rb) + rs.next = &next + return io.EOF + default: + errorf("unexpected message after execute: %q", t) + } + } +} + +func (rs *rows) HasNextResultSet() bool { + hasNext := rs.next != nil && !rs.done + return hasNext +} + +func (rs *rows) NextResultSet() error { + if rs.next == nil { + return io.EOF + } + rs.rowsHeader = *rs.next + rs.next = nil + return nil +} + +// QuoteIdentifier quotes an "identifier" (e.g. a table or a column name) to be +// used as part of an SQL statement. For example: +// +// tblname := "my_table" +// data := "my_data" +// quoted := pq.QuoteIdentifier(tblname) +// err := db.Exec(fmt.Sprintf("INSERT INTO %s VALUES ($1)", quoted), data) +// +// Any double quotes in name will be escaped. The quoted identifier will be +// case sensitive when used in a query. If the input string contains a zero +// byte, the result will be truncated immediately before it. +func QuoteIdentifier(name string) string { + end := strings.IndexRune(name, 0) + if end > -1 { + name = name[:end] + } + return `"` + strings.Replace(name, `"`, `""`, -1) + `"` +} + +// QuoteLiteral quotes a 'literal' (e.g. a parameter, often used to pass literal +// to DDL and other statements that do not accept parameters) to be used as part +// of an SQL statement. For example: +// +// exp_date := pq.QuoteLiteral("2023-01-05 15:00:00Z") +// err := db.Exec(fmt.Sprintf("CREATE ROLE my_user VALID UNTIL %s", exp_date)) +// +// Any single quotes in name will be escaped. Any backslashes (i.e. "\") will be +// replaced by two backslashes (i.e. "\\") and the C-style escape identifier +// that PostgreSQL provides ('E') will be prepended to the string. +func QuoteLiteral(literal string) string { + // This follows the PostgreSQL internal algorithm for handling quoted literals + // from libpq, which can be found in the "PQEscapeStringInternal" function, + // which is found in the libpq/fe-exec.c source file: + // https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/interfaces/libpq/fe-exec.c + // + // substitute any single-quotes (') with two single-quotes ('') + literal = strings.Replace(literal, `'`, `''`, -1) + // determine if the string has any backslashes (\) in it. + // if it does, replace any backslashes (\) with two backslashes (\\) + // then, we need to wrap the entire string with a PostgreSQL + // C-style escape. Per how "PQEscapeStringInternal" handles this case, we + // also add a space before the "E" + if strings.Contains(literal, `\`) { + literal = strings.Replace(literal, `\`, `\\`, -1) + literal = ` E'` + literal + `'` + } else { + // otherwise, we can just wrap the literal with a pair of single quotes + literal = `'` + literal + `'` + } + return literal +} + +func md5s(s string) string { + h := md5.New() + h.Write([]byte(s)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func (cn *conn) sendBinaryParameters(b *writeBuf, args []driver.Value) { + // Do one pass over the parameters to see if we're going to send any of + // them over in binary. If we are, create a paramFormats array at the + // same time. + var paramFormats []int + for i, x := range args { + _, ok := x.([]byte) + if ok { + if paramFormats == nil { + paramFormats = make([]int, len(args)) + } + paramFormats[i] = 1 + } + } + if paramFormats == nil { + b.int16(0) + } else { + b.int16(len(paramFormats)) + for _, x := range paramFormats { + b.int16(x) + } + } + + b.int16(len(args)) + for _, x := range args { + if x == nil { + b.int32(-1) + } else { + datum := binaryEncode(&cn.parameterStatus, x) + b.int32(len(datum)) + b.bytes(datum) + } + } +} + +func (cn *conn) sendBinaryModeQuery(query string, args []driver.Value) { + if len(args) >= 65536 { + errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(args)) + } + + b := cn.writeBuf('P') + b.byte(0) // unnamed statement + b.string(query) + b.int16(0) + + b.next('B') + b.int16(0) // unnamed portal and statement + cn.sendBinaryParameters(b, args) + b.bytes(colFmtDataAllText) + + b.next('D') + b.byte('P') + b.byte(0) // unnamed portal + + b.next('E') + b.byte(0) + b.int32(0) + + b.next('S') + cn.send(b) +} + +func (cn *conn) processParameterStatus(r *readBuf) { + var err error + + param := r.string() + switch param { + case "server_version": + var major1 int + var major2 int + var minor int + _, err = fmt.Sscanf(r.string(), "%d.%d.%d", &major1, &major2, &minor) + if err == nil { + cn.parameterStatus.serverVersion = major1*10000 + major2*100 + minor + } + + case "TimeZone": + cn.parameterStatus.currentLocation, err = time.LoadLocation(r.string()) + if err != nil { + cn.parameterStatus.currentLocation = nil + } + + default: + // ignore + } +} + +func (cn *conn) processReadyForQuery(r *readBuf) { + cn.txnStatus = transactionStatus(r.byte()) +} + +func (cn *conn) readReadyForQuery() { + t, r := cn.recv1() + switch t { + case 'Z': + cn.processReadyForQuery(r) + return + default: + cn.bad = true + errorf("unexpected message %q; expected ReadyForQuery", t) + } +} + +func (cn *conn) processBackendKeyData(r *readBuf) { + cn.processID = r.int32() + cn.secretKey = r.int32() +} + +func (cn *conn) readParseResponse() { + t, r := cn.recv1() + switch t { + case '1': + return + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Parse response %q", t) + } +} + +func (cn *conn) readStatementDescribeResponse() (paramTyps []oid.Oid, colNames []string, colTyps []fieldDesc) { + for { + t, r := cn.recv1() + switch t { + case 't': + nparams := r.int16() + paramTyps = make([]oid.Oid, nparams) + for i := range paramTyps { + paramTyps[i] = r.oid() + } + case 'n': + return paramTyps, nil, nil + case 'T': + colNames, colTyps = parseStatementRowDescribe(r) + return paramTyps, colNames, colTyps + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Describe statement response %q", t) + } + } +} + +func (cn *conn) readPortalDescribeResponse() rowsHeader { + t, r := cn.recv1() + switch t { + case 'T': + return parsePortalRowDescribe(r) + case 'n': + return rowsHeader{} + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Describe response %q", t) + } + panic("not reached") +} + +func (cn *conn) readBindResponse() { + t, r := cn.recv1() + switch t { + case '2': + return + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Bind response %q", t) + } +} + +func (cn *conn) postExecuteWorkaround() { + // Work around a bug in sql.DB.QueryRow: in Go 1.2 and earlier it ignores + // any errors from rows.Next, which masks errors that happened during the + // execution of the query. To avoid the problem in common cases, we wait + // here for one more message from the database. If it's not an error the + // query will likely succeed (or perhaps has already, if it's a + // CommandComplete), so we push the message into the conn struct; recv1 + // will return it as the next message for rows.Next or rows.Close. + // However, if it's an error, we wait until ReadyForQuery and then return + // the error to our caller. + for { + t, r := cn.recv1() + switch t { + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + case 'C', 'D', 'I': + // the query didn't fail, but we can't process this message + cn.saveMessage(t, r) + return + default: + cn.bad = true + errorf("unexpected message during extended query execution: %q", t) + } + } +} + +// Only for Exec(), since we ignore the returned data +func (cn *conn) readExecuteResponse(protocolState string) (res driver.Result, commandTag string, err error) { + for { + t, r := cn.recv1() + switch t { + case 'C': + if err != nil { + cn.bad = true + errorf("unexpected CommandComplete after error %s", err) + } + res, commandTag = cn.parseComplete(r.string()) + case 'Z': + cn.processReadyForQuery(r) + if res == nil && err == nil { + err = errUnexpectedReady + } + return res, commandTag, err + case 'E': + err = parseError(r) + case 'T', 'D', 'I': + if err != nil { + cn.bad = true + errorf("unexpected %q after error %s", t, err) + } + if t == 'I' { + res = emptyRows + } + // ignore any results + default: + cn.bad = true + errorf("unknown %s response: %q", protocolState, t) + } + } +} + +func parseStatementRowDescribe(r *readBuf) (colNames []string, colTyps []fieldDesc) { + n := r.int16() + colNames = make([]string, n) + colTyps = make([]fieldDesc, n) + for i := range colNames { + colNames[i] = r.string() + r.next(6) + colTyps[i].OID = r.oid() + colTyps[i].Len = r.int16() + colTyps[i].Mod = r.int32() + // format code not known when describing a statement; always 0 + r.next(2) + } + return +} + +func parsePortalRowDescribe(r *readBuf) rowsHeader { + n := r.int16() + colNames := make([]string, n) + colFmts := make([]format, n) + colTyps := make([]fieldDesc, n) + for i := range colNames { + colNames[i] = r.string() + r.next(6) + colTyps[i].OID = r.oid() + colTyps[i].Len = r.int16() + colTyps[i].Mod = r.int32() + colFmts[i] = format(r.int16()) + } + return rowsHeader{ + colNames: colNames, + colFmts: colFmts, + colTyps: colTyps, + } +} + +// parseEnviron tries to mimic some of libpq's environment handling +// +// To ease testing, it does not directly reference os.Environ, but is +// designed to accept its output. +// +// Environment-set connection information is intended to have a higher +// precedence than a library default but lower than any explicitly +// passed information (such as in the URL or connection string). +func parseEnviron(env []string) (out map[string]string) { + out = make(map[string]string) + + for _, v := range env { + parts := strings.SplitN(v, "=", 2) + + accrue := func(keyname string) { + out[keyname] = parts[1] + } + unsupported := func() { + panic(fmt.Sprintf("setting %v not supported", parts[0])) + } + + // The order of these is the same as is seen in the + // PostgreSQL 9.1 manual. Unsupported but well-defined + // keys cause a panic; these should be unset prior to + // execution. Options which pq expects to be set to a + // certain value are allowed, but must be set to that + // value if present (they can, of course, be absent). + switch parts[0] { + case "PGHOST": + accrue("host") + case "PGHOSTADDR": + unsupported() + case "PGPORT": + accrue("port") + case "PGDATABASE": + accrue("dbname") + case "PGUSER": + accrue("user") + case "PGPASSWORD": + accrue("password") + case "PGSERVICE", "PGSERVICEFILE", "PGREALM": + unsupported() + case "PGOPTIONS": + accrue("options") + case "PGAPPNAME": + accrue("application_name") + case "PGSSLMODE": + accrue("sslmode") + case "PGSSLCERT": + accrue("sslcert") + case "PGSSLKEY": + accrue("sslkey") + case "PGSSLROOTCERT": + accrue("sslrootcert") + case "PGREQUIRESSL", "PGSSLCRL": + unsupported() + case "PGREQUIREPEER": + unsupported() + case "PGKRBSRVNAME", "PGGSSLIB": + unsupported() + case "PGCONNECT_TIMEOUT": + accrue("connect_timeout") + case "PGCLIENTENCODING": + accrue("client_encoding") + case "PGDATESTYLE": + accrue("datestyle") + case "PGTZ": + accrue("timezone") + case "PGGEQO": + accrue("geqo") + case "PGSYSCONFDIR", "PGLOCALEDIR": + unsupported() + } + } + + return out +} + +// isUTF8 returns whether name is a fuzzy variation of the string "UTF-8". +func isUTF8(name string) bool { + // Recognize all sorts of silly things as "UTF-8", like Postgres does + s := strings.Map(alnumLowerASCII, name) + return s == "utf8" || s == "unicode" +} + +func alnumLowerASCII(ch rune) rune { + if 'A' <= ch && ch <= 'Z' { + return ch + ('a' - 'A') + } + if 'a' <= ch && ch <= 'z' || '0' <= ch && ch <= '9' { + return ch + } + return -1 // discard +} diff --git a/vendor/github.com/lib/pq/conn_go18.go b/vendor/github.com/lib/pq/conn_go18.go new file mode 100644 index 00000000000..09e2ea4648e --- /dev/null +++ b/vendor/github.com/lib/pq/conn_go18.go @@ -0,0 +1,149 @@ +package pq + +import ( + "context" + "database/sql" + "database/sql/driver" + "fmt" + "io" + "io/ioutil" + "time" +) + +// Implement the "QueryerContext" interface +func (cn *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { + list := make([]driver.Value, len(args)) + for i, nv := range args { + list[i] = nv.Value + } + finish := cn.watchCancel(ctx) + r, err := cn.query(query, list) + if err != nil { + if finish != nil { + finish() + } + return nil, err + } + r.finish = finish + return r, nil +} + +// Implement the "ExecerContext" interface +func (cn *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { + list := make([]driver.Value, len(args)) + for i, nv := range args { + list[i] = nv.Value + } + + if finish := cn.watchCancel(ctx); finish != nil { + defer finish() + } + + return cn.Exec(query, list) +} + +// Implement the "ConnBeginTx" interface +func (cn *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { + var mode string + + switch sql.IsolationLevel(opts.Isolation) { + case sql.LevelDefault: + // Don't touch mode: use the server's default + case sql.LevelReadUncommitted: + mode = " ISOLATION LEVEL READ UNCOMMITTED" + case sql.LevelReadCommitted: + mode = " ISOLATION LEVEL READ COMMITTED" + case sql.LevelRepeatableRead: + mode = " ISOLATION LEVEL REPEATABLE READ" + case sql.LevelSerializable: + mode = " ISOLATION LEVEL SERIALIZABLE" + default: + return nil, fmt.Errorf("pq: isolation level not supported: %d", opts.Isolation) + } + + if opts.ReadOnly { + mode += " READ ONLY" + } else { + mode += " READ WRITE" + } + + tx, err := cn.begin(mode) + if err != nil { + return nil, err + } + cn.txnFinish = cn.watchCancel(ctx) + return tx, nil +} + +func (cn *conn) Ping(ctx context.Context) error { + if finish := cn.watchCancel(ctx); finish != nil { + defer finish() + } + rows, err := cn.simpleQuery(";") + if err != nil { + return driver.ErrBadConn // https://golang.org/pkg/database/sql/driver/#Pinger + } + rows.Close() + return nil +} + +func (cn *conn) watchCancel(ctx context.Context) func() { + if done := ctx.Done(); done != nil { + finished := make(chan struct{}) + go func() { + select { + case <-done: + // At this point the function level context is canceled, + // so it must not be used for the additional network + // request to cancel the query. + // Create a new context to pass into the dial. + ctxCancel, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + _ = cn.cancel(ctxCancel) + finished <- struct{}{} + case <-finished: + } + }() + return func() { + select { + case <-finished: + case finished <- struct{}{}: + } + } + } + return nil +} + +func (cn *conn) cancel(ctx context.Context) error { + c, err := dial(ctx, cn.dialer, cn.opts) + if err != nil { + return err + } + defer c.Close() + + { + can := conn{ + c: c, + } + err = can.ssl(cn.opts) + if err != nil { + return err + } + + w := can.writeBuf(0) + w.int32(80877102) // cancel request code + w.int32(cn.processID) + w.int32(cn.secretKey) + + if err := can.sendStartupPacket(w); err != nil { + return err + } + } + + // Read until EOF to ensure that the server received the cancel. + { + _, err := io.Copy(ioutil.Discard, c) + return err + } +} diff --git a/vendor/github.com/lib/pq/connector.go b/vendor/github.com/lib/pq/connector.go new file mode 100644 index 00000000000..2f8ced6737d --- /dev/null +++ b/vendor/github.com/lib/pq/connector.go @@ -0,0 +1,110 @@ +package pq + +import ( + "context" + "database/sql/driver" + "errors" + "fmt" + "os" + "strings" +) + +// Connector represents a fixed configuration for the pq driver with a given +// name. Connector satisfies the database/sql/driver Connector interface and +// can be used to create any number of DB Conn's via the database/sql OpenDB +// function. +// +// See https://golang.org/pkg/database/sql/driver/#Connector. +// See https://golang.org/pkg/database/sql/#OpenDB. +type Connector struct { + opts values + dialer Dialer +} + +// Connect returns a connection to the database using the fixed configuration +// of this Connector. Context is not used. +func (c *Connector) Connect(ctx context.Context) (driver.Conn, error) { + return c.open(ctx) +} + +// Driver returnst the underlying driver of this Connector. +func (c *Connector) Driver() driver.Driver { + return &Driver{} +} + +// NewConnector returns a connector for the pq driver in a fixed configuration +// with the given dsn. The returned connector can be used to create any number +// of equivalent Conn's. The returned connector is intended to be used with +// database/sql.OpenDB. +// +// See https://golang.org/pkg/database/sql/driver/#Connector. +// See https://golang.org/pkg/database/sql/#OpenDB. +func NewConnector(dsn string) (*Connector, error) { + var err error + o := make(values) + + // A number of defaults are applied here, in this order: + // + // * Very low precedence defaults applied in every situation + // * Environment variables + // * Explicitly passed connection information + o["host"] = "localhost" + o["port"] = "5432" + // N.B.: Extra float digits should be set to 3, but that breaks + // Postgres 8.4 and older, where the max is 2. + o["extra_float_digits"] = "2" + for k, v := range parseEnviron(os.Environ()) { + o[k] = v + } + + if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { + dsn, err = ParseURL(dsn) + if err != nil { + return nil, err + } + } + + if err := parseOpts(dsn, o); err != nil { + return nil, err + } + + // Use the "fallback" application name if necessary + if fallback, ok := o["fallback_application_name"]; ok { + if _, ok := o["application_name"]; !ok { + o["application_name"] = fallback + } + } + + // We can't work with any client_encoding other than UTF-8 currently. + // However, we have historically allowed the user to set it to UTF-8 + // explicitly, and there's no reason to break such programs, so allow that. + // Note that the "options" setting could also set client_encoding, but + // parsing its value is not worth it. Instead, we always explicitly send + // client_encoding as a separate run-time parameter, which should override + // anything set in options. + if enc, ok := o["client_encoding"]; ok && !isUTF8(enc) { + return nil, errors.New("client_encoding must be absent or 'UTF8'") + } + o["client_encoding"] = "UTF8" + // DateStyle needs a similar treatment. + if datestyle, ok := o["datestyle"]; ok { + if datestyle != "ISO, MDY" { + return nil, fmt.Errorf("setting datestyle must be absent or %v; got %v", "ISO, MDY", datestyle) + } + } else { + o["datestyle"] = "ISO, MDY" + } + + // If a user is not provided by any other means, the last + // resort is to use the current operating system provided user + // name. + if _, ok := o["user"]; !ok { + u, err := userCurrent() + if err != nil { + return nil, err + } + o["user"] = u + } + + return &Connector{opts: o, dialer: defaultDialer{}}, nil +} diff --git a/vendor/github.com/lib/pq/copy.go b/vendor/github.com/lib/pq/copy.go new file mode 100644 index 00000000000..55378d5b119 --- /dev/null +++ b/vendor/github.com/lib/pq/copy.go @@ -0,0 +1,282 @@ +package pq + +import ( + "database/sql/driver" + "encoding/binary" + "errors" + "fmt" + "sync" +) + +var ( + errCopyInClosed = errors.New("pq: copyin statement has already been closed") + errBinaryCopyNotSupported = errors.New("pq: only text format supported for COPY") + errCopyToNotSupported = errors.New("pq: COPY TO is not supported") + errCopyNotSupportedOutsideTxn = errors.New("pq: COPY is only allowed inside a transaction") + errCopyInProgress = errors.New("pq: COPY in progress") +) + +// CopyIn creates a COPY FROM statement which can be prepared with +// Tx.Prepare(). The target table should be visible in search_path. +func CopyIn(table string, columns ...string) string { + stmt := "COPY " + QuoteIdentifier(table) + " (" + for i, col := range columns { + if i != 0 { + stmt += ", " + } + stmt += QuoteIdentifier(col) + } + stmt += ") FROM STDIN" + return stmt +} + +// CopyInSchema creates a COPY FROM statement which can be prepared with +// Tx.Prepare(). +func CopyInSchema(schema, table string, columns ...string) string { + stmt := "COPY " + QuoteIdentifier(schema) + "." + QuoteIdentifier(table) + " (" + for i, col := range columns { + if i != 0 { + stmt += ", " + } + stmt += QuoteIdentifier(col) + } + stmt += ") FROM STDIN" + return stmt +} + +type copyin struct { + cn *conn + buffer []byte + rowData chan []byte + done chan bool + + closed bool + + sync.Mutex // guards err + err error +} + +const ciBufferSize = 64 * 1024 + +// flush buffer before the buffer is filled up and needs reallocation +const ciBufferFlushSize = 63 * 1024 + +func (cn *conn) prepareCopyIn(q string) (_ driver.Stmt, err error) { + if !cn.isInTransaction() { + return nil, errCopyNotSupportedOutsideTxn + } + + ci := ©in{ + cn: cn, + buffer: make([]byte, 0, ciBufferSize), + rowData: make(chan []byte), + done: make(chan bool, 1), + } + // add CopyData identifier + 4 bytes for message length + ci.buffer = append(ci.buffer, 'd', 0, 0, 0, 0) + + b := cn.writeBuf('Q') + b.string(q) + cn.send(b) + +awaitCopyInResponse: + for { + t, r := cn.recv1() + switch t { + case 'G': + if r.byte() != 0 { + err = errBinaryCopyNotSupported + break awaitCopyInResponse + } + go ci.resploop() + return ci, nil + case 'H': + err = errCopyToNotSupported + break awaitCopyInResponse + case 'E': + err = parseError(r) + case 'Z': + if err == nil { + ci.setBad() + errorf("unexpected ReadyForQuery in response to COPY") + } + cn.processReadyForQuery(r) + return nil, err + default: + ci.setBad() + errorf("unknown response for copy query: %q", t) + } + } + + // something went wrong, abort COPY before we return + b = cn.writeBuf('f') + b.string(err.Error()) + cn.send(b) + + for { + t, r := cn.recv1() + switch t { + case 'c', 'C', 'E': + case 'Z': + // correctly aborted, we're done + cn.processReadyForQuery(r) + return nil, err + default: + ci.setBad() + errorf("unknown response for CopyFail: %q", t) + } + } +} + +func (ci *copyin) flush(buf []byte) { + // set message length (without message identifier) + binary.BigEndian.PutUint32(buf[1:], uint32(len(buf)-1)) + + _, err := ci.cn.c.Write(buf) + if err != nil { + panic(err) + } +} + +func (ci *copyin) resploop() { + for { + var r readBuf + t, err := ci.cn.recvMessage(&r) + if err != nil { + ci.setBad() + ci.setError(err) + ci.done <- true + return + } + switch t { + case 'C': + // complete + case 'N': + // NoticeResponse + case 'Z': + ci.cn.processReadyForQuery(&r) + ci.done <- true + return + case 'E': + err := parseError(&r) + ci.setError(err) + default: + ci.setBad() + ci.setError(fmt.Errorf("unknown response during CopyIn: %q", t)) + ci.done <- true + return + } + } +} + +func (ci *copyin) setBad() { + ci.Lock() + ci.cn.bad = true + ci.Unlock() +} + +func (ci *copyin) isBad() bool { + ci.Lock() + b := ci.cn.bad + ci.Unlock() + return b +} + +func (ci *copyin) isErrorSet() bool { + ci.Lock() + isSet := (ci.err != nil) + ci.Unlock() + return isSet +} + +// setError() sets ci.err if one has not been set already. Caller must not be +// holding ci.Mutex. +func (ci *copyin) setError(err error) { + ci.Lock() + if ci.err == nil { + ci.err = err + } + ci.Unlock() +} + +func (ci *copyin) NumInput() int { + return -1 +} + +func (ci *copyin) Query(v []driver.Value) (r driver.Rows, err error) { + return nil, ErrNotSupported +} + +// Exec inserts values into the COPY stream. The insert is asynchronous +// and Exec can return errors from previous Exec calls to the same +// COPY stmt. +// +// You need to call Exec(nil) to sync the COPY stream and to get any +// errors from pending data, since Stmt.Close() doesn't return errors +// to the user. +func (ci *copyin) Exec(v []driver.Value) (r driver.Result, err error) { + if ci.closed { + return nil, errCopyInClosed + } + + if ci.isBad() { + return nil, driver.ErrBadConn + } + defer ci.cn.errRecover(&err) + + if ci.isErrorSet() { + return nil, ci.err + } + + if len(v) == 0 { + return driver.RowsAffected(0), ci.Close() + } + + numValues := len(v) + for i, value := range v { + ci.buffer = appendEncodedText(&ci.cn.parameterStatus, ci.buffer, value) + if i < numValues-1 { + ci.buffer = append(ci.buffer, '\t') + } + } + + ci.buffer = append(ci.buffer, '\n') + + if len(ci.buffer) > ciBufferFlushSize { + ci.flush(ci.buffer) + // reset buffer, keep bytes for message identifier and length + ci.buffer = ci.buffer[:5] + } + + return driver.RowsAffected(0), nil +} + +func (ci *copyin) Close() (err error) { + if ci.closed { // Don't do anything, we're already closed + return nil + } + ci.closed = true + + if ci.isBad() { + return driver.ErrBadConn + } + defer ci.cn.errRecover(&err) + + if len(ci.buffer) > 0 { + ci.flush(ci.buffer) + } + // Avoid touching the scratch buffer as resploop could be using it. + err = ci.cn.sendSimpleMessage('c') + if err != nil { + return err + } + + <-ci.done + ci.cn.inCopy = false + + if ci.isErrorSet() { + err = ci.err + return err + } + return nil +} diff --git a/vendor/github.com/lib/pq/doc.go b/vendor/github.com/lib/pq/doc.go new file mode 100644 index 00000000000..2a60054e2e0 --- /dev/null +++ b/vendor/github.com/lib/pq/doc.go @@ -0,0 +1,245 @@ +/* +Package pq is a pure Go Postgres driver for the database/sql package. + +In most cases clients will use the database/sql package instead of +using this package directly. For example: + + import ( + "database/sql" + + _ "github.com/lib/pq" + ) + + func main() { + connStr := "user=pqgotest dbname=pqgotest sslmode=verify-full" + db, err := sql.Open("postgres", connStr) + if err != nil { + log.Fatal(err) + } + + age := 21 + rows, err := db.Query("SELECT name FROM users WHERE age = $1", age) + … + } + +You can also connect to a database using a URL. For example: + + connStr := "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full" + db, err := sql.Open("postgres", connStr) + + +Connection String Parameters + + +Similarly to libpq, when establishing a connection using pq you are expected to +supply a connection string containing zero or more parameters. +A subset of the connection parameters supported by libpq are also supported by pq. +Additionally, pq also lets you specify run-time parameters (such as search_path or work_mem) +directly in the connection string. This is different from libpq, which does not allow +run-time parameters in the connection string, instead requiring you to supply +them in the options parameter. + +For compatibility with libpq, the following special connection parameters are +supported: + + * dbname - The name of the database to connect to + * user - The user to sign in as + * password - The user's password + * host - The host to connect to. Values that start with / are for unix + domain sockets. (default is localhost) + * port - The port to bind to. (default is 5432) + * sslmode - Whether or not to use SSL (default is require, this is not + the default for libpq) + * fallback_application_name - An application_name to fall back to if one isn't provided. + * connect_timeout - Maximum wait for connection, in seconds. Zero or + not specified means wait indefinitely. + * sslcert - Cert file location. The file must contain PEM encoded data. + * sslkey - Key file location. The file must contain PEM encoded data. + * sslrootcert - The location of the root certificate file. The file + must contain PEM encoded data. + +Valid values for sslmode are: + + * disable - No SSL + * require - Always SSL (skip verification) + * verify-ca - Always SSL (verify that the certificate presented by the + server was signed by a trusted CA) + * verify-full - Always SSL (verify that the certification presented by + the server was signed by a trusted CA and the server host name + matches the one in the certificate) + +See http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING +for more information about connection string parameters. + +Use single quotes for values that contain whitespace: + + "user=pqgotest password='with spaces'" + +A backslash will escape the next character in values: + + "user=space\ man password='it\'s valid'" + +Note that the connection parameter client_encoding (which sets the +text encoding for the connection) may be set but must be "UTF8", +matching with the same rules as Postgres. It is an error to provide +any other value. + +In addition to the parameters listed above, any run-time parameter that can be +set at backend start time can be set in the connection string. For more +information, see +http://www.postgresql.org/docs/current/static/runtime-config.html. + +Most environment variables as specified at http://www.postgresql.org/docs/current/static/libpq-envars.html +supported by libpq are also supported by pq. If any of the environment +variables not supported by pq are set, pq will panic during connection +establishment. Environment variables have a lower precedence than explicitly +provided connection parameters. + +The pgpass mechanism as described in http://www.postgresql.org/docs/current/static/libpq-pgpass.html +is supported, but on Windows PGPASSFILE must be specified explicitly. + + +Queries + + +database/sql does not dictate any specific format for parameter +markers in query strings, and pq uses the Postgres-native ordinal markers, +as shown above. The same marker can be reused for the same parameter: + + rows, err := db.Query(`SELECT name FROM users WHERE favorite_fruit = $1 + OR age BETWEEN $2 AND $2 + 3`, "orange", 64) + +pq does not support the LastInsertId() method of the Result type in database/sql. +To return the identifier of an INSERT (or UPDATE or DELETE), use the Postgres +RETURNING clause with a standard Query or QueryRow call: + + var userid int + err := db.QueryRow(`INSERT INTO users(name, favorite_fruit, age) + VALUES('beatrice', 'starfruit', 93) RETURNING id`).Scan(&userid) + +For more details on RETURNING, see the Postgres documentation: + + http://www.postgresql.org/docs/current/static/sql-insert.html + http://www.postgresql.org/docs/current/static/sql-update.html + http://www.postgresql.org/docs/current/static/sql-delete.html + +For additional instructions on querying see the documentation for the database/sql package. + + +Data Types + + +Parameters pass through driver.DefaultParameterConverter before they are handled +by this package. When the binary_parameters connection option is enabled, +[]byte values are sent directly to the backend as data in binary format. + +This package returns the following types for values from the PostgreSQL backend: + + - integer types smallint, integer, and bigint are returned as int64 + - floating-point types real and double precision are returned as float64 + - character types char, varchar, and text are returned as string + - temporal types date, time, timetz, timestamp, and timestamptz are + returned as time.Time + - the boolean type is returned as bool + - the bytea type is returned as []byte + +All other types are returned directly from the backend as []byte values in text format. + + +Errors + + +pq may return errors of type *pq.Error which can be interrogated for error details: + + if err, ok := err.(*pq.Error); ok { + fmt.Println("pq error:", err.Code.Name()) + } + +See the pq.Error type for details. + + +Bulk imports + +You can perform bulk imports by preparing a statement returned by pq.CopyIn (or +pq.CopyInSchema) in an explicit transaction (sql.Tx). The returned statement +handle can then be repeatedly "executed" to copy data into the target table. +After all data has been processed you should call Exec() once with no arguments +to flush all buffered data. Any call to Exec() might return an error which +should be handled appropriately, but because of the internal buffering an error +returned by Exec() might not be related to the data passed in the call that +failed. + +CopyIn uses COPY FROM internally. It is not possible to COPY outside of an +explicit transaction in pq. + +Usage example: + + txn, err := db.Begin() + if err != nil { + log.Fatal(err) + } + + stmt, err := txn.Prepare(pq.CopyIn("users", "name", "age")) + if err != nil { + log.Fatal(err) + } + + for _, user := range users { + _, err = stmt.Exec(user.Name, int64(user.Age)) + if err != nil { + log.Fatal(err) + } + } + + _, err = stmt.Exec() + if err != nil { + log.Fatal(err) + } + + err = stmt.Close() + if err != nil { + log.Fatal(err) + } + + err = txn.Commit() + if err != nil { + log.Fatal(err) + } + + +Notifications + + +PostgreSQL supports a simple publish/subscribe model over database +connections. See http://www.postgresql.org/docs/current/static/sql-notify.html +for more information about the general mechanism. + +To start listening for notifications, you first have to open a new connection +to the database by calling NewListener. This connection can not be used for +anything other than LISTEN / NOTIFY. Calling Listen will open a "notification +channel"; once a notification channel is open, a notification generated on that +channel will effect a send on the Listener.Notify channel. A notification +channel will remain open until Unlisten is called, though connection loss might +result in some notifications being lost. To solve this problem, Listener sends +a nil pointer over the Notify channel any time the connection is re-established +following a connection loss. The application can get information about the +state of the underlying connection by setting an event callback in the call to +NewListener. + +A single Listener can safely be used from concurrent goroutines, which means +that there is often no need to create more than one Listener in your +application. However, a Listener is always connected to a single database, so +you will need to create a new Listener instance for every database you want to +receive notifications in. + +The channel name in both Listen and Unlisten is case sensitive, and can contain +any characters legal in an identifier (see +http://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS +for more information). Note that the channel name will be truncated to 63 +bytes by the PostgreSQL server. + +You can find a complete, working example of Listener usage at +https://godoc.org/github.com/lib/pq/example/listen. + +*/ +package pq diff --git a/vendor/github.com/lib/pq/encode.go b/vendor/github.com/lib/pq/encode.go new file mode 100644 index 00000000000..73cafb89443 --- /dev/null +++ b/vendor/github.com/lib/pq/encode.go @@ -0,0 +1,602 @@ +package pq + +import ( + "bytes" + "database/sql/driver" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "math" + "strconv" + "strings" + "sync" + "time" + + "github.com/lib/pq/oid" +) + +func binaryEncode(parameterStatus *parameterStatus, x interface{}) []byte { + switch v := x.(type) { + case []byte: + return v + default: + return encode(parameterStatus, x, oid.T_unknown) + } +} + +func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte { + switch v := x.(type) { + case int64: + return strconv.AppendInt(nil, v, 10) + case float64: + return strconv.AppendFloat(nil, v, 'f', -1, 64) + case []byte: + if pgtypOid == oid.T_bytea { + return encodeBytea(parameterStatus.serverVersion, v) + } + + return v + case string: + if pgtypOid == oid.T_bytea { + return encodeBytea(parameterStatus.serverVersion, []byte(v)) + } + + return []byte(v) + case bool: + return strconv.AppendBool(nil, v) + case time.Time: + return formatTs(v) + + default: + errorf("encode: unknown type for %T", v) + } + + panic("not reached") +} + +func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid, f format) interface{} { + switch f { + case formatBinary: + return binaryDecode(parameterStatus, s, typ) + case formatText: + return textDecode(parameterStatus, s, typ) + default: + panic("not reached") + } +} + +func binaryDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} { + switch typ { + case oid.T_bytea: + return s + case oid.T_int8: + return int64(binary.BigEndian.Uint64(s)) + case oid.T_int4: + return int64(int32(binary.BigEndian.Uint32(s))) + case oid.T_int2: + return int64(int16(binary.BigEndian.Uint16(s))) + case oid.T_uuid: + b, err := decodeUUIDBinary(s) + if err != nil { + panic(err) + } + return b + + default: + errorf("don't know how to decode binary parameter of type %d", uint32(typ)) + } + + panic("not reached") +} + +func textDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} { + switch typ { + case oid.T_char, oid.T_varchar, oid.T_text: + return string(s) + case oid.T_bytea: + b, err := parseBytea(s) + if err != nil { + errorf("%s", err) + } + return b + case oid.T_timestamptz: + return parseTs(parameterStatus.currentLocation, string(s)) + case oid.T_timestamp, oid.T_date: + return parseTs(nil, string(s)) + case oid.T_time: + return mustParse("15:04:05", typ, s) + case oid.T_timetz: + return mustParse("15:04:05-07", typ, s) + case oid.T_bool: + return s[0] == 't' + case oid.T_int8, oid.T_int4, oid.T_int2: + i, err := strconv.ParseInt(string(s), 10, 64) + if err != nil { + errorf("%s", err) + } + return i + case oid.T_float4, oid.T_float8: + // We always use 64 bit parsing, regardless of whether the input text is for + // a float4 or float8, because clients expect float64s for all float datatypes + // and returning a 32-bit parsed float64 produces lossy results. + f, err := strconv.ParseFloat(string(s), 64) + if err != nil { + errorf("%s", err) + } + return f + } + + return s +} + +// appendEncodedText encodes item in text format as required by COPY +// and appends to buf +func appendEncodedText(parameterStatus *parameterStatus, buf []byte, x interface{}) []byte { + switch v := x.(type) { + case int64: + return strconv.AppendInt(buf, v, 10) + case float64: + return strconv.AppendFloat(buf, v, 'f', -1, 64) + case []byte: + encodedBytea := encodeBytea(parameterStatus.serverVersion, v) + return appendEscapedText(buf, string(encodedBytea)) + case string: + return appendEscapedText(buf, v) + case bool: + return strconv.AppendBool(buf, v) + case time.Time: + return append(buf, formatTs(v)...) + case nil: + return append(buf, "\\N"...) + default: + errorf("encode: unknown type for %T", v) + } + + panic("not reached") +} + +func appendEscapedText(buf []byte, text string) []byte { + escapeNeeded := false + startPos := 0 + var c byte + + // check if we need to escape + for i := 0; i < len(text); i++ { + c = text[i] + if c == '\\' || c == '\n' || c == '\r' || c == '\t' { + escapeNeeded = true + startPos = i + break + } + } + if !escapeNeeded { + return append(buf, text...) + } + + // copy till first char to escape, iterate the rest + result := append(buf, text[:startPos]...) + for i := startPos; i < len(text); i++ { + c = text[i] + switch c { + case '\\': + result = append(result, '\\', '\\') + case '\n': + result = append(result, '\\', 'n') + case '\r': + result = append(result, '\\', 'r') + case '\t': + result = append(result, '\\', 't') + default: + result = append(result, c) + } + } + return result +} + +func mustParse(f string, typ oid.Oid, s []byte) time.Time { + str := string(s) + + // check for a 30-minute-offset timezone + if (typ == oid.T_timestamptz || typ == oid.T_timetz) && + str[len(str)-3] == ':' { + f += ":00" + } + t, err := time.Parse(f, str) + if err != nil { + errorf("decode: %s", err) + } + return t +} + +var errInvalidTimestamp = errors.New("invalid timestamp") + +type timestampParser struct { + err error +} + +func (p *timestampParser) expect(str string, char byte, pos int) { + if p.err != nil { + return + } + if pos+1 > len(str) { + p.err = errInvalidTimestamp + return + } + if c := str[pos]; c != char && p.err == nil { + p.err = fmt.Errorf("expected '%v' at position %v; got '%v'", char, pos, c) + } +} + +func (p *timestampParser) mustAtoi(str string, begin int, end int) int { + if p.err != nil { + return 0 + } + if begin < 0 || end < 0 || begin > end || end > len(str) { + p.err = errInvalidTimestamp + return 0 + } + result, err := strconv.Atoi(str[begin:end]) + if err != nil { + if p.err == nil { + p.err = fmt.Errorf("expected number; got '%v'", str) + } + return 0 + } + return result +} + +// The location cache caches the time zones typically used by the client. +type locationCache struct { + cache map[int]*time.Location + lock sync.Mutex +} + +// All connections share the same list of timezones. Benchmarking shows that +// about 5% speed could be gained by putting the cache in the connection and +// losing the mutex, at the cost of a small amount of memory and a somewhat +// significant increase in code complexity. +var globalLocationCache = newLocationCache() + +func newLocationCache() *locationCache { + return &locationCache{cache: make(map[int]*time.Location)} +} + +// Returns the cached timezone for the specified offset, creating and caching +// it if necessary. +func (c *locationCache) getLocation(offset int) *time.Location { + c.lock.Lock() + defer c.lock.Unlock() + + location, ok := c.cache[offset] + if !ok { + location = time.FixedZone("", offset) + c.cache[offset] = location + } + + return location +} + +var infinityTsEnabled = false +var infinityTsNegative time.Time +var infinityTsPositive time.Time + +const ( + infinityTsEnabledAlready = "pq: infinity timestamp enabled already" + infinityTsNegativeMustBeSmaller = "pq: infinity timestamp: negative value must be smaller (before) than positive" +) + +// EnableInfinityTs controls the handling of Postgres' "-infinity" and +// "infinity" "timestamp"s. +// +// If EnableInfinityTs is not called, "-infinity" and "infinity" will return +// []byte("-infinity") and []byte("infinity") respectively, and potentially +// cause error "sql: Scan error on column index 0: unsupported driver -> Scan +// pair: []uint8 -> *time.Time", when scanning into a time.Time value. +// +// Once EnableInfinityTs has been called, all connections created using this +// driver will decode Postgres' "-infinity" and "infinity" for "timestamp", +// "timestamp with time zone" and "date" types to the predefined minimum and +// maximum times, respectively. When encoding time.Time values, any time which +// equals or precedes the predefined minimum time will be encoded to +// "-infinity". Any values at or past the maximum time will similarly be +// encoded to "infinity". +// +// If EnableInfinityTs is called with negative >= positive, it will panic. +// Calling EnableInfinityTs after a connection has been established results in +// undefined behavior. If EnableInfinityTs is called more than once, it will +// panic. +func EnableInfinityTs(negative time.Time, positive time.Time) { + if infinityTsEnabled { + panic(infinityTsEnabledAlready) + } + if !negative.Before(positive) { + panic(infinityTsNegativeMustBeSmaller) + } + infinityTsEnabled = true + infinityTsNegative = negative + infinityTsPositive = positive +} + +/* + * Testing might want to toggle infinityTsEnabled + */ +func disableInfinityTs() { + infinityTsEnabled = false +} + +// This is a time function specific to the Postgres default DateStyle +// setting ("ISO, MDY"), the only one we currently support. This +// accounts for the discrepancies between the parsing available with +// time.Parse and the Postgres date formatting quirks. +func parseTs(currentLocation *time.Location, str string) interface{} { + switch str { + case "-infinity": + if infinityTsEnabled { + return infinityTsNegative + } + return []byte(str) + case "infinity": + if infinityTsEnabled { + return infinityTsPositive + } + return []byte(str) + } + t, err := ParseTimestamp(currentLocation, str) + if err != nil { + panic(err) + } + return t +} + +// ParseTimestamp parses Postgres' text format. It returns a time.Time in +// currentLocation iff that time's offset agrees with the offset sent from the +// Postgres server. Otherwise, ParseTimestamp returns a time.Time with the +// fixed offset offset provided by the Postgres server. +func ParseTimestamp(currentLocation *time.Location, str string) (time.Time, error) { + p := timestampParser{} + + monSep := strings.IndexRune(str, '-') + // this is Gregorian year, not ISO Year + // In Gregorian system, the year 1 BC is followed by AD 1 + year := p.mustAtoi(str, 0, monSep) + daySep := monSep + 3 + month := p.mustAtoi(str, monSep+1, daySep) + p.expect(str, '-', daySep) + timeSep := daySep + 3 + day := p.mustAtoi(str, daySep+1, timeSep) + + minLen := monSep + len("01-01") + 1 + + isBC := strings.HasSuffix(str, " BC") + if isBC { + minLen += 3 + } + + var hour, minute, second int + if len(str) > minLen { + p.expect(str, ' ', timeSep) + minSep := timeSep + 3 + p.expect(str, ':', minSep) + hour = p.mustAtoi(str, timeSep+1, minSep) + secSep := minSep + 3 + p.expect(str, ':', secSep) + minute = p.mustAtoi(str, minSep+1, secSep) + secEnd := secSep + 3 + second = p.mustAtoi(str, secSep+1, secEnd) + } + remainderIdx := monSep + len("01-01 00:00:00") + 1 + // Three optional (but ordered) sections follow: the + // fractional seconds, the time zone offset, and the BC + // designation. We set them up here and adjust the other + // offsets if the preceding sections exist. + + nanoSec := 0 + tzOff := 0 + + if remainderIdx < len(str) && str[remainderIdx] == '.' { + fracStart := remainderIdx + 1 + fracOff := strings.IndexAny(str[fracStart:], "-+ ") + if fracOff < 0 { + fracOff = len(str) - fracStart + } + fracSec := p.mustAtoi(str, fracStart, fracStart+fracOff) + nanoSec = fracSec * (1000000000 / int(math.Pow(10, float64(fracOff)))) + + remainderIdx += fracOff + 1 + } + if tzStart := remainderIdx; tzStart < len(str) && (str[tzStart] == '-' || str[tzStart] == '+') { + // time zone separator is always '-' or '+' (UTC is +00) + var tzSign int + switch c := str[tzStart]; c { + case '-': + tzSign = -1 + case '+': + tzSign = +1 + default: + return time.Time{}, fmt.Errorf("expected '-' or '+' at position %v; got %v", tzStart, c) + } + tzHours := p.mustAtoi(str, tzStart+1, tzStart+3) + remainderIdx += 3 + var tzMin, tzSec int + if remainderIdx < len(str) && str[remainderIdx] == ':' { + tzMin = p.mustAtoi(str, remainderIdx+1, remainderIdx+3) + remainderIdx += 3 + } + if remainderIdx < len(str) && str[remainderIdx] == ':' { + tzSec = p.mustAtoi(str, remainderIdx+1, remainderIdx+3) + remainderIdx += 3 + } + tzOff = tzSign * ((tzHours * 60 * 60) + (tzMin * 60) + tzSec) + } + var isoYear int + + if isBC { + isoYear = 1 - year + remainderIdx += 3 + } else { + isoYear = year + } + if remainderIdx < len(str) { + return time.Time{}, fmt.Errorf("expected end of input, got %v", str[remainderIdx:]) + } + t := time.Date(isoYear, time.Month(month), day, + hour, minute, second, nanoSec, + globalLocationCache.getLocation(tzOff)) + + if currentLocation != nil { + // Set the location of the returned Time based on the session's + // TimeZone value, but only if the local time zone database agrees with + // the remote database on the offset. + lt := t.In(currentLocation) + _, newOff := lt.Zone() + if newOff == tzOff { + t = lt + } + } + + return t, p.err +} + +// formatTs formats t into a format postgres understands. +func formatTs(t time.Time) []byte { + if infinityTsEnabled { + // t <= -infinity : ! (t > -infinity) + if !t.After(infinityTsNegative) { + return []byte("-infinity") + } + // t >= infinity : ! (!t < infinity) + if !t.Before(infinityTsPositive) { + return []byte("infinity") + } + } + return FormatTimestamp(t) +} + +// FormatTimestamp formats t into Postgres' text format for timestamps. +func FormatTimestamp(t time.Time) []byte { + // Need to send dates before 0001 A.D. with " BC" suffix, instead of the + // minus sign preferred by Go. + // Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on + bc := false + if t.Year() <= 0 { + // flip year sign, and add 1, e.g: "0" will be "1", and "-10" will be "11" + t = t.AddDate((-t.Year())*2+1, 0, 0) + bc = true + } + b := []byte(t.Format("2006-01-02 15:04:05.999999999Z07:00")) + + _, offset := t.Zone() + offset %= 60 + if offset != 0 { + // RFC3339Nano already printed the minus sign + if offset < 0 { + offset = -offset + } + + b = append(b, ':') + if offset < 10 { + b = append(b, '0') + } + b = strconv.AppendInt(b, int64(offset), 10) + } + + if bc { + b = append(b, " BC"...) + } + return b +} + +// Parse a bytea value received from the server. Both "hex" and the legacy +// "escape" format are supported. +func parseBytea(s []byte) (result []byte, err error) { + if len(s) >= 2 && bytes.Equal(s[:2], []byte("\\x")) { + // bytea_output = hex + s = s[2:] // trim off leading "\\x" + result = make([]byte, hex.DecodedLen(len(s))) + _, err := hex.Decode(result, s) + if err != nil { + return nil, err + } + } else { + // bytea_output = escape + for len(s) > 0 { + if s[0] == '\\' { + // escaped '\\' + if len(s) >= 2 && s[1] == '\\' { + result = append(result, '\\') + s = s[2:] + continue + } + + // '\\' followed by an octal number + if len(s) < 4 { + return nil, fmt.Errorf("invalid bytea sequence %v", s) + } + r, err := strconv.ParseInt(string(s[1:4]), 8, 9) + if err != nil { + return nil, fmt.Errorf("could not parse bytea value: %s", err.Error()) + } + result = append(result, byte(r)) + s = s[4:] + } else { + // We hit an unescaped, raw byte. Try to read in as many as + // possible in one go. + i := bytes.IndexByte(s, '\\') + if i == -1 { + result = append(result, s...) + break + } + result = append(result, s[:i]...) + s = s[i:] + } + } + } + + return result, nil +} + +func encodeBytea(serverVersion int, v []byte) (result []byte) { + if serverVersion >= 90000 { + // Use the hex format if we know that the server supports it + result = make([]byte, 2+hex.EncodedLen(len(v))) + result[0] = '\\' + result[1] = 'x' + hex.Encode(result[2:], v) + } else { + // .. or resort to "escape" + for _, b := range v { + if b == '\\' { + result = append(result, '\\', '\\') + } else if b < 0x20 || b > 0x7e { + result = append(result, []byte(fmt.Sprintf("\\%03o", b))...) + } else { + result = append(result, b) + } + } + } + + return result +} + +// NullTime represents a time.Time that may be null. NullTime implements the +// sql.Scanner interface so it can be used as a scan destination, similar to +// sql.NullString. +type NullTime struct { + Time time.Time + Valid bool // Valid is true if Time is not NULL +} + +// Scan implements the Scanner interface. +func (nt *NullTime) Scan(value interface{}) error { + nt.Time, nt.Valid = value.(time.Time) + return nil +} + +// Value implements the driver Valuer interface. +func (nt NullTime) Value() (driver.Value, error) { + if !nt.Valid { + return nil, nil + } + return nt.Time, nil +} diff --git a/vendor/github.com/lib/pq/error.go b/vendor/github.com/lib/pq/error.go new file mode 100644 index 00000000000..3d66ba7c52e --- /dev/null +++ b/vendor/github.com/lib/pq/error.go @@ -0,0 +1,515 @@ +package pq + +import ( + "database/sql/driver" + "fmt" + "io" + "net" + "runtime" +) + +// Error severities +const ( + Efatal = "FATAL" + Epanic = "PANIC" + Ewarning = "WARNING" + Enotice = "NOTICE" + Edebug = "DEBUG" + Einfo = "INFO" + Elog = "LOG" +) + +// Error represents an error communicating with the server. +// +// See http://www.postgresql.org/docs/current/static/protocol-error-fields.html for details of the fields +type Error struct { + Severity string + Code ErrorCode + Message string + Detail string + Hint string + Position string + InternalPosition string + InternalQuery string + Where string + Schema string + Table string + Column string + DataTypeName string + Constraint string + File string + Line string + Routine string +} + +// ErrorCode is a five-character error code. +type ErrorCode string + +// Name returns a more human friendly rendering of the error code, namely the +// "condition name". +// +// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for +// details. +func (ec ErrorCode) Name() string { + return errorCodeNames[ec] +} + +// ErrorClass is only the class part of an error code. +type ErrorClass string + +// Name returns the condition name of an error class. It is equivalent to the +// condition name of the "standard" error code (i.e. the one having the last +// three characters "000"). +func (ec ErrorClass) Name() string { + return errorCodeNames[ErrorCode(ec+"000")] +} + +// Class returns the error class, e.g. "28". +// +// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for +// details. +func (ec ErrorCode) Class() ErrorClass { + return ErrorClass(ec[0:2]) +} + +// errorCodeNames is a mapping between the five-character error codes and the +// human readable "condition names". It is derived from the list at +// http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html +var errorCodeNames = map[ErrorCode]string{ + // Class 00 - Successful Completion + "00000": "successful_completion", + // Class 01 - Warning + "01000": "warning", + "0100C": "dynamic_result_sets_returned", + "01008": "implicit_zero_bit_padding", + "01003": "null_value_eliminated_in_set_function", + "01007": "privilege_not_granted", + "01006": "privilege_not_revoked", + "01004": "string_data_right_truncation", + "01P01": "deprecated_feature", + // Class 02 - No Data (this is also a warning class per the SQL standard) + "02000": "no_data", + "02001": "no_additional_dynamic_result_sets_returned", + // Class 03 - SQL Statement Not Yet Complete + "03000": "sql_statement_not_yet_complete", + // Class 08 - Connection Exception + "08000": "connection_exception", + "08003": "connection_does_not_exist", + "08006": "connection_failure", + "08001": "sqlclient_unable_to_establish_sqlconnection", + "08004": "sqlserver_rejected_establishment_of_sqlconnection", + "08007": "transaction_resolution_unknown", + "08P01": "protocol_violation", + // Class 09 - Triggered Action Exception + "09000": "triggered_action_exception", + // Class 0A - Feature Not Supported + "0A000": "feature_not_supported", + // Class 0B - Invalid Transaction Initiation + "0B000": "invalid_transaction_initiation", + // Class 0F - Locator Exception + "0F000": "locator_exception", + "0F001": "invalid_locator_specification", + // Class 0L - Invalid Grantor + "0L000": "invalid_grantor", + "0LP01": "invalid_grant_operation", + // Class 0P - Invalid Role Specification + "0P000": "invalid_role_specification", + // Class 0Z - Diagnostics Exception + "0Z000": "diagnostics_exception", + "0Z002": "stacked_diagnostics_accessed_without_active_handler", + // Class 20 - Case Not Found + "20000": "case_not_found", + // Class 21 - Cardinality Violation + "21000": "cardinality_violation", + // Class 22 - Data Exception + "22000": "data_exception", + "2202E": "array_subscript_error", + "22021": "character_not_in_repertoire", + "22008": "datetime_field_overflow", + "22012": "division_by_zero", + "22005": "error_in_assignment", + "2200B": "escape_character_conflict", + "22022": "indicator_overflow", + "22015": "interval_field_overflow", + "2201E": "invalid_argument_for_logarithm", + "22014": "invalid_argument_for_ntile_function", + "22016": "invalid_argument_for_nth_value_function", + "2201F": "invalid_argument_for_power_function", + "2201G": "invalid_argument_for_width_bucket_function", + "22018": "invalid_character_value_for_cast", + "22007": "invalid_datetime_format", + "22019": "invalid_escape_character", + "2200D": "invalid_escape_octet", + "22025": "invalid_escape_sequence", + "22P06": "nonstandard_use_of_escape_character", + "22010": "invalid_indicator_parameter_value", + "22023": "invalid_parameter_value", + "2201B": "invalid_regular_expression", + "2201W": "invalid_row_count_in_limit_clause", + "2201X": "invalid_row_count_in_result_offset_clause", + "22009": "invalid_time_zone_displacement_value", + "2200C": "invalid_use_of_escape_character", + "2200G": "most_specific_type_mismatch", + "22004": "null_value_not_allowed", + "22002": "null_value_no_indicator_parameter", + "22003": "numeric_value_out_of_range", + "2200H": "sequence_generator_limit_exceeded", + "22026": "string_data_length_mismatch", + "22001": "string_data_right_truncation", + "22011": "substring_error", + "22027": "trim_error", + "22024": "unterminated_c_string", + "2200F": "zero_length_character_string", + "22P01": "floating_point_exception", + "22P02": "invalid_text_representation", + "22P03": "invalid_binary_representation", + "22P04": "bad_copy_file_format", + "22P05": "untranslatable_character", + "2200L": "not_an_xml_document", + "2200M": "invalid_xml_document", + "2200N": "invalid_xml_content", + "2200S": "invalid_xml_comment", + "2200T": "invalid_xml_processing_instruction", + // Class 23 - Integrity Constraint Violation + "23000": "integrity_constraint_violation", + "23001": "restrict_violation", + "23502": "not_null_violation", + "23503": "foreign_key_violation", + "23505": "unique_violation", + "23514": "check_violation", + "23P01": "exclusion_violation", + // Class 24 - Invalid Cursor State + "24000": "invalid_cursor_state", + // Class 25 - Invalid Transaction State + "25000": "invalid_transaction_state", + "25001": "active_sql_transaction", + "25002": "branch_transaction_already_active", + "25008": "held_cursor_requires_same_isolation_level", + "25003": "inappropriate_access_mode_for_branch_transaction", + "25004": "inappropriate_isolation_level_for_branch_transaction", + "25005": "no_active_sql_transaction_for_branch_transaction", + "25006": "read_only_sql_transaction", + "25007": "schema_and_data_statement_mixing_not_supported", + "25P01": "no_active_sql_transaction", + "25P02": "in_failed_sql_transaction", + // Class 26 - Invalid SQL Statement Name + "26000": "invalid_sql_statement_name", + // Class 27 - Triggered Data Change Violation + "27000": "triggered_data_change_violation", + // Class 28 - Invalid Authorization Specification + "28000": "invalid_authorization_specification", + "28P01": "invalid_password", + // Class 2B - Dependent Privilege Descriptors Still Exist + "2B000": "dependent_privilege_descriptors_still_exist", + "2BP01": "dependent_objects_still_exist", + // Class 2D - Invalid Transaction Termination + "2D000": "invalid_transaction_termination", + // Class 2F - SQL Routine Exception + "2F000": "sql_routine_exception", + "2F005": "function_executed_no_return_statement", + "2F002": "modifying_sql_data_not_permitted", + "2F003": "prohibited_sql_statement_attempted", + "2F004": "reading_sql_data_not_permitted", + // Class 34 - Invalid Cursor Name + "34000": "invalid_cursor_name", + // Class 38 - External Routine Exception + "38000": "external_routine_exception", + "38001": "containing_sql_not_permitted", + "38002": "modifying_sql_data_not_permitted", + "38003": "prohibited_sql_statement_attempted", + "38004": "reading_sql_data_not_permitted", + // Class 39 - External Routine Invocation Exception + "39000": "external_routine_invocation_exception", + "39001": "invalid_sqlstate_returned", + "39004": "null_value_not_allowed", + "39P01": "trigger_protocol_violated", + "39P02": "srf_protocol_violated", + // Class 3B - Savepoint Exception + "3B000": "savepoint_exception", + "3B001": "invalid_savepoint_specification", + // Class 3D - Invalid Catalog Name + "3D000": "invalid_catalog_name", + // Class 3F - Invalid Schema Name + "3F000": "invalid_schema_name", + // Class 40 - Transaction Rollback + "40000": "transaction_rollback", + "40002": "transaction_integrity_constraint_violation", + "40001": "serialization_failure", + "40003": "statement_completion_unknown", + "40P01": "deadlock_detected", + // Class 42 - Syntax Error or Access Rule Violation + "42000": "syntax_error_or_access_rule_violation", + "42601": "syntax_error", + "42501": "insufficient_privilege", + "42846": "cannot_coerce", + "42803": "grouping_error", + "42P20": "windowing_error", + "42P19": "invalid_recursion", + "42830": "invalid_foreign_key", + "42602": "invalid_name", + "42622": "name_too_long", + "42939": "reserved_name", + "42804": "datatype_mismatch", + "42P18": "indeterminate_datatype", + "42P21": "collation_mismatch", + "42P22": "indeterminate_collation", + "42809": "wrong_object_type", + "42703": "undefined_column", + "42883": "undefined_function", + "42P01": "undefined_table", + "42P02": "undefined_parameter", + "42704": "undefined_object", + "42701": "duplicate_column", + "42P03": "duplicate_cursor", + "42P04": "duplicate_database", + "42723": "duplicate_function", + "42P05": "duplicate_prepared_statement", + "42P06": "duplicate_schema", + "42P07": "duplicate_table", + "42712": "duplicate_alias", + "42710": "duplicate_object", + "42702": "ambiguous_column", + "42725": "ambiguous_function", + "42P08": "ambiguous_parameter", + "42P09": "ambiguous_alias", + "42P10": "invalid_column_reference", + "42611": "invalid_column_definition", + "42P11": "invalid_cursor_definition", + "42P12": "invalid_database_definition", + "42P13": "invalid_function_definition", + "42P14": "invalid_prepared_statement_definition", + "42P15": "invalid_schema_definition", + "42P16": "invalid_table_definition", + "42P17": "invalid_object_definition", + // Class 44 - WITH CHECK OPTION Violation + "44000": "with_check_option_violation", + // Class 53 - Insufficient Resources + "53000": "insufficient_resources", + "53100": "disk_full", + "53200": "out_of_memory", + "53300": "too_many_connections", + "53400": "configuration_limit_exceeded", + // Class 54 - Program Limit Exceeded + "54000": "program_limit_exceeded", + "54001": "statement_too_complex", + "54011": "too_many_columns", + "54023": "too_many_arguments", + // Class 55 - Object Not In Prerequisite State + "55000": "object_not_in_prerequisite_state", + "55006": "object_in_use", + "55P02": "cant_change_runtime_param", + "55P03": "lock_not_available", + // Class 57 - Operator Intervention + "57000": "operator_intervention", + "57014": "query_canceled", + "57P01": "admin_shutdown", + "57P02": "crash_shutdown", + "57P03": "cannot_connect_now", + "57P04": "database_dropped", + // Class 58 - System Error (errors external to PostgreSQL itself) + "58000": "system_error", + "58030": "io_error", + "58P01": "undefined_file", + "58P02": "duplicate_file", + // Class F0 - Configuration File Error + "F0000": "config_file_error", + "F0001": "lock_file_exists", + // Class HV - Foreign Data Wrapper Error (SQL/MED) + "HV000": "fdw_error", + "HV005": "fdw_column_name_not_found", + "HV002": "fdw_dynamic_parameter_value_needed", + "HV010": "fdw_function_sequence_error", + "HV021": "fdw_inconsistent_descriptor_information", + "HV024": "fdw_invalid_attribute_value", + "HV007": "fdw_invalid_column_name", + "HV008": "fdw_invalid_column_number", + "HV004": "fdw_invalid_data_type", + "HV006": "fdw_invalid_data_type_descriptors", + "HV091": "fdw_invalid_descriptor_field_identifier", + "HV00B": "fdw_invalid_handle", + "HV00C": "fdw_invalid_option_index", + "HV00D": "fdw_invalid_option_name", + "HV090": "fdw_invalid_string_length_or_buffer_length", + "HV00A": "fdw_invalid_string_format", + "HV009": "fdw_invalid_use_of_null_pointer", + "HV014": "fdw_too_many_handles", + "HV001": "fdw_out_of_memory", + "HV00P": "fdw_no_schemas", + "HV00J": "fdw_option_name_not_found", + "HV00K": "fdw_reply_handle", + "HV00Q": "fdw_schema_not_found", + "HV00R": "fdw_table_not_found", + "HV00L": "fdw_unable_to_create_execution", + "HV00M": "fdw_unable_to_create_reply", + "HV00N": "fdw_unable_to_establish_connection", + // Class P0 - PL/pgSQL Error + "P0000": "plpgsql_error", + "P0001": "raise_exception", + "P0002": "no_data_found", + "P0003": "too_many_rows", + // Class XX - Internal Error + "XX000": "internal_error", + "XX001": "data_corrupted", + "XX002": "index_corrupted", +} + +func parseError(r *readBuf) *Error { + err := new(Error) + for t := r.byte(); t != 0; t = r.byte() { + msg := r.string() + switch t { + case 'S': + err.Severity = msg + case 'C': + err.Code = ErrorCode(msg) + case 'M': + err.Message = msg + case 'D': + err.Detail = msg + case 'H': + err.Hint = msg + case 'P': + err.Position = msg + case 'p': + err.InternalPosition = msg + case 'q': + err.InternalQuery = msg + case 'W': + err.Where = msg + case 's': + err.Schema = msg + case 't': + err.Table = msg + case 'c': + err.Column = msg + case 'd': + err.DataTypeName = msg + case 'n': + err.Constraint = msg + case 'F': + err.File = msg + case 'L': + err.Line = msg + case 'R': + err.Routine = msg + } + } + return err +} + +// Fatal returns true if the Error Severity is fatal. +func (err *Error) Fatal() bool { + return err.Severity == Efatal +} + +// Get implements the legacy PGError interface. New code should use the fields +// of the Error struct directly. +func (err *Error) Get(k byte) (v string) { + switch k { + case 'S': + return err.Severity + case 'C': + return string(err.Code) + case 'M': + return err.Message + case 'D': + return err.Detail + case 'H': + return err.Hint + case 'P': + return err.Position + case 'p': + return err.InternalPosition + case 'q': + return err.InternalQuery + case 'W': + return err.Where + case 's': + return err.Schema + case 't': + return err.Table + case 'c': + return err.Column + case 'd': + return err.DataTypeName + case 'n': + return err.Constraint + case 'F': + return err.File + case 'L': + return err.Line + case 'R': + return err.Routine + } + return "" +} + +func (err Error) Error() string { + return "pq: " + err.Message +} + +// PGError is an interface used by previous versions of pq. It is provided +// only to support legacy code. New code should use the Error type. +type PGError interface { + Error() string + Fatal() bool + Get(k byte) (v string) +} + +func errorf(s string, args ...interface{}) { + panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) +} + +// TODO(ainar-g) Rename to errorf after removing panics. +func fmterrorf(s string, args ...interface{}) error { + return fmt.Errorf("pq: %s", fmt.Sprintf(s, args...)) +} + +func errRecoverNoErrBadConn(err *error) { + e := recover() + if e == nil { + // Do nothing + return + } + var ok bool + *err, ok = e.(error) + if !ok { + *err = fmt.Errorf("pq: unexpected error: %#v", e) + } +} + +func (cn *conn) errRecover(err *error) { + e := recover() + switch v := e.(type) { + case nil: + // Do nothing + case runtime.Error: + cn.bad = true + panic(v) + case *Error: + if v.Fatal() { + *err = driver.ErrBadConn + } else { + *err = v + } + case *net.OpError: + cn.bad = true + *err = v + case error: + if v == io.EOF || v.(error).Error() == "remote error: handshake failure" { + *err = driver.ErrBadConn + } else { + *err = v + } + + default: + cn.bad = true + panic(fmt.Sprintf("unknown error: %#v", e)) + } + + // Any time we return ErrBadConn, we need to remember it since *Tx doesn't + // mark the connection bad in database/sql. + if *err == driver.ErrBadConn { + cn.bad = true + } +} diff --git a/vendor/github.com/lib/pq/go.mod b/vendor/github.com/lib/pq/go.mod new file mode 100644 index 00000000000..edf0b343fd1 --- /dev/null +++ b/vendor/github.com/lib/pq/go.mod @@ -0,0 +1 @@ +module github.com/lib/pq diff --git a/vendor/github.com/lib/pq/notify.go b/vendor/github.com/lib/pq/notify.go new file mode 100644 index 00000000000..850bb9040c3 --- /dev/null +++ b/vendor/github.com/lib/pq/notify.go @@ -0,0 +1,797 @@ +package pq + +// Package pq is a pure Go Postgres driver for the database/sql package. +// This module contains support for Postgres LISTEN/NOTIFY. + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "time" +) + +// Notification represents a single notification from the database. +type Notification struct { + // Process ID (PID) of the notifying postgres backend. + BePid int + // Name of the channel the notification was sent on. + Channel string + // Payload, or the empty string if unspecified. + Extra string +} + +func recvNotification(r *readBuf) *Notification { + bePid := r.int32() + channel := r.string() + extra := r.string() + + return &Notification{bePid, channel, extra} +} + +const ( + connStateIdle int32 = iota + connStateExpectResponse + connStateExpectReadyForQuery +) + +type message struct { + typ byte + err error +} + +var errListenerConnClosed = errors.New("pq: ListenerConn has been closed") + +// ListenerConn is a low-level interface for waiting for notifications. You +// should use Listener instead. +type ListenerConn struct { + // guards cn and err + connectionLock sync.Mutex + cn *conn + err error + + connState int32 + + // the sending goroutine will be holding this lock + senderLock sync.Mutex + + notificationChan chan<- *Notification + + replyChan chan message +} + +// NewListenerConn creates a new ListenerConn. Use NewListener instead. +func NewListenerConn(name string, notificationChan chan<- *Notification) (*ListenerConn, error) { + return newDialListenerConn(defaultDialer{}, name, notificationChan) +} + +func newDialListenerConn(d Dialer, name string, c chan<- *Notification) (*ListenerConn, error) { + cn, err := DialOpen(d, name) + if err != nil { + return nil, err + } + + l := &ListenerConn{ + cn: cn.(*conn), + notificationChan: c, + connState: connStateIdle, + replyChan: make(chan message, 2), + } + + go l.listenerConnMain() + + return l, nil +} + +// We can only allow one goroutine at a time to be running a query on the +// connection for various reasons, so the goroutine sending on the connection +// must be holding senderLock. +// +// Returns an error if an unrecoverable error has occurred and the ListenerConn +// should be abandoned. +func (l *ListenerConn) acquireSenderLock() error { + // we must acquire senderLock first to avoid deadlocks; see ExecSimpleQuery + l.senderLock.Lock() + + l.connectionLock.Lock() + err := l.err + l.connectionLock.Unlock() + if err != nil { + l.senderLock.Unlock() + return err + } + return nil +} + +func (l *ListenerConn) releaseSenderLock() { + l.senderLock.Unlock() +} + +// setState advances the protocol state to newState. Returns false if moving +// to that state from the current state is not allowed. +func (l *ListenerConn) setState(newState int32) bool { + var expectedState int32 + + switch newState { + case connStateIdle: + expectedState = connStateExpectReadyForQuery + case connStateExpectResponse: + expectedState = connStateIdle + case connStateExpectReadyForQuery: + expectedState = connStateExpectResponse + default: + panic(fmt.Sprintf("unexpected listenerConnState %d", newState)) + } + + return atomic.CompareAndSwapInt32(&l.connState, expectedState, newState) +} + +// Main logic is here: receive messages from the postgres backend, forward +// notifications and query replies and keep the internal state in sync with the +// protocol state. Returns when the connection has been lost, is about to go +// away or should be discarded because we couldn't agree on the state with the +// server backend. +func (l *ListenerConn) listenerConnLoop() (err error) { + defer errRecoverNoErrBadConn(&err) + + r := &readBuf{} + for { + t, err := l.cn.recvMessage(r) + if err != nil { + return err + } + + switch t { + case 'A': + // recvNotification copies all the data so we don't need to worry + // about the scratch buffer being overwritten. + l.notificationChan <- recvNotification(r) + + case 'T', 'D': + // only used by tests; ignore + + case 'E': + // We might receive an ErrorResponse even when not in a query; it + // is expected that the server will close the connection after + // that, but we should make sure that the error we display is the + // one from the stray ErrorResponse, not io.ErrUnexpectedEOF. + if !l.setState(connStateExpectReadyForQuery) { + return parseError(r) + } + l.replyChan <- message{t, parseError(r)} + + case 'C', 'I': + if !l.setState(connStateExpectReadyForQuery) { + // protocol out of sync + return fmt.Errorf("unexpected CommandComplete") + } + // ExecSimpleQuery doesn't need to know about this message + + case 'Z': + if !l.setState(connStateIdle) { + // protocol out of sync + return fmt.Errorf("unexpected ReadyForQuery") + } + l.replyChan <- message{t, nil} + + case 'N', 'S': + // ignore + default: + return fmt.Errorf("unexpected message %q from server in listenerConnLoop", t) + } + } +} + +// This is the main routine for the goroutine receiving on the database +// connection. Most of the main logic is in listenerConnLoop. +func (l *ListenerConn) listenerConnMain() { + err := l.listenerConnLoop() + + // listenerConnLoop terminated; we're done, but we still have to clean up. + // Make sure nobody tries to start any new queries by making sure the err + // pointer is set. It is important that we do not overwrite its value; a + // connection could be closed by either this goroutine or one sending on + // the connection -- whoever closes the connection is assumed to have the + // more meaningful error message (as the other one will probably get + // net.errClosed), so that goroutine sets the error we expose while the + // other error is discarded. If the connection is lost while two + // goroutines are operating on the socket, it probably doesn't matter which + // error we expose so we don't try to do anything more complex. + l.connectionLock.Lock() + if l.err == nil { + l.err = err + } + l.cn.Close() + l.connectionLock.Unlock() + + // There might be a query in-flight; make sure nobody's waiting for a + // response to it, since there's not going to be one. + close(l.replyChan) + + // let the listener know we're done + close(l.notificationChan) + + // this ListenerConn is done +} + +// Listen sends a LISTEN query to the server. See ExecSimpleQuery. +func (l *ListenerConn) Listen(channel string) (bool, error) { + return l.ExecSimpleQuery("LISTEN " + QuoteIdentifier(channel)) +} + +// Unlisten sends an UNLISTEN query to the server. See ExecSimpleQuery. +func (l *ListenerConn) Unlisten(channel string) (bool, error) { + return l.ExecSimpleQuery("UNLISTEN " + QuoteIdentifier(channel)) +} + +// UnlistenAll sends an `UNLISTEN *` query to the server. See ExecSimpleQuery. +func (l *ListenerConn) UnlistenAll() (bool, error) { + return l.ExecSimpleQuery("UNLISTEN *") +} + +// Ping the remote server to make sure it's alive. Non-nil error means the +// connection has failed and should be abandoned. +func (l *ListenerConn) Ping() error { + sent, err := l.ExecSimpleQuery("") + if !sent { + return err + } + if err != nil { + // shouldn't happen + panic(err) + } + return nil +} + +// Attempt to send a query on the connection. Returns an error if sending the +// query failed, and the caller should initiate closure of this connection. +// The caller must be holding senderLock (see acquireSenderLock and +// releaseSenderLock). +func (l *ListenerConn) sendSimpleQuery(q string) (err error) { + defer errRecoverNoErrBadConn(&err) + + // must set connection state before sending the query + if !l.setState(connStateExpectResponse) { + panic("two queries running at the same time") + } + + // Can't use l.cn.writeBuf here because it uses the scratch buffer which + // might get overwritten by listenerConnLoop. + b := &writeBuf{ + buf: []byte("Q\x00\x00\x00\x00"), + pos: 1, + } + b.string(q) + l.cn.send(b) + + return nil +} + +// ExecSimpleQuery executes a "simple query" (i.e. one with no bindable +// parameters) on the connection. The possible return values are: +// 1) "executed" is true; the query was executed to completion on the +// database server. If the query failed, err will be set to the error +// returned by the database, otherwise err will be nil. +// 2) If "executed" is false, the query could not be executed on the remote +// server. err will be non-nil. +// +// After a call to ExecSimpleQuery has returned an executed=false value, the +// connection has either been closed or will be closed shortly thereafter, and +// all subsequently executed queries will return an error. +func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) { + if err = l.acquireSenderLock(); err != nil { + return false, err + } + defer l.releaseSenderLock() + + err = l.sendSimpleQuery(q) + if err != nil { + // We can't know what state the protocol is in, so we need to abandon + // this connection. + l.connectionLock.Lock() + // Set the error pointer if it hasn't been set already; see + // listenerConnMain. + if l.err == nil { + l.err = err + } + l.connectionLock.Unlock() + l.cn.c.Close() + return false, err + } + + // now we just wait for a reply.. + for { + m, ok := <-l.replyChan + if !ok { + // We lost the connection to server, don't bother waiting for a + // a response. err should have been set already. + l.connectionLock.Lock() + err := l.err + l.connectionLock.Unlock() + return false, err + } + switch m.typ { + case 'Z': + // sanity check + if m.err != nil { + panic("m.err != nil") + } + // done; err might or might not be set + return true, err + + case 'E': + // sanity check + if m.err == nil { + panic("m.err == nil") + } + // server responded with an error; ReadyForQuery to follow + err = m.err + + default: + return false, fmt.Errorf("unknown response for simple query: %q", m.typ) + } + } +} + +// Close closes the connection. +func (l *ListenerConn) Close() error { + l.connectionLock.Lock() + if l.err != nil { + l.connectionLock.Unlock() + return errListenerConnClosed + } + l.err = errListenerConnClosed + l.connectionLock.Unlock() + // We can't send anything on the connection without holding senderLock. + // Simply close the net.Conn to wake up everyone operating on it. + return l.cn.c.Close() +} + +// Err returns the reason the connection was closed. It is not safe to call +// this function until l.Notify has been closed. +func (l *ListenerConn) Err() error { + return l.err +} + +var errListenerClosed = errors.New("pq: Listener has been closed") + +// ErrChannelAlreadyOpen is returned from Listen when a channel is already +// open. +var ErrChannelAlreadyOpen = errors.New("pq: channel is already open") + +// ErrChannelNotOpen is returned from Unlisten when a channel is not open. +var ErrChannelNotOpen = errors.New("pq: channel is not open") + +// ListenerEventType is an enumeration of listener event types. +type ListenerEventType int + +const ( + // ListenerEventConnected is emitted only when the database connection + // has been initially initialized. The err argument of the callback + // will always be nil. + ListenerEventConnected ListenerEventType = iota + + // ListenerEventDisconnected is emitted after a database connection has + // been lost, either because of an error or because Close has been + // called. The err argument will be set to the reason the database + // connection was lost. + ListenerEventDisconnected + + // ListenerEventReconnected is emitted after a database connection has + // been re-established after connection loss. The err argument of the + // callback will always be nil. After this event has been emitted, a + // nil pq.Notification is sent on the Listener.Notify channel. + ListenerEventReconnected + + // ListenerEventConnectionAttemptFailed is emitted after a connection + // to the database was attempted, but failed. The err argument will be + // set to an error describing why the connection attempt did not + // succeed. + ListenerEventConnectionAttemptFailed +) + +// EventCallbackType is the event callback type. See also ListenerEventType +// constants' documentation. +type EventCallbackType func(event ListenerEventType, err error) + +// Listener provides an interface for listening to notifications from a +// PostgreSQL database. For general usage information, see section +// "Notifications". +// +// Listener can safely be used from concurrently running goroutines. +type Listener struct { + // Channel for receiving notifications from the database. In some cases a + // nil value will be sent. See section "Notifications" above. + Notify chan *Notification + + name string + minReconnectInterval time.Duration + maxReconnectInterval time.Duration + dialer Dialer + eventCallback EventCallbackType + + lock sync.Mutex + isClosed bool + reconnectCond *sync.Cond + cn *ListenerConn + connNotificationChan <-chan *Notification + channels map[string]struct{} +} + +// NewListener creates a new database connection dedicated to LISTEN / NOTIFY. +// +// name should be set to a connection string to be used to establish the +// database connection (see section "Connection String Parameters" above). +// +// minReconnectInterval controls the duration to wait before trying to +// re-establish the database connection after connection loss. After each +// consecutive failure this interval is doubled, until maxReconnectInterval is +// reached. Successfully completing the connection establishment procedure +// resets the interval back to minReconnectInterval. +// +// The last parameter eventCallback can be set to a function which will be +// called by the Listener when the state of the underlying database connection +// changes. This callback will be called by the goroutine which dispatches the +// notifications over the Notify channel, so you should try to avoid doing +// potentially time-consuming operations from the callback. +func NewListener(name string, + minReconnectInterval time.Duration, + maxReconnectInterval time.Duration, + eventCallback EventCallbackType) *Listener { + return NewDialListener(defaultDialer{}, name, minReconnectInterval, maxReconnectInterval, eventCallback) +} + +// NewDialListener is like NewListener but it takes a Dialer. +func NewDialListener(d Dialer, + name string, + minReconnectInterval time.Duration, + maxReconnectInterval time.Duration, + eventCallback EventCallbackType) *Listener { + + l := &Listener{ + name: name, + minReconnectInterval: minReconnectInterval, + maxReconnectInterval: maxReconnectInterval, + dialer: d, + eventCallback: eventCallback, + + channels: make(map[string]struct{}), + + Notify: make(chan *Notification, 32), + } + l.reconnectCond = sync.NewCond(&l.lock) + + go l.listenerMain() + + return l +} + +// NotificationChannel returns the notification channel for this listener. +// This is the same channel as Notify, and will not be recreated during the +// life time of the Listener. +func (l *Listener) NotificationChannel() <-chan *Notification { + return l.Notify +} + +// Listen starts listening for notifications on a channel. Calls to this +// function will block until an acknowledgement has been received from the +// server. Note that Listener automatically re-establishes the connection +// after connection loss, so this function may block indefinitely if the +// connection can not be re-established. +// +// Listen will only fail in three conditions: +// 1) The channel is already open. The returned error will be +// ErrChannelAlreadyOpen. +// 2) The query was executed on the remote server, but PostgreSQL returned an +// error message in response to the query. The returned error will be a +// pq.Error containing the information the server supplied. +// 3) Close is called on the Listener before the request could be completed. +// +// The channel name is case-sensitive. +func (l *Listener) Listen(channel string) error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + // The server allows you to issue a LISTEN on a channel which is already + // open, but it seems useful to be able to detect this case to spot for + // mistakes in application logic. If the application genuinely does't + // care, it can check the exported error and ignore it. + _, exists := l.channels[channel] + if exists { + return ErrChannelAlreadyOpen + } + + if l.cn != nil { + // If gotResponse is true but error is set, the query was executed on + // the remote server, but resulted in an error. This should be + // relatively rare, so it's fine if we just pass the error to our + // caller. However, if gotResponse is false, we could not complete the + // query on the remote server and our underlying connection is about + // to go away, so we only add relname to l.channels, and wait for + // resync() to take care of the rest. + gotResponse, err := l.cn.Listen(channel) + if gotResponse && err != nil { + return err + } + } + + l.channels[channel] = struct{}{} + for l.cn == nil { + l.reconnectCond.Wait() + // we let go of the mutex for a while + if l.isClosed { + return errListenerClosed + } + } + + return nil +} + +// Unlisten removes a channel from the Listener's channel list. Returns +// ErrChannelNotOpen if the Listener is not listening on the specified channel. +// Returns immediately with no error if there is no connection. Note that you +// might still get notifications for this channel even after Unlisten has +// returned. +// +// The channel name is case-sensitive. +func (l *Listener) Unlisten(channel string) error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + // Similarly to LISTEN, this is not an error in Postgres, but it seems + // useful to distinguish from the normal conditions. + _, exists := l.channels[channel] + if !exists { + return ErrChannelNotOpen + } + + if l.cn != nil { + // Similarly to Listen (see comment in that function), the caller + // should only be bothered with an error if it came from the backend as + // a response to our query. + gotResponse, err := l.cn.Unlisten(channel) + if gotResponse && err != nil { + return err + } + } + + // Don't bother waiting for resync if there's no connection. + delete(l.channels, channel) + return nil +} + +// UnlistenAll removes all channels from the Listener's channel list. Returns +// immediately with no error if there is no connection. Note that you might +// still get notifications for any of the deleted channels even after +// UnlistenAll has returned. +func (l *Listener) UnlistenAll() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + if l.cn != nil { + // Similarly to Listen (see comment in that function), the caller + // should only be bothered with an error if it came from the backend as + // a response to our query. + gotResponse, err := l.cn.UnlistenAll() + if gotResponse && err != nil { + return err + } + } + + // Don't bother waiting for resync if there's no connection. + l.channels = make(map[string]struct{}) + return nil +} + +// Ping the remote server to make sure it's alive. Non-nil return value means +// that there is no active connection. +func (l *Listener) Ping() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + if l.cn == nil { + return errors.New("no connection") + } + + return l.cn.Ping() +} + +// Clean up after losing the server connection. Returns l.cn.Err(), which +// should have the reason the connection was lost. +func (l *Listener) disconnectCleanup() error { + l.lock.Lock() + defer l.lock.Unlock() + + // sanity check; can't look at Err() until the channel has been closed + select { + case _, ok := <-l.connNotificationChan: + if ok { + panic("connNotificationChan not closed") + } + default: + panic("connNotificationChan not closed") + } + + err := l.cn.Err() + l.cn.Close() + l.cn = nil + return err +} + +// Synchronize the list of channels we want to be listening on with the server +// after the connection has been established. +func (l *Listener) resync(cn *ListenerConn, notificationChan <-chan *Notification) error { + doneChan := make(chan error) + go func(notificationChan <-chan *Notification) { + for channel := range l.channels { + // If we got a response, return that error to our caller as it's + // going to be more descriptive than cn.Err(). + gotResponse, err := cn.Listen(channel) + if gotResponse && err != nil { + doneChan <- err + return + } + + // If we couldn't reach the server, wait for notificationChan to + // close and then return the error message from the connection, as + // per ListenerConn's interface. + if err != nil { + for range notificationChan { + } + doneChan <- cn.Err() + return + } + } + doneChan <- nil + }(notificationChan) + + // Ignore notifications while synchronization is going on to avoid + // deadlocks. We have to send a nil notification over Notify anyway as + // we can't possibly know which notifications (if any) were lost while + // the connection was down, so there's no reason to try and process + // these messages at all. + for { + select { + case _, ok := <-notificationChan: + if !ok { + notificationChan = nil + } + + case err := <-doneChan: + return err + } + } +} + +// caller should NOT be holding l.lock +func (l *Listener) closed() bool { + l.lock.Lock() + defer l.lock.Unlock() + + return l.isClosed +} + +func (l *Listener) connect() error { + notificationChan := make(chan *Notification, 32) + cn, err := newDialListenerConn(l.dialer, l.name, notificationChan) + if err != nil { + return err + } + + l.lock.Lock() + defer l.lock.Unlock() + + err = l.resync(cn, notificationChan) + if err != nil { + cn.Close() + return err + } + + l.cn = cn + l.connNotificationChan = notificationChan + l.reconnectCond.Broadcast() + + return nil +} + +// Close disconnects the Listener from the database and shuts it down. +// Subsequent calls to its methods will return an error. Close returns an +// error if the connection has already been closed. +func (l *Listener) Close() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + if l.cn != nil { + l.cn.Close() + } + l.isClosed = true + + // Unblock calls to Listen() + l.reconnectCond.Broadcast() + + return nil +} + +func (l *Listener) emitEvent(event ListenerEventType, err error) { + if l.eventCallback != nil { + l.eventCallback(event, err) + } +} + +// Main logic here: maintain a connection to the server when possible, wait +// for notifications and emit events. +func (l *Listener) listenerConnLoop() { + var nextReconnect time.Time + + reconnectInterval := l.minReconnectInterval + for { + for { + err := l.connect() + if err == nil { + break + } + + if l.closed() { + return + } + l.emitEvent(ListenerEventConnectionAttemptFailed, err) + + time.Sleep(reconnectInterval) + reconnectInterval *= 2 + if reconnectInterval > l.maxReconnectInterval { + reconnectInterval = l.maxReconnectInterval + } + } + + if nextReconnect.IsZero() { + l.emitEvent(ListenerEventConnected, nil) + } else { + l.emitEvent(ListenerEventReconnected, nil) + l.Notify <- nil + } + + reconnectInterval = l.minReconnectInterval + nextReconnect = time.Now().Add(reconnectInterval) + + for { + notification, ok := <-l.connNotificationChan + if !ok { + // lost connection, loop again + break + } + l.Notify <- notification + } + + err := l.disconnectCleanup() + if l.closed() { + return + } + l.emitEvent(ListenerEventDisconnected, err) + + time.Sleep(time.Until(nextReconnect)) + } +} + +func (l *Listener) listenerMain() { + l.listenerConnLoop() + close(l.Notify) +} diff --git a/vendor/github.com/lib/pq/oid/doc.go b/vendor/github.com/lib/pq/oid/doc.go new file mode 100644 index 00000000000..caaede2489d --- /dev/null +++ b/vendor/github.com/lib/pq/oid/doc.go @@ -0,0 +1,6 @@ +// Package oid contains OID constants +// as defined by the Postgres server. +package oid + +// Oid is a Postgres Object ID. +type Oid uint32 diff --git a/vendor/github.com/lib/pq/oid/types.go b/vendor/github.com/lib/pq/oid/types.go new file mode 100644 index 00000000000..ecc84c2c862 --- /dev/null +++ b/vendor/github.com/lib/pq/oid/types.go @@ -0,0 +1,343 @@ +// Code generated by gen.go. DO NOT EDIT. + +package oid + +const ( + T_bool Oid = 16 + T_bytea Oid = 17 + T_char Oid = 18 + T_name Oid = 19 + T_int8 Oid = 20 + T_int2 Oid = 21 + T_int2vector Oid = 22 + T_int4 Oid = 23 + T_regproc Oid = 24 + T_text Oid = 25 + T_oid Oid = 26 + T_tid Oid = 27 + T_xid Oid = 28 + T_cid Oid = 29 + T_oidvector Oid = 30 + T_pg_ddl_command Oid = 32 + T_pg_type Oid = 71 + T_pg_attribute Oid = 75 + T_pg_proc Oid = 81 + T_pg_class Oid = 83 + T_json Oid = 114 + T_xml Oid = 142 + T__xml Oid = 143 + T_pg_node_tree Oid = 194 + T__json Oid = 199 + T_smgr Oid = 210 + T_index_am_handler Oid = 325 + T_point Oid = 600 + T_lseg Oid = 601 + T_path Oid = 602 + T_box Oid = 603 + T_polygon Oid = 604 + T_line Oid = 628 + T__line Oid = 629 + T_cidr Oid = 650 + T__cidr Oid = 651 + T_float4 Oid = 700 + T_float8 Oid = 701 + T_abstime Oid = 702 + T_reltime Oid = 703 + T_tinterval Oid = 704 + T_unknown Oid = 705 + T_circle Oid = 718 + T__circle Oid = 719 + T_money Oid = 790 + T__money Oid = 791 + T_macaddr Oid = 829 + T_inet Oid = 869 + T__bool Oid = 1000 + T__bytea Oid = 1001 + T__char Oid = 1002 + T__name Oid = 1003 + T__int2 Oid = 1005 + T__int2vector Oid = 1006 + T__int4 Oid = 1007 + T__regproc Oid = 1008 + T__text Oid = 1009 + T__tid Oid = 1010 + T__xid Oid = 1011 + T__cid Oid = 1012 + T__oidvector Oid = 1013 + T__bpchar Oid = 1014 + T__varchar Oid = 1015 + T__int8 Oid = 1016 + T__point Oid = 1017 + T__lseg Oid = 1018 + T__path Oid = 1019 + T__box Oid = 1020 + T__float4 Oid = 1021 + T__float8 Oid = 1022 + T__abstime Oid = 1023 + T__reltime Oid = 1024 + T__tinterval Oid = 1025 + T__polygon Oid = 1027 + T__oid Oid = 1028 + T_aclitem Oid = 1033 + T__aclitem Oid = 1034 + T__macaddr Oid = 1040 + T__inet Oid = 1041 + T_bpchar Oid = 1042 + T_varchar Oid = 1043 + T_date Oid = 1082 + T_time Oid = 1083 + T_timestamp Oid = 1114 + T__timestamp Oid = 1115 + T__date Oid = 1182 + T__time Oid = 1183 + T_timestamptz Oid = 1184 + T__timestamptz Oid = 1185 + T_interval Oid = 1186 + T__interval Oid = 1187 + T__numeric Oid = 1231 + T_pg_database Oid = 1248 + T__cstring Oid = 1263 + T_timetz Oid = 1266 + T__timetz Oid = 1270 + T_bit Oid = 1560 + T__bit Oid = 1561 + T_varbit Oid = 1562 + T__varbit Oid = 1563 + T_numeric Oid = 1700 + T_refcursor Oid = 1790 + T__refcursor Oid = 2201 + T_regprocedure Oid = 2202 + T_regoper Oid = 2203 + T_regoperator Oid = 2204 + T_regclass Oid = 2205 + T_regtype Oid = 2206 + T__regprocedure Oid = 2207 + T__regoper Oid = 2208 + T__regoperator Oid = 2209 + T__regclass Oid = 2210 + T__regtype Oid = 2211 + T_record Oid = 2249 + T_cstring Oid = 2275 + T_any Oid = 2276 + T_anyarray Oid = 2277 + T_void Oid = 2278 + T_trigger Oid = 2279 + T_language_handler Oid = 2280 + T_internal Oid = 2281 + T_opaque Oid = 2282 + T_anyelement Oid = 2283 + T__record Oid = 2287 + T_anynonarray Oid = 2776 + T_pg_authid Oid = 2842 + T_pg_auth_members Oid = 2843 + T__txid_snapshot Oid = 2949 + T_uuid Oid = 2950 + T__uuid Oid = 2951 + T_txid_snapshot Oid = 2970 + T_fdw_handler Oid = 3115 + T_pg_lsn Oid = 3220 + T__pg_lsn Oid = 3221 + T_tsm_handler Oid = 3310 + T_anyenum Oid = 3500 + T_tsvector Oid = 3614 + T_tsquery Oid = 3615 + T_gtsvector Oid = 3642 + T__tsvector Oid = 3643 + T__gtsvector Oid = 3644 + T__tsquery Oid = 3645 + T_regconfig Oid = 3734 + T__regconfig Oid = 3735 + T_regdictionary Oid = 3769 + T__regdictionary Oid = 3770 + T_jsonb Oid = 3802 + T__jsonb Oid = 3807 + T_anyrange Oid = 3831 + T_event_trigger Oid = 3838 + T_int4range Oid = 3904 + T__int4range Oid = 3905 + T_numrange Oid = 3906 + T__numrange Oid = 3907 + T_tsrange Oid = 3908 + T__tsrange Oid = 3909 + T_tstzrange Oid = 3910 + T__tstzrange Oid = 3911 + T_daterange Oid = 3912 + T__daterange Oid = 3913 + T_int8range Oid = 3926 + T__int8range Oid = 3927 + T_pg_shseclabel Oid = 4066 + T_regnamespace Oid = 4089 + T__regnamespace Oid = 4090 + T_regrole Oid = 4096 + T__regrole Oid = 4097 +) + +var TypeName = map[Oid]string{ + T_bool: "BOOL", + T_bytea: "BYTEA", + T_char: "CHAR", + T_name: "NAME", + T_int8: "INT8", + T_int2: "INT2", + T_int2vector: "INT2VECTOR", + T_int4: "INT4", + T_regproc: "REGPROC", + T_text: "TEXT", + T_oid: "OID", + T_tid: "TID", + T_xid: "XID", + T_cid: "CID", + T_oidvector: "OIDVECTOR", + T_pg_ddl_command: "PG_DDL_COMMAND", + T_pg_type: "PG_TYPE", + T_pg_attribute: "PG_ATTRIBUTE", + T_pg_proc: "PG_PROC", + T_pg_class: "PG_CLASS", + T_json: "JSON", + T_xml: "XML", + T__xml: "_XML", + T_pg_node_tree: "PG_NODE_TREE", + T__json: "_JSON", + T_smgr: "SMGR", + T_index_am_handler: "INDEX_AM_HANDLER", + T_point: "POINT", + T_lseg: "LSEG", + T_path: "PATH", + T_box: "BOX", + T_polygon: "POLYGON", + T_line: "LINE", + T__line: "_LINE", + T_cidr: "CIDR", + T__cidr: "_CIDR", + T_float4: "FLOAT4", + T_float8: "FLOAT8", + T_abstime: "ABSTIME", + T_reltime: "RELTIME", + T_tinterval: "TINTERVAL", + T_unknown: "UNKNOWN", + T_circle: "CIRCLE", + T__circle: "_CIRCLE", + T_money: "MONEY", + T__money: "_MONEY", + T_macaddr: "MACADDR", + T_inet: "INET", + T__bool: "_BOOL", + T__bytea: "_BYTEA", + T__char: "_CHAR", + T__name: "_NAME", + T__int2: "_INT2", + T__int2vector: "_INT2VECTOR", + T__int4: "_INT4", + T__regproc: "_REGPROC", + T__text: "_TEXT", + T__tid: "_TID", + T__xid: "_XID", + T__cid: "_CID", + T__oidvector: "_OIDVECTOR", + T__bpchar: "_BPCHAR", + T__varchar: "_VARCHAR", + T__int8: "_INT8", + T__point: "_POINT", + T__lseg: "_LSEG", + T__path: "_PATH", + T__box: "_BOX", + T__float4: "_FLOAT4", + T__float8: "_FLOAT8", + T__abstime: "_ABSTIME", + T__reltime: "_RELTIME", + T__tinterval: "_TINTERVAL", + T__polygon: "_POLYGON", + T__oid: "_OID", + T_aclitem: "ACLITEM", + T__aclitem: "_ACLITEM", + T__macaddr: "_MACADDR", + T__inet: "_INET", + T_bpchar: "BPCHAR", + T_varchar: "VARCHAR", + T_date: "DATE", + T_time: "TIME", + T_timestamp: "TIMESTAMP", + T__timestamp: "_TIMESTAMP", + T__date: "_DATE", + T__time: "_TIME", + T_timestamptz: "TIMESTAMPTZ", + T__timestamptz: "_TIMESTAMPTZ", + T_interval: "INTERVAL", + T__interval: "_INTERVAL", + T__numeric: "_NUMERIC", + T_pg_database: "PG_DATABASE", + T__cstring: "_CSTRING", + T_timetz: "TIMETZ", + T__timetz: "_TIMETZ", + T_bit: "BIT", + T__bit: "_BIT", + T_varbit: "VARBIT", + T__varbit: "_VARBIT", + T_numeric: "NUMERIC", + T_refcursor: "REFCURSOR", + T__refcursor: "_REFCURSOR", + T_regprocedure: "REGPROCEDURE", + T_regoper: "REGOPER", + T_regoperator: "REGOPERATOR", + T_regclass: "REGCLASS", + T_regtype: "REGTYPE", + T__regprocedure: "_REGPROCEDURE", + T__regoper: "_REGOPER", + T__regoperator: "_REGOPERATOR", + T__regclass: "_REGCLASS", + T__regtype: "_REGTYPE", + T_record: "RECORD", + T_cstring: "CSTRING", + T_any: "ANY", + T_anyarray: "ANYARRAY", + T_void: "VOID", + T_trigger: "TRIGGER", + T_language_handler: "LANGUAGE_HANDLER", + T_internal: "INTERNAL", + T_opaque: "OPAQUE", + T_anyelement: "ANYELEMENT", + T__record: "_RECORD", + T_anynonarray: "ANYNONARRAY", + T_pg_authid: "PG_AUTHID", + T_pg_auth_members: "PG_AUTH_MEMBERS", + T__txid_snapshot: "_TXID_SNAPSHOT", + T_uuid: "UUID", + T__uuid: "_UUID", + T_txid_snapshot: "TXID_SNAPSHOT", + T_fdw_handler: "FDW_HANDLER", + T_pg_lsn: "PG_LSN", + T__pg_lsn: "_PG_LSN", + T_tsm_handler: "TSM_HANDLER", + T_anyenum: "ANYENUM", + T_tsvector: "TSVECTOR", + T_tsquery: "TSQUERY", + T_gtsvector: "GTSVECTOR", + T__tsvector: "_TSVECTOR", + T__gtsvector: "_GTSVECTOR", + T__tsquery: "_TSQUERY", + T_regconfig: "REGCONFIG", + T__regconfig: "_REGCONFIG", + T_regdictionary: "REGDICTIONARY", + T__regdictionary: "_REGDICTIONARY", + T_jsonb: "JSONB", + T__jsonb: "_JSONB", + T_anyrange: "ANYRANGE", + T_event_trigger: "EVENT_TRIGGER", + T_int4range: "INT4RANGE", + T__int4range: "_INT4RANGE", + T_numrange: "NUMRANGE", + T__numrange: "_NUMRANGE", + T_tsrange: "TSRANGE", + T__tsrange: "_TSRANGE", + T_tstzrange: "TSTZRANGE", + T__tstzrange: "_TSTZRANGE", + T_daterange: "DATERANGE", + T__daterange: "_DATERANGE", + T_int8range: "INT8RANGE", + T__int8range: "_INT8RANGE", + T_pg_shseclabel: "PG_SHSECLABEL", + T_regnamespace: "REGNAMESPACE", + T__regnamespace: "_REGNAMESPACE", + T_regrole: "REGROLE", + T__regrole: "_REGROLE", +} diff --git a/vendor/github.com/lib/pq/rows.go b/vendor/github.com/lib/pq/rows.go new file mode 100644 index 00000000000..c6aa5b9a36a --- /dev/null +++ b/vendor/github.com/lib/pq/rows.go @@ -0,0 +1,93 @@ +package pq + +import ( + "math" + "reflect" + "time" + + "github.com/lib/pq/oid" +) + +const headerSize = 4 + +type fieldDesc struct { + // The object ID of the data type. + OID oid.Oid + // The data type size (see pg_type.typlen). + // Note that negative values denote variable-width types. + Len int + // The type modifier (see pg_attribute.atttypmod). + // The meaning of the modifier is type-specific. + Mod int +} + +func (fd fieldDesc) Type() reflect.Type { + switch fd.OID { + case oid.T_int8: + return reflect.TypeOf(int64(0)) + case oid.T_int4: + return reflect.TypeOf(int32(0)) + case oid.T_int2: + return reflect.TypeOf(int16(0)) + case oid.T_varchar, oid.T_text: + return reflect.TypeOf("") + case oid.T_bool: + return reflect.TypeOf(false) + case oid.T_date, oid.T_time, oid.T_timetz, oid.T_timestamp, oid.T_timestamptz: + return reflect.TypeOf(time.Time{}) + case oid.T_bytea: + return reflect.TypeOf([]byte(nil)) + default: + return reflect.TypeOf(new(interface{})).Elem() + } +} + +func (fd fieldDesc) Name() string { + return oid.TypeName[fd.OID] +} + +func (fd fieldDesc) Length() (length int64, ok bool) { + switch fd.OID { + case oid.T_text, oid.T_bytea: + return math.MaxInt64, true + case oid.T_varchar, oid.T_bpchar: + return int64(fd.Mod - headerSize), true + default: + return 0, false + } +} + +func (fd fieldDesc) PrecisionScale() (precision, scale int64, ok bool) { + switch fd.OID { + case oid.T_numeric, oid.T__numeric: + mod := fd.Mod - headerSize + precision = int64((mod >> 16) & 0xffff) + scale = int64(mod & 0xffff) + return precision, scale, true + default: + return 0, 0, false + } +} + +// ColumnTypeScanType returns the value type that can be used to scan types into. +func (rs *rows) ColumnTypeScanType(index int) reflect.Type { + return rs.colTyps[index].Type() +} + +// ColumnTypeDatabaseTypeName return the database system type name. +func (rs *rows) ColumnTypeDatabaseTypeName(index int) string { + return rs.colTyps[index].Name() +} + +// ColumnTypeLength returns the length of the column type if the column is a +// variable length type. If the column is not a variable length type ok +// should return false. +func (rs *rows) ColumnTypeLength(index int) (length int64, ok bool) { + return rs.colTyps[index].Length() +} + +// ColumnTypePrecisionScale should return the precision and scale for decimal +// types. If not applicable, ok should be false. +func (rs *rows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { + return rs.colTyps[index].PrecisionScale() +} diff --git a/vendor/github.com/lib/pq/scram/scram.go b/vendor/github.com/lib/pq/scram/scram.go new file mode 100644 index 00000000000..477216b6008 --- /dev/null +++ b/vendor/github.com/lib/pq/scram/scram.go @@ -0,0 +1,264 @@ +// Copyright (c) 2014 - Gustavo Niemeyer +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Package scram implements a SCRAM-{SHA-1,etc} client per RFC5802. +// +// http://tools.ietf.org/html/rfc5802 +// +package scram + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "encoding/base64" + "fmt" + "hash" + "strconv" + "strings" +) + +// Client implements a SCRAM-* client (SCRAM-SHA-1, SCRAM-SHA-256, etc). +// +// A Client may be used within a SASL conversation with logic resembling: +// +// var in []byte +// var client = scram.NewClient(sha1.New, user, pass) +// for client.Step(in) { +// out := client.Out() +// // send out to server +// in := serverOut +// } +// if client.Err() != nil { +// // auth failed +// } +// +type Client struct { + newHash func() hash.Hash + + user string + pass string + step int + out bytes.Buffer + err error + + clientNonce []byte + serverNonce []byte + saltedPass []byte + authMsg bytes.Buffer +} + +// NewClient returns a new SCRAM-* client with the provided hash algorithm. +// +// For SCRAM-SHA-256, for example, use: +// +// client := scram.NewClient(sha256.New, user, pass) +// +func NewClient(newHash func() hash.Hash, user, pass string) *Client { + c := &Client{ + newHash: newHash, + user: user, + pass: pass, + } + c.out.Grow(256) + c.authMsg.Grow(256) + return c +} + +// Out returns the data to be sent to the server in the current step. +func (c *Client) Out() []byte { + if c.out.Len() == 0 { + return nil + } + return c.out.Bytes() +} + +// Err returns the error that occurred, or nil if there were no errors. +func (c *Client) Err() error { + return c.err +} + +// SetNonce sets the client nonce to the provided value. +// If not set, the nonce is generated automatically out of crypto/rand on the first step. +func (c *Client) SetNonce(nonce []byte) { + c.clientNonce = nonce +} + +var escaper = strings.NewReplacer("=", "=3D", ",", "=2C") + +// Step processes the incoming data from the server and makes the +// next round of data for the server available via Client.Out. +// Step returns false if there are no errors and more data is +// still expected. +func (c *Client) Step(in []byte) bool { + c.out.Reset() + if c.step > 2 || c.err != nil { + return false + } + c.step++ + switch c.step { + case 1: + c.err = c.step1(in) + case 2: + c.err = c.step2(in) + case 3: + c.err = c.step3(in) + } + return c.step > 2 || c.err != nil +} + +func (c *Client) step1(in []byte) error { + if len(c.clientNonce) == 0 { + const nonceLen = 16 + buf := make([]byte, nonceLen+b64.EncodedLen(nonceLen)) + if _, err := rand.Read(buf[:nonceLen]); err != nil { + return fmt.Errorf("cannot read random SCRAM-SHA-256 nonce from operating system: %v", err) + } + c.clientNonce = buf[nonceLen:] + b64.Encode(c.clientNonce, buf[:nonceLen]) + } + c.authMsg.WriteString("n=") + escaper.WriteString(&c.authMsg, c.user) + c.authMsg.WriteString(",r=") + c.authMsg.Write(c.clientNonce) + + c.out.WriteString("n,,") + c.out.Write(c.authMsg.Bytes()) + return nil +} + +var b64 = base64.StdEncoding + +func (c *Client) step2(in []byte) error { + c.authMsg.WriteByte(',') + c.authMsg.Write(in) + + fields := bytes.Split(in, []byte(",")) + if len(fields) != 3 { + return fmt.Errorf("expected 3 fields in first SCRAM-SHA-256 server message, got %d: %q", len(fields), in) + } + if !bytes.HasPrefix(fields[0], []byte("r=")) || len(fields[0]) < 2 { + return fmt.Errorf("server sent an invalid SCRAM-SHA-256 nonce: %q", fields[0]) + } + if !bytes.HasPrefix(fields[1], []byte("s=")) || len(fields[1]) < 6 { + return fmt.Errorf("server sent an invalid SCRAM-SHA-256 salt: %q", fields[1]) + } + if !bytes.HasPrefix(fields[2], []byte("i=")) || len(fields[2]) < 6 { + return fmt.Errorf("server sent an invalid SCRAM-SHA-256 iteration count: %q", fields[2]) + } + + c.serverNonce = fields[0][2:] + if !bytes.HasPrefix(c.serverNonce, c.clientNonce) { + return fmt.Errorf("server SCRAM-SHA-256 nonce is not prefixed by client nonce: got %q, want %q+\"...\"", c.serverNonce, c.clientNonce) + } + + salt := make([]byte, b64.DecodedLen(len(fields[1][2:]))) + n, err := b64.Decode(salt, fields[1][2:]) + if err != nil { + return fmt.Errorf("cannot decode SCRAM-SHA-256 salt sent by server: %q", fields[1]) + } + salt = salt[:n] + iterCount, err := strconv.Atoi(string(fields[2][2:])) + if err != nil { + return fmt.Errorf("server sent an invalid SCRAM-SHA-256 iteration count: %q", fields[2]) + } + c.saltPassword(salt, iterCount) + + c.authMsg.WriteString(",c=biws,r=") + c.authMsg.Write(c.serverNonce) + + c.out.WriteString("c=biws,r=") + c.out.Write(c.serverNonce) + c.out.WriteString(",p=") + c.out.Write(c.clientProof()) + return nil +} + +func (c *Client) step3(in []byte) error { + var isv, ise bool + var fields = bytes.Split(in, []byte(",")) + if len(fields) == 1 { + isv = bytes.HasPrefix(fields[0], []byte("v=")) + ise = bytes.HasPrefix(fields[0], []byte("e=")) + } + if ise { + return fmt.Errorf("SCRAM-SHA-256 authentication error: %s", fields[0][2:]) + } else if !isv { + return fmt.Errorf("unsupported SCRAM-SHA-256 final message from server: %q", in) + } + if !bytes.Equal(c.serverSignature(), fields[0][2:]) { + return fmt.Errorf("cannot authenticate SCRAM-SHA-256 server signature: %q", fields[0][2:]) + } + return nil +} + +func (c *Client) saltPassword(salt []byte, iterCount int) { + mac := hmac.New(c.newHash, []byte(c.pass)) + mac.Write(salt) + mac.Write([]byte{0, 0, 0, 1}) + ui := mac.Sum(nil) + hi := make([]byte, len(ui)) + copy(hi, ui) + for i := 1; i < iterCount; i++ { + mac.Reset() + mac.Write(ui) + mac.Sum(ui[:0]) + for j, b := range ui { + hi[j] ^= b + } + } + c.saltedPass = hi +} + +func (c *Client) clientProof() []byte { + mac := hmac.New(c.newHash, c.saltedPass) + mac.Write([]byte("Client Key")) + clientKey := mac.Sum(nil) + hash := c.newHash() + hash.Write(clientKey) + storedKey := hash.Sum(nil) + mac = hmac.New(c.newHash, storedKey) + mac.Write(c.authMsg.Bytes()) + clientProof := mac.Sum(nil) + for i, b := range clientKey { + clientProof[i] ^= b + } + clientProof64 := make([]byte, b64.EncodedLen(len(clientProof))) + b64.Encode(clientProof64, clientProof) + return clientProof64 +} + +func (c *Client) serverSignature() []byte { + mac := hmac.New(c.newHash, c.saltedPass) + mac.Write([]byte("Server Key")) + serverKey := mac.Sum(nil) + + mac = hmac.New(c.newHash, serverKey) + mac.Write(c.authMsg.Bytes()) + serverSignature := mac.Sum(nil) + + encoded := make([]byte, b64.EncodedLen(len(serverSignature))) + b64.Encode(encoded, serverSignature) + return encoded +} diff --git a/vendor/github.com/lib/pq/ssl.go b/vendor/github.com/lib/pq/ssl.go new file mode 100644 index 00000000000..d9020845585 --- /dev/null +++ b/vendor/github.com/lib/pq/ssl.go @@ -0,0 +1,175 @@ +package pq + +import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" + "net" + "os" + "os/user" + "path/filepath" +) + +// ssl generates a function to upgrade a net.Conn based on the "sslmode" and +// related settings. The function is nil when no upgrade should take place. +func ssl(o values) (func(net.Conn) (net.Conn, error), error) { + verifyCaOnly := false + tlsConf := tls.Config{} + switch mode := o["sslmode"]; mode { + // "require" is the default. + case "", "require": + // We must skip TLS's own verification since it requires full + // verification since Go 1.3. + tlsConf.InsecureSkipVerify = true + + // From http://www.postgresql.org/docs/current/static/libpq-ssl.html: + // + // Note: For backwards compatibility with earlier versions of + // PostgreSQL, if a root CA file exists, the behavior of + // sslmode=require will be the same as that of verify-ca, meaning the + // server certificate is validated against the CA. Relying on this + // behavior is discouraged, and applications that need certificate + // validation should always use verify-ca or verify-full. + if sslrootcert, ok := o["sslrootcert"]; ok { + if _, err := os.Stat(sslrootcert); err == nil { + verifyCaOnly = true + } else { + delete(o, "sslrootcert") + } + } + case "verify-ca": + // We must skip TLS's own verification since it requires full + // verification since Go 1.3. + tlsConf.InsecureSkipVerify = true + verifyCaOnly = true + case "verify-full": + tlsConf.ServerName = o["host"] + case "disable": + return nil, nil + default: + return nil, fmterrorf(`unsupported sslmode %q; only "require" (default), "verify-full", "verify-ca", and "disable" supported`, mode) + } + + err := sslClientCertificates(&tlsConf, o) + if err != nil { + return nil, err + } + err = sslCertificateAuthority(&tlsConf, o) + if err != nil { + return nil, err + } + + // Accept renegotiation requests initiated by the backend. + // + // Renegotiation was deprecated then removed from PostgreSQL 9.5, but + // the default configuration of older versions has it enabled. Redshift + // also initiates renegotiations and cannot be reconfigured. + tlsConf.Renegotiation = tls.RenegotiateFreelyAsClient + + return func(conn net.Conn) (net.Conn, error) { + client := tls.Client(conn, &tlsConf) + if verifyCaOnly { + err := sslVerifyCertificateAuthority(client, &tlsConf) + if err != nil { + return nil, err + } + } + return client, nil + }, nil +} + +// sslClientCertificates adds the certificate specified in the "sslcert" and +// "sslkey" settings, or if they aren't set, from the .postgresql directory +// in the user's home directory. The configured files must exist and have +// the correct permissions. +func sslClientCertificates(tlsConf *tls.Config, o values) error { + // user.Current() might fail when cross-compiling. We have to ignore the + // error and continue without home directory defaults, since we wouldn't + // know from where to load them. + user, _ := user.Current() + + // In libpq, the client certificate is only loaded if the setting is not blank. + // + // https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L1036-L1037 + sslcert := o["sslcert"] + if len(sslcert) == 0 && user != nil { + sslcert = filepath.Join(user.HomeDir, ".postgresql", "postgresql.crt") + } + // https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L1045 + if len(sslcert) == 0 { + return nil + } + // https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L1050:L1054 + if _, err := os.Stat(sslcert); os.IsNotExist(err) { + return nil + } else if err != nil { + return err + } + + // In libpq, the ssl key is only loaded if the setting is not blank. + // + // https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L1123-L1222 + sslkey := o["sslkey"] + if len(sslkey) == 0 && user != nil { + sslkey = filepath.Join(user.HomeDir, ".postgresql", "postgresql.key") + } + + if len(sslkey) > 0 { + if err := sslKeyPermissions(sslkey); err != nil { + return err + } + } + + cert, err := tls.LoadX509KeyPair(sslcert, sslkey) + if err != nil { + return err + } + + tlsConf.Certificates = []tls.Certificate{cert} + return nil +} + +// sslCertificateAuthority adds the RootCA specified in the "sslrootcert" setting. +func sslCertificateAuthority(tlsConf *tls.Config, o values) error { + // In libpq, the root certificate is only loaded if the setting is not blank. + // + // https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L950-L951 + if sslrootcert := o["sslrootcert"]; len(sslrootcert) > 0 { + tlsConf.RootCAs = x509.NewCertPool() + + cert, err := ioutil.ReadFile(sslrootcert) + if err != nil { + return err + } + + if !tlsConf.RootCAs.AppendCertsFromPEM(cert) { + return fmterrorf("couldn't parse pem in sslrootcert") + } + } + + return nil +} + +// sslVerifyCertificateAuthority carries out a TLS handshake to the server and +// verifies the presented certificate against the CA, i.e. the one specified in +// sslrootcert or the system CA if sslrootcert was not specified. +func sslVerifyCertificateAuthority(client *tls.Conn, tlsConf *tls.Config) error { + err := client.Handshake() + if err != nil { + return err + } + certs := client.ConnectionState().PeerCertificates + opts := x509.VerifyOptions{ + DNSName: client.ConnectionState().ServerName, + Intermediates: x509.NewCertPool(), + Roots: tlsConf.RootCAs, + } + for i, cert := range certs { + if i == 0 { + continue + } + opts.Intermediates.AddCert(cert) + } + _, err = certs[0].Verify(opts) + return err +} diff --git a/vendor/github.com/lib/pq/ssl_permissions.go b/vendor/github.com/lib/pq/ssl_permissions.go new file mode 100644 index 00000000000..3b7c3a2a319 --- /dev/null +++ b/vendor/github.com/lib/pq/ssl_permissions.go @@ -0,0 +1,20 @@ +// +build !windows + +package pq + +import "os" + +// sslKeyPermissions checks the permissions on user-supplied ssl key files. +// The key file should have very little access. +// +// libpq does not check key file permissions on Windows. +func sslKeyPermissions(sslkey string) error { + info, err := os.Stat(sslkey) + if err != nil { + return err + } + if info.Mode().Perm()&0077 != 0 { + return ErrSSLKeyHasWorldPermissions + } + return nil +} diff --git a/vendor/github.com/lib/pq/ssl_windows.go b/vendor/github.com/lib/pq/ssl_windows.go new file mode 100644 index 00000000000..5d2c763cebc --- /dev/null +++ b/vendor/github.com/lib/pq/ssl_windows.go @@ -0,0 +1,9 @@ +// +build windows + +package pq + +// sslKeyPermissions checks the permissions on user-supplied ssl key files. +// The key file should have very little access. +// +// libpq does not check key file permissions on Windows. +func sslKeyPermissions(string) error { return nil } diff --git a/vendor/github.com/lib/pq/url.go b/vendor/github.com/lib/pq/url.go new file mode 100644 index 00000000000..f4d8a7c2062 --- /dev/null +++ b/vendor/github.com/lib/pq/url.go @@ -0,0 +1,76 @@ +package pq + +import ( + "fmt" + "net" + nurl "net/url" + "sort" + "strings" +) + +// ParseURL no longer needs to be used by clients of this library since supplying a URL as a +// connection string to sql.Open() is now supported: +// +// sql.Open("postgres", "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full") +// +// It remains exported here for backwards-compatibility. +// +// ParseURL converts a url to a connection string for driver.Open. +// Example: +// +// "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full" +// +// converts to: +// +// "user=bob password=secret host=1.2.3.4 port=5432 dbname=mydb sslmode=verify-full" +// +// A minimal example: +// +// "postgres://" +// +// This will be blank, causing driver.Open to use all of the defaults +func ParseURL(url string) (string, error) { + u, err := nurl.Parse(url) + if err != nil { + return "", err + } + + if u.Scheme != "postgres" && u.Scheme != "postgresql" { + return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) + } + + var kvs []string + escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`) + accrue := func(k, v string) { + if v != "" { + kvs = append(kvs, k+"="+escaper.Replace(v)) + } + } + + if u.User != nil { + v := u.User.Username() + accrue("user", v) + + v, _ = u.User.Password() + accrue("password", v) + } + + if host, port, err := net.SplitHostPort(u.Host); err != nil { + accrue("host", u.Host) + } else { + accrue("host", host) + accrue("port", port) + } + + if u.Path != "" { + accrue("dbname", u.Path[1:]) + } + + q := u.Query() + for k := range q { + accrue(k, q.Get(k)) + } + + sort.Strings(kvs) // Makes testing easier (not a performance concern) + return strings.Join(kvs, " "), nil +} diff --git a/vendor/github.com/lib/pq/user_posix.go b/vendor/github.com/lib/pq/user_posix.go new file mode 100644 index 00000000000..bf982524f93 --- /dev/null +++ b/vendor/github.com/lib/pq/user_posix.go @@ -0,0 +1,24 @@ +// Package pq is a pure Go Postgres driver for the database/sql package. + +// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris rumprun + +package pq + +import ( + "os" + "os/user" +) + +func userCurrent() (string, error) { + u, err := user.Current() + if err == nil { + return u.Username, nil + } + + name := os.Getenv("USER") + if name != "" { + return name, nil + } + + return "", ErrCouldNotDetectUsername +} diff --git a/vendor/github.com/lib/pq/user_windows.go b/vendor/github.com/lib/pq/user_windows.go new file mode 100644 index 00000000000..2b691267b97 --- /dev/null +++ b/vendor/github.com/lib/pq/user_windows.go @@ -0,0 +1,27 @@ +// Package pq is a pure Go Postgres driver for the database/sql package. +package pq + +import ( + "path/filepath" + "syscall" +) + +// Perform Windows user name lookup identically to libpq. +// +// The PostgreSQL code makes use of the legacy Win32 function +// GetUserName, and that function has not been imported into stock Go. +// GetUserNameEx is available though, the difference being that a +// wider range of names are available. To get the output to be the +// same as GetUserName, only the base (or last) component of the +// result is returned. +func userCurrent() (string, error) { + pw_name := make([]uint16, 128) + pwname_size := uint32(len(pw_name)) - 1 + err := syscall.GetUserNameEx(syscall.NameSamCompatible, &pw_name[0], &pwname_size) + if err != nil { + return "", ErrCouldNotDetectUsername + } + s := syscall.UTF16ToString(pw_name) + u := filepath.Base(s) + return u, nil +} diff --git a/vendor/github.com/lib/pq/uuid.go b/vendor/github.com/lib/pq/uuid.go new file mode 100644 index 00000000000..9a1b9e0748e --- /dev/null +++ b/vendor/github.com/lib/pq/uuid.go @@ -0,0 +1,23 @@ +package pq + +import ( + "encoding/hex" + "fmt" +) + +// decodeUUIDBinary interprets the binary format of a uuid, returning it in text format. +func decodeUUIDBinary(src []byte) ([]byte, error) { + if len(src) != 16 { + return nil, fmt.Errorf("pq: unable to decode uuid; bad length: %d", len(src)) + } + + dst := make([]byte, 36) + dst[8], dst[13], dst[18], dst[23] = '-', '-', '-', '-' + hex.Encode(dst[0:], src[0:4]) + hex.Encode(dst[9:], src[4:6]) + hex.Encode(dst[14:], src[6:8]) + hex.Encode(dst[19:], src[8:10]) + hex.Encode(dst[24:], src[10:16]) + + return dst, nil +} diff --git a/vendor/github.com/spf13/afero/go.mod b/vendor/github.com/spf13/afero/go.mod index ce1c3c53b9d..08685509957 100644 --- a/vendor/github.com/spf13/afero/go.mod +++ b/vendor/github.com/spf13/afero/go.mod @@ -1,5 +1,3 @@ module github.com/spf13/afero require golang.org/x/text v0.3.0 - -go 1.13 diff --git a/vendor/github.com/spf13/pflag/.travis.yml b/vendor/github.com/spf13/pflag/.travis.yml index f8a63b308ba..00d04cb9b02 100644 --- a/vendor/github.com/spf13/pflag/.travis.yml +++ b/vendor/github.com/spf13/pflag/.travis.yml @@ -3,8 +3,9 @@ sudo: false language: go go: - - 1.7.3 - - 1.8.1 + - 1.9.x + - 1.10.x + - 1.11.x - tip matrix: @@ -12,7 +13,7 @@ matrix: - go: tip install: - - go get github.com/golang/lint/golint + - go get golang.org/x/lint/golint - export PATH=$GOPATH/bin:$PATH - go install ./... diff --git a/vendor/github.com/spf13/pflag/README.md b/vendor/github.com/spf13/pflag/README.md index b052414d129..7eacc5bdbe5 100644 --- a/vendor/github.com/spf13/pflag/README.md +++ b/vendor/github.com/spf13/pflag/README.md @@ -86,8 +86,8 @@ fmt.Println("ip has value ", *ip) fmt.Println("flagvar has value ", flagvar) ``` -There are helpers function to get values later if you have the FlagSet but -it was difficult to keep up with all of the flag pointers in your code. +There are helper functions available to get the value stored in a Flag if you have a FlagSet but find +it difficult to keep up with all of the pointers in your code. If you have a pflag.FlagSet with a flag called 'flagname' of type int you can use GetInt() to get the int value. But notice that 'flagname' must exist and it must be an int. GetString("flagname") will fail. diff --git a/vendor/github.com/spf13/pflag/bool_slice.go b/vendor/github.com/spf13/pflag/bool_slice.go index 5af02f1a75a..3731370d6a5 100644 --- a/vendor/github.com/spf13/pflag/bool_slice.go +++ b/vendor/github.com/spf13/pflag/bool_slice.go @@ -71,6 +71,44 @@ func (s *boolSliceValue) String() string { return "[" + out + "]" } +func (s *boolSliceValue) fromString(val string) (bool, error) { + return strconv.ParseBool(val) +} + +func (s *boolSliceValue) toString(val bool) string { + return strconv.FormatBool(val) +} + +func (s *boolSliceValue) Append(val string) error { + i, err := s.fromString(val) + if err != nil { + return err + } + *s.value = append(*s.value, i) + return nil +} + +func (s *boolSliceValue) Replace(val []string) error { + out := make([]bool, len(val)) + for i, d := range val { + var err error + out[i], err = s.fromString(d) + if err != nil { + return err + } + } + *s.value = out + return nil +} + +func (s *boolSliceValue) GetSlice() []string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = s.toString(d) + } + return out +} + func boolSliceConv(val string) (interface{}, error) { val = strings.Trim(val, "[]") // Empty string would cause a slice with one (empty) entry diff --git a/vendor/github.com/spf13/pflag/count.go b/vendor/github.com/spf13/pflag/count.go index aa126e44d1c..a0b2679f71c 100644 --- a/vendor/github.com/spf13/pflag/count.go +++ b/vendor/github.com/spf13/pflag/count.go @@ -46,7 +46,7 @@ func (f *FlagSet) GetCount(name string) (int, error) { // CountVar defines a count flag with specified name, default value, and usage string. // The argument p points to an int variable in which to store the value of the flag. -// A count flag will add 1 to its value evey time it is found on the command line +// A count flag will add 1 to its value every time it is found on the command line func (f *FlagSet) CountVar(p *int, name string, usage string) { f.CountVarP(p, name, "", usage) } @@ -69,7 +69,7 @@ func CountVarP(p *int, name, shorthand string, usage string) { // Count defines a count flag with specified name, default value, and usage string. // The return value is the address of an int variable that stores the value of the flag. -// A count flag will add 1 to its value evey time it is found on the command line +// A count flag will add 1 to its value every time it is found on the command line func (f *FlagSet) Count(name string, usage string) *int { p := new(int) f.CountVarP(p, name, "", usage) diff --git a/vendor/github.com/spf13/pflag/duration_slice.go b/vendor/github.com/spf13/pflag/duration_slice.go index 52c6b6dc104..badadda53fd 100644 --- a/vendor/github.com/spf13/pflag/duration_slice.go +++ b/vendor/github.com/spf13/pflag/duration_slice.go @@ -51,6 +51,44 @@ func (s *durationSliceValue) String() string { return "[" + strings.Join(out, ",") + "]" } +func (s *durationSliceValue) fromString(val string) (time.Duration, error) { + return time.ParseDuration(val) +} + +func (s *durationSliceValue) toString(val time.Duration) string { + return fmt.Sprintf("%s", val) +} + +func (s *durationSliceValue) Append(val string) error { + i, err := s.fromString(val) + if err != nil { + return err + } + *s.value = append(*s.value, i) + return nil +} + +func (s *durationSliceValue) Replace(val []string) error { + out := make([]time.Duration, len(val)) + for i, d := range val { + var err error + out[i], err = s.fromString(d) + if err != nil { + return err + } + } + *s.value = out + return nil +} + +func (s *durationSliceValue) GetSlice() []string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = s.toString(d) + } + return out +} + func durationSliceConv(val string) (interface{}, error) { val = strings.Trim(val, "[]") // Empty string would cause a slice with one (empty) entry diff --git a/vendor/github.com/spf13/pflag/flag.go b/vendor/github.com/spf13/pflag/flag.go index 9beeda8ecca..24a5036e95b 100644 --- a/vendor/github.com/spf13/pflag/flag.go +++ b/vendor/github.com/spf13/pflag/flag.go @@ -57,9 +57,9 @@ that give one-letter shorthands for flags. You can use these by appending var ip = flag.IntP("flagname", "f", 1234, "help message") var flagvar bool func init() { - flag.BoolVarP("boolname", "b", true, "help message") + flag.BoolVarP(&flagvar, "boolname", "b", true, "help message") } - flag.VarP(&flagVar, "varname", "v", 1234, "help message") + flag.VarP(&flagval, "varname", "v", "help message") Shorthand letters can be used with single dashes on the command line. Boolean shorthand flags can be combined with other shorthand flags. @@ -190,6 +190,18 @@ type Value interface { Type() string } +// SliceValue is a secondary interface to all flags which hold a list +// of values. This allows full control over the value of list flags, +// and avoids complicated marshalling and unmarshalling to csv. +type SliceValue interface { + // Append adds the specified value to the end of the flag value list. + Append(string) error + // Replace will fully overwrite any data currently in the flag value list. + Replace([]string) error + // GetSlice returns the flag value list as an array of strings. + GetSlice() []string +} + // sortFlags returns the flags as a slice in lexicographical sorted order. func sortFlags(flags map[NormalizedName]*Flag) []*Flag { list := make(sort.StringSlice, len(flags)) diff --git a/vendor/github.com/spf13/pflag/float32_slice.go b/vendor/github.com/spf13/pflag/float32_slice.go new file mode 100644 index 00000000000..caa352741a6 --- /dev/null +++ b/vendor/github.com/spf13/pflag/float32_slice.go @@ -0,0 +1,174 @@ +package pflag + +import ( + "fmt" + "strconv" + "strings" +) + +// -- float32Slice Value +type float32SliceValue struct { + value *[]float32 + changed bool +} + +func newFloat32SliceValue(val []float32, p *[]float32) *float32SliceValue { + isv := new(float32SliceValue) + isv.value = p + *isv.value = val + return isv +} + +func (s *float32SliceValue) Set(val string) error { + ss := strings.Split(val, ",") + out := make([]float32, len(ss)) + for i, d := range ss { + var err error + var temp64 float64 + temp64, err = strconv.ParseFloat(d, 32) + if err != nil { + return err + } + out[i] = float32(temp64) + + } + if !s.changed { + *s.value = out + } else { + *s.value = append(*s.value, out...) + } + s.changed = true + return nil +} + +func (s *float32SliceValue) Type() string { + return "float32Slice" +} + +func (s *float32SliceValue) String() string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = fmt.Sprintf("%f", d) + } + return "[" + strings.Join(out, ",") + "]" +} + +func (s *float32SliceValue) fromString(val string) (float32, error) { + t64, err := strconv.ParseFloat(val, 32) + if err != nil { + return 0, err + } + return float32(t64), nil +} + +func (s *float32SliceValue) toString(val float32) string { + return fmt.Sprintf("%f", val) +} + +func (s *float32SliceValue) Append(val string) error { + i, err := s.fromString(val) + if err != nil { + return err + } + *s.value = append(*s.value, i) + return nil +} + +func (s *float32SliceValue) Replace(val []string) error { + out := make([]float32, len(val)) + for i, d := range val { + var err error + out[i], err = s.fromString(d) + if err != nil { + return err + } + } + *s.value = out + return nil +} + +func (s *float32SliceValue) GetSlice() []string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = s.toString(d) + } + return out +} + +func float32SliceConv(val string) (interface{}, error) { + val = strings.Trim(val, "[]") + // Empty string would cause a slice with one (empty) entry + if len(val) == 0 { + return []float32{}, nil + } + ss := strings.Split(val, ",") + out := make([]float32, len(ss)) + for i, d := range ss { + var err error + var temp64 float64 + temp64, err = strconv.ParseFloat(d, 32) + if err != nil { + return nil, err + } + out[i] = float32(temp64) + + } + return out, nil +} + +// GetFloat32Slice return the []float32 value of a flag with the given name +func (f *FlagSet) GetFloat32Slice(name string) ([]float32, error) { + val, err := f.getFlagType(name, "float32Slice", float32SliceConv) + if err != nil { + return []float32{}, err + } + return val.([]float32), nil +} + +// Float32SliceVar defines a float32Slice flag with specified name, default value, and usage string. +// The argument p points to a []float32 variable in which to store the value of the flag. +func (f *FlagSet) Float32SliceVar(p *[]float32, name string, value []float32, usage string) { + f.VarP(newFloat32SliceValue(value, p), name, "", usage) +} + +// Float32SliceVarP is like Float32SliceVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Float32SliceVarP(p *[]float32, name, shorthand string, value []float32, usage string) { + f.VarP(newFloat32SliceValue(value, p), name, shorthand, usage) +} + +// Float32SliceVar defines a float32[] flag with specified name, default value, and usage string. +// The argument p points to a float32[] variable in which to store the value of the flag. +func Float32SliceVar(p *[]float32, name string, value []float32, usage string) { + CommandLine.VarP(newFloat32SliceValue(value, p), name, "", usage) +} + +// Float32SliceVarP is like Float32SliceVar, but accepts a shorthand letter that can be used after a single dash. +func Float32SliceVarP(p *[]float32, name, shorthand string, value []float32, usage string) { + CommandLine.VarP(newFloat32SliceValue(value, p), name, shorthand, usage) +} + +// Float32Slice defines a []float32 flag with specified name, default value, and usage string. +// The return value is the address of a []float32 variable that stores the value of the flag. +func (f *FlagSet) Float32Slice(name string, value []float32, usage string) *[]float32 { + p := []float32{} + f.Float32SliceVarP(&p, name, "", value, usage) + return &p +} + +// Float32SliceP is like Float32Slice, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Float32SliceP(name, shorthand string, value []float32, usage string) *[]float32 { + p := []float32{} + f.Float32SliceVarP(&p, name, shorthand, value, usage) + return &p +} + +// Float32Slice defines a []float32 flag with specified name, default value, and usage string. +// The return value is the address of a []float32 variable that stores the value of the flag. +func Float32Slice(name string, value []float32, usage string) *[]float32 { + return CommandLine.Float32SliceP(name, "", value, usage) +} + +// Float32SliceP is like Float32Slice, but accepts a shorthand letter that can be used after a single dash. +func Float32SliceP(name, shorthand string, value []float32, usage string) *[]float32 { + return CommandLine.Float32SliceP(name, shorthand, value, usage) +} diff --git a/vendor/github.com/spf13/pflag/float64_slice.go b/vendor/github.com/spf13/pflag/float64_slice.go new file mode 100644 index 00000000000..85bf3073d50 --- /dev/null +++ b/vendor/github.com/spf13/pflag/float64_slice.go @@ -0,0 +1,166 @@ +package pflag + +import ( + "fmt" + "strconv" + "strings" +) + +// -- float64Slice Value +type float64SliceValue struct { + value *[]float64 + changed bool +} + +func newFloat64SliceValue(val []float64, p *[]float64) *float64SliceValue { + isv := new(float64SliceValue) + isv.value = p + *isv.value = val + return isv +} + +func (s *float64SliceValue) Set(val string) error { + ss := strings.Split(val, ",") + out := make([]float64, len(ss)) + for i, d := range ss { + var err error + out[i], err = strconv.ParseFloat(d, 64) + if err != nil { + return err + } + + } + if !s.changed { + *s.value = out + } else { + *s.value = append(*s.value, out...) + } + s.changed = true + return nil +} + +func (s *float64SliceValue) Type() string { + return "float64Slice" +} + +func (s *float64SliceValue) String() string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = fmt.Sprintf("%f", d) + } + return "[" + strings.Join(out, ",") + "]" +} + +func (s *float64SliceValue) fromString(val string) (float64, error) { + return strconv.ParseFloat(val, 64) +} + +func (s *float64SliceValue) toString(val float64) string { + return fmt.Sprintf("%f", val) +} + +func (s *float64SliceValue) Append(val string) error { + i, err := s.fromString(val) + if err != nil { + return err + } + *s.value = append(*s.value, i) + return nil +} + +func (s *float64SliceValue) Replace(val []string) error { + out := make([]float64, len(val)) + for i, d := range val { + var err error + out[i], err = s.fromString(d) + if err != nil { + return err + } + } + *s.value = out + return nil +} + +func (s *float64SliceValue) GetSlice() []string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = s.toString(d) + } + return out +} + +func float64SliceConv(val string) (interface{}, error) { + val = strings.Trim(val, "[]") + // Empty string would cause a slice with one (empty) entry + if len(val) == 0 { + return []float64{}, nil + } + ss := strings.Split(val, ",") + out := make([]float64, len(ss)) + for i, d := range ss { + var err error + out[i], err = strconv.ParseFloat(d, 64) + if err != nil { + return nil, err + } + + } + return out, nil +} + +// GetFloat64Slice return the []float64 value of a flag with the given name +func (f *FlagSet) GetFloat64Slice(name string) ([]float64, error) { + val, err := f.getFlagType(name, "float64Slice", float64SliceConv) + if err != nil { + return []float64{}, err + } + return val.([]float64), nil +} + +// Float64SliceVar defines a float64Slice flag with specified name, default value, and usage string. +// The argument p points to a []float64 variable in which to store the value of the flag. +func (f *FlagSet) Float64SliceVar(p *[]float64, name string, value []float64, usage string) { + f.VarP(newFloat64SliceValue(value, p), name, "", usage) +} + +// Float64SliceVarP is like Float64SliceVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Float64SliceVarP(p *[]float64, name, shorthand string, value []float64, usage string) { + f.VarP(newFloat64SliceValue(value, p), name, shorthand, usage) +} + +// Float64SliceVar defines a float64[] flag with specified name, default value, and usage string. +// The argument p points to a float64[] variable in which to store the value of the flag. +func Float64SliceVar(p *[]float64, name string, value []float64, usage string) { + CommandLine.VarP(newFloat64SliceValue(value, p), name, "", usage) +} + +// Float64SliceVarP is like Float64SliceVar, but accepts a shorthand letter that can be used after a single dash. +func Float64SliceVarP(p *[]float64, name, shorthand string, value []float64, usage string) { + CommandLine.VarP(newFloat64SliceValue(value, p), name, shorthand, usage) +} + +// Float64Slice defines a []float64 flag with specified name, default value, and usage string. +// The return value is the address of a []float64 variable that stores the value of the flag. +func (f *FlagSet) Float64Slice(name string, value []float64, usage string) *[]float64 { + p := []float64{} + f.Float64SliceVarP(&p, name, "", value, usage) + return &p +} + +// Float64SliceP is like Float64Slice, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Float64SliceP(name, shorthand string, value []float64, usage string) *[]float64 { + p := []float64{} + f.Float64SliceVarP(&p, name, shorthand, value, usage) + return &p +} + +// Float64Slice defines a []float64 flag with specified name, default value, and usage string. +// The return value is the address of a []float64 variable that stores the value of the flag. +func Float64Slice(name string, value []float64, usage string) *[]float64 { + return CommandLine.Float64SliceP(name, "", value, usage) +} + +// Float64SliceP is like Float64Slice, but accepts a shorthand letter that can be used after a single dash. +func Float64SliceP(name, shorthand string, value []float64, usage string) *[]float64 { + return CommandLine.Float64SliceP(name, shorthand, value, usage) +} diff --git a/vendor/github.com/spf13/pflag/go.mod b/vendor/github.com/spf13/pflag/go.mod new file mode 100644 index 00000000000..b2287eec134 --- /dev/null +++ b/vendor/github.com/spf13/pflag/go.mod @@ -0,0 +1,3 @@ +module github.com/spf13/pflag + +go 1.12 diff --git a/vendor/github.com/spf13/pflag/go.sum b/vendor/github.com/spf13/pflag/go.sum new file mode 100644 index 00000000000..e69de29bb2d diff --git a/vendor/github.com/spf13/pflag/int32_slice.go b/vendor/github.com/spf13/pflag/int32_slice.go new file mode 100644 index 00000000000..ff128ff06d8 --- /dev/null +++ b/vendor/github.com/spf13/pflag/int32_slice.go @@ -0,0 +1,174 @@ +package pflag + +import ( + "fmt" + "strconv" + "strings" +) + +// -- int32Slice Value +type int32SliceValue struct { + value *[]int32 + changed bool +} + +func newInt32SliceValue(val []int32, p *[]int32) *int32SliceValue { + isv := new(int32SliceValue) + isv.value = p + *isv.value = val + return isv +} + +func (s *int32SliceValue) Set(val string) error { + ss := strings.Split(val, ",") + out := make([]int32, len(ss)) + for i, d := range ss { + var err error + var temp64 int64 + temp64, err = strconv.ParseInt(d, 0, 32) + if err != nil { + return err + } + out[i] = int32(temp64) + + } + if !s.changed { + *s.value = out + } else { + *s.value = append(*s.value, out...) + } + s.changed = true + return nil +} + +func (s *int32SliceValue) Type() string { + return "int32Slice" +} + +func (s *int32SliceValue) String() string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = fmt.Sprintf("%d", d) + } + return "[" + strings.Join(out, ",") + "]" +} + +func (s *int32SliceValue) fromString(val string) (int32, error) { + t64, err := strconv.ParseInt(val, 0, 32) + if err != nil { + return 0, err + } + return int32(t64), nil +} + +func (s *int32SliceValue) toString(val int32) string { + return fmt.Sprintf("%d", val) +} + +func (s *int32SliceValue) Append(val string) error { + i, err := s.fromString(val) + if err != nil { + return err + } + *s.value = append(*s.value, i) + return nil +} + +func (s *int32SliceValue) Replace(val []string) error { + out := make([]int32, len(val)) + for i, d := range val { + var err error + out[i], err = s.fromString(d) + if err != nil { + return err + } + } + *s.value = out + return nil +} + +func (s *int32SliceValue) GetSlice() []string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = s.toString(d) + } + return out +} + +func int32SliceConv(val string) (interface{}, error) { + val = strings.Trim(val, "[]") + // Empty string would cause a slice with one (empty) entry + if len(val) == 0 { + return []int32{}, nil + } + ss := strings.Split(val, ",") + out := make([]int32, len(ss)) + for i, d := range ss { + var err error + var temp64 int64 + temp64, err = strconv.ParseInt(d, 0, 32) + if err != nil { + return nil, err + } + out[i] = int32(temp64) + + } + return out, nil +} + +// GetInt32Slice return the []int32 value of a flag with the given name +func (f *FlagSet) GetInt32Slice(name string) ([]int32, error) { + val, err := f.getFlagType(name, "int32Slice", int32SliceConv) + if err != nil { + return []int32{}, err + } + return val.([]int32), nil +} + +// Int32SliceVar defines a int32Slice flag with specified name, default value, and usage string. +// The argument p points to a []int32 variable in which to store the value of the flag. +func (f *FlagSet) Int32SliceVar(p *[]int32, name string, value []int32, usage string) { + f.VarP(newInt32SliceValue(value, p), name, "", usage) +} + +// Int32SliceVarP is like Int32SliceVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Int32SliceVarP(p *[]int32, name, shorthand string, value []int32, usage string) { + f.VarP(newInt32SliceValue(value, p), name, shorthand, usage) +} + +// Int32SliceVar defines a int32[] flag with specified name, default value, and usage string. +// The argument p points to a int32[] variable in which to store the value of the flag. +func Int32SliceVar(p *[]int32, name string, value []int32, usage string) { + CommandLine.VarP(newInt32SliceValue(value, p), name, "", usage) +} + +// Int32SliceVarP is like Int32SliceVar, but accepts a shorthand letter that can be used after a single dash. +func Int32SliceVarP(p *[]int32, name, shorthand string, value []int32, usage string) { + CommandLine.VarP(newInt32SliceValue(value, p), name, shorthand, usage) +} + +// Int32Slice defines a []int32 flag with specified name, default value, and usage string. +// The return value is the address of a []int32 variable that stores the value of the flag. +func (f *FlagSet) Int32Slice(name string, value []int32, usage string) *[]int32 { + p := []int32{} + f.Int32SliceVarP(&p, name, "", value, usage) + return &p +} + +// Int32SliceP is like Int32Slice, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Int32SliceP(name, shorthand string, value []int32, usage string) *[]int32 { + p := []int32{} + f.Int32SliceVarP(&p, name, shorthand, value, usage) + return &p +} + +// Int32Slice defines a []int32 flag with specified name, default value, and usage string. +// The return value is the address of a []int32 variable that stores the value of the flag. +func Int32Slice(name string, value []int32, usage string) *[]int32 { + return CommandLine.Int32SliceP(name, "", value, usage) +} + +// Int32SliceP is like Int32Slice, but accepts a shorthand letter that can be used after a single dash. +func Int32SliceP(name, shorthand string, value []int32, usage string) *[]int32 { + return CommandLine.Int32SliceP(name, shorthand, value, usage) +} diff --git a/vendor/github.com/spf13/pflag/int64_slice.go b/vendor/github.com/spf13/pflag/int64_slice.go new file mode 100644 index 00000000000..25464638f3a --- /dev/null +++ b/vendor/github.com/spf13/pflag/int64_slice.go @@ -0,0 +1,166 @@ +package pflag + +import ( + "fmt" + "strconv" + "strings" +) + +// -- int64Slice Value +type int64SliceValue struct { + value *[]int64 + changed bool +} + +func newInt64SliceValue(val []int64, p *[]int64) *int64SliceValue { + isv := new(int64SliceValue) + isv.value = p + *isv.value = val + return isv +} + +func (s *int64SliceValue) Set(val string) error { + ss := strings.Split(val, ",") + out := make([]int64, len(ss)) + for i, d := range ss { + var err error + out[i], err = strconv.ParseInt(d, 0, 64) + if err != nil { + return err + } + + } + if !s.changed { + *s.value = out + } else { + *s.value = append(*s.value, out...) + } + s.changed = true + return nil +} + +func (s *int64SliceValue) Type() string { + return "int64Slice" +} + +func (s *int64SliceValue) String() string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = fmt.Sprintf("%d", d) + } + return "[" + strings.Join(out, ",") + "]" +} + +func (s *int64SliceValue) fromString(val string) (int64, error) { + return strconv.ParseInt(val, 0, 64) +} + +func (s *int64SliceValue) toString(val int64) string { + return fmt.Sprintf("%d", val) +} + +func (s *int64SliceValue) Append(val string) error { + i, err := s.fromString(val) + if err != nil { + return err + } + *s.value = append(*s.value, i) + return nil +} + +func (s *int64SliceValue) Replace(val []string) error { + out := make([]int64, len(val)) + for i, d := range val { + var err error + out[i], err = s.fromString(d) + if err != nil { + return err + } + } + *s.value = out + return nil +} + +func (s *int64SliceValue) GetSlice() []string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = s.toString(d) + } + return out +} + +func int64SliceConv(val string) (interface{}, error) { + val = strings.Trim(val, "[]") + // Empty string would cause a slice with one (empty) entry + if len(val) == 0 { + return []int64{}, nil + } + ss := strings.Split(val, ",") + out := make([]int64, len(ss)) + for i, d := range ss { + var err error + out[i], err = strconv.ParseInt(d, 0, 64) + if err != nil { + return nil, err + } + + } + return out, nil +} + +// GetInt64Slice return the []int64 value of a flag with the given name +func (f *FlagSet) GetInt64Slice(name string) ([]int64, error) { + val, err := f.getFlagType(name, "int64Slice", int64SliceConv) + if err != nil { + return []int64{}, err + } + return val.([]int64), nil +} + +// Int64SliceVar defines a int64Slice flag with specified name, default value, and usage string. +// The argument p points to a []int64 variable in which to store the value of the flag. +func (f *FlagSet) Int64SliceVar(p *[]int64, name string, value []int64, usage string) { + f.VarP(newInt64SliceValue(value, p), name, "", usage) +} + +// Int64SliceVarP is like Int64SliceVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Int64SliceVarP(p *[]int64, name, shorthand string, value []int64, usage string) { + f.VarP(newInt64SliceValue(value, p), name, shorthand, usage) +} + +// Int64SliceVar defines a int64[] flag with specified name, default value, and usage string. +// The argument p points to a int64[] variable in which to store the value of the flag. +func Int64SliceVar(p *[]int64, name string, value []int64, usage string) { + CommandLine.VarP(newInt64SliceValue(value, p), name, "", usage) +} + +// Int64SliceVarP is like Int64SliceVar, but accepts a shorthand letter that can be used after a single dash. +func Int64SliceVarP(p *[]int64, name, shorthand string, value []int64, usage string) { + CommandLine.VarP(newInt64SliceValue(value, p), name, shorthand, usage) +} + +// Int64Slice defines a []int64 flag with specified name, default value, and usage string. +// The return value is the address of a []int64 variable that stores the value of the flag. +func (f *FlagSet) Int64Slice(name string, value []int64, usage string) *[]int64 { + p := []int64{} + f.Int64SliceVarP(&p, name, "", value, usage) + return &p +} + +// Int64SliceP is like Int64Slice, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) Int64SliceP(name, shorthand string, value []int64, usage string) *[]int64 { + p := []int64{} + f.Int64SliceVarP(&p, name, shorthand, value, usage) + return &p +} + +// Int64Slice defines a []int64 flag with specified name, default value, and usage string. +// The return value is the address of a []int64 variable that stores the value of the flag. +func Int64Slice(name string, value []int64, usage string) *[]int64 { + return CommandLine.Int64SliceP(name, "", value, usage) +} + +// Int64SliceP is like Int64Slice, but accepts a shorthand letter that can be used after a single dash. +func Int64SliceP(name, shorthand string, value []int64, usage string) *[]int64 { + return CommandLine.Int64SliceP(name, shorthand, value, usage) +} diff --git a/vendor/github.com/spf13/pflag/int_slice.go b/vendor/github.com/spf13/pflag/int_slice.go index 1e7c9edde95..e71c39d91aa 100644 --- a/vendor/github.com/spf13/pflag/int_slice.go +++ b/vendor/github.com/spf13/pflag/int_slice.go @@ -51,6 +51,36 @@ func (s *intSliceValue) String() string { return "[" + strings.Join(out, ",") + "]" } +func (s *intSliceValue) Append(val string) error { + i, err := strconv.Atoi(val) + if err != nil { + return err + } + *s.value = append(*s.value, i) + return nil +} + +func (s *intSliceValue) Replace(val []string) error { + out := make([]int, len(val)) + for i, d := range val { + var err error + out[i], err = strconv.Atoi(d) + if err != nil { + return err + } + } + *s.value = out + return nil +} + +func (s *intSliceValue) GetSlice() []string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = strconv.Itoa(d) + } + return out +} + func intSliceConv(val string) (interface{}, error) { val = strings.Trim(val, "[]") // Empty string would cause a slice with one (empty) entry diff --git a/vendor/github.com/spf13/pflag/ip_slice.go b/vendor/github.com/spf13/pflag/ip_slice.go index 7dd196fe3fb..775faae4fd8 100644 --- a/vendor/github.com/spf13/pflag/ip_slice.go +++ b/vendor/github.com/spf13/pflag/ip_slice.go @@ -72,9 +72,47 @@ func (s *ipSliceValue) String() string { return "[" + out + "]" } +func (s *ipSliceValue) fromString(val string) (net.IP, error) { + return net.ParseIP(strings.TrimSpace(val)), nil +} + +func (s *ipSliceValue) toString(val net.IP) string { + return val.String() +} + +func (s *ipSliceValue) Append(val string) error { + i, err := s.fromString(val) + if err != nil { + return err + } + *s.value = append(*s.value, i) + return nil +} + +func (s *ipSliceValue) Replace(val []string) error { + out := make([]net.IP, len(val)) + for i, d := range val { + var err error + out[i], err = s.fromString(d) + if err != nil { + return err + } + } + *s.value = out + return nil +} + +func (s *ipSliceValue) GetSlice() []string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = s.toString(d) + } + return out +} + func ipSliceConv(val string) (interface{}, error) { val = strings.Trim(val, "[]") - // Emtpy string would cause a slice with one (empty) entry + // Empty string would cause a slice with one (empty) entry if len(val) == 0 { return []net.IP{}, nil } diff --git a/vendor/github.com/spf13/pflag/string_array.go b/vendor/github.com/spf13/pflag/string_array.go index fa7bc60187a..4894af81802 100644 --- a/vendor/github.com/spf13/pflag/string_array.go +++ b/vendor/github.com/spf13/pflag/string_array.go @@ -23,6 +23,32 @@ func (s *stringArrayValue) Set(val string) error { return nil } +func (s *stringArrayValue) Append(val string) error { + *s.value = append(*s.value, val) + return nil +} + +func (s *stringArrayValue) Replace(val []string) error { + out := make([]string, len(val)) + for i, d := range val { + var err error + out[i] = d + if err != nil { + return err + } + } + *s.value = out + return nil +} + +func (s *stringArrayValue) GetSlice() []string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = d + } + return out +} + func (s *stringArrayValue) Type() string { return "stringArray" } diff --git a/vendor/github.com/spf13/pflag/string_slice.go b/vendor/github.com/spf13/pflag/string_slice.go index 0cd3ccc083e..3cb2e69dba0 100644 --- a/vendor/github.com/spf13/pflag/string_slice.go +++ b/vendor/github.com/spf13/pflag/string_slice.go @@ -62,6 +62,20 @@ func (s *stringSliceValue) String() string { return "[" + str + "]" } +func (s *stringSliceValue) Append(val string) error { + *s.value = append(*s.value, val) + return nil +} + +func (s *stringSliceValue) Replace(val []string) error { + *s.value = val + return nil +} + +func (s *stringSliceValue) GetSlice() []string { + return *s.value +} + func stringSliceConv(sval string) (interface{}, error) { sval = sval[1 : len(sval)-1] // An empty string would cause a slice with one (empty) string @@ -84,7 +98,7 @@ func (f *FlagSet) GetStringSlice(name string) ([]string, error) { // The argument p points to a []string variable in which to store the value of the flag. // Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. // For example: -// --ss="v1,v2" -ss="v3" +// --ss="v1,v2" --ss="v3" // will result in // []string{"v1", "v2", "v3"} func (f *FlagSet) StringSliceVar(p *[]string, name string, value []string, usage string) { @@ -100,7 +114,7 @@ func (f *FlagSet) StringSliceVarP(p *[]string, name, shorthand string, value []s // The argument p points to a []string variable in which to store the value of the flag. // Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. // For example: -// --ss="v1,v2" -ss="v3" +// --ss="v1,v2" --ss="v3" // will result in // []string{"v1", "v2", "v3"} func StringSliceVar(p *[]string, name string, value []string, usage string) { @@ -116,7 +130,7 @@ func StringSliceVarP(p *[]string, name, shorthand string, value []string, usage // The return value is the address of a []string variable that stores the value of the flag. // Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. // For example: -// --ss="v1,v2" -ss="v3" +// --ss="v1,v2" --ss="v3" // will result in // []string{"v1", "v2", "v3"} func (f *FlagSet) StringSlice(name string, value []string, usage string) *[]string { @@ -136,7 +150,7 @@ func (f *FlagSet) StringSliceP(name, shorthand string, value []string, usage str // The return value is the address of a []string variable that stores the value of the flag. // Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly. // For example: -// --ss="v1,v2" -ss="v3" +// --ss="v1,v2" --ss="v3" // will result in // []string{"v1", "v2", "v3"} func StringSlice(name string, value []string, usage string) *[]string { diff --git a/vendor/github.com/spf13/pflag/string_to_int64.go b/vendor/github.com/spf13/pflag/string_to_int64.go new file mode 100644 index 00000000000..a807a04a0ba --- /dev/null +++ b/vendor/github.com/spf13/pflag/string_to_int64.go @@ -0,0 +1,149 @@ +package pflag + +import ( + "bytes" + "fmt" + "strconv" + "strings" +) + +// -- stringToInt64 Value +type stringToInt64Value struct { + value *map[string]int64 + changed bool +} + +func newStringToInt64Value(val map[string]int64, p *map[string]int64) *stringToInt64Value { + ssv := new(stringToInt64Value) + ssv.value = p + *ssv.value = val + return ssv +} + +// Format: a=1,b=2 +func (s *stringToInt64Value) Set(val string) error { + ss := strings.Split(val, ",") + out := make(map[string]int64, len(ss)) + for _, pair := range ss { + kv := strings.SplitN(pair, "=", 2) + if len(kv) != 2 { + return fmt.Errorf("%s must be formatted as key=value", pair) + } + var err error + out[kv[0]], err = strconv.ParseInt(kv[1], 10, 64) + if err != nil { + return err + } + } + if !s.changed { + *s.value = out + } else { + for k, v := range out { + (*s.value)[k] = v + } + } + s.changed = true + return nil +} + +func (s *stringToInt64Value) Type() string { + return "stringToInt64" +} + +func (s *stringToInt64Value) String() string { + var buf bytes.Buffer + i := 0 + for k, v := range *s.value { + if i > 0 { + buf.WriteRune(',') + } + buf.WriteString(k) + buf.WriteRune('=') + buf.WriteString(strconv.FormatInt(v, 10)) + i++ + } + return "[" + buf.String() + "]" +} + +func stringToInt64Conv(val string) (interface{}, error) { + val = strings.Trim(val, "[]") + // An empty string would cause an empty map + if len(val) == 0 { + return map[string]int64{}, nil + } + ss := strings.Split(val, ",") + out := make(map[string]int64, len(ss)) + for _, pair := range ss { + kv := strings.SplitN(pair, "=", 2) + if len(kv) != 2 { + return nil, fmt.Errorf("%s must be formatted as key=value", pair) + } + var err error + out[kv[0]], err = strconv.ParseInt(kv[1], 10, 64) + if err != nil { + return nil, err + } + } + return out, nil +} + +// GetStringToInt64 return the map[string]int64 value of a flag with the given name +func (f *FlagSet) GetStringToInt64(name string) (map[string]int64, error) { + val, err := f.getFlagType(name, "stringToInt64", stringToInt64Conv) + if err != nil { + return map[string]int64{}, err + } + return val.(map[string]int64), nil +} + +// StringToInt64Var defines a string flag with specified name, default value, and usage string. +// The argument p point64s to a map[string]int64 variable in which to store the values of the multiple flags. +// The value of each argument will not try to be separated by comma +func (f *FlagSet) StringToInt64Var(p *map[string]int64, name string, value map[string]int64, usage string) { + f.VarP(newStringToInt64Value(value, p), name, "", usage) +} + +// StringToInt64VarP is like StringToInt64Var, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) StringToInt64VarP(p *map[string]int64, name, shorthand string, value map[string]int64, usage string) { + f.VarP(newStringToInt64Value(value, p), name, shorthand, usage) +} + +// StringToInt64Var defines a string flag with specified name, default value, and usage string. +// The argument p point64s to a map[string]int64 variable in which to store the value of the flag. +// The value of each argument will not try to be separated by comma +func StringToInt64Var(p *map[string]int64, name string, value map[string]int64, usage string) { + CommandLine.VarP(newStringToInt64Value(value, p), name, "", usage) +} + +// StringToInt64VarP is like StringToInt64Var, but accepts a shorthand letter that can be used after a single dash. +func StringToInt64VarP(p *map[string]int64, name, shorthand string, value map[string]int64, usage string) { + CommandLine.VarP(newStringToInt64Value(value, p), name, shorthand, usage) +} + +// StringToInt64 defines a string flag with specified name, default value, and usage string. +// The return value is the address of a map[string]int64 variable that stores the value of the flag. +// The value of each argument will not try to be separated by comma +func (f *FlagSet) StringToInt64(name string, value map[string]int64, usage string) *map[string]int64 { + p := map[string]int64{} + f.StringToInt64VarP(&p, name, "", value, usage) + return &p +} + +// StringToInt64P is like StringToInt64, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) StringToInt64P(name, shorthand string, value map[string]int64, usage string) *map[string]int64 { + p := map[string]int64{} + f.StringToInt64VarP(&p, name, shorthand, value, usage) + return &p +} + +// StringToInt64 defines a string flag with specified name, default value, and usage string. +// The return value is the address of a map[string]int64 variable that stores the value of the flag. +// The value of each argument will not try to be separated by comma +func StringToInt64(name string, value map[string]int64, usage string) *map[string]int64 { + return CommandLine.StringToInt64P(name, "", value, usage) +} + +// StringToInt64P is like StringToInt64, but accepts a shorthand letter that can be used after a single dash. +func StringToInt64P(name, shorthand string, value map[string]int64, usage string) *map[string]int64 { + return CommandLine.StringToInt64P(name, shorthand, value, usage) +} diff --git a/vendor/github.com/spf13/pflag/uint_slice.go b/vendor/github.com/spf13/pflag/uint_slice.go index edd94c600af..5fa924835ed 100644 --- a/vendor/github.com/spf13/pflag/uint_slice.go +++ b/vendor/github.com/spf13/pflag/uint_slice.go @@ -50,6 +50,48 @@ func (s *uintSliceValue) String() string { return "[" + strings.Join(out, ",") + "]" } +func (s *uintSliceValue) fromString(val string) (uint, error) { + t, err := strconv.ParseUint(val, 10, 0) + if err != nil { + return 0, err + } + return uint(t), nil +} + +func (s *uintSliceValue) toString(val uint) string { + return fmt.Sprintf("%d", val) +} + +func (s *uintSliceValue) Append(val string) error { + i, err := s.fromString(val) + if err != nil { + return err + } + *s.value = append(*s.value, i) + return nil +} + +func (s *uintSliceValue) Replace(val []string) error { + out := make([]uint, len(val)) + for i, d := range val { + var err error + out[i], err = s.fromString(d) + if err != nil { + return err + } + } + *s.value = out + return nil +} + +func (s *uintSliceValue) GetSlice() []string { + out := make([]string, len(*s.value)) + for i, d := range *s.value { + out[i] = s.toString(d) + } + return out +} + func uintSliceConv(val string) (interface{}, error) { val = strings.Trim(val, "[]") // Empty string would cause a slice with one (empty) entry diff --git a/vendor/github.com/spf13/viper/go.mod b/vendor/github.com/spf13/viper/go.mod index 439d2d879ca..86e801c150b 100644 --- a/vendor/github.com/spf13/viper/go.mod +++ b/vendor/github.com/spf13/viper/go.mod @@ -22,5 +22,3 @@ require ( golang.org/x/text v0.3.0 // indirect gopkg.in/yaml.v2 v2.2.2 ) - -go 1.13 diff --git a/vendor/gopkg.in/testfixtures.v2/.travis.yml b/vendor/gopkg.in/testfixtures.v2/.travis.yml deleted file mode 100644 index 97f66adf6b0..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -language: go - -go: - - '1.10' - - '1.11' - -services: - - postgresql - - mysql - -addons: - postgresql: "9.4" - -before_script: - - mysql -e 'CREATE DATABASE testfixtures_test;' - - psql -c 'CREATE DATABASE testfixtures_test;' -U postgres - -install: - - go get -t -tags 'sqlite postgresql mysql' ./... - - curl -s https://raw.githubusercontent.com/go-task/task/master/install-task.sh | sh - - bin/task dl-deps - - cp .sample.env .env - -script: - - bin/task lint - - bin/task test-free diff --git a/vendor/gopkg.in/testfixtures.v2/README.md b/vendor/gopkg.in/testfixtures.v2/README.md deleted file mode 100644 index 7375de97d63..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/README.md +++ /dev/null @@ -1,388 +0,0 @@ -# Go Test Fixtures - -[![GoDoc](https://godoc.org/gopkg.in/testfixtures.v2?status.svg)](https://godoc.org/gopkg.in/testfixtures.v2) -[![Go Report Card](https://goreportcard.com/badge/github.com/go-testfixtures/testfixtures)](https://goreportcard.com/report/github.com/go-testfixtures/testfixtures) -[![Build Status](https://travis-ci.org/go-testfixtures/testfixtures.svg?branch=master)](https://travis-ci.org/go-testfixtures/testfixtures) -[![Build status](https://ci.appveyor.com/api/projects/status/d2h6gq37wxbus1x7?svg=true)](https://ci.appveyor.com/project/andreynering/testfixtures) - -> ***Warning***: this package will wipe the database data before loading the -fixtures! It is supposed to be used on a test database. Please, double check -if you are running it against the correct database. - -Writing tests is hard, even more when you have to deal with an SQL database. -This package aims to make writing functional tests for web apps written in -Go easier. - -Basically this package mimics the ["Rails' way"][railstests] of writing tests -for database applications, where sample data is kept in fixtures files. Before -the execution of every test, the test database is cleaned and the fixture data -is loaded into the database. - -The idea is running tests against a real database, instead of relying in mocks, -which is boring to setup and may lead to production bugs not being caught in -the tests. - -## Installation - -First, get it: - -```bash -go get -u -v gopkg.in/testfixtures.v2 -``` - -## Usage - -Create a folder for the fixture files. Each file should contain data for a -single table and have the name `.yml`: - -``` -myapp/ - myapp.go - myapp_test.go - ... - fixtures/ - posts.yml - comments.yml - tags.yml - posts_tags.yml - ... -``` - -The file would look like this (it can have as many record you want): - -```yml -# comments.yml -- id: 1 - post_id: 1 - content: A comment... - author_name: John Doe - author_email: john@doe.com - created_at: 2016-01-01 12:30:12 - updated_at: 2016-01-01 12:30:12 - -- id: 2 - post_id: 2 - content: Another comment... - author_name: John Doe - author_email: john@doe.com - created_at: 2016-01-01 12:30:12 - updated_at: 2016-01-01 12:30:12 - -# ... -``` - -An YAML object or array will be converted to JSON. It can be stored on a native -JSON type like JSONB on PostgreSQL or as a TEXT or VARCHAR column on other -databases. - -```yml -- id: 1 - post_attributes: - author: John Due - author_email: john@due.com - title: "..." - tags: - - programming - - go - - testing - post: "..." -``` - -If you need to write raw SQL, probably to call a function, prefix the value -of the column with `RAW=`: - -```yml -- id: 1 - uuid_column: RAW=uuid_generate_v4() - postgis_type_column: RAW=ST_GeomFromText('params...') - created_at: RAW=NOW() - updated_at: RAW=NOW() -``` - -Your tests would look like this: - -```go -package myapp - -import ( - "database/sql" - "log" - - _ "github.com/lib/pq" - "gopkg.in/testfixtures.v2" -) - -var ( - db *sql.DB - fixtures *testfixtures.Context -) - -func TestMain(m *testing.M) { - var err error - - // Open connection with the test database. - // Do NOT import fixtures in a production database! - // Existing data would be deleted - db, err = sql.Open("postgres", "dbname=myapp_test") - if err != nil { - log.Fatal(err) - } - - // creating the context that hold the fixtures - // see about all compatible databases in this page below -    fixtures, err = testfixtures.NewFolder(db, &testfixtures.PostgreSQL{}, "testdata/fixtures") - if err != nil { - log.Fatal(err) - } - - os.Exit(m.Run()) -} - -func prepareTestDatabase() { - if err := fixtures.Load(); err != nil { - log.Fatal(err) - } -} - -func TestX(t *testing.T) { - prepareTestDatabase() - // your test here ... -} - -func TestY(t *testing.T) { - prepareTestDatabase() - // your test here ... -} - -func TestZ(t *testing.T) { - prepareTestDatabase() - // your test here ... -} -``` - -Alternatively, you can use the `NewFiles` function, to specify which -files you want to load into the database: - -```go -fixtures, err := testfixtures.NewFiles(db, &testfixtures.PostgreSQL{}, - "fixtures/orders.yml", - "fixtures/customers.yml", - // add as many files you want -) -if err != nil { - log.Fatal(err) -} -``` - -## Security check - -In order to prevent you from accidentally wiping the wrong database, this -package will refuse to load fixtures if the database name (or database -filename for SQLite) doesn't contains "test". If you want to disable this -check, use: - -```go -testfixtures.SkipDatabaseNameCheck(true) -``` - -## Sequences - -For PostgreSQL or Oracle, this package also resets all sequences to a high -number to prevent duplicated primary keys while running the tests. -The default is 10000, but you can change that with: - -```go -testfixtures.ResetSequencesTo(10000) -``` - -## Compatible databases - -### PostgreSQL - -This package has two approaches to disable foreign keys while importing fixtures -in PostgreSQL databases: - -#### With `DISABLE TRIGGER` - -This is the default approach. For that use: - -```go -&testfixtures.PostgreSQL{} -``` - -With the above snippet this package will use `DISABLE TRIGGER` to temporarily -disabling foreign key constraints while loading fixtures. This work with any -version of PostgreSQL, but it is **required** to be connected in the database -as a SUPERUSER. You can make a PostgreSQL user a SUPERUSER with: - -```sql -ALTER USER your_user SUPERUSER; -``` - -#### With `ALTER CONSTRAINT` - -This approach don't require to be connected as a SUPERUSER, but only work with -PostgreSQL versions >= 9.4. Try this if you are getting foreign key violation -errors with the previous approach. It is as simple as using: - -```go -&testfixtures.PostgreSQL{UseAlterConstraint: true} -``` - -#### Skipping reset of sequences - -You can skip the reset of PostgreSQL sequences if you're debugging a problem -with it, but most of the time you shouldn't do it: - -```go -&testfixtures.PostgreSQL{SkipResetSequences: true} -``` - -### MySQL / MariaDB - -Just make sure the connection string have -[the multistatement parameter](https://github.com/go-sql-driver/mysql#multistatements) -set to true, and use: - -```go -&testfixtures.MySQL{} -``` - -### SQLite - -SQLite is also supported. It is recommended to create foreign keys as -`DEFERRABLE` (the default) to prevent problems. See more -[on the SQLite documentation](https://www.sqlite.org/foreignkeys.html#fk_deferred). -(Foreign key constraints are no-op by default on SQLite, but enabling it is -recommended). - -```go -&testfixtures.SQLite{} -``` - -### Microsoft SQL Server - -SQL Server support requires SQL Server >= 2008. Inserting on `IDENTITY` columns -are handled as well. Just make sure you are logged in with a user with -`ALTER TABLE` permission. - -```go -&testfixtures.SQLServer{} -``` - -### Oracle - -Oracle is supported as well. Use: - -```go -&testfixtures.Oracle{} -``` - -## Generating fixtures for a existing database (experimental) - -The following code will generate a YAML file for each table of the database in -the given folder. It may be useful to boostrap a test scenario from a sample -database of your app. - -```go -err := testfixtures.GenerateFixtures(db, &testfixtures.PostgreSQL{}, "testdata/fixtures") -if err != nil { - log.Fatalf("Error generating fixtures: %v", err) -} -``` - -Or - -```go -err := testfixtures.GenerateFixturesForTables( - db, - []*TableInfo{ - &TableInfo{Name: "table_name", Where: "foo = 'bar'"}, - // ... - }, - &testfixtures.PostgreSQL{}, - "testdata/fixtures", -) -if err != nil { - log.Fatalf("Error generating fixtures: %v", err) -} -``` - -> This was thought to run in small sample databases. It will likely break -if run in a production/big database. - -## Gotchas - -### Parallel testing - -This library doesn't yet support running tests in parallel! Running tests -in parallel can result in random data being present in the database, which -will likely cause tests to randomly/intermittently fail. - -This is specially tricky since it's not immediately clear that `go test ./...` -run tests for each package in parallel. If more than one package use this -library, you can face this issue. Please, use `go test -p 1 ./...` or run tests -for each package in separated commands to fix this issue. - -See [#40](https://github.com/go-testfixtures/testfixtures/issues/40) -and [golang/go#11521](https://github.com/golang/go/issues/11521) for more information. - -We're also planning to implement transactional tests to allow running tests -in parallel (see [#24](https://github.com/go-testfixtures/testfixtures/issues/24)). -Running each test package in a separated database would also allow you to do that. -Open issues for other ideas :slightly_smiling_face:. - -## Contributing - -Tests were written to ensure everything work as expected. You can run the tests -with: - -```bash -# running tests for PostgreSQL -go test -tags postgresql - -# running test for MySQL -go test -tags mysql - -# running tests for SQLite -go test -tags sqlite - -# running tests for SQL Server -go test -tags sqlserver - -# running tests for Oracle -go test -tags oracle - -# running test for multiple databases at once -go test -tags 'sqlite postgresql mysql' - -# running tests + benchmark -go test -v -bench=. -tags postgresql -``` - -Travis runs tests for PostgreSQL, MySQL and SQLite. AppVeyor run for all -these and also Microsoft SQL Server. - -To set the connection string of tests for each database, copy the `.sample.env` -file as `.env` and edit it according to your environment. - -## Alternatives - -If you don't think using fixtures is a good idea, you can try one of these -packages instead: - -- [factory-go][factorygo]: Factory for Go. Inspired by Python's Factory Boy -and Ruby's Factory Girl -- [go-txdb (Single transaction SQL driver for Go)][gotxdb]: Use a single -database transaction for each functional test, so you can rollback to -previous state between tests to have the same database state in all tests -- [go-sqlmock][gosqlmock]: A mock for the sql.DB interface. This allow you to -unit test database code without having to connect to a real database -- [dbcleaner][dbcleaner] - Clean database for testing, inspired by -database_cleaner for Ruby - -[railstests]: http://guides.rubyonrails.org/testing.html#the-test-database -[gotxdb]: https://github.com/DATA-DOG/go-txdb -[gosqlmock]: https://github.com/DATA-DOG/go-sqlmock -[factorygo]: https://github.com/bluele/factory-go -[dbcleaner]: https://github.com/khaiql/dbcleaner diff --git a/vendor/gopkg.in/testfixtures.v2/Taskfile.yml b/vendor/gopkg.in/testfixtures.v2/Taskfile.yml deleted file mode 100644 index 86d5645526b..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/Taskfile.yml +++ /dev/null @@ -1,64 +0,0 @@ -# https://taskfile.org - -version: '2' - -tasks: - dl-deps: - desc: Download cli deps - cmds: - - go get -u golang.org/x/lint/golint - - lint: - desc: Runs golint - cmds: - - golint . - - test-free: - desc: Test free databases (PG, MySQL and SQLite) - cmds: - - task: test-pg - - task: test-mysql - - task: test-sqlite - - test-all: - desc: Test all databases (PG, MySQL, SQLite, SQLServer and Oracle) - cmds: - - task: test-pg - - task: test-mysql - - task: test-sqlite - - task: test-sqlserver - - task: test-oracle - - test-pg: - desc: Test PostgreSQL - cmds: - - task: test-db - vars: {DATABASE: postgresql} - - test-mysql: - desc: Test MySQL - cmds: - - task: test-db - vars: {DATABASE: mysql} - - test-sqlite: - desc: Test SQLite - cmds: - - task: test-db - vars: {DATABASE: sqlite} - - test-sqlserver: - desc: Test SQLServer - cmds: - - task: test-db - vars: {DATABASE: sqlserver} - - test-oracle: - desc: Test Oracle - cmds: - - task: test-db - vars: {DATABASE: oracle} - - test-db: - cmds: - - go test -v -tags {{.DATABASE}} diff --git a/vendor/gopkg.in/testfixtures.v2/appveyor.yml b/vendor/gopkg.in/testfixtures.v2/appveyor.yml deleted file mode 100644 index ed3cb3d4305..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/appveyor.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: '{build}' - -clone_folder: C:\GOPATH\src\gopkg.in\testfixtures.v2 - -build: false -deploy: false - -services: - - postgresql96 - - mysql - - mssql2017 - -environment: - POSTGRES_PATH: C:\Program Files\PostgreSQL\9.6 - PGUSER: postgres - PGPASSWORD: Password12! - PG_CONN_STRING: 'user=postgres password=Password12! dbname=testfixtures_test sslmode=disable' - - MYSQL_PATH: C:\Program Files\MySql\MySQL Server 5.7 - MYSQL_PWD: Password12! - MYSQL_CONN_STRING: 'root:Password12!@/testfixtures_test?multiStatements=true' - - SQLITE_CONN_STRING: 'testdb.sqlite3' - - SQLSERVER_CONN_STRING: 'server=localhost;database=testfixtures_test;user id=sa;password=Password12!;encrypt=disable' - - MINGW_PATH: C:\MinGW - - GOPATH: C:\GOPATH - GOVERSION: 1.11.2 - -install: - - SET PATH=%POSTGRES_PATH%\bin;%MYSQL_PATH%\bin;%MINGW_PATH%\bin;%PATH% - - - rmdir C:\go /s /q - - appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-386.msi - - msiexec /i go%GOVERSION%.windows-386.msi /q - - go version - -build_script: - - createdb testfixtures_test - - mysql -e "CREATE DATABASE testfixtures_test;" --user=root - - sqlcmd -S localhost,1433 -U sa -P Password12! -Q "CREATE DATABASE testfixtures_test" -d "master" - -test_script: - - go get -t -tags "sqlite postgresql mysql sqlserver" ./... - - go install -v ./... - - go test -v -tags postgresql - - go test -v -tags mysql - - go test -v -tags sqlserver - - go test -v -tags sqlite diff --git a/vendor/gopkg.in/testfixtures.v2/deprecated.go b/vendor/gopkg.in/testfixtures.v2/deprecated.go deleted file mode 100644 index 16e0969e339..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/deprecated.go +++ /dev/null @@ -1,75 +0,0 @@ -package testfixtures - -import ( - "database/sql" -) - -type ( - // DataBaseHelper is the helper interface - // Deprecated: Use Helper instead - DataBaseHelper Helper - - // PostgreSQLHelper is the PostgreSQL helper - // Deprecated: Use PostgreSQL{} instead - PostgreSQLHelper struct { - PostgreSQL - UseAlterConstraint bool - } - - // MySQLHelper is the MySQL helper - // Deprecated: Use MySQL{} instead - MySQLHelper struct { - MySQL - } - - // SQLiteHelper is the SQLite helper - // Deprecated: Use SQLite{} instead - SQLiteHelper struct { - SQLite - } - - // SQLServerHelper is the SQLServer helper - // Deprecated: Use SQLServer{} instead - SQLServerHelper struct { - SQLServer - } - - // OracleHelper is the Oracle helper - // Deprecated: Use Oracle{} instead - OracleHelper struct { - Oracle - } -) - -func (h *PostgreSQLHelper) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) error { - h.PostgreSQL.UseAlterConstraint = h.UseAlterConstraint - return h.PostgreSQL.disableReferentialIntegrity(db, loadFn) -} - -// LoadFixtureFiles load all specified fixtures files into database: -// LoadFixtureFiles(db, &PostgreSQL{}, -// "fixtures/customers.yml", "fixtures/orders.yml") -// // add as many files you want -// -// Deprecated: Use NewFiles() and Load() instead. -func LoadFixtureFiles(db *sql.DB, helper Helper, files ...string) error { - c, err := NewFiles(db, helper, files...) - if err != nil { - return err - } - - return c.Load() -} - -// LoadFixtures loads all fixtures in a given folder into the database: -// LoadFixtures("myfixturesfolder", db, &PostgreSQL{}) -// -// Deprecated: Use NewFolder() and Load() instead. -func LoadFixtures(folderName string, db *sql.DB, helper Helper) error { - c, err := NewFolder(db, helper, folderName) - if err != nil { - return err - } - - return c.Load() -} diff --git a/vendor/gopkg.in/testfixtures.v2/errors.go b/vendor/gopkg.in/testfixtures.v2/errors.go deleted file mode 100644 index 17eb284c6cd..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/errors.go +++ /dev/null @@ -1,41 +0,0 @@ -package testfixtures - -import ( - "errors" - "fmt" -) - -var ( - // ErrWrongCastNotAMap is returned when a map is not a map[interface{}]interface{} - ErrWrongCastNotAMap = errors.New("Could not cast record: not a map[interface{}]interface{}") - - // ErrFileIsNotSliceOrMap is returned the the fixture file is not a slice or map. - ErrFileIsNotSliceOrMap = errors.New("The fixture file is not a slice or map") - - // ErrKeyIsNotString is returned when a record is not of type string - ErrKeyIsNotString = errors.New("Record map key is not string") - - // ErrNotTestDatabase is returned when the database name doesn't contains "test" - ErrNotTestDatabase = errors.New(`Loading aborted because the database name does not contains "test"`) -) - -// InsertError will be returned if any error happens on database while -// inserting the record -type InsertError struct { - Err error - File string - Index int - SQL string - Params []interface{} -} - -func (e *InsertError) Error() string { - return fmt.Sprintf( - "testfixtures: error inserting record: %v, on file: %s, index: %d, sql: %s, params: %v", - e.Err, - e.File, - e.Index, - e.SQL, - e.Params, - ) -} diff --git a/vendor/gopkg.in/testfixtures.v2/generate.go b/vendor/gopkg.in/testfixtures.v2/generate.go deleted file mode 100644 index 844814007c4..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/generate.go +++ /dev/null @@ -1,110 +0,0 @@ -package testfixtures - -import ( - "database/sql" - "fmt" - "os" - "path" - "unicode/utf8" - - "gopkg.in/yaml.v2" -) - -// TableInfo is settings for generating a fixture for table. -type TableInfo struct { - Name string // Table name - Where string // A condition for extracting records. If this value is empty, extracts all records. -} - -func (ti *TableInfo) whereClause() string { - if ti.Where == "" { - return "" - } - return fmt.Sprintf(" WHERE %s", ti.Where) -} - -// GenerateFixtures generates fixtures for the current contents of a database, and saves -// them to the specified directory -func GenerateFixtures(db *sql.DB, helper Helper, dir string) error { - tables, err := helper.tableNames(db) - if err != nil { - return err - } - for _, table := range tables { - filename := path.Join(dir, table+".yml") - if err := generateFixturesForTable(db, helper, &TableInfo{Name: table}, filename); err != nil { - return err - } - } - return nil -} - -// GenerateFixturesForTables generates fixtures for the current contents of specified tables in a database, and saves -// them to the specified directory -func GenerateFixturesForTables(db *sql.DB, tables []*TableInfo, helper Helper, dir string) error { - for _, table := range tables { - filename := path.Join(dir, table.Name+".yml") - if err := generateFixturesForTable(db, helper, table, filename); err != nil { - return err - } - } - return nil -} - -func generateFixturesForTable(db *sql.DB, h Helper, table *TableInfo, filename string) error { - query := fmt.Sprintf("SELECT * FROM %s%s", h.quoteKeyword(table.Name), table.whereClause()) - rows, err := db.Query(query) - if err != nil { - return err - } - defer rows.Close() - - columns, err := rows.Columns() - if err != nil { - return err - } - - fixtures := make([]interface{}, 0, 10) - for rows.Next() { - entries := make([]interface{}, len(columns)) - entryPtrs := make([]interface{}, len(entries)) - for i := range entries { - entryPtrs[i] = &entries[i] - } - if err := rows.Scan(entryPtrs...); err != nil { - return err - } - - entryMap := make(map[string]interface{}, len(entries)) - for i, column := range columns { - entryMap[column] = convertValue(entries[i]) - } - fixtures = append(fixtures, entryMap) - } - if err = rows.Err(); err != nil { - return err - } - - f, err := os.Create(filename) - if err != nil { - return err - } - defer f.Close() - - marshaled, err := yaml.Marshal(fixtures) - if err != nil { - return err - } - _, err = f.Write(marshaled) - return err -} - -func convertValue(value interface{}) interface{} { - switch v := value.(type) { - case []byte: - if utf8.Valid(v) { - return string(v) - } - } - return value -} diff --git a/vendor/gopkg.in/testfixtures.v2/go.mod b/vendor/gopkg.in/testfixtures.v2/go.mod deleted file mode 100644 index 2e2f466179b..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module gopkg.in/testfixtures.v2 - -require ( - cloud.google.com/go v0.33.1 // indirect - github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f - github.com/go-sql-driver/mysql v1.4.1 - github.com/google/go-cmp v0.2.0 // indirect - github.com/joho/godotenv v1.3.0 - github.com/lib/pq v1.0.0 - github.com/mattn/go-oci8 v0.0.0-20181115070430-6eefff3c767c - github.com/mattn/go-sqlite3 v1.10.0 - golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 // indirect - google.golang.org/appengine v1.3.0 // indirect - gopkg.in/yaml.v2 v2.2.1 -) - -go 1.13 diff --git a/vendor/gopkg.in/testfixtures.v2/go.sum b/vendor/gopkg.in/testfixtures.v2/go.sum deleted file mode 100644 index 73e347d39a2..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/go.sum +++ /dev/null @@ -1,27 +0,0 @@ -cloud.google.com/go v0.33.1 h1:fmJQWZ1w9PGkHR1YL/P7HloDvqlmKQ4Vpb7PC2e+aCk= -cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f h1:WH0w/R4Yoey+04HhFxqZ6VX6I0d7RMyw5aXQ9UTvQPs= -github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= -github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/mattn/go-oci8 v0.0.0-20181115070430-6eefff3c767c h1:RkC3vqmJwowDCqtL7d8cFEMNdoGHBcqoR4jKO9/mWuA= -github.com/mattn/go-oci8 v0.0.0-20181115070430-6eefff3c767c/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8= -github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= -github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU= -golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/gopkg.in/testfixtures.v2/options.go b/vendor/gopkg.in/testfixtures.v2/options.go deleted file mode 100644 index 83199d4e267..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/options.go +++ /dev/null @@ -1,19 +0,0 @@ -package testfixtures - -var ( - skipDatabaseNameCheck bool - resetSequencesTo int64 = 10000 -) - -// SkipDatabaseNameCheck If true, loading fixtures will not check if the database -// name constaint "test". Use with caution! -func SkipDatabaseNameCheck(value bool) { - skipDatabaseNameCheck = value -} - -// ResetSequencesTo sets the value the sequences will be reset to. -// This is used by PostgreSQL and Oracle. -// Defaults to 10000. -func ResetSequencesTo(value int64) { - resetSequencesTo = value -} diff --git a/vendor/gopkg.in/testfixtures.v2/oracle.go b/vendor/gopkg.in/testfixtures.v2/oracle.go deleted file mode 100644 index af5c92ddb3c..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/oracle.go +++ /dev/null @@ -1,171 +0,0 @@ -package testfixtures - -import ( - "database/sql" - "fmt" - "strings" -) - -// Oracle is the Oracle database helper for this package -type Oracle struct { - baseHelper - - enabledConstraints []oracleConstraint - sequences []string -} - -type oracleConstraint struct { - tableName string - constraintName string -} - -func (h *Oracle) init(db *sql.DB) error { - var err error - - h.enabledConstraints, err = h.getEnabledConstraints(db) - if err != nil { - return err - } - - h.sequences, err = h.getSequences(db) - if err != nil { - return err - } - - return nil -} - -func (*Oracle) paramType() int { - return paramTypeColon -} - -func (*Oracle) quoteKeyword(str string) string { - return fmt.Sprintf("\"%s\"", strings.ToUpper(str)) -} - -func (*Oracle) databaseName(q queryable) (string, error) { - var dbName string - err := q.QueryRow("SELECT user FROM DUAL").Scan(&dbName) - return dbName, err -} - -func (*Oracle) tableNames(q queryable) ([]string, error) { - query := ` - SELECT TABLE_NAME - FROM USER_TABLES - ` - rows, err := q.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var tables []string - for rows.Next() { - var table string - if err = rows.Scan(&table); err != nil { - return nil, err - } - tables = append(tables, table) - } - if err = rows.Err(); err != nil { - return nil, err - } - return tables, nil - -} - -func (*Oracle) getEnabledConstraints(q queryable) ([]oracleConstraint, error) { - var constraints []oracleConstraint - rows, err := q.Query(` - SELECT table_name, constraint_name - FROM user_constraints - WHERE constraint_type = 'R' - AND status = 'ENABLED' - `) - if err != nil { - return nil, err - } - defer rows.Close() - - for rows.Next() { - var constraint oracleConstraint - rows.Scan(&constraint.tableName, &constraint.constraintName) - constraints = append(constraints, constraint) - } - if err = rows.Err(); err != nil { - return nil, err - } - return constraints, nil -} - -func (*Oracle) getSequences(q queryable) ([]string, error) { - var sequences []string - rows, err := q.Query("SELECT sequence_name FROM user_sequences") - if err != nil { - return nil, err - } - defer rows.Close() - - for rows.Next() { - var sequence string - if err = rows.Scan(&sequence); err != nil { - return nil, err - } - sequences = append(sequences, sequence) - } - if err = rows.Err(); err != nil { - return nil, err - } - return sequences, nil -} - -func (h *Oracle) resetSequences(q queryable) error { - for _, sequence := range h.sequences { - _, err := q.Exec(fmt.Sprintf("DROP SEQUENCE %s", h.quoteKeyword(sequence))) - if err != nil { - return err - } - _, err = q.Exec(fmt.Sprintf("CREATE SEQUENCE %s START WITH %d", h.quoteKeyword(sequence), resetSequencesTo)) - if err != nil { - return err - } - } - return nil -} - -func (h *Oracle) disableReferentialIntegrity(db *sql.DB, loadFn loadFunction) (err error) { - // re-enable after load - defer func() { - for _, c := range h.enabledConstraints { - _, err2 := db.Exec(fmt.Sprintf("ALTER TABLE %s ENABLE CONSTRAINT %s", h.quoteKeyword(c.tableName), h.quoteKeyword(c.constraintName))) - if err2 != nil && err == nil { - err = err2 - } - } - }() - - // disable foreign keys - for _, c := range h.enabledConstraints { - _, err := db.Exec(fmt.Sprintf("ALTER TABLE %s DISABLE CONSTRAINT %s", h.quoteKeyword(c.tableName), h.quoteKeyword(c.constraintName))) - if err != nil { - return err - } - } - - tx, err := db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - - if err = loadFn(tx); err != nil { - return err - } - - if err = tx.Commit(); err != nil { - return err - } - - return h.resetSequences(db) -} diff --git a/vendor/gopkg.in/testfixtures.v2/testfixtures.go b/vendor/gopkg.in/testfixtures.v2/testfixtures.go deleted file mode 100644 index f59030f817d..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/testfixtures.go +++ /dev/null @@ -1,306 +0,0 @@ -package testfixtures - -import ( - "database/sql" - "fmt" - "io/ioutil" - "path" - "path/filepath" - "regexp" - "strings" - - "gopkg.in/yaml.v2" -) - -// Context holds the fixtures to be loaded in the database. -type Context struct { - db *sql.DB - helper Helper - fixturesFiles []*fixtureFile -} - -type fixtureFile struct { - path string - fileName string - content []byte - insertSQLs []insertSQL -} - -type insertSQL struct { - sql string - params []interface{} -} - -var ( - dbnameRegexp = regexp.MustCompile("(?i)test") -) - -// NewFolder creates a context for all fixtures in a given folder into the database: -// NewFolder(db, &PostgreSQL{}, "my/fixtures/folder") -func NewFolder(db *sql.DB, helper Helper, folderName string) (*Context, error) { - fixtures, err := fixturesFromFolder(folderName) - if err != nil { - return nil, err - } - - c, err := newContext(db, helper, fixtures) - if err != nil { - return nil, err - } - - return c, nil -} - -// NewFiles creates a context for all specified fixtures files into database: -// NewFiles(db, &PostgreSQL{}, -// "fixtures/customers.yml", -// "fixtures/orders.yml" -// // add as many files you want -// ) -func NewFiles(db *sql.DB, helper Helper, fileNames ...string) (*Context, error) { - fixtures, err := fixturesFromFiles(fileNames...) - if err != nil { - return nil, err - } - - c, err := newContext(db, helper, fixtures) - if err != nil { - return nil, err - } - - return c, nil -} - -func newContext(db *sql.DB, helper Helper, fixtures []*fixtureFile) (*Context, error) { - c := &Context{ - db: db, - helper: helper, - fixturesFiles: fixtures, - } - - if err := c.helper.init(c.db); err != nil { - return nil, err - } - - if err := c.buildInsertSQLs(); err != nil { - return nil, err - } - - return c, nil -} - -// DetectTestDatabase returns nil if databaseName matches regexp -// if err := fixtures.DetectTestDatabase(); err != nil { -// log.Fatal(err) -// } -func (c *Context) DetectTestDatabase() error { - dbName, err := c.helper.databaseName(c.db) - if err != nil { - return err - } - if !dbnameRegexp.MatchString(dbName) { - return ErrNotTestDatabase - } - return nil -} - -// Load wipes and after load all fixtures in the database. -// if err := fixtures.Load(); err != nil { -// log.Fatal(err) -// } -func (c *Context) Load() error { - if !skipDatabaseNameCheck { - if err := c.DetectTestDatabase(); err != nil { - return err - } - } - - err := c.helper.disableReferentialIntegrity(c.db, func(tx *sql.Tx) error { - for _, file := range c.fixturesFiles { - modified, err := c.helper.isTableModified(tx, file.fileNameWithoutExtension()) - if err != nil { - return err - } - if !modified { - continue - } - if err := file.delete(tx, c.helper); err != nil { - return err - } - - err = c.helper.whileInsertOnTable(tx, file.fileNameWithoutExtension(), func() error { - for j, i := range file.insertSQLs { - if _, err := tx.Exec(i.sql, i.params...); err != nil { - return &InsertError{ - Err: err, - File: file.fileName, - Index: j, - SQL: i.sql, - Params: i.params, - } - } - } - return nil - }) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - return err - } - return c.helper.afterLoad(c.db) -} - -func (c *Context) buildInsertSQLs() error { - for _, f := range c.fixturesFiles { - var records interface{} - if err := yaml.Unmarshal(f.content, &records); err != nil { - return err - } - - switch records := records.(type) { - case []interface{}: - for _, record := range records { - recordMap, ok := record.(map[interface{}]interface{}) - if !ok { - return ErrWrongCastNotAMap - } - - sql, values, err := f.buildInsertSQL(c.helper, recordMap) - if err != nil { - return err - } - - f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values}) - } - case map[interface{}]interface{}: - for _, record := range records { - recordMap, ok := record.(map[interface{}]interface{}) - if !ok { - return ErrWrongCastNotAMap - } - - sql, values, err := f.buildInsertSQL(c.helper, recordMap) - if err != nil { - return err - } - - f.insertSQLs = append(f.insertSQLs, insertSQL{sql, values}) - } - default: - return ErrFileIsNotSliceOrMap - } - } - - return nil -} - -func (f *fixtureFile) fileNameWithoutExtension() string { - return strings.Replace(f.fileName, filepath.Ext(f.fileName), "", 1) -} - -func (f *fixtureFile) delete(tx *sql.Tx, h Helper) error { - _, err := tx.Exec(fmt.Sprintf("DELETE FROM %s", h.quoteKeyword(f.fileNameWithoutExtension()))) - return err -} - -func (f *fixtureFile) buildInsertSQL(h Helper, record map[interface{}]interface{}) (sqlStr string, values []interface{}, err error) { - var ( - sqlColumns []string - sqlValues []string - i = 1 - ) - for key, value := range record { - keyStr, ok := key.(string) - if !ok { - err = ErrKeyIsNotString - return - } - - sqlColumns = append(sqlColumns, h.quoteKeyword(keyStr)) - - // if string, try convert to SQL or time - // if map or array, convert to json - switch v := value.(type) { - case string: - if strings.HasPrefix(v, "RAW=") { - sqlValues = append(sqlValues, strings.TrimPrefix(v, "RAW=")) - continue - } - - if t, err := tryStrToDate(v); err == nil { - value = t - } - case []interface{}, map[interface{}]interface{}: - value = recursiveToJSON(v) - } - - switch h.paramType() { - case paramTypeDollar: - sqlValues = append(sqlValues, fmt.Sprintf("$%d", i)) - case paramTypeQuestion: - sqlValues = append(sqlValues, "?") - case paramTypeColon: - sqlValues = append(sqlValues, fmt.Sprintf(":%d", i)) - } - - values = append(values, value) - i++ - } - - sqlStr = fmt.Sprintf( - "INSERT INTO %s (%s) VALUES (%s)", - h.quoteKeyword(f.fileNameWithoutExtension()), - strings.Join(sqlColumns, ", "), - strings.Join(sqlValues, ", "), - ) - return -} - -func fixturesFromFolder(folderName string) ([]*fixtureFile, error) { - var files []*fixtureFile - fileinfos, err := ioutil.ReadDir(folderName) - if err != nil { - return nil, err - } - - for _, fileinfo := range fileinfos { - fileExt := filepath.Ext(fileinfo.Name()) - if !fileinfo.IsDir() && (fileExt == ".yml" || fileExt == ".yaml") { - fixture := &fixtureFile{ - path: path.Join(folderName, fileinfo.Name()), - fileName: fileinfo.Name(), - } - fixture.content, err = ioutil.ReadFile(fixture.path) - if err != nil { - return nil, err - } - files = append(files, fixture) - } - } - return files, nil -} - -func fixturesFromFiles(fileNames ...string) ([]*fixtureFile, error) { - var ( - fixtureFiles []*fixtureFile - err error - ) - - for _, f := range fileNames { - fixture := &fixtureFile{ - path: f, - fileName: filepath.Base(f), - } - fixture.content, err = ioutil.ReadFile(fixture.path) - if err != nil { - return nil, err - } - fixtureFiles = append(fixtureFiles, fixture) - } - - return fixtureFiles, nil -} diff --git a/vendor/gopkg.in/testfixtures.v2/time.go b/vendor/gopkg.in/testfixtures.v2/time.go deleted file mode 100644 index 8c5cba1d035..00000000000 --- a/vendor/gopkg.in/testfixtures.v2/time.go +++ /dev/null @@ -1,34 +0,0 @@ -package testfixtures - -import ( - "errors" - "time" -) - -var timeFormats = []string{ - "2006-01-02", - "2006-01-02 15:04", - "2006-01-02 15:04:05", - "20060102", - "20060102 15:04", - "20060102 15:04:05", - "02/01/2006", - "02/01/2006 15:04", - "02/01/2006 15:04:05", - "2006-01-02T15:04-07:00", - "2006-01-02T15:04:05-07:00", -} - -// ErrCouldNotConvertToTime is returns when a string is not a reconizable time format -var ErrCouldNotConvertToTime = errors.New("Could not convert string to time") - -func tryStrToDate(s string) (time.Time, error) { - for _, f := range timeFormats { - t, err := time.ParseInLocation(f, s, time.Local) - if err != nil { - continue - } - return t, nil - } - return time.Time{}, ErrCouldNotConvertToTime -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4ab58dff1eb..976497a4a26 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -66,6 +66,8 @@ github.com/go-redis/redis/internal/proto github.com/go-redis/redis/internal/util # github.com/go-sql-driver/mysql v1.5.0 github.com/go-sql-driver/mysql +# github.com/go-testfixtures/testfixtures/v3 v3.1.1 +github.com/go-testfixtures/testfixtures/v3 # github.com/golang/protobuf v1.3.2 github.com/golang/protobuf/proto # github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc @@ -98,6 +100,10 @@ github.com/labstack/gommon/log github.com/labstack/gommon/random # github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef github.com/laurent22/ical-go +# github.com/lib/pq v1.3.0 +github.com/lib/pq +github.com/lib/pq/oid +github.com/lib/pq/scram # github.com/magiconair/properties v1.8.0 github.com/magiconair/properties # github.com/mailru/easyjson v0.7.0 @@ -172,7 +178,7 @@ github.com/spf13/cast github.com/spf13/cobra # github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/jwalterweatherman -# github.com/spf13/pflag v1.0.3 +# github.com/spf13/pflag v1.0.5 github.com/spf13/pflag # github.com/spf13/viper v1.3.2 github.com/spf13/viper @@ -235,8 +241,6 @@ gopkg.in/alexcesaro/quotedprintable.v3 gopkg.in/d4l3k/messagediff.v1 # gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 -# gopkg.in/testfixtures.v2 v2.5.3 -gopkg.in/testfixtures.v2 # gopkg.in/yaml.v2 v2.2.7 gopkg.in/yaml.v2 # honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a