upgrade to use testfixtures v3 (#11904)
* upgrade to use testfixtures v3 * simplify logic * make vendor * update per @lunny * Update templates/repo/empty.tmpl * Update templates/repo/empty.tmpl Co-authored-by: Lauris BH <lauris@nix.lv>mj-v1.14.3
parent
1645d4a5d8
commit
9e6a79bea9
@ -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 <andrey.nering@gmail.com>
|
||||
description: Ruby on Rails like test fixtures for Go.
|
||||
license: MIT
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"
|
@ -0,0 +1,93 @@
|
||||
# Changelog
|
||||
|
||||
## v3.2.0 - 2020-05-10
|
||||
|
||||
- Add support for loading multiple files and directories
|
||||
([#65](https://github.com/go-testfixtures/testfixtures/pull/65)).
|
||||
|
||||
## v3.1.2 - 2020-04-26
|
||||
|
||||
- Dump: Fix column order in generated YAML files
|
||||
([#62](https://github.com/go-testfixtures/testfixtures/pull/62)).
|
||||
|
||||
## v3.1.1 - 2020-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 - 2020-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 😢.
|
@ -0,0 +1,9 @@
|
||||
FROM golang:1.14-alpine
|
||||
|
||||
RUN apk update
|
||||
RUN apk add alpine-sdk
|
||||
|
||||
WORKDIR /testfixtures
|
||||
COPY . .
|
||||
|
||||
RUN go mod download
|
@ -0,0 +1,483 @@
|
||||
# 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 `<table_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 {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
With `Paths` option, you can specify the paths that fixtures will load
|
||||
from. Path can be directory or file. If directory, we will search YAML files
|
||||
in it.
|
||||
|
||||
```go
|
||||
fixtures, err := testfixtures.New(
|
||||
testfixtures.Database(db),
|
||||
testfixtures.Dialect("postgres"),
|
||||
testfixtures.Paths(
|
||||
"fixtures/orders.yml",
|
||||
"fixtures/customers.yml",
|
||||
"common_fixtures/users"
|
||||
),
|
||||
)
|
||||
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
|
@ -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'
|
@ -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
|
@ -0,0 +1,165 @@
|
||||
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))
|
||||
|
||||
stmt, err := d.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
rows, err := stmt.Query()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fixtures := make([]yaml.MapSlice, 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([]yaml.MapItem, len(entries))
|
||||
for i, column := range columns {
|
||||
entryMap[i] = yaml.MapItem{
|
||||
Key: column,
|
||||
Value: 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
|
||||
}
|
@ -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
|
@ -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=
|
@ -0,0 +1,599 @@
|
||||
package testfixtures // import "github.com/go-testfixtures/testfixtures/v3"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"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 = append(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 = append(l.fixturesFiles, fixtures...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Paths inform Loader to load a given set of YAML files and directories.
|
||||
func Paths(paths ...string) func(*Loader) error {
|
||||
return func(l *Loader) error {
|
||||
fixtures, err := l.fixturesFromPaths(paths...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.fixturesFiles = append(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) fixturesFromPaths(paths ...string) ([]*fixtureFile, error) {
|
||||
fixtureExtractor := func(p string, isDir bool) ([]*fixtureFile, error) {
|
||||
if isDir {
|
||||
return l.fixturesFromDir(p)
|
||||
}
|
||||
|
||||
return l.fixturesFromFiles(p)
|
||||
}
|
||||
|
||||
var fixtureFiles []*fixtureFile
|
||||
|
||||
for _, p := range paths {
|
||||
f, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`testfixtures: could not stat path "%s": %w`, p, err)
|
||||
}
|
||||
|
||||
fixtures, err := fixtureExtractor(p, f.IsDir())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fixtureFiles = append(fixtureFiles, fixtures...)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -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)
|
||||
}
|
@ -0,0 +1,299 @@
|
||||
// Extracted from Go database/sql source code
|
||||
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Type conversions for Scan.
|
||||
|
||||
package sqlite3
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errNilPtr = errors.New("destination pointer is nil") // embedded in descriptive error
|
||||
|
||||
// convertAssign copies to dest the value in src, converting it if possible.
|
||||
// An error is returned if the copy would result in loss of information.
|
||||
// dest should be a pointer type.
|
||||
func convertAssign(dest, src interface{}) error {
|
||||
// Common cases, without reflect.
|
||||
switch s := src.(type) {
|
||||
case string:
|
||||
switch d := dest.(type) {
|
||||
case *string:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = s
|
||||
return nil
|
||||
case *[]byte:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = []byte(s)
|
||||
return nil
|
||||
case *sql.RawBytes:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = append((*d)[:0], s...)
|
||||
return nil
|
||||
}
|
||||
case []byte:
|
||||
switch d := dest.(type) {
|
||||
case *string:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = string(s)
|
||||
return nil
|
||||
case *interface{}:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = cloneBytes(s)
|
||||
return nil
|
||||
case *[]byte:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = cloneBytes(s)
|
||||
return nil
|
||||
case *sql.RawBytes:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = s
|
||||
return nil
|
||||
}
|
||||
case time.Time:
|
||||
switch d := dest.(type) {
|
||||
case *time.Time:
|
||||
*d = s
|
||||
return nil
|
||||
case *string:
|
||||
*d = s.Format(time.RFC3339Nano)
|
||||
return nil
|
||||
case *[]byte:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = []byte(s.Format(time.RFC3339Nano))
|
||||
return nil
|
||||
case *sql.RawBytes:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = s.AppendFormat((*d)[:0], time.RFC3339Nano)
|
||||
return nil
|
||||
}
|
||||
case nil:
|
||||
switch d := dest.(type) {
|
||||
case *interface{}:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = nil
|
||||
return nil
|
||||
case *[]byte:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = nil
|
||||
return nil
|
||||
case *sql.RawBytes:
|
||||
if d == nil {
|
||||
return errNilPtr
|
||||
}
|
||||
*d = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var sv reflect.Value
|
||||
|
||||
switch d := dest.(type) {
|
||||
case *string:
|
||||
sv = reflect.ValueOf(src)
|
||||
switch sv.Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
*d = asString(src)
|
||||
return nil
|
||||
}
|
||||
case *[]byte:
|
||||
sv = reflect.ValueOf(src)
|
||||
if b, ok := asBytes(nil, sv); ok {
|
||||
*d = b
|
||||
return nil
|
||||
}
|
||||
case *sql.RawBytes:
|
||||
sv = reflect.ValueOf(src)
|
||||
if b, ok := asBytes([]byte(*d)[:0], sv); ok {
|
||||
*d = sql.RawBytes(b)
|
||||
return nil
|
||||
}
|
||||
case *bool:
|
||||
bv, err := driver.Bool.ConvertValue(src)
|
||||
if err == nil {
|
||||
*d = bv.(bool)
|
||||
}
|
||||
return err
|
||||
case *interface{}:
|
||||
*d = src
|
||||
return nil
|
||||
}
|
||||
|
||||
if scanner, ok := dest.(sql.Scanner); ok {
|
||||
return scanner.Scan(src)
|
||||
}
|
||||
|
||||
dpv := reflect.ValueOf(dest)
|
||||
if dpv.Kind() != reflect.Ptr {
|
||||
return errors.New("destination not a pointer")
|
||||
}
|
||||
if dpv.IsNil() {
|
||||
return errNilPtr
|
||||
}
|
||||
|
||||
if !sv.IsValid() {
|
||||
sv = reflect.ValueOf(src)
|
||||
}
|
||||
|
||||
dv := reflect.Indirect(dpv)
|
||||
if sv.IsValid() && sv.Type().AssignableTo(dv.Type()) {
|
||||
switch b := src.(type) {
|
||||
case []byte:
|
||||
dv.Set(reflect.ValueOf(cloneBytes(b)))
|
||||
default:
|
||||
dv.Set(sv)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if dv.Kind() == sv.Kind() && sv.Type().ConvertibleTo(dv.Type()) {
|
||||
dv.Set(sv.Convert(dv.Type()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// The following conversions use a string value as an intermediate representation
|
||||
// to convert between various numeric types.
|
||||
//
|
||||
// This also allows scanning into user defined types such as "type Int int64".
|
||||
// For symmetry, also check for string destination types.
|
||||
switch dv.Kind() {
|
||||
case reflect.Ptr:
|
||||
if src == nil {
|
||||
dv.Set(reflect.Zero(dv.Type()))
|
||||
return nil
|
||||
}
|
||||
dv.Set(reflect.New(dv.Type().Elem()))
|
||||
return convertAssign(dv.Interface(), src)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
s := asString(src)
|
||||
i64, err := strconv.ParseInt(s, 10, dv.Type().Bits())
|
||||
if err != nil {
|
||||
err = strconvErr(err)
|
||||
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err)
|
||||
}
|
||||
dv.SetInt(i64)
|
||||
return nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
s := asString(src)
|
||||
u64, err := strconv.ParseUint(s, 10, dv.Type().Bits())
|
||||
if err != nil {
|
||||
err = strconvErr(err)
|
||||
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err)
|
||||
}
|
||||
dv.SetUint(u64)
|
||||
return nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
s := asString(src)
|
||||
f64, err := strconv.ParseFloat(s, dv.Type().Bits())
|
||||
if err != nil {
|
||||
err = strconvErr(err)
|
||||
return fmt.Errorf("converting driver.Value type %T (%q) to a %s: %v", src, s, dv.Kind(), err)
|
||||
}
|
||||
dv.SetFloat(f64)
|
||||
return nil
|
||||
case reflect.String:
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
dv.SetString(v)
|
||||
return nil
|
||||
case []byte:
|
||||
dv.SetString(string(v))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, dest)
|
||||
}
|
||||
|
||||
func strconvErr(err error) error {
|
||||
if ne, ok := err.(*strconv.NumError); ok {
|
||||
return ne.Err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cloneBytes(b []byte) []byte {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := make([]byte, len(b))
|
||||
copy(c, b)
|
||||
return c
|
||||
}
|
||||
|
||||
func asString(src interface{}) string {
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []byte:
|
||||
return string(v)
|
||||
}
|
||||
rv := reflect.ValueOf(src)
|
||||
switch rv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.FormatInt(rv.Int(), 10)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return strconv.FormatUint(rv.Uint(), 10)
|
||||
case reflect.Float64:
|
||||
return strconv.FormatFloat(rv.Float(), 'g', -1, 64)
|
||||
case reflect.Float32:
|
||||
return strconv.FormatFloat(rv.Float(), 'g', -1, 32)
|
||||
case reflect.Bool:
|
||||
return strconv.FormatBool(rv.Bool())
|
||||
}
|
||||
return fmt.Sprintf("%v", src)
|
||||
}
|
||||
|
||||
func asBytes(buf []byte, rv reflect.Value) (b []byte, ok bool) {
|
||||
switch rv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.AppendInt(buf, rv.Int(), 10), true
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return strconv.AppendUint(buf, rv.Uint(), 10), true
|
||||
case reflect.Float32:
|
||||
return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 32), true
|
||||
case reflect.Float64:
|
||||
return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 64), true
|
||||
case reflect.Bool:
|
||||
return strconv.AppendBool(buf, rv.Bool()), true
|
||||
case reflect.String:
|
||||
s := rv.String()
|
||||
return append(buf, s...), true
|
||||
}
|
||||
return
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,20 @@
|
||||
// Copyright (C) 2019 G.J.R. Timmer <gjr.timmer@gmail.com>.
|
||||
// Copyright (C) 2018 segment.com <friends@segment.com>
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build cgo
|
||||
|
||||
package sqlite3
|
||||
|
||||
// SQLitePreUpdateData represents all of the data available during a
|
||||
// pre-update hook call.
|
||||
type SQLitePreUpdateData struct {
|
||||
Conn *SQLiteConn
|
||||
Op int
|
||||
DatabaseName string
|
||||
TableName string
|
||||
OldRowID int64
|
||||
NewRowID int64
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
// Copyright (C) 2019 G.J.R. Timmer <gjr.timmer@gmail.com>.
|
||||
// Copyright (C) 2018 segment.com <friends@segment.com>
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build sqlite_preupdate_hook
|
||||
|
||||
package sqlite3
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -DSQLITE_ENABLE_PREUPDATE_HOOK
|
||||
#cgo LDFLAGS: -lm
|
||||
|
||||
#ifndef USE_LIBSQLITE3
|
||||
#include <sqlite3-binding.h>
|
||||
#else
|
||||
#include <sqlite3.h>
|
||||
#endif
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void preUpdateHookTrampoline(void*, sqlite3 *, int, char *, char *, sqlite3_int64, sqlite3_int64);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// RegisterPreUpdateHook sets the pre-update hook for a connection.
|
||||
//
|
||||
// The callback is passed a SQLitePreUpdateData struct with the data for
|
||||
// the update, as well as methods for fetching copies of impacted data.
|
||||
//
|
||||
// If there is an existing update hook for this connection, it will be
|
||||
// removed. If callback is nil the existing hook (if any) will be removed
|
||||
// without creating a new one.
|
||||
func (c *SQLiteConn) RegisterPreUpdateHook(callback func(SQLitePreUpdateData)) {
|
||||
if callback == nil {
|
||||
C.sqlite3_preupdate_hook(c.db, nil, nil)
|
||||
} else {
|
||||
C.sqlite3_preupdate_hook(c.db, (*[0]byte)(unsafe.Pointer(C.preUpdateHookTrampoline)), unsafe.Pointer(newHandle(c, callback)))
|
||||
}
|
||||
}
|
||||
|
||||
// Depth returns the source path of the write, see sqlite3_preupdate_depth()
|
||||
func (d *SQLitePreUpdateData) Depth() int {
|
||||
return int(C.sqlite3_preupdate_depth(d.Conn.db))
|
||||
}
|
||||
|
||||
// Count returns the number of columns in the row
|
||||
func (d *SQLitePreUpdateData) Count() int {
|
||||
return int(C.sqlite3_preupdate_count(d.Conn.db))
|
||||
}
|
||||
|
||||
func (d *SQLitePreUpdateData) row(dest []interface{}, new bool) error {
|
||||
for i := 0; i < d.Count() && i < len(dest); i++ {
|
||||
var val *C.sqlite3_value
|
||||
var src interface{}
|
||||
|
||||
// Initially I tried making this just a function pointer argument, but
|
||||
// it's absurdly complicated to pass C function pointers.
|
||||
if new {
|
||||
C.sqlite3_preupdate_new(d.Conn.db, C.int(i), &val)
|
||||
} else {
|
||||
C.sqlite3_preupdate_old(d.Conn.db, C.int(i), &val)
|
||||
}
|
||||
|
||||
switch C.sqlite3_value_type(val) {
|
||||
case C.SQLITE_INTEGER:
|
||||
src = int64(C.sqlite3_value_int64(val))
|
||||
case C.SQLITE_FLOAT:
|
||||
src = float64(C.sqlite3_value_double(val))
|
||||
case C.SQLITE_BLOB:
|
||||
len := C.sqlite3_value_bytes(val)
|
||||
blobptr := C.sqlite3_value_blob(val)
|
||||
src = C.GoBytes(blobptr, len)
|
||||
case C.SQLITE_TEXT:
|
||||
len := C.sqlite3_value_bytes(val)
|
||||
cstrptr := unsafe.Pointer(C.sqlite3_value_text(val))
|
||||
src = C.GoBytes(cstrptr, len)
|
||||
case C.SQLITE_NULL:
|
||||
src = nil
|
||||
}
|
||||
|
||||
err := convertAssign(&dest[i], src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Old populates dest with the row data to be replaced. This works similar to
|
||||
// database/sql's Rows.Scan()
|
||||
func (d *SQLitePreUpdateData) Old(dest ...interface{}) error {
|
||||
if d.Op == SQLITE_INSERT {
|
||||
return errors.New("There is no old row for INSERT operations")
|
||||
}
|
||||
return d.row(dest, false)
|
||||
}
|
||||
|
||||
// New populates dest with the replacement row data. This works similar to
|
||||
// database/sql's Rows.Scan()
|
||||
func (d *SQLitePreUpdateData) New(dest ...interface{}) error {
|
||||
if d.Op == SQLITE_DELETE {
|
||||
return errors.New("There is no new row for DELETE operations")
|
||||
}
|
||||
return d.row(dest, true)
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
// Copyright (C) 2019 G.J.R. Timmer <gjr.timmer@gmail.com>.
|
||||
// Copyright (C) 2018 segment.com <friends@segment.com>
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !sqlite_preupdate_hook,cgo
|
||||
|
||||
package sqlite3
|
||||
|
||||
// RegisterPreUpdateHook sets the pre-update hook for a connection.
|
||||
//
|
||||
// The callback is passed a SQLitePreUpdateData struct with the data for
|
||||
// the update, as well as methods for fetching copies of impacted data.
|
||||
//
|
||||
// If there is an existing update hook for this connection, it will be
|
||||
// removed. If callback is nil the existing hook (if any) will be removed
|
||||
// without creating a new one.
|
||||
func (c *SQLiteConn) RegisterPreUpdateHook(callback func(SQLitePreUpdateData)) {
|
||||
// NOOP
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
module github.com/spf13/pflag
|
||||
|
||||
go 1.12
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- '1.9'
|
||||
- '1.10'
|
||||
|
||||
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
|
@ -1,64 +0,0 @@
|
||||
# github.com/go-task/task
|
||||
|
||||
version: '2'
|
||||
|
||||
tasks:
|
||||
dl-deps:
|
||||
desc: Download cli deps
|
||||
cmds:
|
||||
- go get -u github.com/golang/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}}
|
@ -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.10.3
|
||||
|
||||
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
|
@ -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()
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -1,305 +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 {
|
||||
if !fileinfo.IsDir() && filepath.Ext(fileinfo.Name()) == ".yml" {
|
||||
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
|
||||
}
|
@ -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
|
||||
}
|
Loading…
Reference in new issue