├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── conn.go ├── conn_test.go ├── doc.go ├── driver.go ├── driver_test.go ├── encoding ├── decoder.go ├── doc.go ├── encoder.go ├── encoder_test.go └── util.go ├── errors ├── doc.go └── errors.go ├── examples ├── playground │ ├── README.md │ └── playground.go ├── quick_n_dirty │ └── quick_n_dirty.go └── slow_n_clean │ └── slow_n_clean.go ├── log ├── doc.go └── log.go ├── recorder.go ├── recordings ├── README.md ├── TestBoltConn_Close.json ├── TestBoltConn_CloseStatementOnError.json ├── TestBoltConn_ExecNeo.json ├── TestBoltConn_FailureMessageError.json ├── TestBoltConn_Ignored.json ├── TestBoltConn_IgnoredPipeline.json ├── TestBoltConn_PipelineExec.json ├── TestBoltConn_PipelineQuery.json ├── TestBoltConn_SelectAll.json ├── TestBoltConn_SelectOne.json ├── TestBoltStmt_CreateArgs.json ├── TestBoltStmt_Discard.json ├── TestBoltStmt_ExecNeo.json ├── TestBoltStmt_Failure.json ├── TestBoltStmt_InvalidArgs.json ├── TestBoltStmt_ManyChunks.json ├── TestBoltStmt_MixedObjects.json ├── TestBoltStmt_Path.json ├── TestBoltStmt_PipelineExec.json ├── TestBoltStmt_PipelineQuery.json ├── TestBoltStmt_PipelineQueryCloseBeginning.json ├── TestBoltStmt_PipelineQueryCloseMiddle.json ├── TestBoltStmt_SelectIntLimits.json ├── TestBoltStmt_SelectMany.json ├── TestBoltStmt_SelectMapLimits.json ├── TestBoltStmt_SelectOne.json ├── TestBoltStmt_SelectSliceLimits.json ├── TestBoltStmt_SelectStringLimits.json ├── TestBoltStmt_SingleNode.json ├── TestBoltStmt_SingleRel.json ├── TestBoltTx_Commit.json └── TestBoltTx_Rollback.json ├── result.go ├── rows.go ├── scripts ├── pre-commit └── pre-push ├── stmt.go ├── stmt_test.go ├── structures ├── doc.go ├── graph │ ├── doc.go │ ├── node.go │ ├── path.go │ ├── relationship.go │ └── unbound_relationship.go ├── messages │ ├── ack_failure.go │ ├── discard_all.go │ ├── doc.go │ ├── failure.go │ ├── ignored.go │ ├── init.go │ ├── pull_all.go │ ├── record.go │ ├── reset.go │ ├── run.go │ └── success.go └── structures.go ├── tx.go ├── tx_test.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | /src 10 | /bin 11 | /pkg 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | *.cpuprofile 29 | *.memprofile 30 | 31 | # Sublime Text 32 | *.sublime-project 33 | *.sublime-workspace 34 | 35 | # Vagrant 36 | /.vagrant 37 | /Vagrantfile 38 | 39 | # AWS Ignores 40 | .elasticbeanstalk/ 41 | 42 | # IntelliJ Ignores 43 | *.iml 44 | .idea/ 45 | atlassian-ide-plugin.xml 46 | 47 | # Apple ignores 48 | .DS_Store 49 | 50 | # Vim ignores 51 | tags 52 | 53 | # JUnit test report files 54 | *.xml 55 | 56 | # Visual Studio code 57 | .vscode/ 58 | 59 | # Don't commit a local neo4j instance for testing 60 | /neo4j/ 61 | 62 | # Don't commit folder with temp data 63 | /tmp/ 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | os: 6 | - linux 7 | go: 8 | - "1.4.3" 9 | - "1.5.4" 10 | - "1.6.4" 11 | - "1.7.6" 12 | - "1.8.4" 13 | - "1.9.1" 14 | - "tip" 15 | 16 | #exclude specific go versions in osx build because according to https://github.com/golang/go/issues/17824, go 1.6.4 (and anything prior does not support osx 10.12) which results in unpredicable behavior (sometimes pass, sometimes hangs, sometimes fail due to segfault) 17 | 18 | matrix: 19 | include: 20 | - os: osx 21 | go: "1.7.6" 22 | - os: osx 23 | go: "1.8.4" 24 | - os: osx 25 | go: "1.9.1" 26 | - os: osx 27 | go: "tip" 28 | 29 | #before_install: 30 | #- go get github.com/mattn/goveralls 31 | #- go get golang.org/x/tools/cmd/cover 32 | #script: 33 | #- $HOME/gopath/bin/goveralls -service=travis-ci 34 | #env: 35 | #global: 36 | #secure: NNmqWMijFv3E9APqlAWlmQgI7h4QOzah/1p4Q3Shl2R9LD/uC0lhyajbprbUaR/dUrHGV0yamdRrkCXwB/GV4ZS1YdYQhqgXOy0MYdAFNgRbuCoTkCOwSpx6M9iF1/qtak1nSdu3gOT3dyW07GGcGYnxN+1qH4/qQN6h4RQasPQwFmr3qIKLCaYEhb0DvAfVCxeWySeCQLKd5sIrpOJB3/raaNug2aOBYPMzh1iIwchpViY7hbyRx58cYR8sQiweQLjhEI1OK4K6qiYkMuCpxbLIf78pw0zzzsgOV3zyCm3v1aXUWCwRYrOvIMsU2AYn8ck+d7Pv+2lg8OgZqUNFcScz2+8j5X2i1KqO7/l+FpyvPr3TOf7FyHMktdqVOH4eUvPu1JRPS2wxrN5dT0xQx9k24ssla6uSVmrGrLJc0trbpUnoZeQM7LbX/2rv8FDjt8RBbyq4cHUdwlh9pB1Q1kohD0LbinwzUr2an2Hyo7BuX8AmZp/qaCWq3pKrdtwQbt/euhoR4DpBMuu/DQYY9OqFbIznlljOBTRRRmMkLRoCM2uUip7tDUKD2wFKq1ykE75IvW44kxdtzD0mr76bR50+1RHTarfW4UQ0hHOchIv00HBIviiE/JDOENCe+bDwcI0DOW2Cp/YrS2HkpIztbTgTkAa9ScAy+grwE57v9+M= 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 John A. Nadratowski III 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Neo4J Bolt Driver 2 | [![Build Status](https://travis-ci.org/johnnadratowski/golang-neo4j-bolt-driver.svg?branch=master)](https://travis-ci.org/johnnadratowski/golang-neo4j-bolt-driver) 3 | [![GoDoc](https://godoc.org/github.com/johnnadratowski/golang-neo4j-bolt-driver?status.svg)](https://godoc.org/github.com/johnnadratowski/golang-neo4j-bolt-driver) 4 | 5 | 6 | **ANNOUNCEMENT: I must apologize to the community for not being more responsive. Because of personal life events I am really not able to properly maintain this codebase. Certain other events lead me to believe an official Neo4J Golang driver was to be released soon, but it seems like that may not necessarily be the case. Since I am unable to properly maintain this codebase at this juncture, at this point I think it makes sense to open up this repo to collaborators who are interested in helping with maintenance. Please feel free to email me directly if you're interested.** 7 | 8 | Implements the Neo4J Bolt Protocol specification: 9 | As of the time of writing this, the current version is v3.1.0-M02 10 | 11 | ``` 12 | go get github.com/johnnadratowski/golang-neo4j-bolt-driver 13 | ``` 14 | 15 | ## Features 16 | 17 | * Neo4j Bolt low-level binary protocol support 18 | * Message Pipelining for high concurrency 19 | * Connection Pooling 20 | * TLS support 21 | * Compatible with sql.driver 22 | 23 | ## Usage 24 | 25 | *_Please see [the statement tests](./stmt_test.go) or [the conn tests](./conn_test.go) for A LOT of examples of usage_* 26 | 27 | ### Examples 28 | 29 | #### Quick n’ Dirty 30 | 31 | ```go 32 | func quickNDirty() { 33 | driver := bolt.NewDriver() 34 | conn, _ := driver.OpenNeo("bolt://username:password@localhost:7687") 35 | defer conn.Close() 36 | 37 | // Start by creating a node 38 | result, _ := conn.ExecNeo("CREATE (n:NODE {foo: {foo}, bar: {bar}})", map[string]interface{}{"foo": 1, "bar": 2.2}) 39 | numResult, _ := result.RowsAffected() 40 | fmt.Printf("CREATED ROWS: %d\n", numResult) // CREATED ROWS: 1 41 | 42 | // Lets get the node 43 | data, rowsMetadata, _, _ := conn.QueryNeoAll("MATCH (n:NODE) RETURN n.foo, n.bar", nil) 44 | fmt.Printf("COLUMNS: %#v\n", rowsMetadata["fields"].([]interface{})) // COLUMNS: n.foo,n.bar 45 | fmt.Printf("FIELDS: %d %f\n", data[0][0].(int64), data[0][1].(float64)) // FIELDS: 1 2.2 46 | 47 | // oh cool, that worked. lets blast this baby and tell it to run a bunch of statements 48 | // in neo concurrently with a pipeline 49 | results, _ := conn.ExecPipeline([]string{ 50 | "MATCH (n:NODE) CREATE (n)-[:REL]->(f:FOO)", 51 | "MATCH (n:NODE) CREATE (n)-[:REL]->(b:BAR)", 52 | "MATCH (n:NODE) CREATE (n)-[:REL]->(z:BAZ)", 53 | "MATCH (n:NODE) CREATE (n)-[:REL]->(f:FOO)", 54 | "MATCH (n:NODE) CREATE (n)-[:REL]->(b:BAR)", 55 | "MATCH (n:NODE) CREATE (n)-[:REL]->(z:BAZ)", 56 | }, nil, nil, nil, nil, nil, nil) 57 | for _, result := range results { 58 | numResult, _ := result.RowsAffected() 59 | fmt.Printf("CREATED ROWS: %d\n", numResult) // CREATED ROWS: 2 (per each iteration) 60 | } 61 | 62 | data, _, _, _ = conn.QueryNeoAll("MATCH (n:NODE)-[:REL]->(m) RETURN m", nil) 63 | for _, row := range data { 64 | fmt.Printf("NODE: %#v\n", row[0].(graph.Node)) // Prints all nodes 65 | } 66 | 67 | result, _ = conn.ExecNeo(`MATCH (n) DETACH DELETE n`, nil) 68 | numResult, _ = result.RowsAffected() 69 | fmt.Printf("Rows Deleted: %d", numResult) // Rows Deleted: 13 70 | } 71 | ``` 72 | 73 | #### Slow n' Clean 74 | 75 | ```go 76 | 77 | // Constants to be used throughout the example 78 | const ( 79 | URI = "bolt://username:password@localhost:7687" 80 | CreateNode = "CREATE (n:NODE {foo: {foo}, bar: {bar}})" 81 | GetNode = "MATCH (n:NODE) RETURN n.foo, n.bar" 82 | RelationNode = "MATCH path=(n:NODE)-[:REL]->(m) RETURN path" 83 | DeleteNodes = "MATCH (n) DETACH DELETE n" 84 | ) 85 | 86 | func main() { 87 | con := createConnection() 88 | defer con.Close() 89 | 90 | st := prepareSatement(CreateNode, con) 91 | executeStatement(st) 92 | 93 | st = prepareSatement(GetNode, con) 94 | rows := queryStatement(st) 95 | consumeRows(rows, st) 96 | 97 | pipe := preparePipeline(con) 98 | executePipeline(pipe) 99 | 100 | st = prepareSatement(RelationNode, con) 101 | rows = queryStatement(st) 102 | consumeMetadata(rows, st) 103 | 104 | cleanUp(DeleteNodes, con) 105 | } 106 | 107 | func createConnection() bolt.Conn { 108 | driver := bolt.NewDriver() 109 | con, err := driver.OpenNeo(URI) 110 | handleError(err) 111 | return con 112 | } 113 | 114 | // Here we prepare a new statement. This gives us the flexibility to 115 | // cancel that statement without any request sent to Neo 116 | func prepareSatement(query string, con bolt.Conn) bolt.Stmt { 117 | st, err := con.PrepareNeo(query) 118 | handleError(err) 119 | return st 120 | } 121 | 122 | // Here we prepare a new pipeline statement for running multiple 123 | // queries concurrently 124 | func preparePipeline(con bolt.Conn) bolt.PipelineStmt { 125 | pipeline, err := con.PreparePipeline( 126 | "MATCH (n:NODE) CREATE (n)-[:REL]->(f:FOO)", 127 | "MATCH (n:NODE) CREATE (n)-[:REL]->(b:BAR)", 128 | "MATCH (n:NODE) CREATE (n)-[:REL]->(z:BAZ)", 129 | "MATCH (n:NODE) CREATE (n)-[:REL]->(f:FOO)", 130 | "MATCH (n:NODE) CREATE (n)-[:REL]->(b:BAR)", 131 | "MATCH (n:NODE) CREATE (n)-[:REL]->(z:BAZ)", 132 | ) 133 | handleError(err) 134 | return pipeline 135 | } 136 | 137 | func executePipeline(pipeline bolt.PipelineStmt) { 138 | pipelineResults, err := pipeline.ExecPipeline(nil, nil, nil, nil, nil, nil) 139 | handleError(err) 140 | 141 | for _, result := range pipelineResults { 142 | numResult, _ := result.RowsAffected() 143 | fmt.Printf("CREATED ROWS: %d\n", numResult) // CREATED ROWS: 2 (per each iteration) 144 | } 145 | 146 | err = pipeline.Close() 147 | handleError(err) 148 | } 149 | 150 | func queryStatement(st bolt.Stmt) bolt.Rows { 151 | // Even once I get the rows, if I do not consume them and close the 152 | // rows, Neo will discard and not send the data 153 | rows, err := st.QueryNeo(nil) 154 | handleError(err) 155 | return rows 156 | } 157 | 158 | func consumeMetadata(rows bolt.Rows, st bolt.Stmt) { 159 | // Here we loop through the rows until we get the metadata object 160 | // back, meaning the row stream has been fully consumed 161 | 162 | var err error 163 | err = nil 164 | 165 | for err == nil { 166 | var row []interface{} 167 | row, _, err = rows.NextNeo() 168 | if err != nil && err != io.EOF { 169 | panic(err) 170 | } else if err != io.EOF { 171 | fmt.Printf("PATH: %#v\n", row[0].(graph.Path)) // Prints all paths 172 | } 173 | } 174 | st.Close() 175 | } 176 | 177 | func consumeRows(rows bolt.Rows, st bolt.Stmt) { 178 | // This interface allows you to consume rows one-by-one, as they 179 | // come off the bolt stream. This is more efficient especially 180 | // if you're only looking for a particular row/set of rows, as 181 | // you don't need to load up the entire dataset into memory 182 | data, _, err := rows.NextNeo() 183 | handleError(err) 184 | 185 | // This query only returns 1 row, so once it's done, it will return 186 | // the metadata associated with the query completion, along with 187 | // io.EOF as the error 188 | _, _, err = rows.NextNeo() 189 | handleError(err) 190 | fmt.Printf("COLUMNS: %#v\n", rows.Metadata()["fields"].([]interface{})) // COLUMNS: n.foo,n.bar 191 | fmt.Printf("FIELDS: %d %f\n", data[0].(int64), data[1].(float64)) // FIELDS: 1 2.2 192 | 193 | st.Close() 194 | } 195 | 196 | // Executing a statement just returns summary information 197 | func executeStatement(st bolt.Stmt) { 198 | result, err := st.ExecNeo(map[string]interface{}{"foo": 1, "bar": 2.2}) 199 | handleError(err) 200 | numResult, err := result.RowsAffected() 201 | handleError(err) 202 | fmt.Printf("CREATED ROWS: %d\n", numResult) // CREATED ROWS: 1 203 | 204 | // Closing the statment will also close the rows 205 | st.Close() 206 | } 207 | 208 | func cleanUp(query string, con bolt.Conn) { 209 | result, _ := con.ExecNeo(query, nil) 210 | fmt.Println(result) 211 | numResult, _ := result.RowsAffected() 212 | fmt.Printf("Rows Deleted: %d", numResult) // Rows Deleted: 13 213 | } 214 | 215 | // Here we create a simple function that will take care of errors, helping with some code clean up 216 | func handleError(err error) { 217 | if err != nil { 218 | panic(err) 219 | } 220 | } 221 | ``` 222 | ## API 223 | 224 | *_There is much more detailed information in [the godoc](http://godoc.org/github.com/johnnadratowski/golang-neo4j-bolt-driver)_* 225 | 226 | This implementation attempts to follow the best practices as per the Bolt specification, but also implements compatibility with Golang's `sql.driver` interface. 227 | 228 | As such, these interfaces closely match the `sql.driver` interfaces, but they also provide Neo4j Bolt specific functionality in addition to the `sql.driver` interface. 229 | 230 | It is recommended that you use the Neo4j Bolt-specific interfaces if possible. The implementation is more efficient and can more closely support the Neo4j Bolt feature set. 231 | 232 | The URL format is: `bolt://(user):(password)@(host):(port)` 233 | Schema must be `bolt`. User and password is only necessary if you are authenticating. 234 | 235 | Connection pooling is provided out of the box with the `NewDriverPool` method. You can give it the maximum number of 236 | connections to have at a time. 237 | 238 | You can get logs from the driver by setting the log level using the `log` packages `SetLevel`. 239 | 240 | 241 | ## Dev Quickstart 242 | 243 | ``` 244 | # Put in git hooks 245 | ln -s ../../scripts/pre-commit .git/hooks/pre-commit 246 | ln -s ../../scripts/pre-push .git/hooks/pre-push 247 | 248 | # No special build steps necessary 249 | go build 250 | 251 | # Testing with log info and a local bolt DB, getting coverage output 252 | BOLT_DRIVER_LOG=info NEO4J_BOLT=bolt://localhost:7687 go test -coverprofile=./tmp/cover.out -coverpkg=./... -v -race && go tool cover -html=./tmp/cover.out 253 | 254 | # Testing with trace output for debugging 255 | BOLT_DRIVER_LOG=trace NEO4J_BOLT=bolt://localhost:7687 go test -v -race 256 | 257 | # Testing with running recorder to record tests for CI 258 | BOLT_DRIVER_LOG=trace NEO4J_BOLT=bolt://localhost:7687 RECORD_OUTPUT=1 go test -v -race 259 | ``` 260 | 261 | The tests are written in an integration testing style. Most of them are in the statement tests, but should be made more granular in the future. 262 | 263 | In order to get CI, I made a recorder mechanism so you don't need to run neo4j alongside the tests in the CI server. You run the tests locally against a neo4j instance with the RECORD_OUTPUT=1 environment variable, it generates the recordings in the ./recordings folder. This is necessary if the tests have changed, or if the internals have significantly changed. Installing the git hooks will run the tests automatically on push. If there are updated tests, you will need to re-run the recorder to add them and push them as well. 264 | 265 | You need access to a running Neo4J database to develop for this project, so that you can run the tests to generate the recordings. For the recordings to be generated correctly you also need to make sure authorization is turned off on the Neo4J instance. For more information on Neo4J installation and configuration see the official Neo4j docs: https://neo4j.com/docs/operations-manual/current/installation/ 266 | 267 | ## Supported Builds 268 | * Linux (1.4.x, 1.5.x, 1.6.x, 1.7.x, 1.8.x, 1.9.x and tip) 269 | * MacOs (1.7.x, 1.8.x, 1.9.x and tip) 270 | *_according to https://github.com/golang/go/issues/17824, go 1.6.4 (and anything prior does not support osx 10.12) which results in unpredicable behavior (sometimes it is okay, sometimes build hangs, and sometimes build fail due to segfault)_* 271 | 272 | 273 | 274 | ## TODO 275 | 276 | * Cypher Parser to implement NumInput and pre-flight checking 277 | * More Tests 278 | * Benchmark Tests 279 | -------------------------------------------------------------------------------- /conn_test.go: -------------------------------------------------------------------------------- 1 | package golangNeo4jBoltDriver 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 9 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/messages" 10 | ) 11 | 12 | func TestBoltConn_parseURL(t *testing.T) { 13 | c := &boltConn{connStr: "http://foo:7687"} 14 | 15 | _, err := c.parseURL() 16 | if err == nil { 17 | t.Fatal("Expected error from incorrect protocol") 18 | } 19 | 20 | c = &boltConn{connStr: "bolt://john@foo:7687"} 21 | _, err = c.parseURL() 22 | if err == nil { 23 | t.Fatal("Expected error from missing password") 24 | } 25 | 26 | c = &boltConn{connStr: "bolt://john:password@foo:7687"} 27 | _, err = c.parseURL() 28 | if err != nil { 29 | t.Fatal("Should not error on valid url") 30 | } 31 | if c.user != "john" { 32 | t.Fatal("Expected user to be 'john'") 33 | } 34 | if c.password != "password" { 35 | t.Fatal("Expected password to be 'password'") 36 | } 37 | 38 | c = &boltConn{connStr: "bolt://john:password@foo:7687?tls=true"} 39 | _, err = c.parseURL() 40 | if err != nil { 41 | t.Fatal("Should not error on valid url") 42 | } 43 | if !c.useTLS { 44 | t.Fatal("Expected to use TLS") 45 | } 46 | 47 | c = &boltConn{connStr: "bolt://john:password@foo:7687?tls=true&tls_no_verify=1&tls_ca_cert_file=ca&tls_cert_file=cert&tls_key_file=key"} 48 | _, err = c.parseURL() 49 | if err != nil { 50 | t.Fatal("Should not error on valid url") 51 | } 52 | if !c.useTLS { 53 | t.Fatal("Expected to use TLS") 54 | } 55 | if !c.tlsNoVerify { 56 | t.Fatal("Expected to use TLS with no verification") 57 | } 58 | if c.caCertFile != "ca" { 59 | t.Fatal("Expected ca cert file 'ca'") 60 | } 61 | if c.certFile != "cert" { 62 | t.Fatal("Expected cert file 'cert'") 63 | } 64 | if c.keyFile != "key" { 65 | t.Fatal("Expected key file 'key'") 66 | } 67 | } 68 | 69 | func TestBoltConn_Close(t *testing.T) { 70 | driver := NewDriver() 71 | 72 | // Records session for testing 73 | driver.(*boltDriver).recorder = newRecorder("TestBoltConn_Close", neo4jConnStr) 74 | 75 | conn, err := driver.OpenNeo(neo4jConnStr) 76 | if err != nil { 77 | t.Fatalf("An error occurred opening conn: %s", err) 78 | } 79 | 80 | err = conn.Close() 81 | if err != nil { 82 | t.Fatalf("An error occurred closing conn: %s", err) 83 | } 84 | 85 | if !conn.(*boltConn).closed { 86 | t.Error("Conn not closed at end of test") 87 | } 88 | } 89 | 90 | func TestBoltConn_SelectOne(t *testing.T) { 91 | driver := NewDriver() 92 | 93 | // Records session for testing 94 | driver.(*boltDriver).recorder = newRecorder("TestBoltConn_SelectOne", neo4jConnStr) 95 | 96 | conn, err := driver.OpenNeo(neo4jConnStr) 97 | if err != nil { 98 | t.Fatalf("An error occurred opening conn: %s", err) 99 | } 100 | 101 | rows, err := conn.QueryNeo("RETURN 1;", nil) 102 | if err != nil { 103 | t.Fatalf("An error occurred querying Neo: %s", err) 104 | } 105 | 106 | expectedMetadata := map[string]interface{}{ 107 | "fields": []interface{}{"1"}, 108 | } 109 | resultingMetadata := make(map[string]interface{}) 110 | 111 | for key, value := range rows.Metadata() { 112 | if key != "result_available_after" { 113 | resultingMetadata[key] = value 114 | } 115 | } 116 | 117 | if !reflect.DeepEqual(resultingMetadata, expectedMetadata) { 118 | t.Fatalf("Unexpected success metadata. Expected %#v. Got: %#v", expectedMetadata, rows.Metadata()) 119 | } else if _, ok := rows.Metadata()["result_available_after"]; !ok { 120 | t.Fatalf("Unexpected success metadata. Expected the key 'result_available_after'. Got: %#v", rows.Metadata()) 121 | } 122 | 123 | output, _, err := rows.NextNeo() 124 | if err != nil { 125 | t.Fatalf("An error occurred getting next row: %s", err) 126 | } 127 | 128 | if output[0].(int64) != 1 { 129 | t.Fatalf("Unexpected output. Expected 1. Got: %d", output) 130 | } 131 | 132 | _, metadata, err := rows.NextNeo() 133 | expectedMetadata = map[string]interface{}{ 134 | "type": "r", 135 | } 136 | resultingMetadata = make(map[string]interface{}) 137 | 138 | for key, value := range metadata { 139 | if key != "result_consumed_after" { 140 | resultingMetadata[key] = value 141 | } 142 | } 143 | 144 | if err != io.EOF { 145 | t.Fatalf("Unexpected row closed output. Expected io.EOF. Got: %s", err) 146 | } else if !reflect.DeepEqual(resultingMetadata, expectedMetadata) { 147 | t.Fatalf("Metadata didn't match expected. Expected %#v. Got: %#v", expectedMetadata, metadata) 148 | } else if _, ok := metadata["result_consumed_after"]; !ok { 149 | t.Fatalf("Metadata didn't match expected. Expected the key 'result_consumed_after'. Got: %#v", rows.Metadata()) 150 | } 151 | 152 | err = conn.Close() 153 | if err != nil { 154 | t.Fatalf("Error closing connection: %s", err) 155 | } 156 | } 157 | 158 | func TestBoltConn_SelectAll(t *testing.T) { 159 | driver := NewDriver() 160 | 161 | // Records session for testing 162 | driver.(*boltDriver).recorder = newRecorder("TestBoltConn_SelectAll", neo4jConnStr) 163 | 164 | conn, err := driver.OpenNeo(neo4jConnStr) 165 | if err != nil { 166 | t.Fatalf("An error occurred opening conn: %s", err) 167 | } 168 | 169 | results, err := conn.ExecNeo("CREATE (f:NODE {a: 1}), (b:NODE {a: 2})", nil) 170 | if err != nil { 171 | t.Fatalf("An error occurred querying Neo: %s", err) 172 | } 173 | affected, err := results.RowsAffected() 174 | if err != nil { 175 | t.Fatalf("An error occurred getting rows affected: %s", err) 176 | } 177 | if affected != int64(2) { 178 | t.Fatalf("Incorrect number of rows affected: %d", affected) 179 | } 180 | 181 | data, rowMetadata, metadata, err := conn.QueryNeoAll("MATCH (n:NODE) RETURN n.a ORDER BY n.a", nil) 182 | if data[0][0] != int64(1) { 183 | t.Fatalf("Incorrect data returned for first row: %#v", data[0]) 184 | } 185 | if data[1][0] != int64(2) { 186 | t.Fatalf("Incorrect data returned for second row: %#v", data[1]) 187 | } 188 | 189 | if rowMetadata["fields"].([]interface{})[0] != "n.a" { 190 | t.Fatalf("Unexpected column metadata: %#v", rowMetadata) 191 | } 192 | 193 | if metadata["type"].(string) != "r" { 194 | t.Fatalf("Unexpected request metadata: %#v", metadata) 195 | } 196 | 197 | results, err = conn.ExecNeo("MATCH (n:NODE) DELETE n", nil) 198 | if err != nil { 199 | t.Fatalf("An error occurred querying Neo: %s", err) 200 | } 201 | affected, err = results.RowsAffected() 202 | if err != nil { 203 | t.Fatalf("An error occurred getting rows affected: %s", err) 204 | } 205 | if affected != int64(2) { 206 | t.Fatalf("Incorrect number of rows affected: %d", affected) 207 | } 208 | 209 | err = conn.Close() 210 | if err != nil { 211 | t.Fatalf("Error closing connection: %s", err) 212 | } 213 | } 214 | 215 | func TestBoltConn_Ignored(t *testing.T) { 216 | driver := NewDriver() 217 | 218 | // Records session for testing 219 | driver.(*boltDriver).recorder = newRecorder("TestBoltConn_Ignored", neo4jConnStr) 220 | 221 | conn, _ := driver.OpenNeo(neo4jConnStr) 222 | defer conn.Close() 223 | 224 | // This will make two calls at once - Run and Pull All. The pull all should be ignored, which is what 225 | // we're testing. 226 | _, err := conn.ExecNeo("syntax error", map[string]interface{}{"foo": 1, "bar": 2.2}) 227 | if err == nil { 228 | t.Fatal("Expected an error on syntax error.") 229 | } 230 | 231 | data, _, _, err := conn.QueryNeoAll("RETURN 1;", nil) 232 | if err != nil { 233 | t.Fatalf("Got error when running next query after a failure: %#v", err) 234 | } 235 | 236 | if data[0][0].(int64) != 1 { 237 | t.Fatalf("Expected different data from output: %#v", data) 238 | } 239 | } 240 | 241 | func TestBoltConn_IgnoredPipeline(t *testing.T) { 242 | driver := NewDriver() 243 | 244 | // Records session for testing 245 | driver.(*boltDriver).recorder = newRecorder("TestBoltConn_IgnoredPipeline", neo4jConnStr) 246 | 247 | conn, _ := driver.OpenNeo(neo4jConnStr) 248 | defer conn.Close() 249 | 250 | // This will make two calls at once - Run and Pull All. The pull all should be ignored, which is what 251 | // we're testing. 252 | _, err := conn.ExecPipeline([]string{"syntax error", "syntax error", "syntax error"}, nil) 253 | if err == nil { 254 | t.Fatal("Expected an error on syntax error.") 255 | } 256 | 257 | data, _, _, err := conn.QueryNeoAll("RETURN 1;", nil) 258 | if err != nil { 259 | t.Fatalf("Got error when running next query after a failure: %#v", err) 260 | } 261 | 262 | if data[0][0].(int64) != 1 { 263 | t.Fatalf("Expected different data from output: %#v", data) 264 | } 265 | } 266 | 267 | func TestBoltConn_FailureMessageError(t *testing.T) { 268 | driver := NewDriver() 269 | 270 | // Records session for testing 271 | driver.(*boltDriver).recorder = newRecorder("TestBoltConn_FailureMessageError", neo4jConnStr) 272 | 273 | conn, err := driver.OpenNeo(neo4jConnStr) 274 | defer conn.Close() 275 | if err != nil { 276 | t.Fatalf("An error occurred opening conn: %s", err) 277 | } 278 | 279 | _, err = conn.ExecNeo("THIS IS A BAD QUERY AND SHOULD RETURN A FAILURE MESSAGE", nil) 280 | if err == nil { 281 | t.Fatal("This should have returned a failure message error, but got a nil error") 282 | } 283 | 284 | code := "Neo.ClientError.Statement.SyntaxError" 285 | if err.(*errors.Error).InnerMost().(messages.FailureMessage).Metadata["code"] != code { 286 | t.Fatalf("Expected error message code %s, but got %v", code, err.(*errors.Error).InnerMost().(messages.FailureMessage).Metadata["code"]) 287 | } 288 | } 289 | 290 | func TestBoltConn_CloseStatementOnError(t *testing.T) { 291 | driver := NewDriver() 292 | 293 | // Records session for testing 294 | driver.(*boltDriver).recorder = newRecorder("TestBoltConn_CloseStatementOnError", neo4jConnStr) 295 | 296 | conn, err := driver.OpenNeo(neo4jConnStr) 297 | defer conn.Close() 298 | if err != nil { 299 | t.Fatalf("An error occurred opening conn: %s", err) 300 | } 301 | 302 | _, err = conn.QueryNeo("THIS IS A BAD QUERY AND SHOULD RETURN A FAILURE MESSAGE", nil) 303 | if err == nil { 304 | t.Fatal("This should have returned a failure message error, but got a nil error") 305 | } 306 | 307 | _, err = conn.QueryNeo("RETURN 1;", nil) 308 | if err != nil { 309 | t.Fatalf("Got error when running next query after a failure: %#v", err) 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /*Package golangNeo4jBoltDriver implements a driver for the Neo4J Bolt Protocol. 2 | 3 | The driver is compatible with Golang's sql.driver interface, but 4 | aims to implement a more complete featureset in line with what 5 | Neo4J and Bolt provides. 6 | 7 | As such, there are multiple interfaces the user can choose from. 8 | It's highly recommended that the user use the Neo4J-specific 9 | interfaces as they are more flexible and efficient than the 10 | provided sql.driver compatible methods. 11 | 12 | The interface tries to be consistent throughout. The sql.driver 13 | interfaces are standard, but the Neo4J-specific ones contain a 14 | naming convention of either "Neo" or "Pipeline". 15 | 16 | The "Neo" ones are the basic interfaces for making queries to 17 | Neo4j and it's expected that these would be used the most. 18 | 19 | The "Pipeline" ones are to support Bolt's pipelining features. 20 | Pipelines allow the user to send Neo4j many queries at once and 21 | have them executed by the database concurrently. This is useful 22 | if you have a bunch of queries that aren't necessarily dependant 23 | on one another, and you want to get better performance. The 24 | internal APIs will also pipeline statements where it is able to 25 | reliably do so, but by manually using the pipelining feature 26 | you can maximize your throughput. 27 | 28 | The API provides connection pooling using the `NewDriverPool` method. 29 | This allows you to pass it the maximum number of open connections 30 | to be used in the pool. Once this limit is hit, any new clients will 31 | have to wait for a connection to become available again. 32 | 33 | The sql driver is registered as "neo4j-bolt". The sql.driver interface 34 | is much more limited than what bolt and neo4j supports. In some cases, 35 | concessions were made in order to make that interface work with the 36 | neo4j way of doing things. The main instance of this is the marshalling 37 | of objects to/from the sql.driver.Value interface. In order to support 38 | object types that aren't supported by this interface, the internal encoding 39 | package is used to marshal these objects to byte strings. This ultimately 40 | makes for a less efficient and more 'clunky' implementation. A glaring 41 | instance of this is passing parameters. Neo4j expects named parameters 42 | but the driver interface can only really support positional parameters. 43 | To get around this, the user must create a map[string]interface{} of their 44 | parameters and marshal it to a driver.Value using the encoding.Marshal 45 | function. Similarly, the user must unmarshal data returned from the queries 46 | using the encoding.Unmarshal function, then use type assertions to retrieve 47 | the proper type. 48 | 49 | In most cases the driver will return the data from neo as the proper 50 | go-specific types. For integers they always come back 51 | as int64 and floats always come back as float64. This is for the 52 | convenience of the user and acts similarly to go's JSON interface. 53 | This prevents the user from having to use reflection to get 54 | these values. Internally, the types are always transmitted over 55 | the wire with as few bytes as possible. 56 | 57 | There are also cases where no go-specific type matches the returned values, 58 | such as when you query for a node, relationship, or path. The driver 59 | exposes specific structs which represent this data in the 'structures.graph' 60 | package. There are 4 types - Node, Relationship, UnboundRelationship, and 61 | Path. The driver returns interface{} objects which must have their types 62 | properly asserted to get the data out. 63 | 64 | There are some limitations to the types of collections the driver 65 | supports. Specifically, maps should always be of type map[string]interface{} 66 | and lists should always be of type []interface{}. It doesn't seem that 67 | the Bolt protocol supports uint64 either, so the biggest number it can send 68 | right now is the int64 max. 69 | 70 | The URL format is: `bolt://(user):(password)@(host):(port)` 71 | Schema must be `bolt`. User and password is only necessary if you are authenticating. 72 | TLS is supported by using query parameters on the connection string, like so: 73 | `bolt://host:port?tls=true&tls_no_verify=false` 74 | 75 | The supported query params are: 76 | 77 | * timeout - the number of seconds to set the connection timeout to. Defaults to 60 seconds. 78 | * tls - Set to 'true' or '1' if you want to use TLS encryption 79 | * tls_no_verify - Set to 'true' or '1' if you want to accept any server certificate (for testing, not secure) 80 | * tls_ca_cert_file - path to a custom ca cert for a self-signed TLS cert 81 | * tls_cert_file - path to a cert file for this client (need to verify this is processed by Neo4j) 82 | * tls_key_file - path to a key file for this client (need to verify this is processed by Neo4j) 83 | 84 | Errors returned from the API support wrapping, so if you receive an error 85 | from the library, it might be wrapping other errors. You can get the innermost 86 | error by using the `InnerMost` method. Failure messages from Neo4J are reported, 87 | along with their metadata, as an error. In order to get the failure message metadata 88 | from a wrapped error, you can do so by calling 89 | `err.(*errors.Error).InnerMost().(messages.FailureMessage).Metadata` 90 | 91 | If there is an error with the database connection, you should get a sql/driver ErrBadConn 92 | as per the best practice recommendations of the Golang SQL Driver. However, this error 93 | may be wrapped, so you might have to call `InnerMost` to get it, as specified above. 94 | */ 95 | package golangNeo4jBoltDriver 96 | -------------------------------------------------------------------------------- /driver.go: -------------------------------------------------------------------------------- 1 | package golangNeo4jBoltDriver 2 | 3 | import ( 4 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/log" 5 | "time" 6 | "database/sql" 7 | "database/sql/driver" 8 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 9 | "sync" 10 | ) 11 | 12 | var ( 13 | magicPreamble = []byte{0x60, 0x60, 0xb0, 0x17} 14 | supportedVersions = []byte{ 15 | 0x00, 0x00, 0x00, 0x01, 16 | 0x00, 0x00, 0x00, 0x00, 17 | 0x00, 0x00, 0x00, 0x00, 18 | 0x00, 0x00, 0x00, 0x00, 19 | } 20 | handShake = append(magicPreamble, supportedVersions...) 21 | noVersionSupported = []byte{0x00, 0x00, 0x00, 0x00} 22 | // Version is the current version of this driver 23 | Version = "1.0" 24 | // ClientID is the id of this client 25 | ClientID = "GolangNeo4jBolt/" + Version 26 | ) 27 | 28 | // Driver is a driver allowing connection to Neo4j 29 | // The driver allows you to open a new connection to Neo4j 30 | // 31 | // Implements sql/driver, but also includes its own more neo-friendly interface. 32 | // Some of the features of this interface implement neo-specific features 33 | // unavailable in the sql/driver compatible interface 34 | // 35 | // Driver objects should be THREAD SAFE, so you can use them 36 | // to open connections in multiple threads. The connection objects 37 | // themselves, and any prepared statements/transactions within ARE NOT 38 | // THREAD SAFE. 39 | type Driver interface { 40 | // Open opens a sql.driver compatible connection. Used internally 41 | // by the go sql interface 42 | Open(string) (driver.Conn, error) 43 | // OpenNeo opens a Neo-specific connection. This should be used 44 | // directly when not using the golang sql interface 45 | OpenNeo(string) (Conn, error) 46 | } 47 | 48 | type boltDriver struct { 49 | recorder *recorder 50 | } 51 | 52 | // NewDriver creates a new Driver object 53 | func NewDriver() Driver { 54 | return &boltDriver{} 55 | } 56 | 57 | // Open opens a new Bolt connection to the Neo4J database 58 | func (d *boltDriver) Open(connStr string) (driver.Conn, error) { 59 | return newBoltConn(connStr, d) // Never use pooling when using SQL driver 60 | } 61 | 62 | // Open opens a new Bolt connection to the Neo4J database. Implements a Neo-friendly alternative to sql/driver. 63 | func (d *boltDriver) OpenNeo(connStr string) (Conn, error) { 64 | return newBoltConn(connStr, d) 65 | } 66 | 67 | // DriverPool is a driver allowing connection to Neo4j with support for connection pooling 68 | // The driver allows you to open a new connection to Neo4j 69 | // 70 | // Driver objects should be THREAD SAFE, so you can use them 71 | // to open connections in multiple threads. The connection objects 72 | // themselves, and any prepared statements/transactions within ARE NOT 73 | // THREAD SAFE. 74 | type DriverPool interface { 75 | // OpenPool opens a Neo-specific connection. 76 | OpenPool() (Conn, error) 77 | reclaim(*boltConn) error 78 | } 79 | 80 | // ClosableDriverPool like the DriverPool but with a closable function 81 | type ClosableDriverPool interface { 82 | DriverPool 83 | Close() error 84 | } 85 | 86 | type boltDriverPool struct { 87 | connStr string 88 | maxConns int 89 | pool chan *boltConn 90 | connRefs []*boltConn 91 | refLock sync.Mutex 92 | closed bool 93 | } 94 | 95 | // NewDriverPool creates a new Driver object with connection pooling 96 | func NewDriverPool(connStr string, max int) (DriverPool, error) { 97 | return createDriverPool(connStr, max) 98 | } 99 | 100 | // NewClosableDriverPool create a closable driver pool 101 | func NewClosableDriverPool(connStr string, max int) (ClosableDriverPool, error) { 102 | return createDriverPool(connStr, max) 103 | } 104 | 105 | func createDriverPool(connStr string, max int) (*boltDriverPool, error) { 106 | d := &boltDriverPool{ 107 | connStr: connStr, 108 | maxConns: max, 109 | pool: make(chan *boltConn, max), 110 | } 111 | 112 | for i := 0; i < max; i++ { 113 | conn, err := newPooledBoltConn(connStr, d) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | d.pool <- conn 119 | } 120 | 121 | return d, nil 122 | } 123 | 124 | // OpenPool opens a returns a Bolt connection from the pool to the Neo4J database. 125 | func (d *boltDriverPool) OpenPool() (Conn, error) { 126 | // For each connection request we need to block in case the Close function is called. This gives us a guarantee 127 | // when closing the pool no new connections are made. 128 | d.refLock.Lock() 129 | defer d.refLock.Unlock() 130 | if !d.closed { 131 | conn := <-d.pool 132 | if connectionNilOrClosed(conn) { 133 | if err := conn.initialize(); err != nil { 134 | return nil, err 135 | } 136 | d.connRefs = append(d.connRefs, conn) 137 | } 138 | return conn, nil 139 | } 140 | return nil, errors.New("Driver pool has been closed") 141 | } 142 | 143 | func connectionNilOrClosed(conn *boltConn) (bool) { 144 | if(conn.conn == nil) {//nil check before attempting read 145 | return true 146 | } 147 | conn.conn.SetReadDeadline(time.Now()) 148 | zero := make ([]byte, 0) 149 | _, err := conn.conn.Read(zero)//read zero bytes to validate connection is still alive 150 | if err != nil { 151 | log.Error("Bad Connection state detected", err)//the error caught here could be a io.EOF or a timeout, either way we want to log the error & return true 152 | return true 153 | } 154 | return false 155 | } 156 | 157 | // Close all connections in the pool 158 | func (d *boltDriverPool) Close() error { 159 | // Lock the connection ref so no new connections can be added 160 | d.refLock.Lock() 161 | defer d.refLock.Unlock() 162 | for _, conn := range d.connRefs { 163 | // Remove the reference to the pool, to allow a clean up of the connection 164 | conn.poolDriver = nil 165 | err := conn.Close() 166 | if err != nil { 167 | d.closed = true 168 | return err 169 | } 170 | } 171 | // Mark the pool as closed to stop any new connections 172 | d.closed = true 173 | return nil 174 | } 175 | 176 | func (d *boltDriverPool) reclaim(conn *boltConn) error { 177 | var newConn *boltConn 178 | var err error 179 | if conn.connErr != nil || conn.closed { 180 | newConn, err = newPooledBoltConn(d.connStr, d) 181 | if err != nil { 182 | return err 183 | } 184 | } else { 185 | // sneakily swap out connection so a reference to 186 | // it isn't held on to 187 | newConn = &boltConn{} 188 | *newConn = *conn 189 | } 190 | 191 | d.pool <- newConn 192 | conn = nil 193 | 194 | return nil 195 | } 196 | 197 | func init() { 198 | sql.Register("neo4j-bolt", &boltDriver{}) 199 | } 200 | -------------------------------------------------------------------------------- /driver_test.go: -------------------------------------------------------------------------------- 1 | package golangNeo4jBoltDriver 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "time" 8 | 9 | "sync" 10 | 11 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/log" 12 | ) 13 | 14 | var ( 15 | neo4jConnStr = "" 16 | ) 17 | 18 | func TestMain(m *testing.M) { 19 | log.SetLevel(os.Getenv("BOLT_DRIVER_LOG")) 20 | 21 | neo4jConnStr = os.Getenv("NEO4J_BOLT") 22 | if neo4jConnStr != "" { 23 | log.Info("Using NEO4J for tests:", neo4jConnStr) 24 | } else if os.Getenv("ENSURE_NEO4J_BOLT") != "" { 25 | log.Fatal("Must give NEO4J_BOLT environment variable") 26 | } 27 | 28 | if neo4jConnStr != "" { 29 | // If we're using a DB for testing neo, clear it out after all the test runs 30 | clearNeo() 31 | } 32 | 33 | output := m.Run() 34 | 35 | if neo4jConnStr != "" { 36 | // If we're using a DB for testing neo, clear it out after all the test runs 37 | clearNeo() 38 | } 39 | 40 | os.Exit(output) 41 | } 42 | 43 | func clearNeo() { 44 | driver := NewDriver() 45 | conn, err := driver.OpenNeo(neo4jConnStr) 46 | if err != nil { 47 | panic("Error getting conn to clear DB") 48 | } 49 | 50 | stmt, err := conn.PrepareNeo(`MATCH (n) DETACH DELETE n`) 51 | if err != nil { 52 | panic("Error getting stmt to clear DB") 53 | } 54 | defer stmt.Close() 55 | 56 | _, err = stmt.ExecNeo(nil) 57 | if err != nil { 58 | panic("Error running query to clear DB") 59 | } 60 | } 61 | 62 | func TestBoltDriverPool_OpenNeo(t *testing.T) { 63 | if neo4jConnStr == "" { 64 | t.Skip("Cannot run this test when in recording mode") 65 | } 66 | 67 | pool, err := NewDriverPool(neo4jConnStr, 25) 68 | if err != nil { 69 | t.Fatalf("An error occurred opening driver pool: %#v", err) 70 | } 71 | 72 | now := time.Now().Unix() 73 | for i := 0; i < 25; i++ { 74 | go func() { 75 | c, err := pool.OpenPool() 76 | if err != nil { 77 | t.Fatalf("An error occurred opening conn from pool: %#v", err) 78 | } 79 | defer c.Close() 80 | time.Sleep(time.Millisecond * time.Duration(200)) 81 | }() 82 | } 83 | 84 | c, err := pool.OpenPool() 85 | if !(time.Now().Unix()-now < 200) { 86 | t.Fatalf("An error occurred opening conn from pool at end: %#v", err) 87 | } 88 | defer c.Close() 89 | } 90 | 91 | func TestBoltDriverPool_Concurrent(t *testing.T) { 92 | if neo4jConnStr == "" { 93 | t.Skip("Cannot run this test when in recording mode") 94 | } 95 | 96 | var wg sync.WaitGroup 97 | wg.Add(2) 98 | driver, err := NewDriverPool(neo4jConnStr, 2) 99 | if err != nil { 100 | t.Fatalf("An error occurred opening driver pool: %#v", err) 101 | } 102 | 103 | one := make(chan bool) 104 | two := make(chan bool) 105 | three := make(chan bool) 106 | four := make(chan bool) 107 | five := make(chan bool) 108 | six := make(chan bool) 109 | seven := make(chan bool) 110 | go func() { 111 | defer wg.Done() 112 | 113 | conn, err := driver.OpenPool() 114 | if err != nil { 115 | t.Fatalf("An error occurred opening conn: %s", err) 116 | } 117 | defer conn.Close() 118 | 119 | data, _, _, err := conn.QueryNeoAll(`MATCH (n) RETURN n`, nil) 120 | if err != nil { 121 | t.Fatalf("An error occurred querying neo: %s", err) 122 | } 123 | 124 | log.Info("FIRST: WRITE OUT 1") 125 | one <- true 126 | log.Info("FIRST: FINISHED WRITE OUT 1") 127 | log.Info("FIRST: WAIT ON 2") 128 | <-two 129 | log.Info("FIRST: FINISHED WAIT ON 2") 130 | 131 | if len(data) != 0 { 132 | log.Panicf("Expected no data: %#v", data) 133 | } 134 | 135 | data, _, _, err = conn.QueryNeoAll(`MATCH (n) RETURN n`, nil) 136 | if err != nil { 137 | log.Panicf("An error occurred querying neo: %s", err) 138 | } 139 | 140 | log.Infof("data: %#v", data) 141 | if len(data) != 1 { 142 | log.Panicf("Expected no data: %#v", data) 143 | } 144 | 145 | log.Info("FIRST: WRITE OUT 3") 146 | three <- true 147 | log.Info("FIRST: FINISHED WRITE OUT 3") 148 | log.Info("FIRST: WAIT ON 4") 149 | <-four 150 | log.Info("FIRST: FINISHED WAIT ON 4") 151 | 152 | data, _, _, err = conn.QueryNeoAll(`MATCH path=(:FOO)-[:BAR]->(:BAZ) RETURN path`, nil) 153 | if err != nil { 154 | log.Panicf("An error occurred querying neo: %s", err) 155 | } 156 | 157 | if len(data) != 1 { 158 | log.Panicf("Expected no data: %#v", data) 159 | } 160 | 161 | log.Info("FIRST: WRITE OUT 5") 162 | five <- true 163 | log.Info("FIRST: FINISHED WRITE OUT 5") 164 | log.Info("FIRST: WAIT ON 6") 165 | <-six 166 | log.Info("FIRST: FINISHED WAIT ON 6") 167 | 168 | data, _, _, err = conn.QueryNeoAll(`MATCH path=(:FOO)-[:BAR]->(:BAZ) RETURN path`, nil) 169 | if err != nil { 170 | log.Panicf("An error occurred querying neo: %s", err) 171 | } 172 | 173 | if len(data) != 0 { 174 | log.Panicf("Expected no data: %#v", data) 175 | } 176 | 177 | log.Info("FIRST: WRITE OUT 7") 178 | seven <- true 179 | log.Info("FIRST: FINISHED WRITE OUT 7") 180 | }() 181 | 182 | go func() { 183 | log.Info("SECOND: WAIT ON 1") 184 | <-one 185 | log.Info("SECOND: FINISHED WAIT ON 1") 186 | defer wg.Done() 187 | 188 | conn, err := driver.OpenPool() 189 | if err != nil { 190 | log.Panicf("An error occurred opening conn: %s", err) 191 | } 192 | defer conn.Close() 193 | 194 | _, err = conn.ExecNeo(`CREATE (f:FOO)`, nil) 195 | if err != nil { 196 | log.Panicf("An error occurred creating f neo: %s", err) 197 | } 198 | 199 | log.Info("SECOND: WRITE OUT 2") 200 | two <- true 201 | log.Info("SECOND: FINISHED WRITE OUT 2") 202 | log.Info("SECOND: WAIT ON 3") 203 | <-three 204 | log.Info("SECOND: FINISHED WAIT ON 3") 205 | 206 | _, err = conn.ExecNeo(`MATCH (f:FOO) CREATE UNIQUE (f)-[b:BAR]->(c:BAZ)`, nil) 207 | if err != nil { 208 | log.Panicf("An error occurred creating f neo: %s", err) 209 | } 210 | 211 | log.Info("SECOND: WRITE OUT 4") 212 | four <- true 213 | log.Info("SECOND: FINISHED WRITE OUT 4") 214 | log.Info("SECOND: WAIT ON 5") 215 | <-five 216 | log.Info("SECOND: FINISHED WAIT ON 5") 217 | 218 | _, err = conn.ExecNeo(`MATCH (:FOO)-[b:BAR]->(:BAZ) DELETE b`, nil) 219 | if err != nil { 220 | log.Panicf("An error occurred creating f neo: %s", err) 221 | } 222 | 223 | _, err = conn.ExecNeo(`MATCH (n) DETACH DELETE n`, nil) 224 | if err != nil { 225 | log.Panicf("An error occurred creating f neo: %s", err) 226 | } 227 | 228 | log.Info("SECOND: WRITE OUT 6") 229 | six <- true 230 | log.Info("SECOND: FINISHED WRITE OUT 6") 231 | log.Info("SECOND: WAIT ON 7") 232 | <-seven 233 | log.Info("SECOND: FINISHED WAIT ON 7") 234 | 235 | }() 236 | 237 | wg.Wait() 238 | } 239 | 240 | func TestBoltDriverPool_ReclaimBadConn(t *testing.T) { 241 | if neo4jConnStr == "" { 242 | t.Skip("Cannot run this test when in recording mode") 243 | } 244 | 245 | driver, err := NewDriverPool(neo4jConnStr, 1) 246 | if err != nil { 247 | t.Fatalf("An error occurred opening driver pool: %#v", err) 248 | } 249 | 250 | conn, err := driver.OpenPool() 251 | if err != nil { 252 | t.Fatalf("An error occurred opening conn: %s", err) 253 | } 254 | 255 | _, err = conn.ExecNeo(`CREATE (f:FOO)`, nil) 256 | if err != nil { 257 | t.Fatalf("An error occurred creating f neo: %s", err) 258 | } 259 | 260 | err = conn.(*boltConn).conn.Close() 261 | if err != nil { 262 | t.Fatalf("An error occurred closing underlying connection: %s", err) 263 | } 264 | 265 | _, err = conn.ExecNeo(`CREATE (f:FOO)`, nil) 266 | if err == nil { 267 | t.Fatal("An error should have occurred when trying to make a call with a closed connection") 268 | } else if conn.(*boltConn).connErr == nil { 269 | t.Fatal("A connection error should have been associated to the conn after a bad connection") 270 | } 271 | 272 | err = conn.Close() 273 | if err != nil { 274 | t.Fatalf("Got an error closing a bad connection: %s", err) 275 | } 276 | 277 | conn, err = driver.OpenPool() 278 | if err != nil { 279 | t.Fatalf("An error occurred opening conn: %s", err) 280 | } 281 | 282 | _, err = conn.ExecNeo(`CREATE (f:FOO)`, nil) 283 | if err != nil { 284 | t.Fatalf("A new conn should have been established for an old conn that had an error. However, we got an error: %s", err) 285 | } 286 | } 287 | 288 | func TestBoltDriverPool_RepeatQuieries(t *testing.T) { 289 | if neo4jConnStr == "" { 290 | t.Skip("Cannot run this test when in recording mode") 291 | } 292 | 293 | driver, err := NewDriverPool(neo4jConnStr, 1) 294 | if err != nil { 295 | t.Fatalf("An error occurred opening driver pool: %#v", err) 296 | } 297 | 298 | conn, err := driver.OpenPool() 299 | if err != nil { 300 | t.Fatalf("An error occurred opening conn: %s", err) 301 | } 302 | 303 | _, err = conn.ExecNeo(`CREATE (f:FOO)`, nil) 304 | if err != nil { 305 | t.Fatalf("An error occurred creating f neo: %s", err) 306 | } 307 | 308 | err = conn.Close() 309 | if err != nil { 310 | t.Fatalf("Got an error closing the connection: %s", err) 311 | } 312 | 313 | conn, err = driver.OpenPool() 314 | if err != nil { 315 | t.Fatalf("An error occurred opening conn: %s", err) 316 | } 317 | 318 | _, err = conn.ExecNeo(`MATCH (f:FOO) return f`, nil) 319 | if err != nil { 320 | t.Fatalf("An error occurred matching f neo: %s", err) 321 | } 322 | 323 | err = conn.Close() 324 | if err != nil { 325 | t.Fatalf("Got an error closing the connection: %s", err) 326 | } 327 | 328 | conn, err = driver.OpenPool() 329 | if err != nil { 330 | t.Fatalf("An error occurred opening conn: %s", err) 331 | } 332 | 333 | _, err = conn.ExecNeo(`MATCH (f:FOO) return f`, nil) 334 | if err != nil { 335 | t.Fatalf("An error occurred matching f neo: %s", err) 336 | } 337 | 338 | err = conn.Close() 339 | if err != nil { 340 | t.Fatalf("Got an error closing the connection: %s", err) 341 | } 342 | } 343 | 344 | func TestBoltDriverPool_ClosePool(t *testing.T) { 345 | if neo4jConnStr == "" { 346 | t.Skip("Cannot run this test when in recording mode") 347 | } 348 | 349 | driver, err := NewClosableDriverPool(neo4jConnStr, 1) 350 | if err != nil { 351 | t.Fatalf("An error occurred opening driver pool: %#v", err) 352 | } 353 | 354 | conn, err := driver.OpenPool() 355 | if err != nil { 356 | t.Fatalf("An error occurred opening conn: %s", err) 357 | } 358 | 359 | _, err = conn.ExecNeo(`CREATE (f:FOO)`, nil) 360 | if err != nil { 361 | t.Fatalf("An error occurred creating f neo: %s", err) 362 | } 363 | 364 | err = driver.Close() 365 | if(err != nil) { 366 | t.Fatalf("An error occurred creating trying to close the driver pool: %s", err) 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /encoding/decoder.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | 8 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 9 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/graph" 10 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/messages" 11 | ) 12 | 13 | // Decoder decodes a message from the bolt protocol stream 14 | // Attempts to support all builtin golang types, when it can be confidently 15 | // mapped to a data type from: http://alpha.neohq.net/docs/server-manual/bolt-serialization.html#bolt-packstream-structures 16 | // (version v3.1.0-M02 at the time of writing this. 17 | // 18 | // Maps and Slices are a special case, where only 19 | // map[string]interface{} and []interface{} are supported. 20 | // The interface for maps and slices may be more permissive in the future. 21 | type Decoder struct { 22 | r io.Reader 23 | buf *bytes.Buffer 24 | } 25 | 26 | // NewDecoder Creates a new Decoder object 27 | func NewDecoder(r io.Reader) Decoder { 28 | return Decoder{ 29 | r: r, 30 | buf: &bytes.Buffer{}, 31 | } 32 | } 33 | 34 | // Unmarshal is used to marshal an object to the bolt interface encoded bytes 35 | func Unmarshal(b []byte) (interface{}, error) { 36 | return NewDecoder(bytes.NewBuffer(b)).Decode() 37 | } 38 | 39 | // Read out the object bytes to decode 40 | func (d Decoder) read() (*bytes.Buffer, error) { 41 | output := &bytes.Buffer{} 42 | for { 43 | lengthBytes := make([]byte, 2) 44 | if numRead, err := io.ReadFull(d.r, lengthBytes); numRead != 2 { 45 | return nil, errors.Wrap(err, "Couldn't read expected bytes for message length. Read: %d Expected: 2.", numRead) 46 | } 47 | 48 | // Chunk header contains length of current message 49 | messageLen := binary.BigEndian.Uint16(lengthBytes) 50 | if messageLen == 0 { 51 | // If the length is 0, the chunk is done. 52 | return output, nil 53 | } 54 | 55 | data, err := d.readData(messageLen) 56 | if err != nil { 57 | return output, errors.Wrap(err, "An error occurred reading message data") 58 | } 59 | 60 | numWritten, err := output.Write(data) 61 | if numWritten < len(data) { 62 | return output, errors.New("Didn't write full data on output. Expected: %d Wrote: %d", len(data), numWritten) 63 | } else if err != nil { 64 | return output, errors.Wrap(err, "Error writing data to output") 65 | } 66 | } 67 | } 68 | 69 | func (d Decoder) readData(messageLen uint16) ([]byte, error) { 70 | output := make([]byte, messageLen) 71 | var totalRead uint16 72 | for totalRead < messageLen { 73 | data := make([]byte, messageLen-totalRead) 74 | numRead, err := d.r.Read(data) 75 | if err != nil { 76 | return nil, errors.Wrap(err, "An error occurred reading from stream") 77 | } else if numRead == 0 { 78 | return nil, errors.Wrap(err, "Couldn't read expected bytes for message. Read: %d Expected: %d.", totalRead, messageLen) 79 | } 80 | 81 | for idx, b := range data { 82 | output[uint16(idx)+totalRead] = b 83 | } 84 | 85 | totalRead += uint16(numRead) 86 | } 87 | 88 | return output, nil 89 | } 90 | 91 | // Decode decodes the stream to an object 92 | func (d Decoder) Decode() (interface{}, error) { 93 | data, err := d.read() 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | return d.decode(data) 99 | } 100 | 101 | func (d Decoder) decode(buffer *bytes.Buffer) (interface{}, error) { 102 | 103 | marker, err := buffer.ReadByte() 104 | if err != nil { 105 | return nil, errors.Wrap(err, "Error reading marker") 106 | } 107 | 108 | // Here we have to get the marker as an int to check and see 109 | // if it's a TINYINT 110 | var markerInt int8 111 | err = binary.Read(bytes.NewBuffer([]byte{marker}), binary.BigEndian, &markerInt) 112 | if err != nil { 113 | return nil, errors.Wrap(err, "Error reading marker as int8 from bolt message") 114 | } 115 | 116 | switch { 117 | 118 | // NIL 119 | case marker == NilMarker: 120 | return nil, nil 121 | 122 | // BOOL 123 | case marker == TrueMarker: 124 | return true, nil 125 | case marker == FalseMarker: 126 | return false, nil 127 | 128 | // INT 129 | case markerInt >= -16 && markerInt <= 127: 130 | return int64(int8(marker)), nil 131 | case marker == Int8Marker: 132 | var out int8 133 | err := binary.Read(buffer, binary.BigEndian, &out) 134 | return int64(out), err 135 | case marker == Int16Marker: 136 | var out int16 137 | err := binary.Read(buffer, binary.BigEndian, &out) 138 | return int64(out), err 139 | case marker == Int32Marker: 140 | var out int32 141 | err := binary.Read(buffer, binary.BigEndian, &out) 142 | return int64(out), err 143 | case marker == Int64Marker: 144 | var out int64 145 | err := binary.Read(buffer, binary.BigEndian, &out) 146 | return int64(out), err 147 | 148 | // FLOAT 149 | case marker == FloatMarker: 150 | var out float64 151 | err := binary.Read(buffer, binary.BigEndian, &out) 152 | return out, err 153 | 154 | // STRING 155 | case marker >= TinyStringMarker && marker <= TinyStringMarker+0x0F: 156 | size := int(marker) - int(TinyStringMarker) 157 | if size == 0 { 158 | return "", nil 159 | } 160 | return string(buffer.Next(size)), nil 161 | case marker == String8Marker: 162 | var size int8 163 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 164 | return nil, errors.Wrap(err, "An error occurred reading string size") 165 | } 166 | return string(buffer.Next(int(size))), nil 167 | case marker == String16Marker: 168 | var size int16 169 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 170 | return nil, errors.Wrap(err, "An error occurred reading string size") 171 | } 172 | return string(buffer.Next(int(size))), nil 173 | case marker == String32Marker: 174 | var size int32 175 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 176 | return nil, errors.Wrap(err, "An error occurred reading string size") 177 | } 178 | return string(buffer.Next(int(size))), nil 179 | 180 | // SLICE 181 | case marker >= TinySliceMarker && marker <= TinySliceMarker+0x0F: 182 | size := int(marker) - int(TinySliceMarker) 183 | return d.decodeSlice(buffer, size) 184 | case marker == Slice8Marker: 185 | var size int8 186 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 187 | return nil, errors.Wrap(err, "An error occurred reading slice size") 188 | } 189 | return d.decodeSlice(buffer, int(size)) 190 | case marker == Slice16Marker: 191 | var size int16 192 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 193 | return nil, errors.Wrap(err, "An error occurred reading slice size") 194 | } 195 | return d.decodeSlice(buffer, int(size)) 196 | case marker == Slice32Marker: 197 | var size int32 198 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 199 | return nil, errors.Wrap(err, "An error occurred reading slice size") 200 | } 201 | return d.decodeSlice(buffer, int(size)) 202 | 203 | // MAP 204 | case marker >= TinyMapMarker && marker <= TinyMapMarker+0x0F: 205 | size := int(marker) - int(TinyMapMarker) 206 | return d.decodeMap(buffer, size) 207 | case marker == Map8Marker: 208 | var size int8 209 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 210 | return nil, errors.Wrap(err, "An error occurred reading map size") 211 | } 212 | return d.decodeMap(buffer, int(size)) 213 | case marker == Map16Marker: 214 | var size int16 215 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 216 | return nil, errors.Wrap(err, "An error occurred reading map size") 217 | } 218 | return d.decodeMap(buffer, int(size)) 219 | case marker == Map32Marker: 220 | var size int32 221 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 222 | return nil, errors.Wrap(err, "An error occurred reading map size") 223 | } 224 | return d.decodeMap(buffer, int(size)) 225 | 226 | // STRUCTURES 227 | case marker >= TinyStructMarker && marker <= TinyStructMarker+0x0F: 228 | size := int(marker) - int(TinyStructMarker) 229 | return d.decodeStruct(buffer, size) 230 | case marker == Struct8Marker: 231 | var size int8 232 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 233 | return nil, errors.Wrap(err, "An error occurred reading struct size") 234 | } 235 | return d.decodeStruct(buffer, int(size)) 236 | case marker == Struct16Marker: 237 | var size int16 238 | if err := binary.Read(buffer, binary.BigEndian, &size); err != nil { 239 | return nil, errors.Wrap(err, "An error occurred reading struct size") 240 | } 241 | return d.decodeStruct(buffer, int(size)) 242 | 243 | default: 244 | return nil, errors.New("Unrecognized marker byte!: %x", marker) 245 | } 246 | 247 | } 248 | 249 | func (d Decoder) decodeSlice(buffer *bytes.Buffer, size int) ([]interface{}, error) { 250 | slice := make([]interface{}, size) 251 | for i := 0; i < size; i++ { 252 | item, err := d.decode(buffer) 253 | if err != nil { 254 | return nil, err 255 | } 256 | slice[i] = item 257 | } 258 | 259 | return slice, nil 260 | } 261 | 262 | func (d Decoder) decodeMap(buffer *bytes.Buffer, size int) (map[string]interface{}, error) { 263 | mapp := make(map[string]interface{}, size) 264 | for i := 0; i < size; i++ { 265 | keyInt, err := d.decode(buffer) 266 | if err != nil { 267 | return nil, err 268 | } 269 | val, err := d.decode(buffer) 270 | if err != nil { 271 | return nil, err 272 | } 273 | 274 | key, ok := keyInt.(string) 275 | if !ok { 276 | return nil, errors.New("Unexpected key type: %T with value %+v", keyInt, keyInt) 277 | } 278 | mapp[key] = val 279 | } 280 | 281 | return mapp, nil 282 | } 283 | 284 | func (d Decoder) decodeStruct(buffer *bytes.Buffer, size int) (interface{}, error) { 285 | 286 | signature, err := buffer.ReadByte() 287 | if err != nil { 288 | return nil, errors.Wrap(err, "An error occurred reading struct signature byte") 289 | } 290 | 291 | switch signature { 292 | case graph.NodeSignature: 293 | return d.decodeNode(buffer) 294 | case graph.RelationshipSignature: 295 | return d.decodeRelationship(buffer) 296 | case graph.PathSignature: 297 | return d.decodePath(buffer) 298 | case graph.UnboundRelationshipSignature: 299 | return d.decodeUnboundRelationship(buffer) 300 | case messages.RecordMessageSignature: 301 | return d.decodeRecordMessage(buffer) 302 | case messages.FailureMessageSignature: 303 | return d.decodeFailureMessage(buffer) 304 | case messages.IgnoredMessageSignature: 305 | return d.decodeIgnoredMessage(buffer) 306 | case messages.SuccessMessageSignature: 307 | return d.decodeSuccessMessage(buffer) 308 | case messages.AckFailureMessageSignature: 309 | return d.decodeAckFailureMessage(buffer) 310 | case messages.DiscardAllMessageSignature: 311 | return d.decodeDiscardAllMessage(buffer) 312 | case messages.PullAllMessageSignature: 313 | return d.decodePullAllMessage(buffer) 314 | case messages.ResetMessageSignature: 315 | return d.decodeResetMessage(buffer) 316 | default: 317 | return nil, errors.New("Unrecognized type decoding struct with signature %x", signature) 318 | } 319 | } 320 | 321 | func (d Decoder) decodeNode(buffer *bytes.Buffer) (graph.Node, error) { 322 | node := graph.Node{} 323 | 324 | nodeIdentityInt, err := d.decode(buffer) 325 | if err != nil { 326 | return node, err 327 | } 328 | node.NodeIdentity = nodeIdentityInt.(int64) 329 | 330 | labelInt, err := d.decode(buffer) 331 | if err != nil { 332 | return node, err 333 | } 334 | labelIntSlice, ok := labelInt.([]interface{}) 335 | if !ok { 336 | return node, errors.New("Expected: Labels []string, but got %T %+v", labelInt, labelInt) 337 | } 338 | node.Labels, err = sliceInterfaceToString(labelIntSlice) 339 | if err != nil { 340 | return node, err 341 | } 342 | 343 | propertiesInt, err := d.decode(buffer) 344 | if err != nil { 345 | return node, err 346 | } 347 | node.Properties, ok = propertiesInt.(map[string]interface{}) 348 | if !ok { 349 | return node, errors.New("Expected: Properties map[string]interface{}, but got %T %+v", propertiesInt, propertiesInt) 350 | } 351 | 352 | return node, nil 353 | 354 | } 355 | 356 | func (d Decoder) decodeRelationship(buffer *bytes.Buffer) (graph.Relationship, error) { 357 | rel := graph.Relationship{} 358 | 359 | relIdentityInt, err := d.decode(buffer) 360 | if err != nil { 361 | return rel, err 362 | } 363 | rel.RelIdentity = relIdentityInt.(int64) 364 | 365 | startNodeIdentityInt, err := d.decode(buffer) 366 | if err != nil { 367 | return rel, err 368 | } 369 | rel.StartNodeIdentity = startNodeIdentityInt.(int64) 370 | 371 | endNodeIdentityInt, err := d.decode(buffer) 372 | if err != nil { 373 | return rel, err 374 | } 375 | rel.EndNodeIdentity = endNodeIdentityInt.(int64) 376 | 377 | var ok bool 378 | typeInt, err := d.decode(buffer) 379 | if err != nil { 380 | return rel, err 381 | } 382 | rel.Type, ok = typeInt.(string) 383 | if !ok { 384 | return rel, errors.New("Expected: Type string, but got %T %+v", typeInt, typeInt) 385 | } 386 | 387 | propertiesInt, err := d.decode(buffer) 388 | if err != nil { 389 | return rel, err 390 | } 391 | rel.Properties, ok = propertiesInt.(map[string]interface{}) 392 | if !ok { 393 | return rel, errors.New("Expected: Properties map[string]interface{}, but got %T %+v", propertiesInt, propertiesInt) 394 | } 395 | 396 | return rel, nil 397 | } 398 | 399 | func (d Decoder) decodePath(buffer *bytes.Buffer) (graph.Path, error) { 400 | path := graph.Path{} 401 | 402 | nodesInt, err := d.decode(buffer) 403 | if err != nil { 404 | return path, err 405 | } 406 | nodesIntSlice, ok := nodesInt.([]interface{}) 407 | if !ok { 408 | return path, errors.New("Expected: Nodes []Node, but got %T %+v", nodesInt, nodesInt) 409 | } 410 | path.Nodes, err = sliceInterfaceToNode(nodesIntSlice) 411 | if err != nil { 412 | return path, err 413 | } 414 | 415 | relsInt, err := d.decode(buffer) 416 | if err != nil { 417 | return path, err 418 | } 419 | relsIntSlice, ok := relsInt.([]interface{}) 420 | if !ok { 421 | return path, errors.New("Expected: Relationships []Relationship, but got %T %+v", relsInt, relsInt) 422 | } 423 | path.Relationships, err = sliceInterfaceToUnboundRelationship(relsIntSlice) 424 | if err != nil { 425 | return path, err 426 | } 427 | 428 | seqInt, err := d.decode(buffer) 429 | if err != nil { 430 | return path, err 431 | } 432 | seqIntSlice, ok := seqInt.([]interface{}) 433 | if !ok { 434 | return path, errors.New("Expected: Sequence []int, but got %T %+v", seqInt, seqInt) 435 | } 436 | path.Sequence, err = sliceInterfaceToInt(seqIntSlice) 437 | 438 | return path, err 439 | } 440 | 441 | func (d Decoder) decodeUnboundRelationship(buffer *bytes.Buffer) (graph.UnboundRelationship, error) { 442 | rel := graph.UnboundRelationship{} 443 | 444 | relIdentityInt, err := d.decode(buffer) 445 | if err != nil { 446 | return rel, err 447 | } 448 | rel.RelIdentity = relIdentityInt.(int64) 449 | 450 | var ok bool 451 | typeInt, err := d.decode(buffer) 452 | if err != nil { 453 | return rel, err 454 | } 455 | rel.Type, ok = typeInt.(string) 456 | if !ok { 457 | return rel, errors.New("Expected: Type string, but got %T %+v", typeInt, typeInt) 458 | } 459 | 460 | propertiesInt, err := d.decode(buffer) 461 | if err != nil { 462 | return rel, err 463 | } 464 | rel.Properties, ok = propertiesInt.(map[string]interface{}) 465 | if !ok { 466 | return rel, errors.New("Expected: Properties map[string]interface{}, but got %T %+v", propertiesInt, propertiesInt) 467 | } 468 | 469 | return rel, nil 470 | } 471 | 472 | func (d Decoder) decodeRecordMessage(buffer *bytes.Buffer) (messages.RecordMessage, error) { 473 | fieldsInt, err := d.decode(buffer) 474 | if err != nil { 475 | return messages.RecordMessage{}, err 476 | } 477 | fields, ok := fieldsInt.([]interface{}) 478 | if !ok { 479 | return messages.RecordMessage{}, errors.New("Expected: Fields []interface{}, but got %T %+v", fieldsInt, fieldsInt) 480 | } 481 | 482 | return messages.NewRecordMessage(fields), nil 483 | } 484 | 485 | func (d Decoder) decodeFailureMessage(buffer *bytes.Buffer) (messages.FailureMessage, error) { 486 | metadataInt, err := d.decode(buffer) 487 | if err != nil { 488 | return messages.FailureMessage{}, err 489 | } 490 | metadata, ok := metadataInt.(map[string]interface{}) 491 | if !ok { 492 | return messages.FailureMessage{}, errors.New("Expected: Metadata map[string]interface{}, but got %T %+v", metadataInt, metadataInt) 493 | } 494 | 495 | return messages.NewFailureMessage(metadata), nil 496 | } 497 | 498 | func (d Decoder) decodeIgnoredMessage(buffer *bytes.Buffer) (messages.IgnoredMessage, error) { 499 | return messages.NewIgnoredMessage(), nil 500 | } 501 | 502 | func (d Decoder) decodeSuccessMessage(buffer *bytes.Buffer) (messages.SuccessMessage, error) { 503 | metadataInt, err := d.decode(buffer) 504 | if err != nil { 505 | return messages.SuccessMessage{}, err 506 | } 507 | metadata, ok := metadataInt.(map[string]interface{}) 508 | if !ok { 509 | return messages.SuccessMessage{}, errors.New("Expected: Metadata map[string]interface{}, but got %T %+v", metadataInt, metadataInt) 510 | } 511 | 512 | return messages.NewSuccessMessage(metadata), nil 513 | } 514 | 515 | func (d Decoder) decodeAckFailureMessage(buffer *bytes.Buffer) (messages.AckFailureMessage, error) { 516 | return messages.NewAckFailureMessage(), nil 517 | } 518 | 519 | func (d Decoder) decodeDiscardAllMessage(buffer *bytes.Buffer) (messages.DiscardAllMessage, error) { 520 | return messages.NewDiscardAllMessage(), nil 521 | } 522 | 523 | func (d Decoder) decodePullAllMessage(buffer *bytes.Buffer) (messages.PullAllMessage, error) { 524 | return messages.NewPullAllMessage(), nil 525 | } 526 | 527 | func (d Decoder) decodeResetMessage(buffer *bytes.Buffer) (messages.ResetMessage, error) { 528 | return messages.NewResetMessage(), nil 529 | } 530 | -------------------------------------------------------------------------------- /encoding/doc.go: -------------------------------------------------------------------------------- 1 | /*Package encoding is used to encode/decode data going to/from the bolt protocol.*/ 2 | package encoding 3 | -------------------------------------------------------------------------------- /encoding/encoder.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "math" 7 | "reflect" 8 | 9 | "bytes" 10 | 11 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 12 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures" 13 | ) 14 | 15 | const ( 16 | // NilMarker represents the encoding marker byte for a nil object 17 | NilMarker = 0xC0 18 | 19 | // TrueMarker represents the encoding marker byte for a true boolean object 20 | TrueMarker = 0xC3 21 | // FalseMarker represents the encoding marker byte for a false boolean object 22 | FalseMarker = 0xC2 23 | 24 | // Int8Marker represents the encoding marker byte for a int8 object 25 | Int8Marker = 0xC8 26 | // Int16Marker represents the encoding marker byte for a int16 object 27 | Int16Marker = 0xC9 28 | // Int32Marker represents the encoding marker byte for a int32 object 29 | Int32Marker = 0xCA 30 | // Int64Marker represents the encoding marker byte for a int64 object 31 | Int64Marker = 0xCB 32 | 33 | // FloatMarker represents the encoding marker byte for a float32/64 object 34 | FloatMarker = 0xC1 35 | 36 | // TinyStringMarker represents the encoding marker byte for a string object 37 | TinyStringMarker = 0x80 38 | // String8Marker represents the encoding marker byte for a string object 39 | String8Marker = 0xD0 40 | // String16Marker represents the encoding marker byte for a string object 41 | String16Marker = 0xD1 42 | // String32Marker represents the encoding marker byte for a string object 43 | String32Marker = 0xD2 44 | 45 | // TinySliceMarker represents the encoding marker byte for a slice object 46 | TinySliceMarker = 0x90 47 | // Slice8Marker represents the encoding marker byte for a slice object 48 | Slice8Marker = 0xD4 49 | // Slice16Marker represents the encoding marker byte for a slice object 50 | Slice16Marker = 0xD5 51 | // Slice32Marker represents the encoding marker byte for a slice object 52 | Slice32Marker = 0xD6 53 | 54 | // TinyMapMarker represents the encoding marker byte for a map object 55 | TinyMapMarker = 0xA0 56 | // Map8Marker represents the encoding marker byte for a map object 57 | Map8Marker = 0xD8 58 | // Map16Marker represents the encoding marker byte for a map object 59 | Map16Marker = 0xD9 60 | // Map32Marker represents the encoding marker byte for a map object 61 | Map32Marker = 0xDA 62 | 63 | // TinyStructMarker represents the encoding marker byte for a struct object 64 | TinyStructMarker = 0xB0 65 | // Struct8Marker represents the encoding marker byte for a struct object 66 | Struct8Marker = 0xDC 67 | // Struct16Marker represents the encoding marker byte for a struct object 68 | Struct16Marker = 0xDD 69 | ) 70 | 71 | var ( 72 | // EndMessage is the data to send to end a message 73 | EndMessage = []byte{byte(0x00), byte(0x00)} 74 | ) 75 | 76 | // Encoder encodes objects of different types to the given stream. 77 | // Attempts to support all builtin golang types, when it can be confidently 78 | // mapped to a data type from: http://alpha.neohq.net/docs/server-manual/bolt-serialization.html#bolt-packstream-structures 79 | // (version v3.1.0-M02 at the time of writing this. 80 | // 81 | // Maps and Slices are a special case, where only 82 | // map[string]interface{} and []interface{} are supported. 83 | // The interface for maps and slices may be more permissive in the future. 84 | type Encoder struct { 85 | w io.Writer 86 | buf *bytes.Buffer 87 | chunkSize uint16 88 | } 89 | 90 | // NewEncoder Creates a new Encoder object 91 | func NewEncoder(w io.Writer, chunkSize uint16) Encoder { 92 | return Encoder{ 93 | w: w, 94 | buf: &bytes.Buffer{}, 95 | chunkSize: chunkSize, 96 | } 97 | } 98 | 99 | // Marshal is used to marshal an object to the bolt interface encoded bytes 100 | func Marshal(v interface{}) ([]byte, error) { 101 | x := &bytes.Buffer{} 102 | err := NewEncoder(x, math.MaxUint16).Encode(v) 103 | return x.Bytes(), err 104 | } 105 | 106 | // write writes to the writer. Buffers the writes using chunkSize. 107 | func (e Encoder) Write(p []byte) (n int, err error) { 108 | 109 | n, err = e.buf.Write(p) 110 | if err != nil { 111 | err = errors.Wrap(err, "An error occurred writing to encoder temp buffer") 112 | return n, err 113 | } 114 | 115 | length := e.buf.Len() 116 | for length >= int(e.chunkSize) { 117 | if err := binary.Write(e.w, binary.BigEndian, e.chunkSize); err != nil { 118 | return 0, errors.Wrap(err, "An error occured writing chunksize") 119 | } 120 | 121 | numWritten, err := e.w.Write(e.buf.Next(int(e.chunkSize))) 122 | if err != nil { 123 | err = errors.Wrap(err, "An error occured writing a chunk") 124 | } 125 | 126 | return numWritten, err 127 | } 128 | 129 | return n, nil 130 | } 131 | 132 | // flush finishes the encoding stream by flushing it to the writer 133 | func (e Encoder) flush() error { 134 | length := e.buf.Len() 135 | if length > 0 { 136 | if err := binary.Write(e.w, binary.BigEndian, uint16(length)); err != nil { 137 | return errors.Wrap(err, "An error occured writing length bytes during flush") 138 | } 139 | 140 | if _, err := e.buf.WriteTo(e.w); err != nil { 141 | return errors.Wrap(err, "An error occured writing message bytes during flush") 142 | } 143 | } 144 | 145 | _, err := e.w.Write(EndMessage) 146 | if err != nil { 147 | return errors.Wrap(err, "An error occurred ending encoding message") 148 | } 149 | e.buf.Reset() 150 | 151 | return nil 152 | } 153 | 154 | // Encode encodes an object to the stream 155 | func (e Encoder) Encode(iVal interface{}) error { 156 | 157 | err := e.encode(iVal) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | // Whatever is left in the buffer for the chunk at the end, write it out 163 | return e.flush() 164 | } 165 | 166 | // Encode encodes an object to the stream 167 | func (e Encoder) encode(iVal interface{}) error { 168 | 169 | var err error 170 | switch val := iVal.(type) { 171 | case nil: 172 | err = e.encodeNil() 173 | case bool: 174 | err = e.encodeBool(val) 175 | case int: 176 | err = e.encodeInt(int64(val)) 177 | case int8: 178 | err = e.encodeInt(int64(val)) 179 | case int16: 180 | err = e.encodeInt(int64(val)) 181 | case int32: 182 | err = e.encodeInt(int64(val)) 183 | case int64: 184 | err = e.encodeInt(val) 185 | case uint: 186 | err = e.encodeInt(int64(val)) 187 | case uint8: 188 | err = e.encodeInt(int64(val)) 189 | case uint16: 190 | err = e.encodeInt(int64(val)) 191 | case uint32: 192 | err = e.encodeInt(int64(val)) 193 | case uint64: 194 | if val > math.MaxInt64 { 195 | return errors.New("Integer too big: %d. Max integer supported: %d", val, int64(math.MaxInt64)) 196 | } 197 | err = e.encodeInt(int64(val)) 198 | case float32: 199 | err = e.encodeFloat(float64(val)) 200 | case float64: 201 | err = e.encodeFloat(val) 202 | case string: 203 | err = e.encodeString(val) 204 | case []interface{}: 205 | err = e.encodeSlice(val) 206 | case map[string]interface{}: 207 | err = e.encodeMap(val) 208 | case structures.Structure: 209 | err = e.encodeStructure(val) 210 | default: 211 | // arbitrary slice types 212 | if reflect.TypeOf(iVal).Kind() == reflect.Slice { 213 | s := reflect.ValueOf(iVal) 214 | newSlice := make([]interface{}, s.Len()) 215 | for i := 0; i < s.Len(); i++ { 216 | newSlice[i] = s.Index(i).Interface() 217 | } 218 | return e.encodeSlice(newSlice) 219 | } 220 | 221 | return errors.New("Unrecognized type when encoding data for Bolt transport: %T %+v", val, val) 222 | } 223 | 224 | return err 225 | } 226 | 227 | func (e Encoder) encodeNil() error { 228 | _, err := e.Write([]byte{NilMarker}) 229 | return err 230 | } 231 | 232 | func (e Encoder) encodeBool(val bool) error { 233 | var err error 234 | if val { 235 | _, err = e.Write([]byte{TrueMarker}) 236 | } else { 237 | _, err = e.Write([]byte{FalseMarker}) 238 | } 239 | return err 240 | } 241 | 242 | func (e Encoder) encodeInt(val int64) error { 243 | var err error 244 | switch { 245 | case val >= math.MinInt64 && val < math.MinInt32: 246 | // Write as INT_64 247 | if _, err = e.Write([]byte{Int64Marker}); err != nil { 248 | return err 249 | } 250 | err = binary.Write(e, binary.BigEndian, val) 251 | case val >= math.MinInt32 && val < math.MinInt16: 252 | // Write as INT_32 253 | if _, err = e.Write([]byte{Int32Marker}); err != nil { 254 | return err 255 | } 256 | err = binary.Write(e, binary.BigEndian, int32(val)) 257 | case val >= math.MinInt16 && val < math.MinInt8: 258 | // Write as INT_16 259 | if _, err = e.Write([]byte{Int16Marker}); err != nil { 260 | return err 261 | } 262 | err = binary.Write(e, binary.BigEndian, int16(val)) 263 | case val >= math.MinInt8 && val < -16: 264 | // Write as INT_8 265 | if _, err = e.Write([]byte{Int8Marker}); err != nil { 266 | return err 267 | } 268 | err = binary.Write(e, binary.BigEndian, int8(val)) 269 | case val >= -16 && val <= math.MaxInt8: 270 | // Write as TINY_INT 271 | err = binary.Write(e, binary.BigEndian, int8(val)) 272 | case val > math.MaxInt8 && val <= math.MaxInt16: 273 | // Write as INT_16 274 | if _, err = e.Write([]byte{Int16Marker}); err != nil { 275 | return err 276 | } 277 | err = binary.Write(e, binary.BigEndian, int16(val)) 278 | case val > math.MaxInt16 && val <= math.MaxInt32: 279 | // Write as INT_32 280 | if _, err = e.Write([]byte{Int32Marker}); err != nil { 281 | return err 282 | } 283 | err = binary.Write(e, binary.BigEndian, int32(val)) 284 | case val > math.MaxInt32 && val <= math.MaxInt64: 285 | // Write as INT_64 286 | if _, err = e.Write([]byte{Int64Marker}); err != nil { 287 | return err 288 | } 289 | err = binary.Write(e, binary.BigEndian, val) 290 | default: 291 | return errors.New("Int too long to write: %d", val) 292 | } 293 | if err != nil { 294 | return errors.Wrap(err, "An error occured writing an int to bolt") 295 | } 296 | return err 297 | } 298 | 299 | func (e Encoder) encodeFloat(val float64) error { 300 | if _, err := e.Write([]byte{FloatMarker}); err != nil { 301 | return err 302 | } 303 | 304 | err := binary.Write(e, binary.BigEndian, val) 305 | if err != nil { 306 | return errors.Wrap(err, "An error occured writing a float to bolt") 307 | } 308 | 309 | return err 310 | } 311 | 312 | func (e Encoder) encodeString(val string) error { 313 | var err error 314 | bytes := []byte(val) 315 | 316 | length := len(bytes) 317 | switch { 318 | case length <= 15: 319 | if _, err = e.Write([]byte{byte(TinyStringMarker + length)}); err != nil { 320 | return err 321 | } 322 | _, err = e.Write(bytes) 323 | case length > 15 && length <= math.MaxUint8: 324 | if _, err = e.Write([]byte{String8Marker}); err != nil { 325 | return err 326 | } 327 | if err = binary.Write(e, binary.BigEndian, int8(length)); err != nil { 328 | return err 329 | } 330 | _, err = e.Write(bytes) 331 | case length > math.MaxUint8 && length <= math.MaxUint16: 332 | if _, err = e.Write([]byte{String16Marker}); err != nil { 333 | return err 334 | } 335 | if err = binary.Write(e, binary.BigEndian, int16(length)); err != nil { 336 | return err 337 | } 338 | _, err = e.Write(bytes) 339 | case length > math.MaxUint16 && int64(length) <= math.MaxUint32: 340 | if _, err = e.Write([]byte{String32Marker}); err != nil { 341 | return err 342 | } 343 | if err = binary.Write(e, binary.BigEndian, int32(length)); err != nil { 344 | return err 345 | } 346 | _, err = e.Write(bytes) 347 | default: 348 | return errors.New("String too long to write: %s", val) 349 | } 350 | return err 351 | } 352 | 353 | func (e Encoder) encodeSlice(val []interface{}) error { 354 | length := len(val) 355 | switch { 356 | case length <= 15: 357 | if _, err := e.Write([]byte{byte(TinySliceMarker + length)}); err != nil { 358 | return err 359 | } 360 | case length > 15 && length <= math.MaxUint8: 361 | if _, err := e.Write([]byte{Slice8Marker}); err != nil { 362 | return err 363 | } 364 | if err := binary.Write(e, binary.BigEndian, int8(length)); err != nil { 365 | return err 366 | } 367 | case length > math.MaxUint8 && length <= math.MaxUint16: 368 | if _, err := e.Write([]byte{Slice16Marker}); err != nil { 369 | return err 370 | } 371 | if err := binary.Write(e, binary.BigEndian, int16(length)); err != nil { 372 | return err 373 | } 374 | case length >= math.MaxUint16 && int64(length) <= math.MaxUint32: 375 | if _, err := e.Write([]byte{Slice32Marker}); err != nil { 376 | return err 377 | } 378 | if err := binary.Write(e, binary.BigEndian, int32(length)); err != nil { 379 | return err 380 | } 381 | default: 382 | return errors.New("Slice too long to write: %+v", val) 383 | } 384 | 385 | // Encode Slice values 386 | for _, item := range val { 387 | if err := e.encode(item); err != nil { 388 | return err 389 | } 390 | } 391 | 392 | return nil 393 | } 394 | 395 | func (e Encoder) encodeMap(val map[string]interface{}) error { 396 | length := len(val) 397 | switch { 398 | case length <= 15: 399 | if _, err := e.Write([]byte{byte(TinyMapMarker + length)}); err != nil { 400 | return err 401 | } 402 | case length > 15 && length <= math.MaxUint8: 403 | if _, err := e.Write([]byte{Map8Marker}); err != nil { 404 | return err 405 | } 406 | if err := binary.Write(e, binary.BigEndian, int8(length)); err != nil { 407 | return err 408 | } 409 | case length > math.MaxUint8 && length <= math.MaxUint16: 410 | if _, err := e.Write([]byte{Map16Marker}); err != nil { 411 | return err 412 | } 413 | if err := binary.Write(e, binary.BigEndian, int16(length)); err != nil { 414 | return err 415 | } 416 | case length >= math.MaxUint16 && int64(length) <= math.MaxUint32: 417 | if _, err := e.Write([]byte{Map32Marker}); err != nil { 418 | return err 419 | } 420 | if err := binary.Write(e, binary.BigEndian, int32(length)); err != nil { 421 | return err 422 | } 423 | default: 424 | return errors.New("Map too long to write: %+v", val) 425 | } 426 | 427 | // Encode Map values 428 | for k, v := range val { 429 | if err := e.encode(k); err != nil { 430 | return err 431 | } 432 | if err := e.encode(v); err != nil { 433 | return err 434 | } 435 | } 436 | 437 | return nil 438 | } 439 | 440 | func (e Encoder) encodeStructure(val structures.Structure) error { 441 | 442 | fields := val.AllFields() 443 | length := len(fields) 444 | switch { 445 | case length <= 15: 446 | if _, err := e.Write([]byte{byte(TinyStructMarker + length)}); err != nil { 447 | return err 448 | } 449 | case length > 15 && length <= math.MaxUint8: 450 | if _, err := e.Write([]byte{Struct8Marker}); err != nil { 451 | return err 452 | } 453 | if err := binary.Write(e, binary.BigEndian, int8(length)); err != nil { 454 | return err 455 | } 456 | case length > math.MaxUint8 && length <= math.MaxUint16: 457 | if _, err := e.Write([]byte{Struct16Marker}); err != nil { 458 | return err 459 | } 460 | if err := binary.Write(e, binary.BigEndian, int16(length)); err != nil { 461 | return err 462 | } 463 | default: 464 | return errors.New("Structure too long to write: %+v", val) 465 | } 466 | 467 | _, err := e.Write([]byte{byte(val.Signature())}) 468 | if err != nil { 469 | return errors.Wrap(err, "An error occurred writing to encoder a struct field") 470 | } 471 | 472 | for _, field := range fields { 473 | if err := e.encode(field); err != nil { 474 | return errors.Wrap(err, "An error occurred encoding a struct field") 475 | } 476 | } 477 | 478 | return nil 479 | } 480 | -------------------------------------------------------------------------------- /encoding/encoder_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | "math" 8 | "reflect" 9 | "testing" 10 | "testing/quick" 11 | 12 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 13 | ) 14 | 15 | const ( 16 | maxBufSize = math.MaxUint16 17 | maxKeySize = 10 18 | ) 19 | 20 | func createNewTestEncoder() (Encoder, io.Reader) { 21 | buf := bytes.NewBuffer([]byte{}) 22 | return NewEncoder(buf, maxBufSize), buf 23 | } 24 | 25 | func TestEncodeNil(t *testing.T) { 26 | encoder, buf := createNewTestEncoder() 27 | 28 | err := encoder.Encode(nil) 29 | 30 | if err != nil { 31 | t.Fatalf("Error while encoding: %v", err) 32 | } 33 | 34 | output := make([]byte, maxBufSize) 35 | outputCount, err := buf.Read(output) 36 | 37 | if err != nil { 38 | t.Fatalf("Error while reading output: %v", err) 39 | } 40 | 41 | expectedBuf := bytes.NewBuffer([]byte{}) 42 | expected := make([]byte, maxBufSize) 43 | 44 | binary.Write(expectedBuf, binary.BigEndian, uint16(1)) 45 | expectedBuf.Write([]byte{NilMarker}) 46 | expectedBuf.Write(EndMessage) 47 | 48 | expectedCount, _ := expectedBuf.Read(expected) 49 | 50 | if !reflect.DeepEqual(output[:outputCount], expected[:expectedCount]) { 51 | t.Fatalf("Unexpected Nil encoding. Expected %v. Got %v", expected[:expectedCount], output[:outputCount]) 52 | } 53 | } 54 | 55 | func TestEncodeBool(t *testing.T) { 56 | expected := func(val bool) []byte { 57 | expectedBuf := bytes.NewBuffer([]byte{}) 58 | expected := make([]byte, maxBufSize) 59 | 60 | binary.Write(expectedBuf, binary.BigEndian, uint16(1)) 61 | 62 | var marker byte 63 | 64 | if val == true { 65 | marker = TrueMarker 66 | } else { 67 | marker = FalseMarker 68 | } 69 | 70 | expectedBuf.Write([]byte{marker}) 71 | expectedBuf.Write(EndMessage) 72 | 73 | expectedCount, _ := expectedBuf.Read(expected) 74 | 75 | return expected[:expectedCount] 76 | } 77 | 78 | result := func(val bool) []byte { 79 | encoder, buf := createNewTestEncoder() 80 | 81 | err := encoder.Encode(val) 82 | 83 | if err != nil { 84 | t.Fatalf("Error while encoding: %v", err) 85 | } 86 | 87 | output := make([]byte, maxBufSize) 88 | outputCount, err := buf.Read(output) 89 | 90 | if err != nil { 91 | t.Fatalf("Error while reading output: %v", err) 92 | } 93 | 94 | return output[:outputCount] 95 | } 96 | 97 | if err := quick.CheckEqual(expected, result, nil); err != nil { 98 | t.Fatal(err) 99 | } 100 | } 101 | 102 | func generateIntExpectedBuf(val int64) ([]byte, error) { 103 | expectedBuf := bytes.NewBuffer([]byte{}) 104 | expected := make([]byte, maxBufSize) 105 | 106 | switch { 107 | case val >= math.MinInt64 && val < math.MinInt32: 108 | binary.Write(expectedBuf, binary.BigEndian, uint16(9)) 109 | expectedBuf.Write([]byte{Int64Marker}) 110 | binary.Write(expectedBuf, binary.BigEndian, int64(val)) 111 | case val >= math.MinInt32 && val < math.MinInt16: 112 | binary.Write(expectedBuf, binary.BigEndian, uint16(5)) 113 | expectedBuf.Write([]byte{Int32Marker}) 114 | binary.Write(expectedBuf, binary.BigEndian, int32(val)) 115 | case val >= math.MinInt16 && val < math.MinInt8: 116 | binary.Write(expectedBuf, binary.BigEndian, uint16(3)) 117 | expectedBuf.Write([]byte{Int16Marker}) 118 | binary.Write(expectedBuf, binary.BigEndian, int16(val)) 119 | case val >= math.MinInt8 && val < -16: 120 | binary.Write(expectedBuf, binary.BigEndian, uint16(2)) 121 | expectedBuf.Write([]byte{Int8Marker}) 122 | binary.Write(expectedBuf, binary.BigEndian, int8(val)) 123 | case val >= -16 && val <= math.MaxInt8: 124 | binary.Write(expectedBuf, binary.BigEndian, uint16(1)) 125 | binary.Write(expectedBuf, binary.BigEndian, int8(val)) 126 | case val > math.MaxInt8 && val <= math.MaxInt16: 127 | binary.Write(expectedBuf, binary.BigEndian, uint16(3)) 128 | expectedBuf.Write([]byte{Int16Marker}) 129 | binary.Write(expectedBuf, binary.BigEndian, int16(val)) 130 | case val > math.MaxInt16 && val <= math.MaxInt32: 131 | binary.Write(expectedBuf, binary.BigEndian, uint16(5)) 132 | expectedBuf.Write([]byte{Int32Marker}) 133 | binary.Write(expectedBuf, binary.BigEndian, int32(val)) 134 | case val > math.MaxInt32 && val <= math.MaxInt64: 135 | binary.Write(expectedBuf, binary.BigEndian, uint16(9)) 136 | expectedBuf.Write([]byte{Int64Marker}) 137 | binary.Write(expectedBuf, binary.BigEndian, int64(val)) 138 | default: 139 | return nil, errors.New("Int too long to write: %d", val) 140 | } 141 | expectedBuf.Write(EndMessage) 142 | 143 | expectedCount, _ := expectedBuf.Read(expected) 144 | 145 | return expected[:expectedCount], nil 146 | } 147 | 148 | func generateIntResultBuf(val interface{}) ([]byte, error) { 149 | encoder, buf := createNewTestEncoder() 150 | err := encoder.Encode(val) 151 | 152 | if err != nil { 153 | return nil, errors.New("Error while encoding: %v", err) 154 | } 155 | 156 | output := make([]byte, maxBufSize) 157 | outputCount, err := buf.Read(output) 158 | 159 | if err != nil { 160 | return nil, errors.New("Error while reading output: %v", err) 161 | } 162 | 163 | return output[:outputCount], nil 164 | } 165 | 166 | func TestEncodeInt(t *testing.T) { 167 | expected := func(val int) []byte { 168 | output, err := generateIntExpectedBuf(int64(val)) 169 | 170 | if err != nil { 171 | t.Fatal(err) 172 | } 173 | 174 | return output 175 | } 176 | result := func(val int) []byte { 177 | output, err := generateIntResultBuf(val) 178 | 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | 183 | return output 184 | } 185 | 186 | if err := quick.CheckEqual(expected, result, nil); err != nil { 187 | t.Fatal(err) 188 | } 189 | } 190 | 191 | func TestEncodeUint(t *testing.T) { 192 | expected := func(val uint) []byte { 193 | output, err := generateIntExpectedBuf(int64(val)) 194 | 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | 199 | return output 200 | } 201 | result := func(val uint) []byte { 202 | output, err := generateIntResultBuf(val) 203 | 204 | if err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | return output 209 | } 210 | 211 | if err := quick.CheckEqual(expected, result, nil); err != nil { 212 | t.Fatal(err) 213 | } 214 | } 215 | 216 | func TestEncodeInt8(t *testing.T) { 217 | expected := func(val int8) []byte { 218 | output, err := generateIntExpectedBuf(int64(val)) 219 | 220 | if err != nil { 221 | t.Fatal(err) 222 | } 223 | 224 | return output 225 | } 226 | result := func(val int8) []byte { 227 | output, err := generateIntResultBuf(val) 228 | 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | 233 | return output 234 | } 235 | 236 | if err := quick.CheckEqual(expected, result, nil); err != nil { 237 | t.Fatal(err) 238 | } 239 | } 240 | 241 | func TestEncodeUint8(t *testing.T) { 242 | expected := func(val uint8) []byte { 243 | output, err := generateIntExpectedBuf(int64(val)) 244 | 245 | if err != nil { 246 | t.Fatal(err) 247 | } 248 | 249 | return output 250 | } 251 | result := func(val uint8) []byte { 252 | output, err := generateIntResultBuf(val) 253 | 254 | if err != nil { 255 | t.Fatal(err) 256 | } 257 | 258 | return output 259 | } 260 | 261 | if err := quick.CheckEqual(expected, result, nil); err != nil { 262 | t.Fatal(err) 263 | } 264 | } 265 | 266 | func TestEncodeInt16(t *testing.T) { 267 | expected := func(val int16) []byte { 268 | output, err := generateIntExpectedBuf(int64(val)) 269 | 270 | if err != nil { 271 | t.Fatal(err) 272 | } 273 | 274 | return output 275 | } 276 | result := func(val int16) []byte { 277 | output, err := generateIntResultBuf(val) 278 | 279 | if err != nil { 280 | t.Fatal(err) 281 | } 282 | 283 | return output 284 | } 285 | 286 | if err := quick.CheckEqual(expected, result, nil); err != nil { 287 | t.Fatal(err) 288 | } 289 | } 290 | 291 | func TestEncodeUint16(t *testing.T) { 292 | expected := func(val uint16) []byte { 293 | output, err := generateIntExpectedBuf(int64(val)) 294 | 295 | if err != nil { 296 | t.Fatal(err) 297 | } 298 | 299 | return output 300 | } 301 | result := func(val uint16) []byte { 302 | output, err := generateIntResultBuf(val) 303 | 304 | if err != nil { 305 | t.Fatal(err) 306 | } 307 | 308 | return output 309 | } 310 | 311 | if err := quick.CheckEqual(expected, result, nil); err != nil { 312 | t.Fatal(err) 313 | } 314 | } 315 | 316 | func TestEncodeInt32(t *testing.T) { 317 | expected := func(val int32) []byte { 318 | output, err := generateIntExpectedBuf(int64(val)) 319 | 320 | if err != nil { 321 | t.Fatal(err) 322 | } 323 | 324 | return output 325 | } 326 | result := func(val int32) []byte { 327 | output, err := generateIntResultBuf(val) 328 | 329 | if err != nil { 330 | t.Fatal(err) 331 | } 332 | 333 | return output 334 | } 335 | 336 | if err := quick.CheckEqual(expected, result, nil); err != nil { 337 | t.Fatal(err) 338 | } 339 | } 340 | 341 | func TestEncodeUint32(t *testing.T) { 342 | expected := func(val uint32) []byte { 343 | output, err := generateIntExpectedBuf(int64(val)) 344 | 345 | if err != nil { 346 | t.Fatal(err) 347 | } 348 | 349 | return output 350 | } 351 | result := func(val uint32) []byte { 352 | output, err := generateIntResultBuf(val) 353 | 354 | if err != nil { 355 | t.Fatal(err) 356 | } 357 | 358 | return output 359 | } 360 | 361 | if err := quick.CheckEqual(expected, result, nil); err != nil { 362 | t.Fatal(err) 363 | } 364 | } 365 | 366 | func TestEncodeInt64(t *testing.T) { 367 | expected := func(val int64) []byte { 368 | output, err := generateIntExpectedBuf(int64(val)) 369 | 370 | if err != nil { 371 | t.Fatal(err) 372 | } 373 | 374 | return output 375 | } 376 | result := func(val int64) []byte { 377 | output, err := generateIntResultBuf(val) 378 | 379 | if err != nil { 380 | t.Fatal(err) 381 | } 382 | 383 | return output 384 | } 385 | 386 | if err := quick.CheckEqual(expected, result, nil); err != nil { 387 | t.Fatal(err) 388 | } 389 | } 390 | 391 | func TestEncodeUint64(t *testing.T) { 392 | expected := func(val uint64) []byte { 393 | if val > math.MaxInt64 { 394 | return nil 395 | } 396 | output, err := generateIntExpectedBuf(int64(val)) 397 | 398 | if err != nil { 399 | t.Fatal(err) 400 | } 401 | 402 | return output 403 | } 404 | result := func(val uint64) []byte { 405 | output, err := generateIntResultBuf(val) 406 | 407 | if err != nil && val <= math.MaxInt64 { 408 | t.Fatal(err) 409 | } 410 | 411 | return output 412 | } 413 | 414 | if err := quick.CheckEqual(expected, result, nil); err != nil { 415 | t.Fatal(err) 416 | } 417 | } 418 | 419 | func TestEncodeFloat32(t *testing.T) { 420 | expected := func(val float32) []byte { 421 | expectedBuf := bytes.NewBuffer([]byte{}) 422 | expected := make([]byte, maxBufSize) 423 | 424 | binary.Write(expectedBuf, binary.BigEndian, uint16(9)) 425 | expectedBuf.Write([]byte{FloatMarker}) 426 | binary.Write(expectedBuf, binary.BigEndian, float64(val)) 427 | expectedBuf.Write(EndMessage) 428 | 429 | expectedCount, _ := expectedBuf.Read(expected) 430 | 431 | return expected[:expectedCount] 432 | } 433 | result := func(val float32) []byte { 434 | encoder, buf := createNewTestEncoder() 435 | err := encoder.Encode(val) 436 | 437 | if err != nil { 438 | t.Fatalf("Error while encoding: %v", err) 439 | } 440 | 441 | output := make([]byte, maxBufSize) 442 | outputCount, err := buf.Read(output) 443 | 444 | if err != nil { 445 | t.Fatalf("Error while reading output: %v", err) 446 | } 447 | 448 | return output[:outputCount] 449 | } 450 | 451 | if err := quick.CheckEqual(expected, result, nil); err != nil { 452 | t.Fatal(err) 453 | } 454 | } 455 | 456 | func TestEncodeFloat64(t *testing.T) { 457 | expected := func(val float64) []byte { 458 | expectedBuf := bytes.NewBuffer([]byte{}) 459 | expected := make([]byte, maxBufSize) 460 | 461 | binary.Write(expectedBuf, binary.BigEndian, uint16(9)) 462 | expectedBuf.Write([]byte{FloatMarker}) 463 | binary.Write(expectedBuf, binary.BigEndian, float64(val)) 464 | expectedBuf.Write(EndMessage) 465 | 466 | expectedCount, _ := expectedBuf.Read(expected) 467 | 468 | return expected[:expectedCount] 469 | } 470 | result := func(val float64) []byte { 471 | encoder, buf := createNewTestEncoder() 472 | err := encoder.Encode(val) 473 | 474 | if err != nil { 475 | t.Fatalf("Error while encoding: %v", err) 476 | } 477 | 478 | output := make([]byte, maxBufSize) 479 | outputCount, err := buf.Read(output) 480 | 481 | if err != nil { 482 | t.Fatalf("Error while reading output: %v", err) 483 | } 484 | 485 | return output[:outputCount] 486 | } 487 | 488 | if err := quick.CheckEqual(expected, result, nil); err != nil { 489 | t.Fatal(err) 490 | } 491 | } 492 | 493 | func TestEncodeString(t *testing.T) { 494 | expected := func(val string) []byte { 495 | expectedBuf := bytes.NewBuffer([]byte{}) 496 | resultExpectedBuf := bytes.NewBuffer([]byte{}) 497 | expected := make([]byte, maxBufSize) 498 | 499 | bytes := []byte(val) 500 | 501 | length := len(bytes) 502 | 503 | switch { 504 | case length <= 15: 505 | expectedBuf.Write([]byte{byte(TinyStringMarker + length)}) 506 | expectedBuf.Write(bytes) 507 | case length > 15 && length <= math.MaxUint8: 508 | expectedBuf.Write([]byte{String8Marker}) 509 | binary.Write(expectedBuf, binary.BigEndian, int8(length)) 510 | expectedBuf.Write(bytes) 511 | case length > math.MaxUint8 && length <= math.MaxUint16: 512 | expectedBuf.Write([]byte{String16Marker}) 513 | binary.Write(expectedBuf, binary.BigEndian, int16(length)) 514 | expectedBuf.Write(bytes) 515 | case length > math.MaxUint16 && int64(length) <= math.MaxUint32: 516 | expectedBuf.Write([]byte{String32Marker}) 517 | binary.Write(expectedBuf, binary.BigEndian, int32(length)) 518 | expectedBuf.Write(bytes) 519 | default: 520 | t.Fatalf("String too long to write: %s", val) 521 | } 522 | 523 | binary.Write(resultExpectedBuf, binary.BigEndian, uint16(expectedBuf.Len())) 524 | resultExpectedBuf.ReadFrom(expectedBuf) 525 | resultExpectedBuf.Write(EndMessage) 526 | 527 | expectedCount, _ := resultExpectedBuf.Read(expected) 528 | 529 | return expected[:expectedCount] 530 | } 531 | 532 | result := func(val string) []byte { 533 | encoder, buf := createNewTestEncoder() 534 | err := encoder.Encode(val) 535 | 536 | if err != nil { 537 | t.Fatalf("Error while encoding: %v", err) 538 | } 539 | 540 | output := make([]byte, maxBufSize) 541 | outputCount, err := buf.Read(output) 542 | 543 | if err != nil { 544 | t.Fatalf("Error while reading output: %v", err) 545 | } 546 | 547 | return output[:outputCount] 548 | } 549 | 550 | if err := quick.CheckEqual(expected, result, nil); err != nil { 551 | t.Fatal(err) 552 | } 553 | } 554 | 555 | func TestEncodeInterfaceSlice(t *testing.T) { 556 | expected := func(val []bool) []byte { 557 | expectedBuf := bytes.NewBuffer([]byte{}) 558 | resultExpectedBuf := bytes.NewBuffer([]byte{}) 559 | expected := make([]byte, maxBufSize) 560 | length := len(val) 561 | 562 | switch { 563 | case length <= 15: 564 | expectedBuf.Write([]byte{byte(TinySliceMarker + length)}) 565 | case length > 15 && length <= math.MaxUint8: 566 | expectedBuf.Write([]byte{Slice8Marker}) 567 | binary.Write(expectedBuf, binary.BigEndian, int8(length)) 568 | case length > math.MaxUint8 && length <= math.MaxUint16: 569 | expectedBuf.Write([]byte{Slice16Marker}) 570 | binary.Write(expectedBuf, binary.BigEndian, int16(length)) 571 | case length >= math.MaxUint16 && int64(length) <= math.MaxUint32: 572 | expectedBuf.Write([]byte{Slice32Marker}) 573 | binary.Write(expectedBuf, binary.BigEndian, int32(length)) 574 | default: 575 | t.Fatalf("Slice too long to write: %+v", val) 576 | } 577 | 578 | var marker byte 579 | 580 | for _, item := range val { 581 | if item == true { 582 | marker = TrueMarker 583 | } else { 584 | marker = FalseMarker 585 | } 586 | 587 | expectedBuf.Write([]byte{marker}) 588 | } 589 | 590 | binary.Write(resultExpectedBuf, binary.BigEndian, uint16(expectedBuf.Len())) 591 | resultExpectedBuf.ReadFrom(expectedBuf) 592 | resultExpectedBuf.Write(EndMessage) 593 | 594 | expectedCount, _ := resultExpectedBuf.Read(expected) 595 | 596 | return expected[:expectedCount] 597 | } 598 | 599 | result := func(val []bool) []byte { 600 | encoder, buf := createNewTestEncoder() 601 | err := encoder.Encode(val) 602 | 603 | if err != nil { 604 | t.Fatalf("Error while encoding: %v", err) 605 | } 606 | 607 | output := make([]byte, maxBufSize) 608 | outputCount, err := buf.Read(output) 609 | 610 | if err != nil { 611 | t.Fatalf("Error while reading output: %v", err) 612 | } 613 | 614 | return output[:outputCount] 615 | } 616 | 617 | if err := quick.CheckEqual(expected, result, nil); err != nil { 618 | t.Fatal(err) 619 | } 620 | } 621 | 622 | func TestEncodeStringSlice(t *testing.T) { 623 | expected := func(val []string) []byte { 624 | expectedBuf := bytes.NewBuffer([]byte{}) 625 | resultExpectedBuf := bytes.NewBuffer([]byte{}) 626 | expected := make([]byte, maxBufSize) 627 | length := len(val) 628 | 629 | switch { 630 | case length <= 15: 631 | expectedBuf.Write([]byte{byte(TinySliceMarker + length)}) 632 | case length > 15 && length <= math.MaxUint8: 633 | expectedBuf.Write([]byte{Slice8Marker}) 634 | binary.Write(expectedBuf, binary.BigEndian, int8(length)) 635 | case length > math.MaxUint8 && length <= math.MaxUint16: 636 | expectedBuf.Write([]byte{Slice16Marker}) 637 | binary.Write(expectedBuf, binary.BigEndian, int16(length)) 638 | case length >= math.MaxUint16 && int64(length) <= math.MaxUint32: 639 | expectedBuf.Write([]byte{Slice32Marker}) 640 | binary.Write(expectedBuf, binary.BigEndian, int32(length)) 641 | default: 642 | t.Fatalf("Slice too long to write: %+v", val) 643 | } 644 | 645 | for _, item := range val { 646 | bytes := []byte(item) 647 | 648 | length := len(bytes) 649 | 650 | switch { 651 | case length <= 15: 652 | expectedBuf.Write([]byte{byte(TinyStringMarker + length)}) 653 | expectedBuf.Write(bytes) 654 | case length > 15 && length <= math.MaxUint8: 655 | expectedBuf.Write([]byte{String8Marker}) 656 | binary.Write(expectedBuf, binary.BigEndian, int8(length)) 657 | expectedBuf.Write(bytes) 658 | case length > math.MaxUint8 && length <= math.MaxUint16: 659 | expectedBuf.Write([]byte{String16Marker}) 660 | binary.Write(expectedBuf, binary.BigEndian, int16(length)) 661 | expectedBuf.Write(bytes) 662 | case length > math.MaxUint16 && int64(length) <= math.MaxUint32: 663 | expectedBuf.Write([]byte{String32Marker}) 664 | binary.Write(expectedBuf, binary.BigEndian, int32(length)) 665 | expectedBuf.Write(bytes) 666 | default: 667 | t.Fatalf("String too long to write: %s", val) 668 | } 669 | } 670 | 671 | binary.Write(resultExpectedBuf, binary.BigEndian, uint16(expectedBuf.Len())) 672 | resultExpectedBuf.ReadFrom(expectedBuf) 673 | resultExpectedBuf.Write(EndMessage) 674 | 675 | expectedCount, _ := resultExpectedBuf.Read(expected) 676 | 677 | return expected[:expectedCount] 678 | } 679 | 680 | result := func(val []string) []byte { 681 | encoder, buf := createNewTestEncoder() 682 | err := encoder.Encode(val) 683 | 684 | if err != nil { 685 | t.Fatalf("Error while encoding: %v", err) 686 | } 687 | 688 | output := make([]byte, maxBufSize) 689 | outputCount, err := buf.Read(output) 690 | 691 | if err != nil { 692 | t.Fatalf("Error while reading output: %v", err) 693 | } 694 | 695 | return output[:outputCount] 696 | } 697 | 698 | if err := quick.CheckEqual(expected, result, nil); err != nil { 699 | t.Fatal(err) 700 | } 701 | } 702 | -------------------------------------------------------------------------------- /encoding/util.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 5 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/graph" 6 | ) 7 | 8 | func sliceInterfaceToString(from []interface{}) ([]string, error) { 9 | to := make([]string, len(from)) 10 | for idx, item := range from { 11 | toItem, ok := item.(string) 12 | if !ok { 13 | return nil, errors.New("Expected string value. Got %T %+v", item, item) 14 | } 15 | to[idx] = toItem 16 | } 17 | return to, nil 18 | } 19 | 20 | func sliceInterfaceToInt(from []interface{}) ([]int, error) { 21 | to := make([]int, len(from)) 22 | for idx, item := range from { 23 | to[idx] = int(item.(int64)) 24 | } 25 | return to, nil 26 | } 27 | 28 | func sliceInterfaceToNode(from []interface{}) ([]graph.Node, error) { 29 | to := make([]graph.Node, len(from)) 30 | for idx, item := range from { 31 | toItem, ok := item.(graph.Node) 32 | if !ok { 33 | return nil, errors.New("Expected Node value. Got %T %+v", item, item) 34 | } 35 | to[idx] = toItem 36 | } 37 | return to, nil 38 | } 39 | 40 | func sliceInterfaceToRelationship(from []interface{}) ([]graph.Relationship, error) { 41 | to := make([]graph.Relationship, len(from)) 42 | for idx, item := range from { 43 | toItem, ok := item.(graph.Relationship) 44 | if !ok { 45 | return nil, errors.New("Expected Relationship value. Got %T %+v", item, item) 46 | } 47 | to[idx] = toItem 48 | } 49 | return to, nil 50 | } 51 | 52 | func sliceInterfaceToUnboundRelationship(from []interface{}) ([]graph.UnboundRelationship, error) { 53 | to := make([]graph.UnboundRelationship, len(from)) 54 | for idx, item := range from { 55 | toItem, ok := item.(graph.UnboundRelationship) 56 | if !ok { 57 | return nil, errors.New("Expected UnboundRelationship value. Got %T %+v", item, item) 58 | } 59 | to[idx] = toItem 60 | } 61 | return to, nil 62 | } 63 | -------------------------------------------------------------------------------- /errors/doc.go: -------------------------------------------------------------------------------- 1 | /*Package errors contains the errors used by the bolt driver. Implements wrapped errors and stack traces*/ 2 | package errors 3 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | "strings" 7 | ) 8 | 9 | // Error is the base error type adds stack trace and wrapping errors 10 | type Error struct { 11 | msg string 12 | wrapped error 13 | stack []byte 14 | level int 15 | } 16 | 17 | // New makes a new error 18 | func New(msg string, args ...interface{}) *Error { 19 | return &Error{ 20 | msg: fmt.Sprintf(msg, args...), 21 | stack: debug.Stack(), 22 | level: 0, 23 | } 24 | } 25 | 26 | // Wrap wraps an error with a new error 27 | func Wrap(err error, msg string, args ...interface{}) *Error { 28 | if e, ok := err.(*Error); ok { 29 | return &Error{ 30 | msg: fmt.Sprintf(msg, args...), 31 | wrapped: e, 32 | } 33 | } 34 | 35 | return &Error{ 36 | msg: fmt.Sprintf(msg, args...), 37 | wrapped: err, 38 | stack: debug.Stack(), 39 | } 40 | } 41 | 42 | // Error gets the error output 43 | func (e *Error) Error() string { 44 | return e.error(0) 45 | } 46 | 47 | // Inner returns the inner error wrapped by this error 48 | func (e *Error) Inner() error { 49 | return e.wrapped 50 | } 51 | 52 | // InnerMost returns the innermost error wrapped by this error 53 | func (e *Error) InnerMost() error { 54 | if e.wrapped == nil { 55 | return e 56 | } 57 | 58 | if inner, ok := e.wrapped.(*Error); ok { 59 | return inner.InnerMost() 60 | } 61 | 62 | return e.wrapped 63 | } 64 | 65 | func (e *Error) error(level int) string { 66 | msg := fmt.Sprintf("%s%s", strings.Repeat("\t", level), e.msg) 67 | if e.wrapped != nil { 68 | if wrappedErr, ok := e.wrapped.(*Error); ok { 69 | msg += fmt.Sprintf("\n%s", wrappedErr.error(level+1)) 70 | } else { 71 | msg += fmt.Sprintf("\nInternal Error(%T):%s", e.wrapped, e.wrapped.Error()) 72 | } 73 | } 74 | 75 | if len(e.stack) > 0 { 76 | msg += fmt.Sprintf("\n\n Stack Trace:\n\n%s", e.stack) 77 | } 78 | 79 | return msg 80 | } 81 | -------------------------------------------------------------------------------- /examples/playground/README.md: -------------------------------------------------------------------------------- 1 | # Playground 2 | 3 | I usually set myself up a playground folder in my golang projects to try stuff out as I'm testing, kinda like a scratch pad. 4 | 5 | I figure leaving this in here does no harm, and may give some insight into my thought process at the time. -------------------------------------------------------------------------------- /examples/playground/playground.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "encoding/gob" 9 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/encoding" 10 | "math" 11 | ) 12 | 13 | type test struct { 14 | b []byte 15 | } 16 | 17 | func main() { 18 | 19 | t := &test{[]byte{1, 2, 3}} 20 | t1 := &test{} 21 | fmt.Printf("TEE: %#v\n", t) 22 | fmt.Printf("TEE1: %#v\n", t1) 23 | *t1 = *t 24 | t = nil 25 | fmt.Printf("TEE: %#v\n", t) 26 | fmt.Printf("TEE1: %#v\n", t1) 27 | 28 | args := map[string]interface{}{ 29 | "a": 1, 30 | "b": 34234.34323, 31 | "c": "string", 32 | "d": []interface{}{1, 2, 3}, 33 | "e": true, 34 | "f": nil, 35 | } 36 | 37 | enc, err := encoding.Marshal(args) 38 | if err != nil { 39 | fmt.Printf("MARSHALLERR: %#v\n", err) 40 | } 41 | fmt.Printf("ENCODED: %#v\n", enc) 42 | 43 | decoded, err := encoding.Unmarshal(enc) 44 | if err != nil { 45 | fmt.Printf("UNMARSHALLERR: %#v\n", err) 46 | } 47 | 48 | fmt.Printf("DECODED: %#v\n", decoded) 49 | 50 | buf := &bytes.Buffer{} 51 | err = gob.NewEncoder(buf).Encode(args) 52 | if err != nil { 53 | fmt.Printf("GOBERR: %#v", err) 54 | } 55 | 56 | var y map[string]interface{} 57 | gob.NewDecoder(buf).Decode(&y) 58 | 59 | fmt.Printf("GOB: %#v\n", y) 60 | 61 | fmt.Printf("LenMake: %d\n", len(make([]interface{}, 15))) 62 | 63 | fmt.Println("test.b:", len(test{}.b)) 64 | buf = bytes.NewBuffer([]byte{byte(0xf0)}) 65 | var x int 66 | binary.Read(buf, binary.BigEndian, &x) 67 | fmt.Println("Marker: 0xf0 ", 0xf0, "INT: ", x) 68 | 69 | buf = bytes.NewBuffer([]byte{byte(0xff)}) 70 | binary.Read(buf, binary.BigEndian, &x) 71 | fmt.Println("Marker: 0xff ", 0xff, "INT: ", x) 72 | 73 | buf = bytes.NewBuffer([]byte{byte(0x7f)}) 74 | binary.Read(buf, binary.BigEndian, &x) 75 | fmt.Println("Marker: 0x7f ", 0x7f, "INT: ", x) 76 | 77 | buf = &bytes.Buffer{} 78 | binary.Write(buf, binary.BigEndian, int8(-1)) 79 | fmt.Printf("BYTES: %#x\n", buf.Bytes()) 80 | binary.Write(buf, binary.BigEndian, int8(127)) 81 | fmt.Printf("BYTES: %#x\n", buf.Bytes()) 82 | fmt.Printf("INT1: %#x\n", byte(1)) 83 | fmt.Printf("INT34234234: %#x\n", byte(234)) 84 | fmt.Println("DONE") 85 | 86 | fmt.Printf("MAX INT8: %d\n", math.MaxInt8) 87 | fmt.Printf("MAX INT16: %d\n", math.MaxInt16) 88 | fmt.Printf("MAX INT32: %d\n", math.MaxInt32) 89 | fmt.Printf("MAX INT64: %d\n", math.MaxInt64) 90 | 91 | fmt.Printf("MIN INT8: %d\n", math.MinInt8) 92 | fmt.Printf("MIN INT16: %d\n", math.MinInt16) 93 | fmt.Printf("MIN INT32: %d\n", math.MinInt32) 94 | fmt.Printf("MIN INT64: %d\n", math.MinInt64) 95 | 96 | fmt.Printf("MAX UINT8: %d\n", math.MaxUint8) 97 | fmt.Printf("MAX UINT16: %d\n", math.MaxUint16) 98 | fmt.Printf("MAX UINT32: %d\n", math.MaxUint32) 99 | z := fmt.Sprint(uint64(math.MaxUint64)) 100 | fmt.Printf("MAX UINT64: %s\n", z) 101 | 102 | fmt.Println("Marker: 0xf0 ", 0xf0, "INT: ", int(byte(0xf0))) 103 | fmt.Println("Marker: 0xf0 byte{}", 0xf0, "INT: ", int(0xf0)) 104 | _ = `b1 71 99 cb 80 0 0 0 0 0 0 0 ca 80 0 0 105 | 106 | 0 c9 80 0 c8 80 f0 7f c9 7f ff ca 7f ff ff ff 107 | 108 | cb 7f ff ff ff ff ff ff ff` 109 | } 110 | -------------------------------------------------------------------------------- /examples/quick_n_dirty/quick_n_dirty.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | bolt "github.com/johnnadratowski/golang-neo4j-bolt-driver" 7 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/graph" 8 | ) 9 | 10 | func main() { 11 | driver := bolt.NewDriver() 12 | conn, _ := driver.OpenNeo("bolt://localhost:7687") 13 | defer conn.Close() 14 | 15 | // Start by creating a node 16 | result, _ := conn.ExecNeo("CREATE (n:NODE {foo: {foo}, bar: {bar}})", map[string]interface{}{"foo": 1, "bar": 2.2}) 17 | numResult, _ := result.RowsAffected() 18 | fmt.Printf("CREATED ROWS: %d\n", numResult) // CREATED ROWS: 1 19 | 20 | // Lets get the node 21 | data, rowsMetadata, _, _ := conn.QueryNeoAll("MATCH (n:NODE) RETURN n.foo, n.bar", nil) 22 | fmt.Printf("COLUMNS: %#v\n", rowsMetadata["fields"].([]interface{})) // COLUMNS: n.foo,n.bar 23 | fmt.Printf("FIELDS: %d %f\n", data[0][0].(int64), data[0][1].(float64)) // FIELDS: 1 2.2 24 | 25 | // oh cool, that worked. lets blast this baby and tell it to run a bunch of statements 26 | // in neo concurrently with a pipeline 27 | results, _ := conn.ExecPipeline([]string{ 28 | "MATCH (n:NODE) CREATE (n)-[:REL]->(f:FOO)", 29 | "MATCH (n:NODE) CREATE (n)-[:REL]->(b:BAR)", 30 | "MATCH (n:NODE) CREATE (n)-[:REL]->(z:BAZ)", 31 | "MATCH (n:NODE) CREATE (n)-[:REL]->(f:FOO)", 32 | "MATCH (n:NODE) CREATE (n)-[:REL]->(b:BAR)", 33 | "MATCH (n:NODE) CREATE (n)-[:REL]->(z:BAZ)", 34 | }, nil, nil, nil, nil, nil, nil) 35 | for _, result := range results { 36 | numResult, _ := result.RowsAffected() 37 | fmt.Printf("CREATED ROWS: %d\n", numResult) // CREATED ROWS: 2 (per each iteration) 38 | } 39 | 40 | data, _, _, _ = conn.QueryNeoAll("MATCH (n:NODE)-[:REL]->(m) RETURN m", nil) 41 | for _, row := range data { 42 | fmt.Printf("NODE: %#v\n", row[0].(graph.Node)) // Prints all nodes 43 | } 44 | 45 | result, _ = conn.ExecNeo(`MATCH (n) DETACH DELETE n`, nil) 46 | numResult, _ = result.RowsAffected() 47 | fmt.Printf("Rows Deleted: %d", numResult) // Rows Deleted: 13 48 | } 49 | -------------------------------------------------------------------------------- /examples/slow_n_clean/slow_n_clean.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | bolt "github.com/johnnadratowski/golang-neo4j-bolt-driver" 7 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/graph" 8 | "io" 9 | ) 10 | 11 | func main() { 12 | driver := bolt.NewDriver() 13 | conn, err := driver.OpenNeo("bolt://localhost:7687") 14 | if err != nil { 15 | panic(err) 16 | } 17 | defer conn.Close() 18 | 19 | // Here we prepare a new statement. This gives us the flexibility to 20 | // cancel that statement without any request sent to Neo 21 | stmt, err := conn.PrepareNeo("CREATE (n:NODE {foo: {foo}, bar: {bar}})") 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | // Executing a statement just returns summary information 27 | result, err := stmt.ExecNeo(map[string]interface{}{"foo": 1, "bar": 2.2}) 28 | if err != nil { 29 | panic(err) 30 | } 31 | numResult, err := result.RowsAffected() 32 | if err != nil { 33 | panic(err) 34 | } 35 | fmt.Printf("CREATED ROWS: %d\n", numResult) // CREATED ROWS: 1 36 | 37 | // Closing the statment will also close the rows 38 | stmt.Close() 39 | 40 | // Lets get the node. Once again I can cancel this with no penalty 41 | stmt, err = conn.PrepareNeo("MATCH (n:NODE) RETURN n.foo, n.bar") 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | // Even once I get the rows, if I do not consume them and close the 47 | // rows, Neo will discard and not send the data 48 | rows, err := stmt.QueryNeo(nil) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | // This interface allows you to consume rows one-by-one, as they 54 | // come off the bolt stream. This is more efficient especially 55 | // if you're only looking for a particular row/set of rows, as 56 | // you don't need to load up the entire dataset into memory 57 | data, _, err := rows.NextNeo() 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | // This query only returns 1 row, so once it's done, it will return 63 | // the metadata associated with the query completion, along with 64 | // io.EOF as the error 65 | _, _, err = rows.NextNeo() 66 | if err != io.EOF { 67 | panic(err) 68 | } 69 | fmt.Printf("COLUMNS: %#v\n", rows.Metadata()["fields"].([]interface{})) // COLUMNS: n.foo,n.bar 70 | fmt.Printf("FIELDS: %d %f\n", data[0].(int64), data[1].(float64)) // FIELDS: 1 2.2 71 | 72 | stmt.Close() 73 | 74 | // Here we prepare a new pipeline statement for running multiple 75 | // queries concurrently 76 | pipeline, err := conn.PreparePipeline( 77 | "MATCH (n:NODE) CREATE (n)-[:REL]->(f:FOO)", 78 | "MATCH (n:NODE) CREATE (n)-[:REL]->(b:BAR)", 79 | "MATCH (n:NODE) CREATE (n)-[:REL]->(z:BAZ)", 80 | "MATCH (n:NODE) CREATE (n)-[:REL]->(f:FOO)", 81 | "MATCH (n:NODE) CREATE (n)-[:REL]->(b:BAR)", 82 | "MATCH (n:NODE) CREATE (n)-[:REL]->(z:BAZ)", 83 | ) 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | pipelineResults, err := pipeline.ExecPipeline(nil, nil, nil, nil, nil, nil) 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | for _, result := range pipelineResults { 94 | numResult, _ := result.RowsAffected() 95 | fmt.Printf("CREATED ROWS: %d\n", numResult) // CREATED ROWS: 2 (per each iteration) 96 | } 97 | 98 | err = pipeline.Close() 99 | if err != nil { 100 | panic(err) 101 | } 102 | 103 | stmt, err = conn.PrepareNeo("MATCH path=(n:NODE)-[:REL]->(m) RETURN path") 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | rows, err = stmt.QueryNeo(nil) 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | // Here we loop through the rows until we get the metadata object 114 | // back, meaning the row stream has been fully consumed 115 | for err == nil { 116 | var row []interface{} 117 | row, _, err = rows.NextNeo() 118 | if err != nil && err != io.EOF { 119 | panic(err) 120 | } else if err != io.EOF { 121 | fmt.Printf("PATH: %#v\n", row[0].(graph.Path)) // Prints all paths 122 | } 123 | } 124 | 125 | stmt.Close() 126 | 127 | result, _ = conn.ExecNeo(`MATCH (n) DETACH DELETE n`, nil) 128 | fmt.Println(result) 129 | numResult, _ = result.RowsAffected() 130 | fmt.Printf("Rows Deleted: %d", numResult) // Rows Deleted: 13 131 | } 132 | -------------------------------------------------------------------------------- /log/doc.go: -------------------------------------------------------------------------------- 1 | /*Package log implements the logging for the bolt driver 2 | 3 | There are 3 logging levels - trace, info and error. Setting trace would also set info and error logs. 4 | You can use the SetLevel("trace") to set trace logging, for example. 5 | */ 6 | package log 7 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | l "log" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | // Level is the logging level 10 | type Level int 11 | 12 | const ( 13 | // NoneLevel is no logging 14 | NoneLevel Level = iota 15 | // ErrorLevel is error logging 16 | ErrorLevel Level = iota 17 | // InfoLevel is info logging 18 | InfoLevel Level = iota 19 | // TraceLevel is trace logging 20 | TraceLevel Level = iota 21 | ) 22 | 23 | var ( 24 | level = NoneLevel 25 | // ErrorLog is the logger for error logging. This can be manually overridden. 26 | ErrorLog = l.New(os.Stderr, "[BOLT][ERROR]", l.LstdFlags) 27 | // InfoLog is the logger for info logging. This can be manually overridden. 28 | InfoLog = l.New(os.Stderr, "[BOLT][INFO]", l.LstdFlags) 29 | // TraceLog is the logger for trace logging. This can be manually overridden. 30 | TraceLog = l.New(os.Stderr, "[BOLT][TRACE]", l.LstdFlags) 31 | ) 32 | 33 | // SetLevel sets the logging level of this package. levelStr should be one of "trace", "info", or "error 34 | func SetLevel(levelStr string) { 35 | switch strings.ToLower(levelStr) { 36 | case "trace": 37 | level = TraceLevel 38 | case "info": 39 | level = InfoLevel 40 | case "error": 41 | level = ErrorLevel 42 | default: 43 | level = NoneLevel 44 | } 45 | } 46 | 47 | // GetLevel gets the logging level 48 | func GetLevel() Level { 49 | return level 50 | } 51 | 52 | // Trace writes a trace log in the format of Println 53 | func Trace(args ...interface{}) { 54 | if level >= TraceLevel { 55 | TraceLog.Println(args...) 56 | } 57 | } 58 | 59 | // Tracef writes a trace log in the format of Printf 60 | func Tracef(msg string, args ...interface{}) { 61 | if level >= TraceLevel { 62 | TraceLog.Printf(msg, args...) 63 | } 64 | } 65 | 66 | // Info writes an info log in the format of Println 67 | func Info(args ...interface{}) { 68 | if level >= InfoLevel { 69 | InfoLog.Println(args...) 70 | } 71 | } 72 | 73 | // Infof writes an info log in the format of Printf 74 | func Infof(msg string, args ...interface{}) { 75 | if level >= InfoLevel { 76 | InfoLog.Printf(msg, args...) 77 | } 78 | } 79 | 80 | // Error writes an error log in the format of Println 81 | func Error(args ...interface{}) { 82 | if level >= ErrorLevel { 83 | ErrorLog.Println(args...) 84 | } 85 | } 86 | 87 | // Errorf writes an error log in the format of Printf 88 | func Errorf(msg string, args ...interface{}) { 89 | if level >= ErrorLevel { 90 | ErrorLog.Printf(msg, args...) 91 | } 92 | } 93 | 94 | // Fatal writes an error log in the format of Fatalln 95 | func Fatal(args ...interface{}) { 96 | l.Fatalln(args...) 97 | } 98 | 99 | // Fatalf writes an error log in the format of Fatalf 100 | func Fatalf(msg string, args ...interface{}) { 101 | l.Fatalf(msg, args...) 102 | } 103 | 104 | // Panic writes an error log in the format of Panicln 105 | func Panic(args ...interface{}) { 106 | l.Panicln(args...) 107 | } 108 | 109 | // Panicf writes an error log in the format of Panicf 110 | func Panicf(msg string, args ...interface{}) { 111 | l.Panicf(msg, args...) 112 | } 113 | -------------------------------------------------------------------------------- /recorder.go: -------------------------------------------------------------------------------- 1 | package golangNeo4jBoltDriver 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net" 8 | "os" 9 | "time" 10 | 11 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/encoding" 12 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 13 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/log" 14 | ) 15 | 16 | // recorder records a given session with Neo4j. 17 | // allows for playback of sessions as well 18 | type recorder struct { 19 | net.Conn 20 | name string 21 | events []*Event 22 | connStr string 23 | currentEvent int 24 | } 25 | 26 | func newRecorder(name string, connStr string) *recorder { 27 | r := &recorder{ 28 | name: name, 29 | connStr: connStr, 30 | } 31 | 32 | if r.connStr == "" { 33 | if err := r.load(r.name); err != nil { 34 | log.Fatalf("Couldn't load data from recording files!: %s", err) 35 | } 36 | } 37 | 38 | return r 39 | } 40 | 41 | func (r *recorder) completedLast() bool { 42 | event := r.lastEvent() 43 | if event == nil { 44 | return true 45 | } 46 | 47 | return event.Completed 48 | } 49 | 50 | func (r *recorder) lastEvent() *Event { 51 | if len(r.events) > 0 { 52 | return r.events[len(r.events)-1] 53 | } 54 | return nil 55 | } 56 | 57 | // Read from the net conn, recording the interaction 58 | func (r *recorder) Read(b []byte) (n int, err error) { 59 | if r.Conn != nil { 60 | numRead, err := r.Conn.Read(b) 61 | if numRead > 0 { 62 | r.record(b[:numRead], false) 63 | } 64 | 65 | if err != nil { 66 | r.recordErr(err, false) 67 | } 68 | 69 | return numRead, err 70 | } 71 | 72 | if r.currentEvent >= len(r.events) { 73 | return 0, errors.New("Trying to read past all of the events in the recorder! %#v", r) 74 | } 75 | event := r.events[r.currentEvent] 76 | if event.IsWrite { 77 | return 0, errors.New("Recorder expected Read, got Write! %#v, Event: %#v", r, event) 78 | } 79 | 80 | for i := 0; i < len(b); i++ { 81 | if len(event.Event) == 0 { 82 | return i, errors.New("Attempted to read past current event in recorder! Bytes: %s. Recorder %#v, Event; %#v", b, r, event) 83 | } 84 | b[i] = event.Event[0] 85 | event.Event = event.Event[1:] 86 | } 87 | 88 | if len(event.Event) == 0 { 89 | r.currentEvent++ 90 | } 91 | 92 | return len(b), nil 93 | } 94 | 95 | // Close the net conn, outputting the recording 96 | func (r *recorder) Close() error { 97 | if r.Conn != nil { 98 | err := r.flush() 99 | if err != nil { 100 | return err 101 | } 102 | return r.Conn.Close() 103 | } else if len(r.events) > 0 { 104 | if r.currentEvent != len(r.events) { 105 | return errors.New("Didn't read all of the events in the recorder on close! %#v", r) 106 | } 107 | 108 | if len(r.events[len(r.events)-1].Event) != 0 { 109 | return errors.New("Left data in an event in the recorder on close! %#v", r) 110 | } 111 | 112 | return nil 113 | } 114 | 115 | return nil 116 | } 117 | 118 | // Write to the net conn, recording the interaction 119 | func (r *recorder) Write(b []byte) (n int, err error) { 120 | if r.Conn != nil { 121 | numWritten, err := r.Conn.Write(b) 122 | if numWritten > 0 { 123 | r.record(b[:numWritten], true) 124 | } 125 | 126 | if err != nil { 127 | r.recordErr(err, true) 128 | } 129 | 130 | return numWritten, err 131 | } 132 | 133 | if r.currentEvent >= len(r.events) { 134 | return 0, errors.New("Trying to write past all of the events in the recorder! %#v", r) 135 | } 136 | event := r.events[r.currentEvent] 137 | if !event.IsWrite { 138 | return 0, errors.New("Recorder expected Write, got Read! %#v, Event: %#v", r, event) 139 | } 140 | 141 | for i := 0; i < len(b); i++ { 142 | if len(event.Event) == 0 { 143 | return i, errors.New("Attempted to write past current event in recorder! %#v, Event: %#v", r, event) 144 | } 145 | event.Event = event.Event[1:] 146 | } 147 | 148 | if len(event.Event) == 0 { 149 | r.currentEvent++ 150 | } 151 | 152 | return len(b), nil 153 | } 154 | 155 | func (r *recorder) record(data []byte, isWrite bool) { 156 | event := r.lastEvent() 157 | if event == nil || event.Completed || event.IsWrite != isWrite { 158 | event = newEvent(isWrite) 159 | r.events = append(r.events, event) 160 | } 161 | 162 | event.Event = append(event.Event, data...) 163 | if data[len(data)-2] == byte(0x00) && data[len(data)-1] == byte(0x00) { 164 | event.Completed = true 165 | } 166 | } 167 | 168 | func (r *recorder) recordErr(err error, isWrite bool) { 169 | event := r.lastEvent() 170 | if event == nil || event.Completed || event.IsWrite != isWrite { 171 | event = newEvent(isWrite) 172 | r.events = append(r.events, event) 173 | } 174 | 175 | event.Error = err 176 | event.Completed = true 177 | } 178 | 179 | func (r *recorder) load(name string) error { 180 | file, err := os.OpenFile("./recordings/"+name+".json", os.O_RDONLY, 0660) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | return json.NewDecoder(file).Decode(&r.events) 186 | } 187 | 188 | func (r *recorder) writeRecording() error { 189 | file, err := os.OpenFile("./recordings/"+r.name+".json", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0660) 190 | if err != nil { 191 | return err 192 | } 193 | return json.NewEncoder(file).Encode(r.events) 194 | } 195 | 196 | func (r *recorder) flush() error { 197 | if os.Getenv("RECORD_OUTPUT") == "" { 198 | return nil 199 | } 200 | return r.writeRecording() 201 | } 202 | 203 | func (r *recorder) print() { 204 | fmt.Println("PRINTING RECORDING " + r.name) 205 | 206 | for _, event := range r.events { 207 | 208 | fmt.Println() 209 | fmt.Println() 210 | 211 | typee := "READ" 212 | if event.IsWrite { 213 | typee = "WRITE" 214 | } 215 | fmt.Printf("%s @ %d:\n\n", typee, event.Timestamp) 216 | 217 | decoded, err := encoding.NewDecoder(bytes.NewBuffer(event.Event)).Decode() 218 | if err != nil { 219 | fmt.Printf("Error decoding data! Error: %s\n", err) 220 | } else { 221 | fmt.Printf("Decoded Data:\n\n%+v\n\n", decoded) 222 | } 223 | 224 | fmt.Print("Encoded Bytes:\n\n") 225 | fmt.Print(sprintByteHex(event.Event)) 226 | if !event.Completed { 227 | fmt.Println("EVENT NEVER COMPLETED!!!!!!!!!!!!!!!") 228 | } 229 | 230 | if event.Error != nil { 231 | fmt.Printf("ERROR OCCURRED DURING EVENT!!!!!!!\n\nError: %s\n", event.Error) 232 | } 233 | 234 | fmt.Println() 235 | fmt.Println() 236 | } 237 | 238 | fmt.Println("RECORDING END " + r.name) 239 | } 240 | 241 | func (r *recorder) LocalAddr() net.Addr { 242 | if r.Conn != nil { 243 | return r.Conn.LocalAddr() 244 | } 245 | return nil 246 | } 247 | 248 | func (r *recorder) RemoteAddr() net.Addr { 249 | if r.Conn != nil { 250 | return r.Conn.RemoteAddr() 251 | } 252 | return nil 253 | } 254 | 255 | func (r *recorder) SetDeadline(t time.Time) error { 256 | if r.Conn != nil { 257 | return r.Conn.SetDeadline(t) 258 | } 259 | return nil 260 | } 261 | 262 | func (r *recorder) SetReadDeadline(t time.Time) error { 263 | if r.Conn != nil { 264 | return r.Conn.SetReadDeadline(t) 265 | } 266 | return nil 267 | } 268 | 269 | func (r *recorder) SetWriteDeadline(t time.Time) error { 270 | if r.Conn != nil { 271 | return r.Conn.SetWriteDeadline(t) 272 | } 273 | return nil 274 | } 275 | 276 | // Event represents a single recording (read or write) event in the recorder 277 | type Event struct { 278 | Timestamp int64 `json:"-"` 279 | Event []byte 280 | IsWrite bool 281 | Completed bool 282 | Error error 283 | } 284 | 285 | func newEvent(isWrite bool) *Event { 286 | return &Event{ 287 | Timestamp: time.Now().UnixNano(), 288 | Event: []byte{}, 289 | IsWrite: isWrite, 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /recordings/README.md: -------------------------------------------------------------------------------- 1 | Contains recordings for playback when using tests. These recordings are taken when the tests run against a neo4j database, and stored here. If running the tests w/o a database, these get played back. -------------------------------------------------------------------------------- /recordings/TestBoltConn_Close.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltConn_CloseStatementOnError.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADyyENA3VEhJUyBJUyBBIEJBRCBRVUVSWSBBTkQgU0hPVUxEIFJFVFVSTiBBIEZBSUxVUkUgTUVTU0FHRaAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ALexf6KEY29kZdAlTmVvLkNsaWVudEVycm9yLlN0YXRlbWVudC5TeW50YXhFcnJvcodtZXNzYWdl0H5JbnZhbGlkIGlucHV0ICdUJzogZXhwZWN0ZWQgPGluaXQ+IChsaW5lIDEsIGNvbHVtbiAxIChvZmZzZXQ6IDApKQoiVEhJUyBJUyBBIEJBRCBRVUVSWSBBTkQgU0hPVUxEIFJFVFVSTiBBIEZBSUxVUkUgTUVTU0FHRSIKIF4AAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwDgAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwfgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AA2yEIlSRVRVUk4gMTugAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5GBMQAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltConn_ExecNeo.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AIuyENBgQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn0sIGM6IHtjfSwgZDoge2R9LCBlOiB7ZX0sIGY6IHtmfSwgZzoge2d9LCBoOiB7aH19KS1bYjpCQVJdLT4oYzpCQVopqIFiAYFjw4FkwIFlkwECA4FmwUALMzMzMzMzgWf/gWjCgWGDZm9vAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAuxcKGGZmllbGRzkAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFaxcKKFc3RhdHOkjGxhYmVscy1hZGRlZALQFXJlbGF0aW9uc2hpcHMtY3JlYXRlZAGNbm9kZXMtY3JlYXRlZAKOcHJvcGVydGllcy1zZXQHhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACOyENAeTUFUQ0ggKGY6Rk9PKSBTRVQgZi5hID0gImJhciI7oAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAuxcKGGZmllbGRzkAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACGxcKKFc3RhdHOhjnByb3BlcnRpZXMtc2V0AYR0eXBlgXcAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADKyENAtTUFUQ0ggKGY6Rk9PKS1bYjpCQVJdLT4oYzpCQVopIERFTEVURSBmLCBiLCBjoAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAuxcKGGZmllbGRzkAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcKKFc3RhdHOijW5vZGVzLWRlbGV0ZWQC0BVyZWxhdGlvbnNoaXBzLWRlbGV0ZWQBhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltConn_FailureMessageError.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADyyENA3VEhJUyBJUyBBIEJBRCBRVUVSWSBBTkQgU0hPVUxEIFJFVFVSTiBBIEZBSUxVUkUgTUVTU0FHRaAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ALexf6KEY29kZdAlTmVvLkNsaWVudEVycm9yLlN0YXRlbWVudC5TeW50YXhFcnJvcodtZXNzYWdl0H5JbnZhbGlkIGlucHV0ICdUJzogZXhwZWN0ZWQgPGluaXQ+IChsaW5lIDEsIGNvbHVtbiAxIChvZmZzZXQ6IDApKQoiVEhJUyBJUyBBIEJBRCBRVUVSWSBBTkQgU0hPVUxEIFJFVFVSTiBBIEZBSUxVUkUgTUVTU0FHRSIKIF4AAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwDgAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwfgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltConn_Ignored.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKyEIxzeW50YXggZXJyb3Kig2JhcsFAAZmZmZmZmoNmb28BAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AJyxf6KEY29kZdAlTmVvLkNsaWVudEVycm9yLlN0YXRlbWVudC5TeW50YXhFcnJvcodtZXNzYWdl0GNJbnZhbGlkIGlucHV0ICd5JzogZXhwZWN0ZWQgJ3QvVCcsICdlL0UnIG9yICduL04nIChsaW5lIDEsIGNvbHVtbiAyIChvZmZzZXQ6IDEpKQoic3ludGF4IGVycm9yIgogIF4AAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwDgAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwfgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AA2yEIlSRVRVUk4gMTugAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5GBMQAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AASxcZEBAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgCEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltConn_IgnoredPipeline.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AA2yEIlSRVRVUk4gMTugAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5GBMQAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AASxcZEBAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgGEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltConn_PipelineExec.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AC2yENAfQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn19KaKBYQGBYoN0d28AAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AC+yENAfQ1JFQVRFIChiOkJBUiB7YToge2F9LCBiOiB7Yn19KaKBYQKBYoV0aHJlZQAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AC6yENAfQ1JFQVRFIChjOkJBWiB7YToge2F9LCBiOiB7Yn19KaKBYoRmb3VygWEDAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAuxcKGGZmllbGRzkAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AD6xcKKFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQChHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAuxcKGGZmllbGRzkAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AD6xcKKFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQChHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAuxcKGGZmllbGRzkAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AD6xcKKFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQChHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADSyENAvTUFUQ0ggKGY6Rk9PKSwgKGI6QkFSKSwgKGM6QkFaKSByZXR1cm4gZiwgYiwgYzugAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABGxcKGGZmllbGRzk4FmgWKBYwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AEKxcZOzTskGpJGDRk9PooFhAYFig3R3b7NOyQalkYNCQVKigWECgWKFdGhyZWWzTskGppGDQkFaooFhA4FihGZvdXIAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAqxcKGEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADOyENAuTUFUQ0ggKGY6Rk9PKSwgKGI6QkFSKSwgKGM6QkFaKSBERUxFVEUgZiwgYiwgY6AAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAuxcKGGZmllbGRzkAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACCxcKKFc3RhdHOhjW5vZGVzLWRlbGV0ZWQDhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltConn_PipelineQuery.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADayENAoQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gZqKBYQGBYoN0d28AAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADiyENAoQ1JFQVRFIChiOkJBUiB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gYqKBYQKBYoV0aHJlZQAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADeyENAoQ1JFQVRFIChjOkJBWiB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gY6KBYQOBYoRmb3VyAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AA2xcKGGZmllbGRzkYFmAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABexcZGzTskGnZGDRk9PooFhAYFig3R3bwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AD+xcKKFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQChHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AA2xcKGGZmllbGRzkYFiAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABmxcZGzTskGnpGDQkFSooFhAoFihXRocmVlAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AD+xcKKFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQChHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AA2xcKGGZmllbGRzkYFjAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABixcZGzTskGn5GDQkFaooFhA4FihGZvdXIAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AD+xcKKFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQChHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADOyENAuTUFUQ0ggKGY6Rk9PKSwgKGI6QkFSKSwgKGM6QkFaKSBERUxFVEUgZiwgYiwgY6AAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAuxcKGGZmllbGRzkAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACCxcKKFc3RhdHOhjW5vZGVzLWRlbGV0ZWQDhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltConn_SelectAll.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACyyENAnQ1JFQVRFIChmOk5PREUge2E6IDF9KSwgKGI6Tk9ERSB7YTogMn0poAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXJVhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFaxcKOFc3RhdHOjjGxhYmVscy1hZGRlZAKNbm9kZXMtY3JlYXRlZAKOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACuyENAmTUFUQ0ggKG46Tk9ERSkgUkVUVVJOIG4uYSBPUkRFUiBCWSBuLmGgAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACixcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXJuhmZpZWxkc5GDbi5hAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AASxcZEBAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AASxcZECAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgCEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AByyENAXTUFUQ0ggKG46Tk9ERSkgREVMRVRFIG6gAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIRhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcKOFc3RhdHOhjW5vZGVzLWRlbGV0ZWQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltConn_SelectOne.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AA2yEIlSRVRVUk4gMTugAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIRhmZpZWxkc5GBMQAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AASxcZEBAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgCEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_CreateArgs.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AIuyENBjQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn0sIGM6IHtjfSwgZDoge2R9LCBlOiB7ZX0sIGY6IHtmfX0pIFJFVFVSTiBmLmEsIGYuYiwgZi5jLCBmLmQsIGYuZSwgZi5mpoFmwIFhAYFiwUDgt0r7vXsggWOGc3RyaW5ngWSTAQIDgWXDAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADyxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIWhmZpZWxkc5aDZi5hg2YuYoNmLmODZi5kg2YuZYNmLmYAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABqxcZYBwUDgt0r7vXsghnN0cmluZ5MBAgPDwAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQF0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABuyENAWTUFUQ0ggKGY6Rk9PKSBERUxFVEUgZqAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIKhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcKOFc3RhdHOhjW5vZGVzLWRlbGV0ZWQF0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_Discard.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADqyENA1Q1JFQVRFIChmOkZPTyB7YTogIjEifSksIChiOkZPTyB7YTogIjIifSkgUkVUVVJOIGYsIGKgAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACixcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIRhmZpZWxkc5KBZoFiAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwLwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAKNbm9kZXMtY3JlYXRlZAKOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACqyENAlTUFUQ0ggKGY6Rk9PKSBSRVRVUk4gZi5hIE9SREVSIEJZIGYuYaAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACixcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIZhmZpZWxkc5GDZi5hAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAWxcZGBMQAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAWxcZGBMgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgCEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACqyENAlTUFUQ0ggKGY6Rk9PKSBSRVRVUk4gZi5hIE9SREVSIEJZIGYuYaAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACixcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5GDZi5hAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAWxcZGBMQAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAWxcZGBMgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgGEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABuyENAWTUFUQ0ggKGY6Rk9PKSBERUxFVEUgZqAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcKOFc3RhdHOhjW5vZGVzLWRlbGV0ZWQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_ExecNeo.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AIuyENBgQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn0sIGM6IHtjfSwgZDoge2R9LCBlOiB7ZX0sIGY6IHtmfSwgZzoge2d9LCBoOiB7aH19KS1bYjpCQVJdLT4oYzpCQVopqIFiAYFjw4FlkwECA4FmwUALMzMzMzMzgWf/gWjCgWTAgWGDZm9vAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIshmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AG6xcKOFc3RhdHOkjGxhYmVscy1hZGRlZALQFXJlbGF0aW9uc2hpcHMtY3JlYXRlZAGNbm9kZXMtY3JlYXRlZAKOcHJvcGVydGllcy1zZXQH0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACOyENAeTUFUQ0ggKGY6Rk9PKSBTRVQgZi5hID0gImJhciI7oAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIrhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADmxcKOFc3RhdHOhjnByb3BlcnRpZXMtc2V0BdAVcmVzdWx0X2NvbnN1bWVkX2FmdGVyAIR0eXBlgXcAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADKyENAtTUFUQ0ggKGY6Rk9PKS1bYjpCQVJdLT4oYzpCQVopIERFTEVURSBmLCBiLCBjoAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIUhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFCxcKOFc3RhdHOijW5vZGVzLWRlbGV0ZWQC0BVyZWxhdGlvbnNoaXBzLWRlbGV0ZWQB0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_Failure.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADqyENA1Q1JFQVRFIChmOkZPTyB7YTogIjEifSksIChiOkZPTyB7YTogIjIifSkgUkVUVVJOIGYsIGKgAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACixcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5KBZoFiAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwLwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAKNbm9kZXMtY3JlYXRlZAKOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AB2yENAYVGhpcyBpcyBhbiBpbnZhbGlkIHF1ZXJ5oAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AJixf6KEY29kZdAlTmVvLkNsaWVudEVycm9yLlN0YXRlbWVudC5TeW50YXhFcnJvcodtZXNzYWdl0F9JbnZhbGlkIGlucHV0ICdUJzogZXhwZWN0ZWQgPGluaXQ+IChsaW5lIDEsIGNvbHVtbiAxIChvZmZzZXQ6IDApKQoiVGhpcyBpcyBhbiBpbnZhbGlkIHF1ZXJ5IgogXgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwDgAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACqyENAlTUFUQ0ggKGY6Rk9PKSBSRVRVUk4gZi5hIE9SREVSIEJZIGYuYaAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACixcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIDhmZpZWxkc5GDZi5hAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAWxcZGBMQAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAWxcZGBMgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgGEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACqyENAlTUFUQ0ggKGY6Rk9PKSBSRVRVUk4gZi5hIE9SREVSIEJZIGYuYaAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACixcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5GDZi5hAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAWxcZGBMQAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAWxcZGBMgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgCEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABuyENAWTUFUQ0ggKGY6Rk9PKSBERUxFVEUgZqAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcKOFc3RhdHOhjW5vZGVzLWRlbGV0ZWQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_InvalidArgs.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AHOyENBIQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn0sIGM6IHtjfSwgZDoge2R9LCBlOiB7ZX0sIGY6IHtmfX0pIFJFVFVSTiBmpoFmwIFhAYFiwUDgt0r7vXsggWOGc3RyaW5ngWSVAYEyA8PAgWXDAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AN+xf6KEY29kZdAjTmVvLkNsaWVudEVycm9yLlN0YXRlbWVudC5UeXBlRXJyb3KHbWVzc2FnZdEAp05lbzRqIG9ubHkgc3VwcG9ydHMgYSBzdWJzZXQgb2YgQ3lwaGVyIHR5cGVzIGZvciBzdG9yYWdlIGFzIHNpbmdsZXRvbiBvciBhcnJheSBwcm9wZXJ0aWVzLiBQbGVhc2UgcmVmZXIgdG8gc2VjdGlvbiBjeXBoZXIvc3ludGF4L3ZhbHVlcyBvZiB0aGUgbWFudWFsIGZvciBtb3JlIGRldGFpbHMuAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwDgAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_ManyChunks.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAqyENBdUkVUVVJOAAogIjEgMiAzIDQgAE41IDYgNyA4IDkgMTAiIGFzIGEsICAiMSAyIDMgNCA1IDYgNyA4IDkgMTAiIGFzIGIsICIxIDIgMyA0IDUgNiA3IDggOSAxMCIgYXMgY6AAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACqxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIJhmZpZWxkc5OBYYFigWMAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AEWxcZPQFDEgMiAzIDQgNSA2IDcgOCA5IDEw0BQxIDIgMyA0IDUgNiA3IDggOSAxMNAUMSAyIDMgNCA1IDYgNyA4IDkgMTAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgCEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_MixedObjects.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFOyENBOQ1JFQVRFIChmOkZPTyB7YTogIjEifSktW2I6VE9dLT4oYzpCQVIpPC1bZDpGUk9NXS0oZTpCQVopIFJFVFVSTiBmLCBiLCBjLCBkLCBloAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AC6xcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIWhmZpZWxkc5WBZoFigWOBZIFlAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AESxcZWzTskAtpGDRk9PoYFhgTG1UskArckAtskAhIJUT6CzTskAhJGDQkFSoLVSyQDFI8kAhIRGUk9NoLNOI5GDQkFaoAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AG+xcKOFc3RhdHOkjGxhYmVscy1hZGRlZAPQFXJlbGF0aW9uc2hpcHMtY3JlYXRlZAKNbm9kZXMtY3JlYXRlZAOOcHJvcGVydGllcy1zZXQB0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIBhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AEmyENBETUFUQ0ggKGY6Rk9PKS1bYjpUT10tPihjOkJBUik8LVtkOkZST01dLShlOkJBWikgREVMRVRFIGYsIGIsIGMsIGQsIGWgAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXJYhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFCxcKOFc3RhdHOijW5vZGVzLWRlbGV0ZWQD0BVyZWxhdGlvbnNoaXBzLWRlbGV0ZWQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_Path.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AE+yENBKQ1JFQVRFIHBhdGg9KGY6Rk9PIHthOiAiMSJ9KS1bYjpUT10tPihjOkJBUik8LVtkOkZST01dLShlOkJBWikgUkVUVVJOIHBhdGigAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACmxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIchmZpZWxkc5GEcGF0aAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AD2xcZGzUJOzTiSRg0ZPT6GBYYExs04TkYNCQVKgs04ckYNCQVqgkrNyNYJUT6CzcskA24RGUk9NoJQBAf4CAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AG+xcKOFc3RhdHOkjGxhYmVscy1hZGRlZAPQFXJlbGF0aW9uc2hpcHMtY3JlYXRlZAKNbm9kZXMtY3JlYXRlZAOOcHJvcGVydGllcy1zZXQB0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AEmyENBETUFUQ0ggKGY6Rk9PKS1bYjpUT10tPihjOkJBUik8LVtkOkZST01dLShlOkJBWikgREVMRVRFIGYsIGIsIGMsIGQsIGWgAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIDhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFCxcKOFc3RhdHOijW5vZGVzLWRlbGV0ZWQD0BVyZWxhdGlvbnNoaXBzLWRlbGV0ZWQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_PipelineExec.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AC2yENAfQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn19KaKBYQGBYoN0d28AAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AC+yENAfQ1JFQVRFIChiOkJBUiB7YToge2F9LCBiOiB7Yn19KaKBYQKBYoV0aHJlZQAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AC6yENAfQ1JFQVRFIChjOkJBWiB7YToge2F9LCBiOiB7Yn19KaKBYQOBYoRmb3VyAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXILhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFaxcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXILhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFaxcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIIhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFaxcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADSyENAvTUFUQ0ggKGY6Rk9PKSwgKGI6QkFSKSwgKGM6QkFaKSByZXR1cm4gZiwgYiwgYzugAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACqxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXJehmZpZWxkc5OBZoFigWMAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADyxcZOzThORg0ZPT6KBYQGBYoN0d2+zThyRg0JBUqKBYQKBYoV0aHJlZbNOJJGDQkFaooFhA4FihGZvdXIAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgOEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADOyENAuTUFUQ0ggKGY6Rk9PKSwgKGI6QkFSKSwgKGM6QkFaKSBERUxFVEUgZiwgYiwgY6AAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIhhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcKOFc3RhdHOhjW5vZGVzLWRlbGV0ZWQD0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_PipelineQuery.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADayENAoQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gZqKBYQGBYoN0d28AAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADiyENAoQ1JFQVRFIChiOkJBUiB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gYqKBYQKBYoV0aHJlZQAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADeyENAoQ1JFQVRFIChjOkJBWiB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gY6KBYoRmb3VygWEDAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIPhmZpZWxkc5GBZgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABWxcZGzTlqRg0ZPT6KBYQGBYoN0d28AAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIMhmZpZWxkc5GBYgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABexcZGzTl+Rg0JBUqKBYQKBYoV0aHJlZQAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIBhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIMhmZpZWxkc5GBYwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABaxcZGzTkORg0JBWqKBYQOBYoRmb3VyAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADOyENAuTUFUQ0ggKGY6Rk9PKSwgKGI6QkFSKSwgKGM6QkFaKSBERUxFVEUgZiwgYiwgY6AAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcKOFc3RhdHOhjW5vZGVzLWRlbGV0ZWQD0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_PipelineQueryCloseBeginning.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADayENAoQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gZqKBYoN0d2+BYQEAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADiyENAoQ1JFQVRFIChiOkJBUiB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gYqKBYQKBYoV0aHJlZQAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADeyENAoQ1JFQVRFIChjOkJBWiB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gY6KBYQOBYoRmb3VyAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5GBZgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABexcZGzTskAupGDRk9PooFhAYFig3R3bwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIBhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5GBYgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABmxcZGzTskAv5GDQkFSooFhAoFihXRocmVlAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5GBYwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABaxcZGzTmyRg0JBWqKBYQOBYoRmb3VyAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADOyENAuTUFUQ0ggKGY6Rk9PKSwgKGI6QkFSKSwgKGM6QkFaKSBERUxFVEUgZiwgYiwgY6AAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcKOFc3RhdHOhjW5vZGVzLWRlbGV0ZWQD0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_PipelineQueryCloseMiddle.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADayENAoQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gZqKBYoN0d2+BYQEAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADiyENAoQ1JFQVRFIChiOkJBUiB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gYqKBYQKBYoV0aHJlZQAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADeyENAoQ1JFQVRFIChjOkJBWiB7YToge2F9LCBiOiB7Yn19KSBSRVRVUk4gY6KBYQOBYoRmb3VyAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5GBZgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABWxcZGzTk6Rg0ZPT6KBYQGBYoN0d28AAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5GBYgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABmxcZGzTskAopGDQkFSooFhAoFihXRocmVlAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5GBYwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABixcZGzTskAo5GDQkFaooFhA4FihGZvdXIAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADOyENAuTUFUQ0ggKGY6Rk9PKSwgKGI6QkFSKSwgKGM6QkFaKSBERUxFVEUgZiwgYiwgY6AAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcKOFc3RhdHOhjW5vZGVzLWRlbGV0ZWQD0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_SelectIntLimits.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AO6yENCWUkVUVVJOIHttaW42NH0gYXMgbWluNjQsIHttaW4zMn0gYXMgbWluMzIsIHttaW4xNn0gYXMgbWluMTYsIHttaW44fSBhcyBtaW44LCAtMTYsIHttYXg4fSBhcyBtYXg4LCB7bWF4MTZ9IGFzIG1heDE2LCB7bWF4MzJ9IGFzIG1heDMyLCB7bWF4NjR9IGFzIG1heDY0qIVtaW42NMuAAAAAAAAAAIVtaW4zMsqAAAAAhW1pbjE2yYAAhG1pbjjIgIRtYXg4f4VtYXgxNsl//4VtYXgzMsp/////hW1heDY0y3//////////AAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AFaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIQhmZpZWxkc5mFbWluNjSFbWluMzKFbWluMTaEbWluOIMtMTaEbWF4OIVtYXgxNoVtYXgzMoVtYXg2NAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACmxcZnLgAAAAAAAAADKgAAAAMmAAMiA8H/Jf//Kf////8t//////////wAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgCEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_SelectMany.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AEqyENBFUkVUVVJOIDEsIDM0MjM0LjM0MzIzLCAic3RyaW5nIiwgWzEsICIyIiwgMywgdHJ1ZSwgbnVsbF0sIHRydWUsIG51bGw7oAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AF6xcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXInhmZpZWxkc5aBMYszNDIzNC4zNDMyM4gic3RyaW5nItAXWzEsICIyIiwgMywgdHJ1ZSwgbnVsbF2EdHJ1ZYRudWxsAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AB2xcZYBwUDgt0r7vXsghnN0cmluZ5UBgTIDw8DDwAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgKEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_SelectOne.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AA2yEIlSRVRVUk4gMTugAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIDhmZpZWxkc5GBMQAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AASxcZEBAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgCEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_SingleNode.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AIOyENBYQ1JFQVRFIChmOkZPTyB7YToge2F9LCBiOiB7Yn0sIGM6IHtjfSwgZDoge2R9LCBlOiB7ZX0sIGY6IHtmfSwgZzoge2d9LCBoOiB7aH19KSByZXR1cm4gZqiBaMKBZMCBYYNmb2+BYgGBY8OBZZMBAgOBZsFACzMzMzMzM4Fn/wAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIPhmZpZWxkc5GBZgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AC+xcZGzTkORg0ZPT6eBYYNmb2+BYgGBY8OBZZMBAgOBZsFACzMzMzMzM4Fn/4FowgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFexcKOFc3RhdHOjjGxhYmVscy1hZGRlZAGNbm9kZXMtY3JlYXRlZAGOcHJvcGVydGllcy1zZXQH0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIBhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ABuyENAWTUFUQ0ggKGY6Rk9PKSBERUxFVEUgZqAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcKOFc3RhdHOhjW5vZGVzLWRlbGV0ZWQB0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltStmt_SingleRel.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AKCyENBpQ1JFQVRFIChmOkZPTyktW2I6QkFSIHthOiB7YX0sIGI6IHtifSwgYzoge2N9LCBkOiB7ZH0sIGU6IHtlfSwgZjoge2Z9LCBnOiB7Z30sIGg6IHtofX1dLT4oYzpCQVopIHJldHVybiBiqIFnyn////+BaMKBZMCBYYNmb2+BYst//////////4Fjw4FlkwECA4FmwUALMzMzMzMzAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACaxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIRhmZpZWxkc5GBYgAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ADyxcZG1UlZaX4NCQVKngWGDZm9vgWLLf/////////+BY8OBZZMBAgOBZsFACzMzMzMzM4Fnyn////+BaMIAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AG+xcKOFc3RhdHOkjGxhYmVscy1hZGRlZALQFXJlbGF0aW9uc2hpcHMtY3JlYXRlZAGNbm9kZXMtY3JlYXRlZAKOcHJvcGVydGllcy1zZXQH0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIBhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADKyENAtTUFUQ0ggKGY6Rk9PKS1bYjpCQVJdLT4oYzpCQVopIERFTEVURSBmLCBiLCBjoAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFCxcKOFc3RhdHOijW5vZGVzLWRlbGV0ZWQC0BVyZWxhdGlvbnNoaXBzLWRlbGV0ZWQB0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltTx_Commit.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAmyEIVCRUdJTqAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFOyENBOQ1JFQVRFIChmOkZPTyB7YTogIjEifSktW2I6VE9dLT4oYzpCQVIpPC1bZDpGUk9NXS0oZTpCQVopIFJFVFVSTiBmLCBiLCBjLCBkLCBloAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AC6xcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIChmZpZWxkc5WBZoFigWOBZIFlAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AEKxcZWzTskAmpGDRk9PoYFhgTG1UjjJAJrJALiCVE+gs07JALiRg0JBUqC1UskArSHJALiERlJPTaCzTiGRg0JBWqAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AG+xcKOFc3RhdHOkjGxhYmVscy1hZGRlZAPQFXJlbGF0aW9uc2hpcHMtY3JlYXRlZAKNbm9kZXMtY3JlYXRlZAOOcHJvcGVydGllcy1zZXQB0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAqyEIZDT01NSVSgAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIMhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACaxcKGIYm9va21hcmvQGG5lbzRqOmJvb2ttYXJrOnYxOnR4MjU4NwAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFKyENBNTUFUQ0ggKGY6Rk9PIHthOiAiMSJ9KS1bYjpUT10tPihjOkJBUik8LVtkOkZST01dLShlOkJBWikgUkVUVVJOIGYsIGIsIGMsIGQsIGWgAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AC6xcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXJThmZpZWxkc5WBZoFigWOBZIFlAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AEKxcZWzTskAmpGDRk9PoYFhgTG1UjjJAJrJALiCVE+gs07JALiRg0JBUqC1UskArSHJALiERlJPTaCzTiGRg0JBWqAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgGEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AEmyENBETUFUQ0ggKGY6Rk9PKS1bYjpUT10tPihjOkJBUik8LVtkOkZST01dLShlOkJBWikgREVMRVRFIGYsIGIsIGMsIGQsIGWgAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFCxcKOFc3RhdHOijW5vZGVzLWRlbGV0ZWQD0BVyZWxhdGlvbnNoaXBzLWRlbGV0ZWQC0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWBdwAA","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /recordings/TestBoltTx_Rollback.json: -------------------------------------------------------------------------------- 1 | [{"Event":"YGCwFwAAAAEAAAAAAAAAAAAAAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAAAAQ==","IsWrite":false,"Completed":false,"Error":null},{"Event":"ACSyAdATR29sYW5nTmVvNGpCb2x0LzEuMKGGc2NoZW1lhG5vbmUAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"ABaxcKGGc2VydmVyi05lbzRqLzMuNC42AAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAmyEIVCRUdJTqAAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIAhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFOyENBOQ1JFQVRFIChmOkZPTyB7YTogIjEifSktW2I6VE9dLT4oYzpCQVIpPC1bZDpGUk9NXS0oZTpCQVopIFJFVFVSTiBmLCBiLCBjLCBkLCBloAAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"AC6xcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5WBZoFigWOBZIFlAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"ADixcZWzThSRg0ZPT6GBYYExtVLJAMUUVoJUT6CzTlaRg0JBUqC1UjU7VoRGUk9NoLNOO5GDQkFaoAAA","IsWrite":false,"Completed":true,"Error":null},{"Event":"AG+xcKOFc3RhdHOkjGxhYmVscy1hZGRlZAPQFXJlbGF0aW9uc2hpcHMtY3JlYXRlZAKNbm9kZXMtY3JlYXRlZAOOcHJvcGVydGllcy1zZXQB0BVyZXN1bHRfY29uc3VtZWRfYWZ0ZXIAhHR5cGWCcncAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAyyEIhST0xMQkFDS6AAAA==","IsWrite":true,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACSxcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIAhmZpZWxkc5AAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAOxcKAAAA==","IsWrite":false,"Completed":true,"Error":null},{"Event":"AFKyENBNTUFUQ0ggKGY6Rk9PIHthOiAiMSJ9KS1bYjpUT10tPihjOkJBUik8LVtkOkZST01dLShlOkJBWikgUkVUVVJOIGYsIGIsIGMsIGQsIGWgAAA=","IsWrite":true,"Completed":true,"Error":null},{"Event":"AC6xcKLQFnJlc3VsdF9hdmFpbGFibGVfYWZ0ZXIBhmZpZWxkc5WBZoFigWOBZIFlAAA=","IsWrite":false,"Completed":true,"Error":null},{"Event":"AAKwPwAA","IsWrite":true,"Completed":true,"Error":null},{"Event":"ACKxcKLQFXJlc3VsdF9jb25zdW1lZF9hZnRlcgCEdHlwZYFyAAA=","IsWrite":false,"Completed":true,"Error":null}] 2 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package golangNeo4jBoltDriver 2 | 3 | import "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 4 | 5 | // Result represents a result from a query that returns no data 6 | type Result interface { 7 | // LastInsertId Always returns -1. This is necessary 8 | // to meet the sql.driver interface 9 | LastInsertId() (int64, error) 10 | // RowsAffected returns the number of rows affected 11 | // This doesn't currently support updates, only 12 | // inserts/deletions 13 | RowsAffected() (int64, error) 14 | // Metadata returns the metadata response from neo4j 15 | Metadata() map[string]interface{} 16 | } 17 | 18 | type boltResult struct { 19 | metadata map[string]interface{} 20 | } 21 | 22 | func newResult(metadata map[string]interface{}) boltResult { 23 | return boltResult{metadata: metadata} 24 | } 25 | 26 | // Returns the response metadata from the bolt success message 27 | func (r boltResult) Metadata() map[string]interface{} { 28 | return r.metadata 29 | } 30 | 31 | // LastInsertId gets the last inserted id. This will always return -1. 32 | func (r boltResult) LastInsertId() (int64, error) { 33 | // TODO: Is this possible? 34 | return -1, nil 35 | } 36 | 37 | // RowsAffected returns the number of nodes+rels created/deleted. For reasons of limitations 38 | // on the API, we cannot tell how many nodes+rels were updated, only how many properties were 39 | // updated. If this changes in the future, number updated will be added to the output of this 40 | // interface. 41 | func (r boltResult) RowsAffected() (int64, error) { 42 | // metadata omits stats when rowsAffected == 0, check existence to prevent panic 43 | if _, ok := r.metadata["stats"]; !ok { 44 | return 0, nil 45 | } 46 | 47 | stats, ok := r.metadata["stats"].(map[string]interface{}) 48 | if !ok { 49 | return -1, errors.New("Unrecognized type for stats metadata: %#v", r.metadata) 50 | } 51 | 52 | var rowsAffected int64 53 | nodesCreated, ok := stats["nodes-created"] 54 | if ok { 55 | rowsAffected += nodesCreated.(int64) 56 | } 57 | 58 | relsCreated, ok := stats["relationships-created"] 59 | if ok { 60 | rowsAffected += relsCreated.(int64) 61 | } 62 | 63 | nodesDeleted, ok := stats["nodes-deleted"] 64 | if ok { 65 | rowsAffected += nodesDeleted.(int64) 66 | } 67 | 68 | relsDeleted, ok := stats["relationships-deleted"] 69 | if ok { 70 | rowsAffected += relsDeleted.(int64) 71 | } 72 | 73 | return rowsAffected, nil 74 | } 75 | -------------------------------------------------------------------------------- /rows.go: -------------------------------------------------------------------------------- 1 | package golangNeo4jBoltDriver 2 | 3 | import ( 4 | "database/sql/driver" 5 | "io" 6 | 7 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/encoding" 8 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 9 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/log" 10 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/graph" 11 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/messages" 12 | ) 13 | 14 | // Rows represents results of rows from the DB 15 | // 16 | // Row objects ARE NOT THREAD SAFE. 17 | // If you want to use multiple go routines with these objects, 18 | // you should use a driver to create a new conn for each routine. 19 | type Rows interface { 20 | // Columns Gets the names of the columns in the returned dataset 21 | Columns() []string 22 | // Metadata Gets all of the metadata returned from Neo on query start 23 | Metadata() map[string]interface{} 24 | // Close the rows, flushing any existing datastream 25 | Close() error 26 | // NextNeo gets the next row result 27 | // When the rows are completed, returns the success metadata 28 | // and io.EOF 29 | NextNeo() ([]interface{}, map[string]interface{}, error) 30 | // All gets all of the results from the row set. It's recommended to use NextNeo when 31 | // there are a lot of rows 32 | All() ([][]interface{}, map[string]interface{}, error) 33 | } 34 | 35 | // PipelineRows represents results of a set of rows from the DB 36 | // when running a pipeline statement. 37 | // 38 | // Row objects ARE NOT THREAD SAFE. 39 | // If you want to use multiple go routines with these objects, 40 | // you should use a driver to create a new conn for each routine. 41 | type PipelineRows interface { 42 | // Columns Gets the names of the columns in the returned dataset 43 | Columns() []string 44 | // Metadata Gets all of the metadata returned from Neo on query start 45 | Metadata() map[string]interface{} 46 | // Close the rows, flushing any existing datastream 47 | Close() error 48 | // NextPipeline gets the next row result 49 | // When the rows are completed, returns the success metadata and the next 50 | // set of rows. 51 | // When all rows are completed, returns io.EOF 52 | NextPipeline() ([]interface{}, map[string]interface{}, PipelineRows, error) 53 | } 54 | 55 | type boltRows struct { 56 | metadata map[string]interface{} 57 | statement *boltStmt 58 | closed bool 59 | consumed bool 60 | finishedConsume bool 61 | pipelineIndex int 62 | closeStatement bool 63 | } 64 | 65 | func newRows(statement *boltStmt, metadata map[string]interface{}) *boltRows { 66 | return &boltRows{ 67 | statement: statement, 68 | metadata: metadata, 69 | } 70 | } 71 | 72 | func newQueryRows(statement *boltStmt, metadata map[string]interface{}) *boltRows { 73 | rows := newRows(statement, metadata) 74 | rows.consumed = true // Already consumed from pipeline with PULL_ALL 75 | rows.closeStatement = true // Query rows don't expose a statement, so they need to close the statement when they close 76 | return rows 77 | } 78 | 79 | func newPipelineRows(statement *boltStmt, metadata map[string]interface{}, pipelineIndex int) *boltRows { 80 | rows := newRows(statement, metadata) 81 | rows.consumed = true // Already consumed from pipeline with PULL_ALL 82 | rows.pipelineIndex = pipelineIndex 83 | return rows 84 | } 85 | 86 | func newQueryPipelineRows(statement *boltStmt, metadata map[string]interface{}, pipelineIndex int) *boltRows { 87 | rows := newPipelineRows(statement, metadata, pipelineIndex) 88 | rows.closeStatement = true // Query rows don't expose a statement, so they need to close the statement when they close 89 | return rows 90 | } 91 | 92 | // Columns returns the columns from the result 93 | func (r *boltRows) Columns() []string { 94 | fieldsInt, ok := r.metadata["fields"] 95 | if !ok { 96 | return []string{} 97 | } 98 | 99 | fields, ok := fieldsInt.([]interface{}) 100 | if !ok { 101 | log.Errorf("Unrecognized fields from success message: %#v", fieldsInt) 102 | return []string{} 103 | } 104 | 105 | fieldsStr := make([]string, len(fields)) 106 | for i, f := range fields { 107 | if fieldsStr[i], ok = f.(string); !ok { 108 | log.Errorf("Unrecognized fields from success message: %#v", fieldsInt) 109 | return []string{} 110 | } 111 | } 112 | return fieldsStr 113 | } 114 | 115 | // Metadata Gets all of the metadata returned from Neo on query start 116 | func (r *boltRows) Metadata() map[string]interface{} { 117 | return r.metadata 118 | } 119 | 120 | // Close closes the rows 121 | func (r *boltRows) Close() error { 122 | if r.closed { 123 | return nil 124 | } 125 | 126 | if !r.consumed { 127 | // Discard all messages if not consumed 128 | respInt, err := r.statement.conn.sendDiscardAllConsume() 129 | if err != nil { 130 | return errors.Wrap(err, "An error occurred discarding messages on row close") 131 | } 132 | 133 | switch resp := respInt.(type) { 134 | case messages.SuccessMessage: 135 | log.Infof("Got success message: %#v", resp) 136 | default: 137 | return errors.New("Unrecognized response type discarding all rows: Value: %#v", resp) 138 | } 139 | 140 | } else if !r.finishedConsume { 141 | // If this is a pipeline statement, we need to "consume all" multiple times 142 | numConsume := 1 143 | if r.statement.queries != nil { 144 | numQueries := len(r.statement.queries) 145 | if numQueries > 0 { 146 | // So, every pipeline statement has two successes 147 | // but by the time you get to the row object, one has 148 | // been consumed. Hence we need to clear out the 149 | // rest of the messages on close by taking the current 150 | // index * 2 but removing the first success 151 | numConsume = ((numQueries - r.pipelineIndex) * 2) - 1 152 | } 153 | } 154 | 155 | // Clear out all unconsumed messages if we 156 | // never finished consuming them. 157 | _, _, err := r.statement.conn.consumeAllMultiple(numConsume) 158 | if err != nil { 159 | return errors.Wrap(err, "An error occurred clearing out unconsumed stream") 160 | } 161 | } 162 | 163 | r.closed = true 164 | r.statement.rows = nil 165 | 166 | if r.closeStatement { 167 | return r.statement.Close() 168 | } 169 | return nil 170 | } 171 | 172 | // Next gets the next row result 173 | func (r *boltRows) Next(dest []driver.Value) error { 174 | data, _, err := r.NextNeo() 175 | if err != nil { 176 | return err 177 | } 178 | 179 | for i, item := range data { 180 | switch item := item.(type) { 181 | case []interface{}, map[string]interface{}, graph.Node, graph.Path, graph.Relationship, graph.UnboundRelationship: 182 | dest[i], err = encoding.Marshal(item) 183 | if err != nil { 184 | return err 185 | } 186 | default: 187 | dest[i], err = driver.DefaultParameterConverter.ConvertValue(item) 188 | if err != nil { 189 | return err 190 | } 191 | } 192 | } 193 | 194 | return nil 195 | 196 | } 197 | 198 | // NextNeo gets the next row result 199 | // When the rows are completed, returns the success metadata 200 | // and io.EOF 201 | func (r *boltRows) NextNeo() ([]interface{}, map[string]interface{}, error) { 202 | if r.closed { 203 | return nil, nil, errors.New("Rows are already closed") 204 | } 205 | 206 | if !r.consumed { 207 | r.consumed = true 208 | if err := r.statement.conn.sendPullAll(); err != nil { 209 | r.finishedConsume = true 210 | return nil, nil, err 211 | } 212 | } 213 | 214 | respInt, err := r.statement.conn.consume() 215 | if err != nil { 216 | return nil, nil, err 217 | } 218 | 219 | switch resp := respInt.(type) { 220 | case messages.SuccessMessage: 221 | log.Infof("Got success message: %#v", resp) 222 | r.finishedConsume = true 223 | return nil, resp.Metadata, io.EOF 224 | case messages.RecordMessage: 225 | log.Infof("Got record message: %#v", resp) 226 | return resp.Fields, nil, nil 227 | default: 228 | return nil, nil, errors.New("Unrecognized response type getting next query row: %#v", resp) 229 | } 230 | } 231 | 232 | func (r *boltRows) All() ([][]interface{}, map[string]interface{}, error) { 233 | output := [][]interface{}{} 234 | for { 235 | row, metadata, err := r.NextNeo() 236 | if err != nil || row == nil { 237 | if err == io.EOF { 238 | return output, metadata, nil 239 | } 240 | return output, metadata, err 241 | } 242 | output = append(output, row) 243 | } 244 | } 245 | 246 | // NextPipeline gets the next row result 247 | // When the rows are completed, returns the success metadata and the next 248 | // set of rows. 249 | // When all rows are completed, returns io.EOF 250 | func (r *boltRows) NextPipeline() ([]interface{}, map[string]interface{}, PipelineRows, error) { 251 | if r.closed { 252 | return nil, nil, nil, errors.New("Rows are already closed") 253 | } 254 | 255 | respInt, err := r.statement.conn.consume() 256 | if err != nil { 257 | return nil, nil, nil, err 258 | } 259 | 260 | switch resp := respInt.(type) { 261 | case messages.SuccessMessage: 262 | log.Infof("Got success message: %#v", resp) 263 | 264 | if r.pipelineIndex == len(r.statement.queries)-1 { 265 | r.finishedConsume = true 266 | return nil, nil, nil, err 267 | } 268 | 269 | successResp, err := r.statement.conn.consume() 270 | if err != nil && err != io.EOF { 271 | return nil, nil, nil, errors.Wrap(err, "An error occurred getting next set of rows from pipeline command: %#v", successResp) 272 | } 273 | 274 | success, ok := successResp.(messages.SuccessMessage) 275 | if !ok { 276 | return nil, nil, nil, errors.New("Unexpected response getting next set of rows from pipeline command: %#v", successResp) 277 | } 278 | 279 | r.statement.rows = newPipelineRows(r.statement, success.Metadata, r.pipelineIndex+1) 280 | r.statement.rows.closeStatement = r.closeStatement 281 | return nil, success.Metadata, r.statement.rows, nil 282 | 283 | case messages.RecordMessage: 284 | log.Infof("Got record message: %#v", resp) 285 | return resp.Fields, nil, nil, nil 286 | default: 287 | return nil, nil, nil, errors.New("Unrecognized response type getting next pipeline row: %#v", resp) 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /scripts/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | #if git rev-parse --verify HEAD >/dev/null 2>&1 11 | #then 12 | #against=HEAD 13 | #else 14 | ## Initial commit: diff against an empty tree object 15 | #against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 16 | #fi 17 | 18 | ## If you want to allow non-ASCII filenames set this variable to true. 19 | #allownonascii=$(git config --bool hooks.allownonascii) 20 | 21 | ## Redirect output to stderr. 22 | #exec 1>&2 23 | 24 | ## Cross platform projects tend to avoid non-ASCII filenames; prevent 25 | ## them from being added to the repository. We exploit the fact that the 26 | ## printable range starts at the space character and ends with tilde. 27 | #if [ "$allownonascii" != "true" ] && 28 | ## Note that the use of brackets around a tr range is ok here, (it's 29 | ## even required, for portability to Solaris 10's /usr/bin/tr), since 30 | ## the square bracket bytes happen to fall in the designated range. 31 | #test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | #LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | #then 34 | #cat <<\EOF 35 | #Error: Attempt to add a non-ASCII file name. 36 | 37 | #This can cause problems if you want to work with people on other platforms. 38 | 39 | #To be portable it is advisable to rename the file. 40 | 41 | #If you know what you are doing you can disable this check using: 42 | 43 | #git config hooks.allownonascii true 44 | #EOF 45 | #exit 1 46 | #fi 47 | 48 | ## If there are whitespace errors, print the offending file names and fail. 49 | #exec git diff-index --check --cached $against -- 50 | 51 | go build || { 52 | echo "build failed" 53 | exit 1; 54 | } 55 | 56 | go fmt ./... || { 57 | echo "fmt failed" 58 | exit 1; 59 | } 60 | 61 | go vet ./... || { 62 | echo "vet failed" 63 | exit 1; 64 | } 65 | 66 | golint -set_exit_status ./... || { 67 | echo "lint failed" 68 | exit 1; 69 | } 70 | 71 | echo "PreCommit Success" 72 | exit 0 73 | -------------------------------------------------------------------------------- /scripts/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BOLT_DRIVER_LOG=error NEO4J_BOLT=bolt://neo4j:neo4j1@localhost:7687 go test -v || { 4 | echo "tests failed" 5 | exit 1; 6 | } 7 | 8 | echo "PrePush Success" 9 | exit 0 10 | -------------------------------------------------------------------------------- /stmt.go: -------------------------------------------------------------------------------- 1 | package golangNeo4jBoltDriver 2 | 3 | import ( 4 | "database/sql/driver" 5 | 6 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 7 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/log" 8 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/messages" 9 | ) 10 | 11 | // Stmt represents a statement to run against the database 12 | // 13 | // Stmt objects, and any rows prepared within ARE NOT 14 | // THREAD SAFE. If you want to use multiple go routines with these objects, 15 | // you should use a driver to create a new conn for each routine. 16 | type Stmt interface { 17 | // Close Closes the statement. See sql/driver.Stmt. 18 | Close() error 19 | // ExecNeo executes a query that returns no rows. Implements a Neo-friendly alternative to sql/driver. 20 | ExecNeo(params map[string]interface{}) (Result, error) 21 | // QueryNeo executes a query that returns data. Implements a Neo-friendly alternative to sql/driver. 22 | QueryNeo(params map[string]interface{}) (Rows, error) 23 | } 24 | 25 | // PipelineStmt represents a set of statements to run against the database 26 | // 27 | // PipelineStmt objects, and any rows prepared within ARE NOT 28 | // THREAD SAFE. If you want to use multiple go routines with these objects, 29 | // you should use a driver to create a new conn for each routine. 30 | type PipelineStmt interface { 31 | // Close Closes the statement. See sql/driver.Stmt. 32 | Close() error 33 | // ExecPipeline executes a set of queries that returns no rows. 34 | ExecPipeline(params ...map[string]interface{}) ([]Result, error) 35 | // QueryPipeline executes a set of queries that return data. 36 | // Implements a Neo-friendly alternative to sql/driver. 37 | QueryPipeline(params ...map[string]interface{}) (PipelineRows, error) 38 | } 39 | 40 | type boltStmt struct { 41 | queries []string 42 | query string 43 | conn *boltConn 44 | closed bool 45 | rows *boltRows 46 | } 47 | 48 | func newStmt(query string, conn *boltConn) *boltStmt { 49 | return &boltStmt{query: query, conn: conn} 50 | } 51 | 52 | func newPipelineStmt(queries []string, conn *boltConn) *boltStmt { 53 | return &boltStmt{queries: queries, conn: conn} 54 | } 55 | 56 | // Close Closes the statement. See sql/driver.Stmt. 57 | func (s *boltStmt) Close() error { 58 | if s.closed { 59 | return nil 60 | } 61 | 62 | if s.rows != nil && !s.rows.closeStatement { 63 | if err := s.rows.Close(); err != nil { 64 | return err 65 | } 66 | } 67 | 68 | s.closed = true 69 | s.conn.statement = nil 70 | s.conn = nil 71 | return nil 72 | } 73 | 74 | // NumInput returns the number of placeholder parameters. See sql/driver.Stmt. 75 | // Currently will always return -1 76 | func (s *boltStmt) NumInput() int { 77 | return -1 // TODO: would need a cypher parser for this. disable for now 78 | } 79 | 80 | // Exec executes a query that returns no rows. See sql/driver.Stmt. 81 | // You must bolt encode a map to pass as []bytes for the driver value 82 | func (s *boltStmt) Exec(args []driver.Value) (driver.Result, error) { 83 | params, err := driverArgsToMap(args) 84 | if err != nil { 85 | return nil, err 86 | } 87 | return s.ExecNeo(params) 88 | } 89 | 90 | // ExecNeo executes a query that returns no rows. Implements a Neo-friendly alternative to sql/driver. 91 | func (s *boltStmt) ExecNeo(params map[string]interface{}) (Result, error) { 92 | if s.closed { 93 | return nil, errors.New("Neo4j Bolt statement already closed") 94 | } 95 | if s.rows != nil { 96 | return nil, errors.New("Another query is already open") 97 | } 98 | 99 | runResp, pullResp, _, err := s.conn.sendRunPullAllConsumeAll(s.query, params) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | success, ok := runResp.(messages.SuccessMessage) 105 | if !ok { 106 | return nil, errors.New("Unrecognized response type when running exec query: %#v", success) 107 | 108 | } 109 | 110 | log.Infof("Got run success message: %#v", success) 111 | 112 | success, ok = pullResp.(messages.SuccessMessage) 113 | if !ok { 114 | return nil, errors.New("Unrecognized response when discarding exec rows: %#v", success) 115 | } 116 | 117 | log.Infof("Got discard all success message: %#v", success) 118 | 119 | return newResult(success.Metadata), nil 120 | } 121 | 122 | func (s *boltStmt) ExecPipeline(params ...map[string]interface{}) ([]Result, error) { 123 | if s.closed { 124 | return nil, errors.New("Neo4j Bolt statement already closed") 125 | } 126 | if s.rows != nil { 127 | return nil, errors.New("Another query is already open") 128 | } 129 | 130 | if len(params) != len(s.queries) { 131 | return nil, errors.New("Must pass same number of params as there are queries") 132 | } 133 | 134 | for i, query := range s.queries { 135 | err := s.conn.sendRunPullAll(query, params[i]) 136 | if err != nil { 137 | return nil, errors.Wrap(err, "Error running exec query:\n\n%s\n\nWith Params:\n%#v", query, params[i]) 138 | } 139 | } 140 | 141 | log.Info("Successfully ran all pipeline queries") 142 | 143 | results := make([]Result, len(s.queries)) 144 | for i := range s.queries { 145 | runResp, err := s.conn.consume() 146 | if err != nil { 147 | return nil, errors.Wrap(err, "An error occurred getting result of exec command: %#v", runResp) 148 | } 149 | 150 | success, ok := runResp.(messages.SuccessMessage) 151 | if !ok { 152 | return nil, errors.New("Unexpected response when getting exec query result: %#v", runResp) 153 | } 154 | 155 | _, pullResp, err := s.conn.consumeAll() 156 | if err != nil { 157 | return nil, errors.Wrap(err, "An error occurred getting result of exec discard command: %#v", pullResp) 158 | } 159 | 160 | success, ok = pullResp.(messages.SuccessMessage) 161 | if !ok { 162 | return nil, errors.New("Unexpected response when getting exec query discard result: %#v", pullResp) 163 | } 164 | 165 | results[i] = newResult(success.Metadata) 166 | 167 | } 168 | 169 | return results, nil 170 | } 171 | 172 | // Query executes a query that returns data. See sql/driver.Stmt. 173 | // You must bolt encode a map to pass as []bytes for the driver value 174 | func (s *boltStmt) Query(args []driver.Value) (driver.Rows, error) { 175 | params, err := driverArgsToMap(args) 176 | if err != nil { 177 | return nil, err 178 | } 179 | return s.queryNeo(params) 180 | } 181 | 182 | // QueryNeo executes a query that returns data. Implements a Neo-friendly alternative to sql/driver. 183 | func (s *boltStmt) QueryNeo(params map[string]interface{}) (Rows, error) { 184 | return s.queryNeo(params) 185 | } 186 | 187 | func (s *boltStmt) queryNeo(params map[string]interface{}) (*boltRows, error) { 188 | if s.closed { 189 | return nil, errors.New("Neo4j Bolt statement already closed") 190 | } 191 | if s.rows != nil { 192 | return nil, errors.New("Another query is already open") 193 | } 194 | 195 | respInt, err := s.conn.sendRunConsume(s.query, params) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | resp, ok := respInt.(messages.SuccessMessage) 201 | if !ok { 202 | return nil, errors.New("Unrecognized response type running query: %#v", resp) 203 | } 204 | 205 | log.Infof("Got success message on run query: %#v", resp) 206 | s.rows = newRows(s, resp.Metadata) 207 | return s.rows, nil 208 | } 209 | 210 | func (s *boltStmt) QueryPipeline(params ...map[string]interface{}) (PipelineRows, error) { 211 | if s.closed { 212 | return nil, errors.New("Neo4j Bolt statement already closed") 213 | } 214 | if s.rows != nil { 215 | return nil, errors.New("Another query is already open") 216 | } 217 | 218 | if len(params) != len(s.queries) { 219 | return nil, errors.New("Must pass same number of params as there are queries") 220 | } 221 | 222 | for i, query := range s.queries { 223 | err := s.conn.sendRunPullAll(query, params[i]) 224 | if err != nil { 225 | return nil, errors.Wrap(err, "Error running query:\n\n%s\n\nWith Params:\n%#v", query, params[i]) 226 | } 227 | } 228 | 229 | log.Info("Successfully ran all pipeline queries") 230 | 231 | resp, err := s.conn.consume() 232 | if err != nil { 233 | return nil, errors.Wrap(err, "An error occurred consuming initial pipeline command") 234 | } 235 | 236 | success, ok := resp.(messages.SuccessMessage) 237 | if !ok { 238 | return nil, errors.New("Got unexpected return message when consuming initial pipeline command: %#v", resp) 239 | } 240 | 241 | s.rows = newPipelineRows(s, success.Metadata, 0) 242 | return s.rows, nil 243 | } 244 | -------------------------------------------------------------------------------- /structures/doc.go: -------------------------------------------------------------------------------- 1 | /*Package structures contains various structures which are used by the Bolt protocol*/ 2 | package structures 3 | -------------------------------------------------------------------------------- /structures/graph/doc.go: -------------------------------------------------------------------------------- 1 | /*Package graph contains structs that can be returned from the Neo4j Graph*/ 2 | package graph 3 | -------------------------------------------------------------------------------- /structures/graph/node.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | const ( 4 | // NodeSignature is the signature byte for a Node object 5 | NodeSignature = 0x4E 6 | ) 7 | 8 | // Node Represents a Node structure 9 | type Node struct { 10 | NodeIdentity int64 11 | Labels []string 12 | Properties map[string]interface{} 13 | } 14 | 15 | // Signature gets the signature byte for the struct 16 | func (n Node) Signature() int { 17 | return NodeSignature 18 | } 19 | 20 | // AllFields gets the fields to encode for the struct 21 | func (n Node) AllFields() []interface{} { 22 | labels := make([]interface{}, len(n.Labels)) 23 | for i, label := range n.Labels { 24 | labels[i] = label 25 | } 26 | return []interface{}{n.NodeIdentity, labels, n.Properties} 27 | } 28 | -------------------------------------------------------------------------------- /structures/graph/path.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | const ( 4 | // PathSignature is the signature byte for a Path object 5 | PathSignature = 0x50 6 | ) 7 | 8 | // Path Represents a Path structure 9 | type Path struct { 10 | Nodes []Node 11 | Relationships []UnboundRelationship 12 | Sequence []int 13 | } 14 | 15 | // Signature gets the signature byte for the struct 16 | func (p Path) Signature() int { 17 | return PathSignature 18 | } 19 | 20 | // AllFields gets the fields to encode for the struct 21 | func (p Path) AllFields() []interface{} { 22 | nodes := make([]interface{}, len(p.Nodes)) 23 | for i, node := range p.Nodes { 24 | nodes[i] = node 25 | } 26 | relationships := make([]interface{}, len(p.Relationships)) 27 | for i, relationship := range p.Relationships { 28 | relationships[i] = relationship 29 | } 30 | sequences := make([]interface{}, len(p.Sequence)) 31 | for i, sequence := range p.Sequence { 32 | sequences[i] = sequence 33 | } 34 | return []interface{}{nodes, relationships, sequences} 35 | } 36 | -------------------------------------------------------------------------------- /structures/graph/relationship.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | const ( 4 | // RelationshipSignature is the signature byte for a Relationship object 5 | RelationshipSignature = 0x52 6 | ) 7 | 8 | // Relationship Represents a Relationship structure 9 | type Relationship struct { 10 | RelIdentity int64 11 | StartNodeIdentity int64 12 | EndNodeIdentity int64 13 | Type string 14 | Properties map[string]interface{} 15 | } 16 | 17 | // Signature gets the signature byte for the struct 18 | func (r Relationship) Signature() int { 19 | return RelationshipSignature 20 | } 21 | 22 | // AllFields gets the fields to encode for the struct 23 | func (r Relationship) AllFields() []interface{} { 24 | return []interface{}{r.RelIdentity, r.StartNodeIdentity, r.EndNodeIdentity, r.Type, r.Properties} 25 | } 26 | -------------------------------------------------------------------------------- /structures/graph/unbound_relationship.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | const ( 4 | // UnboundRelationshipSignature is the signature byte for a UnboundRelationship object 5 | UnboundRelationshipSignature = 0x72 6 | ) 7 | 8 | // UnboundRelationship Represents a UnboundRelationship structure 9 | type UnboundRelationship struct { 10 | RelIdentity int64 11 | Type string 12 | Properties map[string]interface{} 13 | } 14 | 15 | // Signature gets the signature byte for the struct 16 | func (r UnboundRelationship) Signature() int { 17 | return UnboundRelationshipSignature 18 | } 19 | 20 | // AllFields gets the fields to encode for the struct 21 | func (r UnboundRelationship) AllFields() []interface{} { 22 | return []interface{}{r.RelIdentity, r.Type, r.Properties} 23 | } 24 | -------------------------------------------------------------------------------- /structures/messages/ack_failure.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | const ( 4 | // AckFailureMessageSignature is the signature byte for the ACK_FAILURE message 5 | AckFailureMessageSignature = 0x0E 6 | ) 7 | 8 | // AckFailureMessage Represents an ACK_FAILURE message 9 | type AckFailureMessage struct{} 10 | 11 | // NewAckFailureMessage Gets a new AckFailureMessage struct 12 | func NewAckFailureMessage() AckFailureMessage { 13 | return AckFailureMessage{} 14 | } 15 | 16 | // Signature gets the signature byte for the struct 17 | func (i AckFailureMessage) Signature() int { 18 | return AckFailureMessageSignature 19 | } 20 | 21 | // AllFields gets the fields to encode for the struct 22 | func (i AckFailureMessage) AllFields() []interface{} { 23 | return []interface{}{} 24 | } 25 | -------------------------------------------------------------------------------- /structures/messages/discard_all.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | const ( 4 | // DiscardAllMessageSignature is the signature byte for the DISCARD_ALL message 5 | DiscardAllMessageSignature = 0x2F 6 | ) 7 | 8 | // DiscardAllMessage Represents an DISCARD_ALL message 9 | type DiscardAllMessage struct{} 10 | 11 | // NewDiscardAllMessage Gets a new DiscardAllMessage struct 12 | func NewDiscardAllMessage() DiscardAllMessage { 13 | return DiscardAllMessage{} 14 | } 15 | 16 | // Signature gets the signature byte for the struct 17 | func (i DiscardAllMessage) Signature() int { 18 | return DiscardAllMessageSignature 19 | } 20 | 21 | // AllFields gets the fields to encode for the struct 22 | func (i DiscardAllMessage) AllFields() []interface{} { 23 | return []interface{}{} 24 | } 25 | -------------------------------------------------------------------------------- /structures/messages/doc.go: -------------------------------------------------------------------------------- 1 | /*Package messages contains structs that represent the messages that get sent using the Bolt protocol*/ 2 | package messages 3 | -------------------------------------------------------------------------------- /structures/messages/failure.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | import "fmt" 4 | 5 | const ( 6 | // FailureMessageSignature is the signature byte for the FAILURE message 7 | FailureMessageSignature = 0x7F 8 | ) 9 | 10 | // FailureMessage Represents an FAILURE message 11 | type FailureMessage struct { 12 | Metadata map[string]interface{} 13 | } 14 | 15 | // NewFailureMessage Gets a new FailureMessage struct 16 | func NewFailureMessage(metadata map[string]interface{}) FailureMessage { 17 | return FailureMessage{ 18 | Metadata: metadata, 19 | } 20 | } 21 | 22 | // Signature gets the signature byte for the struct 23 | func (i FailureMessage) Signature() int { 24 | return FailureMessageSignature 25 | } 26 | 27 | // AllFields gets the fields to encode for the struct 28 | func (i FailureMessage) AllFields() []interface{} { 29 | return []interface{}{i.Metadata} 30 | } 31 | 32 | // Error is the implementation of the Golang error interface so a failure message 33 | // can be treated like a normal error 34 | func (i FailureMessage) Error() string { 35 | return fmt.Sprintf("%#v", i) 36 | } 37 | -------------------------------------------------------------------------------- /structures/messages/ignored.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | const ( 4 | // IgnoredMessageSignature is the signature byte for the IGNORED message 5 | IgnoredMessageSignature = 0x7E 6 | ) 7 | 8 | // IgnoredMessage Represents an IGNORED message 9 | type IgnoredMessage struct{} 10 | 11 | // NewIgnoredMessage Gets a new IgnoredMessage struct 12 | func NewIgnoredMessage() IgnoredMessage { 13 | return IgnoredMessage{} 14 | } 15 | 16 | // Signature gets the signature byte for the struct 17 | func (i IgnoredMessage) Signature() int { 18 | return IgnoredMessageSignature 19 | } 20 | 21 | // AllFields gets the fields to encode for the struct 22 | func (i IgnoredMessage) AllFields() []interface{} { 23 | return []interface{}{} 24 | } 25 | -------------------------------------------------------------------------------- /structures/messages/init.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | const ( 4 | // InitMessageSignature is the signature byte for the INIT message 5 | InitMessageSignature = 0x01 6 | ) 7 | 8 | // InitMessage Represents an INIT message 9 | type InitMessage struct { 10 | clientName string 11 | authToken map[string]interface{} 12 | } 13 | 14 | // NewInitMessage Gets a new InitMessage struct 15 | func NewInitMessage(clientName string, user string, password string) InitMessage { 16 | var authToken map[string]interface{} 17 | if user == "" { 18 | authToken = map[string]interface{}{ 19 | "scheme": "none", 20 | } 21 | } else { 22 | authToken = map[string]interface{}{ 23 | "scheme": "basic", 24 | "principal": user, 25 | "credentials": password, 26 | } 27 | } 28 | 29 | return InitMessage{ 30 | clientName: clientName, 31 | authToken: authToken, 32 | } 33 | } 34 | 35 | // Signature gets the signature byte for the struct 36 | func (i InitMessage) Signature() int { 37 | return InitMessageSignature 38 | } 39 | 40 | // AllFields gets the fields to encode for the struct 41 | func (i InitMessage) AllFields() []interface{} { 42 | return []interface{}{i.clientName, i.authToken} 43 | } 44 | -------------------------------------------------------------------------------- /structures/messages/pull_all.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | const ( 4 | // PullAllMessageSignature is the signature byte for the PULL_ALL message 5 | PullAllMessageSignature = 0x3F 6 | ) 7 | 8 | // PullAllMessage Represents an PULL_ALL message 9 | type PullAllMessage struct{} 10 | 11 | // NewPullAllMessage Gets a new PullAllMessage struct 12 | func NewPullAllMessage() PullAllMessage { 13 | return PullAllMessage{} 14 | } 15 | 16 | // Signature gets the signature byte for the struct 17 | func (i PullAllMessage) Signature() int { 18 | return PullAllMessageSignature 19 | } 20 | 21 | // AllFields gets the fields to encode for the struct 22 | func (i PullAllMessage) AllFields() []interface{} { 23 | return []interface{}{} 24 | } 25 | -------------------------------------------------------------------------------- /structures/messages/record.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | const ( 4 | // RecordMessageSignature is the signature byte for the RECORD message 5 | RecordMessageSignature = 0x71 6 | ) 7 | 8 | // RecordMessage Represents an RECORD message 9 | type RecordMessage struct { 10 | Fields []interface{} 11 | } 12 | 13 | // NewRecordMessage Gets a new RecordMessage struct 14 | func NewRecordMessage(fields []interface{}) RecordMessage { 15 | return RecordMessage{ 16 | Fields: fields, 17 | } 18 | } 19 | 20 | // Signature gets the signature byte for the struct 21 | func (i RecordMessage) Signature() int { 22 | return RecordMessageSignature 23 | } 24 | 25 | // AllFields gets the fields to encode for the struct 26 | func (i RecordMessage) AllFields() []interface{} { 27 | return []interface{}{i.Fields} 28 | } 29 | -------------------------------------------------------------------------------- /structures/messages/reset.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | const ( 4 | // ResetMessageSignature is the signature byte for the RESET message 5 | ResetMessageSignature = 0x0F 6 | ) 7 | 8 | // ResetMessage Represents an RESET message 9 | type ResetMessage struct{} 10 | 11 | // NewResetMessage Gets a new ResetMessage struct 12 | func NewResetMessage() ResetMessage { 13 | return ResetMessage{} 14 | } 15 | 16 | // Signature gets the signature byte for the struct 17 | func (i ResetMessage) Signature() int { 18 | return ResetMessageSignature 19 | } 20 | 21 | // AllFields gets the fields to encode for the struct 22 | func (i ResetMessage) AllFields() []interface{} { 23 | return []interface{}{} 24 | } 25 | -------------------------------------------------------------------------------- /structures/messages/run.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | const ( 4 | // RunMessageSignature is the signature byte for the RUN message 5 | RunMessageSignature = 0x10 6 | ) 7 | 8 | // RunMessage Represents an RUN message 9 | type RunMessage struct { 10 | statement string 11 | parameters map[string]interface{} 12 | } 13 | 14 | // NewRunMessage Gets a new RunMessage struct 15 | func NewRunMessage(statement string, parameters map[string]interface{}) RunMessage { 16 | return RunMessage{ 17 | statement: statement, 18 | parameters: parameters, 19 | } 20 | } 21 | 22 | // Signature gets the signature byte for the struct 23 | func (i RunMessage) Signature() int { 24 | return RunMessageSignature 25 | } 26 | 27 | // AllFields gets the fields to encode for the struct 28 | func (i RunMessage) AllFields() []interface{} { 29 | return []interface{}{i.statement, i.parameters} 30 | } 31 | -------------------------------------------------------------------------------- /structures/messages/success.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | const ( 4 | // SuccessMessageSignature is the signature byte for the SUCCESS message 5 | SuccessMessageSignature = 0x70 6 | ) 7 | 8 | // SuccessMessage Represents an SUCCESS message 9 | type SuccessMessage struct { 10 | Metadata map[string]interface{} 11 | } 12 | 13 | // NewSuccessMessage Gets a new SuccessMessage struct 14 | func NewSuccessMessage(metadata map[string]interface{}) SuccessMessage { 15 | return SuccessMessage{ 16 | Metadata: metadata, 17 | } 18 | } 19 | 20 | // Signature gets the signature byte for the struct 21 | func (i SuccessMessage) Signature() int { 22 | return SuccessMessageSignature 23 | } 24 | 25 | // AllFields gets the fields to encode for the struct 26 | func (i SuccessMessage) AllFields() []interface{} { 27 | return []interface{}{i.Metadata} 28 | } 29 | -------------------------------------------------------------------------------- /structures/structures.go: -------------------------------------------------------------------------------- 1 | package structures 2 | 3 | // Structure represents a Neo4J structure 4 | type Structure interface { 5 | Signature() int 6 | AllFields() []interface{} 7 | } 8 | -------------------------------------------------------------------------------- /tx.go: -------------------------------------------------------------------------------- 1 | package golangNeo4jBoltDriver 2 | 3 | import ( 4 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/errors" 5 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/log" 6 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/messages" 7 | ) 8 | 9 | // Tx represents a transaction 10 | type Tx interface { 11 | // Commit commits the transaction 12 | Commit() error 13 | // Rollback rolls back the transaction 14 | Rollback() error 15 | } 16 | 17 | type boltTx struct { 18 | conn *boltConn 19 | closed bool 20 | } 21 | 22 | func newTx(conn *boltConn) *boltTx { 23 | return &boltTx{ 24 | conn: conn, 25 | } 26 | } 27 | 28 | // Commit commits and closes the transaction 29 | func (t *boltTx) Commit() error { 30 | if t.closed { 31 | return errors.New("Transaction already closed") 32 | } 33 | if t.conn.statement != nil { 34 | if err := t.conn.statement.Close(); err != nil { 35 | return errors.Wrap(err, "An error occurred closing open rows in transaction Commit") 36 | } 37 | } 38 | 39 | successInt, pullInt, err := t.conn.sendRunPullAllConsumeSingle("COMMIT", nil) 40 | if err != nil { 41 | return errors.Wrap(err, "An error occurred committing transaction") 42 | } 43 | 44 | success, ok := successInt.(messages.SuccessMessage) 45 | if !ok { 46 | return errors.New("Unrecognized response type committing transaction: %#v", success) 47 | } 48 | 49 | log.Infof("Got success message committing transaction: %#v", success) 50 | 51 | pull, ok := pullInt.(messages.SuccessMessage) 52 | if !ok { 53 | return errors.New("Unrecognized response type pulling transaction: %#v", pull) 54 | } 55 | 56 | log.Infof("Got success message pulling transaction: %#v", pull) 57 | 58 | t.conn.transaction = nil 59 | t.closed = true 60 | return err 61 | } 62 | 63 | // Rollback rolls back and closes the transaction 64 | func (t *boltTx) Rollback() error { 65 | if t.closed { 66 | return errors.New("Transaction already closed") 67 | } 68 | if t.conn.statement != nil { 69 | if err := t.conn.statement.Close(); err != nil { 70 | return errors.Wrap(err, "An error occurred closing open rows in transaction Rollback") 71 | } 72 | } 73 | 74 | successInt, pullInt, err := t.conn.sendRunPullAllConsumeSingle("ROLLBACK", nil) 75 | if err != nil { 76 | return errors.Wrap(err, "An error occurred rolling back transaction") 77 | } 78 | 79 | success, ok := successInt.(messages.SuccessMessage) 80 | if !ok { 81 | return errors.New("Unrecognized response type rolling back transaction: %#v", success) 82 | } 83 | 84 | log.Infof("Got success message rolling back transaction: %#v", success) 85 | 86 | pull, ok := pullInt.(messages.SuccessMessage) 87 | if !ok { 88 | return errors.New("Unrecognized response type pulling transaction: %#v", pull) 89 | } 90 | 91 | log.Infof("Got success message pulling transaction: %#v", pull) 92 | 93 | t.conn.transaction = nil 94 | t.closed = true 95 | return err 96 | } 97 | -------------------------------------------------------------------------------- /tx_test.go: -------------------------------------------------------------------------------- 1 | package golangNeo4jBoltDriver 2 | 3 | import ( 4 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/structures/graph" 5 | "io" 6 | "testing" 7 | ) 8 | 9 | func TestBoltTx_Commit(t *testing.T) { 10 | driver := NewDriver() 11 | 12 | // Records session for testing 13 | driver.(*boltDriver).recorder = newRecorder("TestBoltTx_Commit", neo4jConnStr) 14 | 15 | conn, err := driver.OpenNeo(neo4jConnStr) 16 | if err != nil { 17 | t.Fatalf("An error occurred opening conn: %s", err) 18 | } 19 | 20 | tx, err := conn.Begin() 21 | if err != nil { 22 | t.Fatalf("An error occurred beginning transaction: %s", err) 23 | } 24 | 25 | stmt, err := conn.PrepareNeo(`CREATE (f:FOO {a: "1"})-[b:TO]->(c:BAR)<-[d:FROM]-(e:BAZ) RETURN f, b, c, d, e`) 26 | if err != nil { 27 | t.Fatalf("An error occurred preparing statement: %s", err) 28 | } 29 | 30 | result, err := stmt.ExecNeo(nil) 31 | if err != nil { 32 | t.Fatalf("An error occurred querying Neo: %s", err) 33 | } 34 | 35 | if num, err := result.RowsAffected(); num != 5 { 36 | t.Fatalf("Expected 5 rows affected: %#v err: %#v", result.Metadata(), err) 37 | } 38 | 39 | err = tx.Commit() 40 | if err != nil { 41 | t.Fatalf("An error occurred committing transaction: %s", err) 42 | } 43 | 44 | err = stmt.Close() 45 | if err != nil { 46 | t.Fatalf("An error occurred closing statement") 47 | } 48 | 49 | stmt, err = conn.PrepareNeo(`MATCH (f:FOO {a: "1"})-[b:TO]->(c:BAR)<-[d:FROM]-(e:BAZ) RETURN f, b, c, d, e`) 50 | if err != nil { 51 | t.Fatalf("An error occurred preparing statement: %s", err) 52 | } 53 | 54 | rows, err := stmt.QueryNeo(nil) 55 | if err != nil { 56 | t.Fatalf("An error occurred querying Neo: %s", err) 57 | } 58 | 59 | output, _, err := rows.NextNeo() 60 | if err != nil { 61 | t.Fatalf("An error occurred getting next row: %s", err) 62 | } 63 | 64 | if output[0].(graph.Node).Labels[0] != "FOO" { 65 | t.Fatalf("Unexpected return data: %s", err) 66 | } 67 | if output[1].(graph.Relationship).Type != "TO" { 68 | t.Fatalf("Unexpected return data: %s", err) 69 | } 70 | if output[2].(graph.Node).Labels[0] != "BAR" { 71 | t.Fatalf("Unexpected return data: %s", err) 72 | } 73 | if output[3].(graph.Relationship).Type != "FROM" { 74 | t.Fatalf("Unexpected return data: %s", err) 75 | } 76 | if output[4].(graph.Node).Labels[0] != "BAZ" { 77 | t.Fatalf("Unexpected return data: %s", err) 78 | } 79 | 80 | // Closing in middle of record stream 81 | stmt.Close() 82 | 83 | stmt, err = conn.PrepareNeo(`MATCH (f:FOO)-[b:TO]->(c:BAR)<-[d:FROM]-(e:BAZ) DELETE f, b, c, d, e`) 84 | if err != nil { 85 | t.Fatalf("An error occurred preparing delete statement: %s", err) 86 | } 87 | 88 | _, err = stmt.ExecNeo(nil) 89 | if err != nil { 90 | t.Fatalf("An error occurred on delete query to Neo: %s", err) 91 | } 92 | 93 | err = conn.Close() 94 | if err != nil { 95 | t.Fatalf("Error closing connection: %s", err) 96 | } 97 | } 98 | 99 | func TestBoltTx_Rollback(t *testing.T) { 100 | driver := NewDriver() 101 | 102 | // Records session for testing 103 | driver.(*boltDriver).recorder = newRecorder("TestBoltTx_Rollback", neo4jConnStr) 104 | 105 | conn, err := driver.OpenNeo(neo4jConnStr) 106 | if err != nil { 107 | t.Fatalf("An error occurred opening conn: %s", err) 108 | } 109 | 110 | tx, err := conn.Begin() 111 | if err != nil { 112 | t.Fatalf("An error occurred beginning transaction: %s", err) 113 | } 114 | 115 | stmt, err := conn.PrepareNeo(`CREATE (f:FOO {a: "1"})-[b:TO]->(c:BAR)<-[d:FROM]-(e:BAZ) RETURN f, b, c, d, e`) 116 | if err != nil { 117 | t.Fatalf("An error occurred preparing statement: %s", err) 118 | } 119 | 120 | result, err := stmt.ExecNeo(nil) 121 | if err != nil { 122 | t.Fatalf("An error occurred querying Neo: %s", err) 123 | } 124 | 125 | if num, err := result.RowsAffected(); num != 5 { 126 | t.Fatalf("Expected 5 rows affected: %#v err: %#v", result.Metadata(), err) 127 | } 128 | 129 | err = tx.Rollback() 130 | if err != nil { 131 | t.Fatalf("An error occurred committing transaction: %s", err) 132 | } 133 | 134 | err = stmt.Close() 135 | if err != nil { 136 | t.Fatalf("An error occurred closing statement") 137 | } 138 | 139 | stmt, err = conn.PrepareNeo(`MATCH (f:FOO {a: "1"})-[b:TO]->(c:BAR)<-[d:FROM]-(e:BAZ) RETURN f, b, c, d, e`) 140 | if err != nil { 141 | t.Fatalf("An error occurred preparing statement: %s", err) 142 | } 143 | 144 | rows, err := stmt.QueryNeo(nil) 145 | if err != nil { 146 | t.Fatalf("An error occurred querying Neo: %s", err) 147 | } 148 | 149 | output, _, err := rows.NextNeo() 150 | if err != io.EOF { 151 | t.Fatalf("Unexpected error returned from getting next rows: %s", err) 152 | } 153 | 154 | if len(output) != 0 { 155 | t.Fatalf("Unexpected return data: %s", err) 156 | } 157 | 158 | err = stmt.Close() 159 | if err != nil { 160 | t.Fatalf("An error occurred closing statement") 161 | } 162 | 163 | err = conn.Close() 164 | if err != nil { 165 | t.Fatalf("Error closing connection: %s", err) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package golangNeo4jBoltDriver 2 | 3 | import ( 4 | "database/sql/driver" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/johnnadratowski/golang-neo4j-bolt-driver/encoding" 9 | ) 10 | 11 | // sprintByteHex returns a formatted string of the byte array in hexadecimal 12 | // with a nicely formatted human-readable output 13 | func sprintByteHex(b []byte) string { 14 | output := "\t" 15 | for i, b := range b { 16 | output += fmt.Sprintf("%x", b) 17 | if (i+1)%16 == 0 { 18 | output += "\n\n\t" 19 | } else if (i+1)%4 == 0 { 20 | output += " " 21 | } else { 22 | output += " " 23 | } 24 | } 25 | output += "\n" 26 | 27 | return output 28 | } 29 | 30 | // driverArgsToMap turns driver.Value list into a parameter map 31 | // for neo4j parameters 32 | func driverArgsToMap(args []driver.Value) (map[string]interface{}, error) { 33 | output := map[string]interface{}{} 34 | for _, arg := range args { 35 | argBytes, ok := arg.([]byte) 36 | if !ok { 37 | return nil, errors.New("You must pass only a gob encoded map to the Exec/Query args") 38 | } 39 | 40 | m, err := encoding.Unmarshal(argBytes) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | for k, v := range m.(map[string]interface{}) { 46 | output[k] = v 47 | } 48 | 49 | } 50 | 51 | return output, nil 52 | } 53 | --------------------------------------------------------------------------------