Skip to content

Commit

Permalink
Allow for multistatement migrations in Neo4j (golang-migrate#328)
Browse files Browse the repository at this point in the history
* allow for multistatement migrations

* multi statement option

* parse bool and package const

* default false multi, change package var to byte[]
  • Loading branch information
mvid authored and dhui committed Jan 23, 2020
1 parent f299233 commit 6909319
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 8 deletions.
2 changes: 1 addition & 1 deletion database/cassandra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Drop command will not work on Cassandra 2.X because it rely on
system_schema table which comes with 3.X
* Other commands should work properly but are **not tested**
* The Cassandra driver (gocql) does not natively support executing multipe statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. There are two important caveats:
* The Cassandra driver (gocql) does not natively support executing multiple statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param. There are two important caveats:
* This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon.
* The queries are not executed in any sort of transaction/batch, meaning you are responsible for fixing partial migrations.

Expand Down
5 changes: 5 additions & 0 deletions database/neo4j/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# neo4j
The Neo4j driver (bolt) does not natively support executing multiple statements in a single query. To allow for multiple statements in a single migration, you can use the `x-multi-statement` param.
This mode splits the migration text into separately-executed statements by a semi-colon `;`. Thus `x-multi-statement` cannot be used when a statement in the migration contains a string with a semi-colon.
The queries **should** run in a single transaction, so partial migrations should not be a concern, but this is untested.


`neo4j://user:password@host:port/`

| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| `x-multi-statement` | `MultiStatement` | Enable multiple statements to be ran in a single migration (See note above) |
| `user` | Contained within `AuthConfig` | The user to sign in as |
| `password` | Contained within `AuthConfig` | The user's password |
| `host` | | The host to connect to. Values that start with / are for unix domain sockets. (default is localhost) |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE (:Movie {name: "Hollow Man"});
CREATE (:Movie {name: "Mystic River"});
;;;
44 changes: 40 additions & 4 deletions database/neo4j/neo4j.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package neo4j

import (
"C" // import C so that we can't compile with CGO_ENABLED=0
"bytes"
"fmt"
"github.com/hashicorp/go-multierror"
"io"
"io/ioutil"
neturl "net/url"
"strconv"
"sync/atomic"

"github.com/golang-migrate/migrate/v4/database"
"github.com/hashicorp/go-multierror"
"github.com/neo4j/neo4j-go-driver/neo4j"
)

Expand All @@ -18,7 +20,9 @@ func init() {
database.Register("neo4j", &db)
}

var DefaultMigrationsLabel = "SchemaMigration"
const DefaultMigrationsLabel = "SchemaMigration"

var StatementSeparator = []byte(";")

var (
ErrNilConfig = fmt.Errorf("no config")
Expand All @@ -28,6 +32,7 @@ type Config struct {
AuthToken neo4j.AuthToken
URL string // if using WithInstance, don't provide auth in the URL, it will be ignored
MigrationsLabel string
MultiStatement bool
}

type Neo4j struct {
Expand Down Expand Up @@ -69,11 +74,21 @@ func (n *Neo4j) Open(url string) (database.Driver, error) {
authToken := neo4j.BasicAuth(uri.User.Username(), password, "")
uri.User = nil
uri.Scheme = "bolt"
msQuery := uri.Query().Get("x-multi-statement")
multi := false
if msQuery != "" {
multi, err = strconv.ParseBool(uri.Query().Get("x-multi-statement"))
if err != nil {
return nil, err
}
}
uri.RawQuery = ""

return WithInstance(&Config{
URL: uri.String(),
AuthToken: authToken,
MigrationsLabel: DefaultMigrationsLabel,
MultiStatement: multi,
})
}

Expand Down Expand Up @@ -113,9 +128,30 @@ func (n *Neo4j) Run(migration io.Reader) (err error) {
}
}()

if _, err := neo4j.Collect(session.Run(string(body[:]), nil)); err != nil {
return err
if n.config.MultiStatement {
statements := bytes.Split(body, StatementSeparator)
_, err = session.WriteTransaction(func(transaction neo4j.Transaction) (interface{}, error) {
for _, stmt := range statements {
trimStmt := bytes.TrimSpace(stmt)
if len(trimStmt) == 0 {
continue
}
result, err := transaction.Run(string(trimStmt[:]), nil)
if _, err := neo4j.Collect(result, err); err != nil {
return nil, err
}
}
return nil, nil
})
if err != nil {
return err
}
} else {
if _, err := neo4j.Collect(session.Run(string(body[:]), nil)); err != nil {
return err
}
}

return nil
}

Expand Down
29 changes: 28 additions & 1 deletion database/neo4j/neo4j_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package neo4j

import (
"bytes"
"context"
"fmt"
"log"
Expand Down Expand Up @@ -88,7 +89,8 @@ func TestMigrate(t *testing.T) {
}

n := &Neo4j{}
d, err := n.Open(neoConnectionString(ip, port))
neoUrl := neoConnectionString(ip, port) + "/?x-multi-statement=true"
d, err := n.Open(neoUrl)
if err != nil {
t.Fatal(err)
}
Expand All @@ -104,3 +106,28 @@ func TestMigrate(t *testing.T) {
dt.TestMigrate(t, m)
})
}

func TestMalformed(t *testing.T) {
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
ip, port, err := c.Port(7687)
if err != nil {
t.Fatal(err)
}

n := &Neo4j{}
d, err := n.Open(neoConnectionString(ip, port))
if err != nil {
t.Fatal(err)
}
defer func() {
if err := d.Close(); err != nil {
t.Error(err)
}
}()

migration := bytes.NewReader([]byte("CREATE (a {qid: 1) RETURN a"))
if err := d.Run(migration); err == nil {
t.Fatal("expected failure for malformed migration")
}
})
}
2 changes: 1 addition & 1 deletion internal/cli/build_clickhouse.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
package cli

import (
_ "github.com/golang-migrate/migrate/v4/database/clickhouse"
_ "github.com/ClickHouse/clickhouse-go"
_ "github.com/golang-migrate/migrate/v4/database/clickhouse"
)
2 changes: 1 addition & 1 deletion internal/cli/build_neo4j.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ package cli

import (
_ "github.com/golang-migrate/migrate/v4/database/neo4j"
)
)

0 comments on commit 6909319

Please sign in to comment.