From 4e30f998a317f222a2aae17433bdf667d34c9161 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 15 May 2025 09:15:37 +0200 Subject: [PATCH 01/13] Vector type API spike --- README.md | 529 ++---------------- original_README.md | 505 +++++++++++++++++ .../src/packstream/packstream-v1.js | 14 +- .../neo4j-driver/test/vector-examples.test.js | 86 +++ 4 files changed, 640 insertions(+), 494 deletions(-) create mode 100644 original_README.md create mode 100644 packages/neo4j-driver/test/vector-examples.test.js diff --git a/README.md b/README.md index b176bd13c..cae6cd09c 100644 --- a/README.md +++ b/README.md @@ -1,505 +1,52 @@ # Neo4j Driver for JavaScript +**This is a preview branch for the new Vector type in the JavaScript driver.** -This is the official Neo4j driver for JavaScript. +The 6.0 release of the JavaScript driver will introduce the ability to read and write Vectors to the database. +These are single type arrays with inner types of int 8/16/32/64 and float 32/64. +To facilitate ease of use and good integration with AI packages in JavaScript, Vectors will be represented by JavaScript TypedArrays. -Starting with 5.0, the Neo4j Drivers will be moving to a monthly release cadence. A minor version will be released on the last Friday of each month so as to maintain versioning consistency with the core product (Neo4j DBMS) which has also moved to a monthly cadence. +This forces 2 changes to the driver API, which we would like feedback on before they are set in stone: +1. **Minor** You can no longer pass a TypedArray, such as a Float32Array, to the driver and get it translated to a list of numbers by the driver. To send a TypedArray as a regular list, you will need to use Array.from() +2. **Major** Raw bytes can no longer be sent using an Int8Array, which was the correct way to send them before. Bytes will now be sent and recieved as ArrayBuffers. These are the object that is wrapper by a Int8Array, so it is not a complex code change, but existing code that is used to read or write raw bytes **WILL** break. -As a policy, patch versions will not be released except on rare occasions. Bug fixes and updates will go into the latest minor version and users should upgrade to that. Driver upgrades within a major version will never contain breaking API changes. +A number of pieces of example code can be found in the Vector types examples test file [here](./packages/neo4j-driver/test/vector-examples.test.js) -See also: https://neo4j.com/developer/kb/neo4j-supported-versions/ +The Vector type is not yet supported on the Bolt Protocol, so in this preview they are actually sent and stored as Lists of Float64. This means that any vector written with this preview will not actually be stored as a vector, and when read from the database it will again be a list of numbers. The vector test files have mock lines to convert the returned lists into the vectors they will be in the final version. -Resources to get you started: +## Example code -- [API Documentation](https://neo4j.com/docs/api/javascript-driver/current/) -- [Neo4j Manual](https://neo4j.com/docs/) -- [Neo4j Refcard](https://neo4j.com/docs/cypher-refcard/current/) - -## What's New in 5.x - -- [Changelog](https://github.com/neo4j/neo4j-javascript-driver/wiki/5.0-changelog) - -## Including the Driver - -### In Node.js application - -Stable channel: - -```shell -npm install neo4j-driver -``` - -Pre-release channel: - -```shell -npm install neo4j-driver@next -``` - -Please note that `@next` only points to pre-releases that are not suitable for production use. -To get the latest stable release omit `@next` part altogether or use `@latest` instead. - -```javascript -var neo4j = require('neo4j-driver') -``` - -Driver instance should be closed when Node.js application exits: - -```javascript -driver.close() // returns a Promise -``` - -otherwise application shutdown might hang or it might exit with a non-zero exit code. - -### In web browser - -We build a special browser version of the driver, which supports connecting to Neo4j over WebSockets. -It can be included in an HTML page using one of the following tags: - -```html - - - - - - - - - - - - -``` - -This will make a global `neo4j` object available, where you can create a driver instance with `neo4j.driver`: - -```javascript -var driver = neo4j.driver( - 'neo4j://localhost', - neo4j.auth.basic('neo4j', 'password') -) -``` - -From `5.4.0`, this version is also exported as ECMA Script Module. -It can be imported from a module using the following statements: - -```javascript -// Direct reference -import neo4j from 'lib/browser/neo4j-web.esm.min.js' - -// unpkg CDN non-minified , version X.Y.Z where X.Y.Z >= 5.4.0 -import neo4j from 'https://unpkg.com/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.js' - -// unpkg CDN minified for production use, version X.Y.Z where X.Y.Z >= 5.4.0 -import neo4j from 'https://unpkg.com/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.min.js' - -// jsDelivr CDN non-minified, version X.Y.Z where X.Y.Z >= 5.4.0 -import neo4j from 'https://cdn.jsdelivr.net/npm/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.js' - -// jsDelivr CDN minified for production use, version X.Y.Z where X.Y.Z >= 5.4.0 -import neo4j from 'https://cdn.jsdelivr.net/npm/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.min.js' - -``` - -It is not required to explicitly close the driver on a web page. Web browser should gracefully close all open -WebSockets when the page is unloaded. However, driver instance should be explicitly closed when it's lifetime -is not the same as the lifetime of the web page: - -```javascript -driver.close() // returns a Promise -``` - -## Usage examples - -### Constructing a Driver - -```javascript -// Create a driver instance, for the user `neo4j` with password `password`. -// It should be enough to have a single driver per database per application. -var driver = neo4j.driver( - 'neo4j://localhost', - neo4j.auth.basic('neo4j', 'password') -) - -// Close the driver when application exits. -// This closes all used network connections. -await driver.close() -``` - -### Acquiring a Session - -#### Regular Session - -```javascript -// Create a session to run Cypher statements in. -// Note: Always make sure to close sessions when you are done using them! -var session = driver.session() -``` - -##### with a Default Access Mode of `READ` - -```javascript -var session = driver.session({ defaultAccessMode: neo4j.session.READ }) -``` - -##### with Bookmarks - -```javascript -var session = driver.session({ - bookmarks: [bookmark1FromPreviousSession, bookmark2FromPreviousSession] -}) -``` - -##### against a Database - -```javascript -var session = driver.session({ - database: 'foo', - defaultAccessMode: neo4j.session.WRITE -}) -``` - -#### Reactive Session - -```javascript -// Create a reactive session to run Cypher statements in. -// Note: Always make sure to close sessions when you are done using them! -var rxSession = driver.rxSession() -``` - -##### with a Default Access Mode of `READ` - -```javascript -var rxSession = driver.rxSession({ defaultAccessMode: neo4j.session.READ }) -``` - -##### with Bookmarks - -```javascript -var rxSession = driver.rxSession({ - bookmarks: [bookmark1FromPreviousSession, bookmark2FromPreviousSession] -}) -``` - -##### against a Database - -```javascript -var rxSession = driver.rxSession({ - database: 'foo', - defaultAccessMode: neo4j.session.WRITE -}) -``` - -### Transaction functions - -```javascript -// Transaction functions provide a convenient API with minimal boilerplate and -// retries on network fluctuations and transient errors. Maximum retry time is -// configured on the driver level and is 30 seconds by default: -// Applies both to standard and reactive sessions. -neo4j.driver('neo4j://localhost', neo4j.auth.basic('neo4j', 'password'), { - maxTransactionRetryTime: 30000 -}) -``` - -#### Reading with Async Session - -```javascript -// It is possible to execute read transactions that will benefit from automatic -// retries on both single instance ('bolt' URI scheme) and Causal Cluster -// ('neo4j' URI scheme) and will get automatic load balancing in cluster deployments -var readTxResultPromise = session.executeRead(txc => { - // used transaction will be committed automatically, no need for explicit commit/rollback - - var result = txc.run('MATCH (person:Person) RETURN person.name AS name') - // at this point it is possible to either return the result or process it and return the - // result of processing it is also possible to run more statements in the same transaction - return result -}) - -// returned Promise can be later consumed like this: -readTxResultPromise - .then(result => { - console.log(result.records) - }) - .catch(error => { - console.log(error) - }) - .then(() => session.close()) -``` - -#### Reading with Reactive Session - -```javascript -rxSession - .executeRead(txc => - txc - .run('MATCH (person:Person) RETURN person.name AS name') - .records() - .pipe(map(record => record.get('name'))) - ) - .subscribe({ - next: data => console.log(data), - complete: () => console.log('completed'), - error: err => console.log(error) - }) -``` - -#### Writing with Async Session - -```javascript -// It is possible to execute write transactions that will benefit from automatic retries -// on both single instance ('bolt' URI scheme) and Causal Cluster ('neo4j' URI scheme) -var writeTxResultPromise = session.executeWrite(async txc => { - // used transaction will be committed automatically, no need for explicit commit/rollback - - var result = await txc.run( - "MERGE (alice:Person {name : 'Alice'}) RETURN alice.name AS name" - ) - // at this point it is possible to either return the result or process it and return the - // result of processing it is also possible to run more statements in the same transaction - return result.records.map(record => record.get('name')) -}) - -// returned Promise can be later consumed like this: -writeTxResultPromise - .then(namesArray => { - console.log(namesArray) - }) - .catch(error => { - console.log(error) - }) - .then(() => session.close()) -``` - -#### Writing with Reactive Session - -```javascript -rxSession - .executeWrite(txc => - txc - .run("MERGE (alice:Person {name: 'James'}) RETURN alice.name AS name") - .records() - .pipe(map(record => record.get('name'))) - ) - .subscribe({ - next: data => console.log(data), - complete: () => console.log('completed'), - error: error => console.log(error) - }) -``` - -### Consuming Records - -#### Consuming Records with Streaming API - -```javascript -// Run a Cypher statement, reading the result in a streaming manner as records arrive: -session - .run('MERGE (alice:Person {name : $nameParam}) RETURN alice.name AS name', { - nameParam: 'Alice' - }) - .subscribe({ - onKeys: keys => { - console.log(keys) - }, - onNext: record => { - console.log(record.get('name')) - }, - onCompleted: () => { - session.close() // returns a Promise - }, - onError: error => { - console.log(error) - } - }) -``` - -Subscriber API allows following combinations of `onKeys`, `onNext`, `onCompleted` and `onError` callback invocations: - -- zero or one `onKeys`, -- zero or more `onNext` followed by `onCompleted` when operation was successful. `onError` will not be invoked in this case -- zero or more `onNext` followed by `onError` when operation failed. Callback `onError` might be invoked after couple `onNext` invocations because records are streamed lazily by the database. `onCompleted` will not be invoked in this case. - -#### Consuming Records with Promise API - -```javascript -// the Promise way, where the complete result is collected before we act on it: -session - .run('MERGE (james:Person {name : $nameParam}) RETURN james.name AS name', { - nameParam: 'James' - }) - .then(result => { - result.records.forEach(record => { - console.log(record.get('name')) +### Read and Write Vectors +```Javascript + const driver = neo4j.driver(uri, sharedNeo4j.authToken) + await driver.executeQuery('CREATE (p:Product) SET p.embeddings = $embeddings', { + embeddings: Float32Array.from([0, 1, 2, 3]), //Typed arrays can be created from a regular list of Numbers }) - }) - .catch(error => { - console.log(error) - }) - .then(() => session.close()) -``` - -#### Consuming Records with Reactive API - -```javascript -rxSession - .run('MERGE (james:Person {name: $nameParam}) RETURN james.name AS name', { - nameParam: 'Bob' - }) - .records() - .pipe( - map(record => record.get('name')), - concatWith(rxSession.close()) - ) - .subscribe({ - next: data => console.log(data), - complete: () => console.log('completed'), - error: err => console.log(err) - }) -``` - -### Explicit Transactions - -#### With Async Session - -```javascript -// run statement in a transaction -const txc = session.beginTransaction() -try { - const result1 = await txc.run( - 'MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', - { - nameParam: 'Bob' - } - ) - result1.records.forEach(r => console.log(r.get('name'))) - console.log('First query completed') - - const result2 = await txc.run( - 'MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', - { - nameParam: 'Adam' - } - ) - result2.records.forEach(r => console.log(r.get('name'))) - console.log('Second query completed') - - await txc.commit() - console.log('committed') -} catch (error) { - console.log(error) - await txc.rollback() - console.log('rolled back') -} finally { - await session.close() -} -``` - -#### With Reactive Session - -```javascript -rxSession - .beginTransaction() - .pipe( - mergeMap(txc => - concatWith( - txc - .run( - 'MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', - { - nameParam: 'Bob' - } - ) - .records() - .pipe(map(r => r.get('name'))), - of('First query completed'), - txc - .run( - 'MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', - { - nameParam: 'Adam' - } - ) - .records() - .pipe(map(r => r.get('name'))), - of('Second query completed'), - txc.commit(), - of('committed') - ).pipe(catchError(err => txc.rollback().pipe(throwError(() => err)))) - ) - ) - .subscribe({ - next: data => console.log(data), - complete: () => console.log('completed'), - error: error => console.log(error) - }) -``` - -### Numbers and the Integer type - -The Neo4j type system uses 64-bit signed integer values. The range of values is between `-(2``64``- 1)` and `(2``63``- 1)`. - -However, JavaScript can only safely represent integers between `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`. - -In order to support the full Neo4j type system, the driver will not automatically convert to javascript integers. -Any time the driver receives an integer value from Neo4j, it will be represented with an internal integer type by the driver. - -_**Any javascript number value passed as a parameter will be recognized as `Float` type.**_ - -#### Writing integers + const res = await driver.executeQuery('MATCH (p:Product) RETURN p.embeddings as embeddings') + + let vector = res.records[0].get('embeddings') + + // THIS LINE IS HERE TO EMULATE THE FINISHED PROPOSED API + vector = Float32Array.from(vector) -Numbers written directly e.g. `session.run("CREATE (n:Node {age: $age})", {age: 22})` will be of type `Float` in Neo4j. + console.log(vector[3]) //3 -To write the `age` as an integer the `neo4j.int` method should be used: - -```javascript -var neo4j = require('neo4j-driver') - -session.run('CREATE (n {age: $myIntParam})', { myIntParam: neo4j.int(22) }) -``` - -To write an integer value that are not within the range of `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`, use a string argument to `neo4j.int`: - -```javascript -session.run('CREATE (n {age: $myIntParam})', { - myIntParam: neo4j.int('9223372036854775807') -}) -``` - -#### Reading integers - -In Neo4j, the type Integer can be larger what can be represented safely as an integer with JavaScript Number. - -It is only safe to convert to a JavaScript Number if you know that the number will be in the range `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`. - -In order to facilitate working with integers the driver include `neo4j.isInt`, `neo4j.integer.inSafeRange`, `neo4j.integer.toNumber`, and `neo4j.integer.toString`. - -```javascript -var smallInteger = neo4j.int(123) -if (neo4j.integer.inSafeRange(smallInteger)) { - var aNumber = smallInteger.toNumber() -} -``` - -If you will be handling integers that is not within the JavaScript safe range of integers, you should convert the value to a string: - -```javascript -var largeInteger = neo4j.int('9223372036854775807') -if (!neo4j.integer.inSafeRange(largeInteger)) { - var integerAsString = largeInteger.toString() -} + await driver.close() ``` -#### Enabling native numbers +### Read and Write Bytes +```Javascript + const driver = neo4j.driver(uri, sharedNeo4j.authToken) + const byteWriter = Int8Array.from([0, 1, 2, 3]) + await driver.executeQuery('CREATE (p:Product) SET p.bytes = $bytes', { + //bytes: byteWriter #THis was the proper way to send bytes before, but this will send an Int8 vector in the proposed API + bytes: byteWriter.buffer //This is the new way to send bytes + }) + const res = await driver.executeQuery('MATCH (p:Product) RETURN p.bytes as bytes') + + let bytes = res.records[0].get('bytes') //In the old API, this would now be an Int8Array, but is now an Arraybuffer + bytes = Int8Array.from(bytes) //This converts the object into an Int8Array, able to be used as before. -Starting from 1.6 version of the driver it is possible to configure it to only return native numbers instead of custom `Integer` objects. -The configuration option affects all integers returned by the driver. **Enabling this option can result in a loss of precision and incorrect numeric -values being returned if the database contains integer numbers outside of the range** `[Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]`. -To enable potentially lossy integer values use the driver's configuration object: + console.log(bytes[3]) //3 -```javascript -var driver = neo4j.driver( - 'neo4j://localhost', - neo4j.auth.basic('neo4j', 'password'), - { disableLosslessIntegers: true } -) -``` + await driver.close() +``` \ No newline at end of file diff --git a/original_README.md b/original_README.md new file mode 100644 index 000000000..b176bd13c --- /dev/null +++ b/original_README.md @@ -0,0 +1,505 @@ +# Neo4j Driver for JavaScript + +This is the official Neo4j driver for JavaScript. + +Starting with 5.0, the Neo4j Drivers will be moving to a monthly release cadence. A minor version will be released on the last Friday of each month so as to maintain versioning consistency with the core product (Neo4j DBMS) which has also moved to a monthly cadence. + +As a policy, patch versions will not be released except on rare occasions. Bug fixes and updates will go into the latest minor version and users should upgrade to that. Driver upgrades within a major version will never contain breaking API changes. + +See also: https://neo4j.com/developer/kb/neo4j-supported-versions/ + +Resources to get you started: + +- [API Documentation](https://neo4j.com/docs/api/javascript-driver/current/) +- [Neo4j Manual](https://neo4j.com/docs/) +- [Neo4j Refcard](https://neo4j.com/docs/cypher-refcard/current/) + +## What's New in 5.x + +- [Changelog](https://github.com/neo4j/neo4j-javascript-driver/wiki/5.0-changelog) + +## Including the Driver + +### In Node.js application + +Stable channel: + +```shell +npm install neo4j-driver +``` + +Pre-release channel: + +```shell +npm install neo4j-driver@next +``` + +Please note that `@next` only points to pre-releases that are not suitable for production use. +To get the latest stable release omit `@next` part altogether or use `@latest` instead. + +```javascript +var neo4j = require('neo4j-driver') +``` + +Driver instance should be closed when Node.js application exits: + +```javascript +driver.close() // returns a Promise +``` + +otherwise application shutdown might hang or it might exit with a non-zero exit code. + +### In web browser + +We build a special browser version of the driver, which supports connecting to Neo4j over WebSockets. +It can be included in an HTML page using one of the following tags: + +```html + + + + + + + + + + + + +``` + +This will make a global `neo4j` object available, where you can create a driver instance with `neo4j.driver`: + +```javascript +var driver = neo4j.driver( + 'neo4j://localhost', + neo4j.auth.basic('neo4j', 'password') +) +``` + +From `5.4.0`, this version is also exported as ECMA Script Module. +It can be imported from a module using the following statements: + +```javascript +// Direct reference +import neo4j from 'lib/browser/neo4j-web.esm.min.js' + +// unpkg CDN non-minified , version X.Y.Z where X.Y.Z >= 5.4.0 +import neo4j from 'https://unpkg.com/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.js' + +// unpkg CDN minified for production use, version X.Y.Z where X.Y.Z >= 5.4.0 +import neo4j from 'https://unpkg.com/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.min.js' + +// jsDelivr CDN non-minified, version X.Y.Z where X.Y.Z >= 5.4.0 +import neo4j from 'https://cdn.jsdelivr.net/npm/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.js' + +// jsDelivr CDN minified for production use, version X.Y.Z where X.Y.Z >= 5.4.0 +import neo4j from 'https://cdn.jsdelivr.net/npm/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.min.js' + +``` + +It is not required to explicitly close the driver on a web page. Web browser should gracefully close all open +WebSockets when the page is unloaded. However, driver instance should be explicitly closed when it's lifetime +is not the same as the lifetime of the web page: + +```javascript +driver.close() // returns a Promise +``` + +## Usage examples + +### Constructing a Driver + +```javascript +// Create a driver instance, for the user `neo4j` with password `password`. +// It should be enough to have a single driver per database per application. +var driver = neo4j.driver( + 'neo4j://localhost', + neo4j.auth.basic('neo4j', 'password') +) + +// Close the driver when application exits. +// This closes all used network connections. +await driver.close() +``` + +### Acquiring a Session + +#### Regular Session + +```javascript +// Create a session to run Cypher statements in. +// Note: Always make sure to close sessions when you are done using them! +var session = driver.session() +``` + +##### with a Default Access Mode of `READ` + +```javascript +var session = driver.session({ defaultAccessMode: neo4j.session.READ }) +``` + +##### with Bookmarks + +```javascript +var session = driver.session({ + bookmarks: [bookmark1FromPreviousSession, bookmark2FromPreviousSession] +}) +``` + +##### against a Database + +```javascript +var session = driver.session({ + database: 'foo', + defaultAccessMode: neo4j.session.WRITE +}) +``` + +#### Reactive Session + +```javascript +// Create a reactive session to run Cypher statements in. +// Note: Always make sure to close sessions when you are done using them! +var rxSession = driver.rxSession() +``` + +##### with a Default Access Mode of `READ` + +```javascript +var rxSession = driver.rxSession({ defaultAccessMode: neo4j.session.READ }) +``` + +##### with Bookmarks + +```javascript +var rxSession = driver.rxSession({ + bookmarks: [bookmark1FromPreviousSession, bookmark2FromPreviousSession] +}) +``` + +##### against a Database + +```javascript +var rxSession = driver.rxSession({ + database: 'foo', + defaultAccessMode: neo4j.session.WRITE +}) +``` + +### Transaction functions + +```javascript +// Transaction functions provide a convenient API with minimal boilerplate and +// retries on network fluctuations and transient errors. Maximum retry time is +// configured on the driver level and is 30 seconds by default: +// Applies both to standard and reactive sessions. +neo4j.driver('neo4j://localhost', neo4j.auth.basic('neo4j', 'password'), { + maxTransactionRetryTime: 30000 +}) +``` + +#### Reading with Async Session + +```javascript +// It is possible to execute read transactions that will benefit from automatic +// retries on both single instance ('bolt' URI scheme) and Causal Cluster +// ('neo4j' URI scheme) and will get automatic load balancing in cluster deployments +var readTxResultPromise = session.executeRead(txc => { + // used transaction will be committed automatically, no need for explicit commit/rollback + + var result = txc.run('MATCH (person:Person) RETURN person.name AS name') + // at this point it is possible to either return the result or process it and return the + // result of processing it is also possible to run more statements in the same transaction + return result +}) + +// returned Promise can be later consumed like this: +readTxResultPromise + .then(result => { + console.log(result.records) + }) + .catch(error => { + console.log(error) + }) + .then(() => session.close()) +``` + +#### Reading with Reactive Session + +```javascript +rxSession + .executeRead(txc => + txc + .run('MATCH (person:Person) RETURN person.name AS name') + .records() + .pipe(map(record => record.get('name'))) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: err => console.log(error) + }) +``` + +#### Writing with Async Session + +```javascript +// It is possible to execute write transactions that will benefit from automatic retries +// on both single instance ('bolt' URI scheme) and Causal Cluster ('neo4j' URI scheme) +var writeTxResultPromise = session.executeWrite(async txc => { + // used transaction will be committed automatically, no need for explicit commit/rollback + + var result = await txc.run( + "MERGE (alice:Person {name : 'Alice'}) RETURN alice.name AS name" + ) + // at this point it is possible to either return the result or process it and return the + // result of processing it is also possible to run more statements in the same transaction + return result.records.map(record => record.get('name')) +}) + +// returned Promise can be later consumed like this: +writeTxResultPromise + .then(namesArray => { + console.log(namesArray) + }) + .catch(error => { + console.log(error) + }) + .then(() => session.close()) +``` + +#### Writing with Reactive Session + +```javascript +rxSession + .executeWrite(txc => + txc + .run("MERGE (alice:Person {name: 'James'}) RETURN alice.name AS name") + .records() + .pipe(map(record => record.get('name'))) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: error => console.log(error) + }) +``` + +### Consuming Records + +#### Consuming Records with Streaming API + +```javascript +// Run a Cypher statement, reading the result in a streaming manner as records arrive: +session + .run('MERGE (alice:Person {name : $nameParam}) RETURN alice.name AS name', { + nameParam: 'Alice' + }) + .subscribe({ + onKeys: keys => { + console.log(keys) + }, + onNext: record => { + console.log(record.get('name')) + }, + onCompleted: () => { + session.close() // returns a Promise + }, + onError: error => { + console.log(error) + } + }) +``` + +Subscriber API allows following combinations of `onKeys`, `onNext`, `onCompleted` and `onError` callback invocations: + +- zero or one `onKeys`, +- zero or more `onNext` followed by `onCompleted` when operation was successful. `onError` will not be invoked in this case +- zero or more `onNext` followed by `onError` when operation failed. Callback `onError` might be invoked after couple `onNext` invocations because records are streamed lazily by the database. `onCompleted` will not be invoked in this case. + +#### Consuming Records with Promise API + +```javascript +// the Promise way, where the complete result is collected before we act on it: +session + .run('MERGE (james:Person {name : $nameParam}) RETURN james.name AS name', { + nameParam: 'James' + }) + .then(result => { + result.records.forEach(record => { + console.log(record.get('name')) + }) + }) + .catch(error => { + console.log(error) + }) + .then(() => session.close()) +``` + +#### Consuming Records with Reactive API + +```javascript +rxSession + .run('MERGE (james:Person {name: $nameParam}) RETURN james.name AS name', { + nameParam: 'Bob' + }) + .records() + .pipe( + map(record => record.get('name')), + concatWith(rxSession.close()) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: err => console.log(err) + }) +``` + +### Explicit Transactions + +#### With Async Session + +```javascript +// run statement in a transaction +const txc = session.beginTransaction() +try { + const result1 = await txc.run( + 'MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', + { + nameParam: 'Bob' + } + ) + result1.records.forEach(r => console.log(r.get('name'))) + console.log('First query completed') + + const result2 = await txc.run( + 'MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', + { + nameParam: 'Adam' + } + ) + result2.records.forEach(r => console.log(r.get('name'))) + console.log('Second query completed') + + await txc.commit() + console.log('committed') +} catch (error) { + console.log(error) + await txc.rollback() + console.log('rolled back') +} finally { + await session.close() +} +``` + +#### With Reactive Session + +```javascript +rxSession + .beginTransaction() + .pipe( + mergeMap(txc => + concatWith( + txc + .run( + 'MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', + { + nameParam: 'Bob' + } + ) + .records() + .pipe(map(r => r.get('name'))), + of('First query completed'), + txc + .run( + 'MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', + { + nameParam: 'Adam' + } + ) + .records() + .pipe(map(r => r.get('name'))), + of('Second query completed'), + txc.commit(), + of('committed') + ).pipe(catchError(err => txc.rollback().pipe(throwError(() => err)))) + ) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: error => console.log(error) + }) +``` + +### Numbers and the Integer type + +The Neo4j type system uses 64-bit signed integer values. The range of values is between `-(2``64``- 1)` and `(2``63``- 1)`. + +However, JavaScript can only safely represent integers between `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`. + +In order to support the full Neo4j type system, the driver will not automatically convert to javascript integers. +Any time the driver receives an integer value from Neo4j, it will be represented with an internal integer type by the driver. + +_**Any javascript number value passed as a parameter will be recognized as `Float` type.**_ + +#### Writing integers + +Numbers written directly e.g. `session.run("CREATE (n:Node {age: $age})", {age: 22})` will be of type `Float` in Neo4j. + +To write the `age` as an integer the `neo4j.int` method should be used: + +```javascript +var neo4j = require('neo4j-driver') + +session.run('CREATE (n {age: $myIntParam})', { myIntParam: neo4j.int(22) }) +``` + +To write an integer value that are not within the range of `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`, use a string argument to `neo4j.int`: + +```javascript +session.run('CREATE (n {age: $myIntParam})', { + myIntParam: neo4j.int('9223372036854775807') +}) +``` + +#### Reading integers + +In Neo4j, the type Integer can be larger what can be represented safely as an integer with JavaScript Number. + +It is only safe to convert to a JavaScript Number if you know that the number will be in the range `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`. + +In order to facilitate working with integers the driver include `neo4j.isInt`, `neo4j.integer.inSafeRange`, `neo4j.integer.toNumber`, and `neo4j.integer.toString`. + +```javascript +var smallInteger = neo4j.int(123) +if (neo4j.integer.inSafeRange(smallInteger)) { + var aNumber = smallInteger.toNumber() +} +``` + +If you will be handling integers that is not within the JavaScript safe range of integers, you should convert the value to a string: + +```javascript +var largeInteger = neo4j.int('9223372036854775807') +if (!neo4j.integer.inSafeRange(largeInteger)) { + var integerAsString = largeInteger.toString() +} +``` + +#### Enabling native numbers + +Starting from 1.6 version of the driver it is possible to configure it to only return native numbers instead of custom `Integer` objects. +The configuration option affects all integers returned by the driver. **Enabling this option can result in a loss of precision and incorrect numeric +values being returned if the database contains integer numbers outside of the range** `[Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]`. +To enable potentially lossy integer values use the driver's configuration object: + +```javascript +var driver = neo4j.driver( + 'neo4j://localhost', + neo4j.auth.basic('neo4j', 'password'), + { disableLosslessIntegers: true } +) +``` diff --git a/packages/bolt-connection/src/packstream/packstream-v1.js b/packages/bolt-connection/src/packstream/packstream-v1.js index 6416f0868..b6a40846a 100644 --- a/packages/bolt-connection/src/packstream/packstream-v1.js +++ b/packages/bolt-connection/src/packstream/packstream-v1.js @@ -94,7 +94,14 @@ class Packer { return () => this.packInteger(int(x)) } else if (isInt(x)) { return () => this.packInteger(x) - } else if (x instanceof Int8Array) { + } else if (x.BYTES_PER_ELEMENT != null) { + return () => { + this.packListHeader(x.length) + for (let i = 0; i < x.length; i++) { + this.packable(x[i] === undefined ? null : x[i], dehydrateStruct)() + } + } + } else if (x instanceof ArrayBuffer) { return () => this.packBytes(x) } else if (x instanceof Array) { return () => { @@ -233,8 +240,9 @@ class Packer { } } - packBytes (array) { + packBytes (buffer) { if (this._byteArraysSupported) { + const array = new Uint8Array(buffer) this.packBytesHeader(array.length) for (let i = 0; i < array.length; i++) { this._ch.writeInt8(array[i]) @@ -486,7 +494,7 @@ class Unpacker { for (let i = 0; i < size; i++) { value[i] = buffer.readInt8() } - return value + return value.buffer } _unpackMap (marker, markerHigh, markerLow, buffer, hydrateStructure) { diff --git a/packages/neo4j-driver/test/vector-examples.test.js b/packages/neo4j-driver/test/vector-examples.test.js new file mode 100644 index 000000000..1d9494c23 --- /dev/null +++ b/packages/neo4j-driver/test/vector-examples.test.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import neo4j from '../src' +import sharedNeo4j from './internal/shared-neo4j' + +describe('#integration vector type api suggestion', () => { + let driverGlobal + const uri = `bolt://${sharedNeo4j.hostnameWithBoltPort}` + + beforeAll(async () => { + driverGlobal = neo4j.driver(uri, sharedNeo4j.authToken) + }) + + beforeEach(async () => { + const driver = driverGlobal + const session = driver.session() + await session.run('MATCH (n) DETACH DELETE n') + await session.close() + }) + + afterAll(async () => { + await driverGlobal.close() + }) + + it('write and read vectors', async () => { + const driver = driverGlobal + + const bufferWriter = Uint8Array.from([1, 1]) + await driver.executeQuery('CREATE (p:Product) SET p.vector_from_array = $vector_from_array, p.vector_from_buffer = $vector_from_buffer', { + vector_from_array: Float32Array.from([1, 2, 3, 4]), // Typed arrays can be created from a regular list of Numbers + vector_from_buffer: new Uint8Array(bufferWriter.buffer) // Or from a bytebuffer + }) + const res = await driver.executeQuery('MATCH (p:Product) RETURN p.vector_from_array as arrayVector, p.vector_from_buffer as bufferVector') + + let arrayVec = res.records[0].get('arrayVector') + let bufferVec = res.records[0].get('bufferVector') + + // THE FOLLOWING 2 LINES ARE HERE TO EMULATE THE FINISHED PROPOSED API, WOULD NOT BE NEEDED IN THE FINISHED PRODUCT + arrayVec = Float32Array.from(arrayVec) + bufferVec = Uint8Array.from(bufferVec) + // END OF MOCK LINES + + expect(arrayVec[0]).toBe(1) + expect(bufferVec[1]).toBe(1) + }) + + it('write and read bytes', async () => { + const driver = driverGlobal + + const bufferWriter = Int8Array.from([1, 1]) + await driver.executeQuery('CREATE (p:Product) SET p.bytes = $bytes', { + bytes: bufferWriter.buffer // New way to write and read bytes, as Int8Arrays are now interpreted as Vector + }) + const res = await driver.executeQuery('MATCH (p:Product) RETURN p.bytes as bytes') + const bytes = res.records[0].get('bytes') + + expect(new Int8Array(bytes)).toEqual(bufferWriter) + }) + + it('write TypedArray as List', async () => { + const driver = driverGlobal + + const float32 = Float32Array.from([1, 1]) + await driver.executeQuery('CREATE (p:Product) SET p.arr = $arr', { + arr: Array.from(float32) // converts the TypedArray to a standard array. + }) + const res = await driver.executeQuery('MATCH (p:Product) RETURN p.arr as arr') + + expect(Float32Array.from(res.records[0].get('arr'))).toEqual(float32) + }) +}) From a008231962e7880d565a58e60582980cb56cb081 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 15 May 2025 09:58:01 +0200 Subject: [PATCH 02/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cae6cd09c..4f15beec5 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,4 @@ The Vector type is not yet supported on the Bolt Protocol, so in this preview th console.log(bytes[3]) //3 await driver.close() -``` \ No newline at end of file +``` From 75fe626066af874314443923686e22fe95337b95 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 15 May 2025 11:28:32 +0200 Subject: [PATCH 03/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f15beec5..8a976414b 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The Vector type is not yet supported on the Bolt Protocol, so in this preview th const driver = neo4j.driver(uri, sharedNeo4j.authToken) const byteWriter = Int8Array.from([0, 1, 2, 3]) await driver.executeQuery('CREATE (p:Product) SET p.bytes = $bytes', { - //bytes: byteWriter #THis was the proper way to send bytes before, but this will send an Int8 vector in the proposed API + //bytes: byteWriter #This was the proper way to send bytes before, but this will send an Int8 vector in the proposed API bytes: byteWriter.buffer //This is the new way to send bytes }) const res = await driver.executeQuery('MATCH (p:Product) RETURN p.bytes as bytes') From f967ef01394ac17bd532c7cc2f7f7574b41f727f Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 19 May 2025 16:28:20 +0200 Subject: [PATCH 04/13] introduce useVectorTypes config --- README.md | 26 ++++++++++++++++--- .../src/bolt/bolt-protocol-v1.js | 13 +++++----- .../src/bolt/bolt-protocol-v2.js | 4 +-- packages/bolt-connection/src/bolt/create.js | 4 ++- .../src/connection/connection-channel.js | 1 + .../src/packstream/packstream-v1.js | 18 ++++++++----- .../src/packstream/packstream-v2.js | 4 +-- packages/core/src/types.ts | 13 ++++++++++ .../bolt-connection/bolt/bolt-protocol-v1.js | 13 +++++----- .../bolt-connection/bolt/bolt-protocol-v2.js | 4 +-- .../lib/bolt-connection/bolt/create.js | 4 ++- .../connection/connection-channel.js | 1 + .../packstream/packstream-v1.js | 20 +++++++++++--- .../packstream/packstream-v2.js | 4 +-- packages/neo4j-driver-deno/lib/core/types.ts | 13 ++++++++++ 15 files changed, 107 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 8a976414b..868a7b074 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,13 @@ This forces 2 changes to the driver API, which we would like feedback on before 1. **Minor** You can no longer pass a TypedArray, such as a Float32Array, to the driver and get it translated to a list of numbers by the driver. To send a TypedArray as a regular list, you will need to use Array.from() 2. **Major** Raw bytes can no longer be sent using an Int8Array, which was the correct way to send them before. Bytes will now be sent and recieved as ArrayBuffers. These are the object that is wrapper by a Int8Array, so it is not a complex code change, but existing code that is used to read or write raw bytes **WILL** break. +UPDATED NOTE: To make the migration simpler, a config option `useVectorTypes` has been introduced that's enabled by default. Disabling this option will avoid both of these breaking changes, at the cost of not supporting vector types. + A number of pieces of example code can be found in the Vector types examples test file [here](./packages/neo4j-driver/test/vector-examples.test.js) -The Vector type is not yet supported on the Bolt Protocol, so in this preview they are actually sent and stored as Lists of Float64. This means that any vector written with this preview will not actually be stored as a vector, and when read from the database it will again be a list of numbers. The vector test files have mock lines to convert the returned lists into the vectors they will be in the final version. +The Vector type is not yet supported on the Bolt Protocol, so in this preview they are actually sent and stored as Lists of Float64. This means that any vector written with this preview will not actually be stored as a vector, and when read from the database it will again be a list of numbers. + +NOTE: The vector test files have mock lines to convert the returned lists into the vectors they will be in the final version. ## Example code @@ -24,9 +28,6 @@ The Vector type is not yet supported on the Bolt Protocol, so in this preview th const res = await driver.executeQuery('MATCH (p:Product) RETURN p.embeddings as embeddings') let vector = res.records[0].get('embeddings') - - // THIS LINE IS HERE TO EMULATE THE FINISHED PROPOSED API - vector = Float32Array.from(vector) console.log(vector[3]) //3 @@ -50,3 +51,20 @@ The Vector type is not yet supported on the Bolt Protocol, so in this preview th await driver.close() ``` + +### Disable vector types +```Javascript + const driver = neo4j.driver(uri, sharedNeo4j.authToken, {useVectorTypes: false}) + const byteWriter = Int8Array.from([0, 1, 2, 3]) + const typedArray = Int32Array.from([0, 1, 2, 3]) + await driver.executeQuery('CREATE (p:Product) SET p.bytes = $bytes, p.arr = $array', { + bytes: byteWriter //With vector types disabled this is once again how to send bytes + array: typedArray + }) + const res = await driver.executeQuery('MATCH (p:Product) RETURN p.bytes as bytes, p.arr as array') + + let bytes = res.records[0].get('bytes') //as in the old API this is now an Int8Array. + let bytes = res.records[0].get('bytes') //As in the old API this is now a list of Numbers. + + await driver.close() +``` diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js index d04b33b4e..98ca40511 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js @@ -68,6 +68,7 @@ export default class BoltProtocol { * @param {Object} packstreamConfig Packstream configuration * @param {boolean} packstreamConfig.disableLosslessIntegers if this connection should convert all received integers to native JS numbers. * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. + * @param {boolean} packstreamConfig.useVectorTypes if this connection should support vector types and treat TypedArras as vectors. * @param {CreateResponseHandler} createResponseHandler Function which creates the response handler * @param {Logger} log the logger * @param {OnProtocolError} onProtocolError handles protocol errors @@ -75,21 +76,21 @@ export default class BoltProtocol { constructor ( server, chunker, - { disableLosslessIntegers, useBigInt } = {}, + { disableLosslessIntegers, useBigInt, useVectorTypes } = {}, createResponseHandler = () => null, log, onProtocolError ) { this._server = server || {} this._chunker = chunker - this._packer = this._createPacker(chunker) - this._unpacker = this._createUnpacker(disableLosslessIntegers, useBigInt) + this._packer = this._createPacker(chunker, useVectorTypes) + this._unpacker = this._createUnpacker(disableLosslessIntegers, useBigInt, useVectorTypes) this._responseHandler = createResponseHandler(this) this._log = log this._onProtocolError = onProtocolError this._fatalError = null this._lastMessageSignature = null - this._config = { disableLosslessIntegers, useBigInt } + this._config = { disableLosslessIntegers, useBigInt, useVectorTypes } } get transformer () { @@ -488,8 +489,8 @@ export default class BoltProtocol { return new v1.Packer(chunker) } - _createUnpacker (disableLosslessIntegers, useBigInt) { - return new v1.Unpacker(disableLosslessIntegers, useBigInt) + _createUnpacker (disableLosslessIntegers, useBigInt, useVectorTypes) { + return new v1.Unpacker(disableLosslessIntegers, useBigInt, useVectorTypes) } /** diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v2.js b/packages/bolt-connection/src/bolt/bolt-protocol-v2.js index d7c0e930b..3c35ba590 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v2.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v2.js @@ -29,8 +29,8 @@ export default class BoltProtocol extends BoltProtocolV1 { return new v2.Packer(chunker) } - _createUnpacker (disableLosslessIntegers, useBigInt) { - return new v2.Unpacker(disableLosslessIntegers, useBigInt) + _createUnpacker (disableLosslessIntegers, useBigInt, useVectorTypes) { + return new v2.Unpacker(disableLosslessIntegers, useBigInt, useVectorTypes) } get transformer () { diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index 1b8f792a5..ac855a1ff 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -49,6 +49,7 @@ import ResponseHandler from './response-handler' * @param {ResponseHandler~Observer} config.observer Observer * @param {boolean} config.disableLosslessIntegers Disable the lossless integers * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. + * @param {boolean} packstreamConfig.useVectorTypes if this connection should support vector types and treat TypedArras as vectors. * @param {boolean} config.serversideRouting It's using server side routing */ export default function create ({ @@ -58,6 +59,7 @@ export default function create ({ channel, disableLosslessIntegers, useBigInt, + useVectorTypes, serversideRouting, server, // server info log, @@ -93,7 +95,7 @@ export default function create ({ version, server, chunker, - { disableLosslessIntegers, useBigInt }, + { disableLosslessIntegers, useBigInt, useVectorTypes }, serversideRouting, createResponseHandler, observer.onProtocolError.bind(observer), diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index b57542462..ec6df1a3e 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -69,6 +69,7 @@ export function createChannelConnection ( dechunker, disableLosslessIntegers: config.disableLosslessIntegers, useBigInt: config.useBigInt, + useVectorTypes: config.useVectorTypes, serversideRouting, server: conn.server, log: conn.logger, diff --git a/packages/bolt-connection/src/packstream/packstream-v1.js b/packages/bolt-connection/src/packstream/packstream-v1.js index b6a40846a..3d1bfd93b 100644 --- a/packages/bolt-connection/src/packstream/packstream-v1.js +++ b/packages/bolt-connection/src/packstream/packstream-v1.js @@ -94,15 +94,17 @@ class Packer { return () => this.packInteger(int(x)) } else if (isInt(x)) { return () => this.packInteger(x) - } else if (x.BYTES_PER_ELEMENT != null) { + } else if (x.BYTES_PER_ELEMENT != null && this._useVectorTypes) { return () => { this.packListHeader(x.length) for (let i = 0; i < x.length; i++) { this.packable(x[i] === undefined ? null : x[i], dehydrateStruct)() } } - } else if (x instanceof ArrayBuffer) { + } else if (x instanceof Int8Array && this._useVectorTypes) { return () => this.packBytes(x) + } else if (x instanceof ArrayBuffer) { + return () => this.packBytes(new Int8Array(x)) } else if (x instanceof Array) { return () => { this.packListHeader(x.length) @@ -240,9 +242,8 @@ class Packer { } } - packBytes (buffer) { + packBytes (array) { if (this._byteArraysSupported) { - const array = new Uint8Array(buffer) this.packBytesHeader(array.length) for (let i = 0; i < array.length; i++) { this._ch.writeInt8(array[i]) @@ -332,9 +333,10 @@ class Unpacker { * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint */ - constructor (disableLosslessIntegers = false, useBigInt = false) { + constructor (disableLosslessIntegers = false, useBigInt = false, useVectorTypes = true) { this._disableLosslessIntegers = disableLosslessIntegers this._useBigInt = useBigInt + this._useVectorTypes = useVectorTypes } unpack (buffer, hydrateStructure = functional.identity) { @@ -494,7 +496,11 @@ class Unpacker { for (let i = 0; i < size; i++) { value[i] = buffer.readInt8() } - return value.buffer + if (this._useVectorTypes) { + return value.buffer + } else { + return value + } } _unpackMap (marker, markerHigh, markerLow, buffer, hydrateStructure) { diff --git a/packages/bolt-connection/src/packstream/packstream-v2.js b/packages/bolt-connection/src/packstream/packstream-v2.js index b083264d5..5d9a52f63 100644 --- a/packages/bolt-connection/src/packstream/packstream-v2.js +++ b/packages/bolt-connection/src/packstream/packstream-v2.js @@ -29,7 +29,7 @@ export class Unpacker extends v1.Unpacker { * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint */ - constructor (disableLosslessIntegers = false, useBigInt = false) { - super(disableLosslessIntegers, useBigInt) + constructor (disableLosslessIntegers = false, useBigInt = false, useVectorTypes = true) { + super(disableLosslessIntegers, useBigInt, useVectorTypes) } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8f60394a3..56a2219ae 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -77,6 +77,7 @@ export class Config { disableLosslessIntegers?: boolean notificationFilter?: NotificationFilter useBigInt?: boolean + useVectorTypes?: boolean logging?: LoggingConfig resolver?: (address: string) => string[] | Promise userAgent?: string @@ -262,6 +263,18 @@ export class Config { */ this.useBigInt = false + /** + * Enables Vector types being sent over bolt, and makes all TypedArrays be interpreted as vectors + * + * **Warning:** This makes TypedArrays be sent as Vectors rather than Lists, and requires bytes be sent as ArrayBuffers rather than Int8Arrays + * If migrating from 5.x drivers and vector types are not needed, this can be disabled to make migration simpler. + * + * **Default**: ```true``` + * + * @type {boolean|undefined} + */ + this.useVectorTypes = true + /** * Specify the logging configuration for the driver. Object should have two properties `level` and `logger`. * diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js index a83f32f5a..0ffd415f3 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js @@ -68,6 +68,7 @@ export default class BoltProtocol { * @param {Object} packstreamConfig Packstream configuration * @param {boolean} packstreamConfig.disableLosslessIntegers if this connection should convert all received integers to native JS numbers. * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. + * @param {boolean} packstreamConfig.useVectorTypes if this connection should support vector types and treat TypedArras as vectors. * @param {CreateResponseHandler} createResponseHandler Function which creates the response handler * @param {Logger} log the logger * @param {OnProtocolError} onProtocolError handles protocol errors @@ -75,21 +76,21 @@ export default class BoltProtocol { constructor ( server, chunker, - { disableLosslessIntegers, useBigInt } = {}, + { disableLosslessIntegers, useBigInt, useVectorTypes } = {}, createResponseHandler = () => null, log, onProtocolError ) { this._server = server || {} this._chunker = chunker - this._packer = this._createPacker(chunker) - this._unpacker = this._createUnpacker(disableLosslessIntegers, useBigInt) + this._packer = this._createPacker(chunker, useVectorTypes) + this._unpacker = this._createUnpacker(disableLosslessIntegers, useBigInt, useVectorTypes) this._responseHandler = createResponseHandler(this) this._log = log this._onProtocolError = onProtocolError this._fatalError = null this._lastMessageSignature = null - this._config = { disableLosslessIntegers, useBigInt } + this._config = { disableLosslessIntegers, useBigInt, useVectorTypes } } get transformer () { @@ -488,8 +489,8 @@ export default class BoltProtocol { return new v1.Packer(chunker) } - _createUnpacker (disableLosslessIntegers, useBigInt) { - return new v1.Unpacker(disableLosslessIntegers, useBigInt) + _createUnpacker (disableLosslessIntegers, useBigInt, useVectorTypes) { + return new v1.Unpacker(disableLosslessIntegers, useBigInt, useVectorTypes) } /** diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js index d0e6757f2..cfb4fc666 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js @@ -29,8 +29,8 @@ export default class BoltProtocol extends BoltProtocolV1 { return new v2.Packer(chunker) } - _createUnpacker (disableLosslessIntegers, useBigInt) { - return new v2.Unpacker(disableLosslessIntegers, useBigInt) + _createUnpacker (disableLosslessIntegers, useBigInt, useVectorTypes) { + return new v2.Unpacker(disableLosslessIntegers, useBigInt, useVectorTypes) } get transformer () { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js index 188e8c6bd..55f42ec04 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -49,6 +49,7 @@ import ResponseHandler from './response-handler.js' * @param {ResponseHandler~Observer} config.observer Observer * @param {boolean} config.disableLosslessIntegers Disable the lossless integers * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. + * @param {boolean} packstreamConfig.useVectorTypes if this connection should support vector types and treat TypedArras as vectors. * @param {boolean} config.serversideRouting It's using server side routing */ export default function create ({ @@ -58,6 +59,7 @@ export default function create ({ channel, disableLosslessIntegers, useBigInt, + useVectorTypes, serversideRouting, server, // server info log, @@ -93,7 +95,7 @@ export default function create ({ version, server, chunker, - { disableLosslessIntegers, useBigInt }, + { disableLosslessIntegers, useBigInt, useVectorTypes }, serversideRouting, createResponseHandler, observer.onProtocolError.bind(observer), diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 7fcf4689c..3e2ff44d1 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -69,6 +69,7 @@ export function createChannelConnection ( dechunker, disableLosslessIntegers: config.disableLosslessIntegers, useBigInt: config.useBigInt, + useVectorTypes: config.useVectorTypes, serversideRouting, server: conn.server, log: conn.logger, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js index 8730501aa..b2a31effd 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js @@ -94,8 +94,17 @@ class Packer { return () => this.packInteger(int(x)) } else if (isInt(x)) { return () => this.packInteger(x) - } else if (x instanceof Int8Array) { + } else if (x.BYTES_PER_ELEMENT != null && this._useVectorTypes) { + return () => { + this.packListHeader(x.length) + for (let i = 0; i < x.length; i++) { + this.packable(x[i] === undefined ? null : x[i], dehydrateStruct)() + } + } + } else if (x instanceof Int8Array && this._useVectorTypes) { return () => this.packBytes(x) + } else if (x instanceof ArrayBuffer) { + return () => this.packBytes(new Int8Array(x)) } else if (x instanceof Array) { return () => { this.packListHeader(x.length) @@ -324,9 +333,10 @@ class Unpacker { * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint */ - constructor (disableLosslessIntegers = false, useBigInt = false) { + constructor (disableLosslessIntegers = false, useBigInt = false, useVectorTypes = true) { this._disableLosslessIntegers = disableLosslessIntegers this._useBigInt = useBigInt + this._useVectorTypes = useVectorTypes } unpack (buffer, hydrateStructure = functional.identity) { @@ -486,7 +496,11 @@ class Unpacker { for (let i = 0; i < size; i++) { value[i] = buffer.readInt8() } - return value + if (this._useVectorTypes) { + return value.buffer + } else { + return value + } } _unpackMap (marker, markerHigh, markerLow, buffer, hydrateStructure) { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js index 19e480919..33d0c78e8 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js @@ -29,7 +29,7 @@ export class Unpacker extends v1.Unpacker { * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint */ - constructor (disableLosslessIntegers = false, useBigInt = false) { - super(disableLosslessIntegers, useBigInt) + constructor (disableLosslessIntegers = false, useBigInt = false, useVectorTypes = true) { + super(disableLosslessIntegers, useBigInt, useVectorTypes) } } diff --git a/packages/neo4j-driver-deno/lib/core/types.ts b/packages/neo4j-driver-deno/lib/core/types.ts index 7cdc1e747..989b0d202 100644 --- a/packages/neo4j-driver-deno/lib/core/types.ts +++ b/packages/neo4j-driver-deno/lib/core/types.ts @@ -77,6 +77,7 @@ export class Config { disableLosslessIntegers?: boolean notificationFilter?: NotificationFilter useBigInt?: boolean + useVectorTypes?: boolean logging?: LoggingConfig resolver?: (address: string) => string[] | Promise userAgent?: string @@ -262,6 +263,18 @@ export class Config { */ this.useBigInt = false + /** + * Enables Vector types being sent over bolt, and makes all TypedArrays be interpreted as vectors + * + * **Warning:** This makes TypedArrays be sent as Vectors rather than Lists, and requires bytes be sent as ArrayBuffers rather than Int8Arrays + * If migrating from 5.x drivers and vector types are not needed, this can be disabled to make migration simpler. + * + * **Default**: ```true``` + * + * @type {boolean|undefined} + */ + this.useVectorTypes = true + /** * Specify the logging configuration for the driver. Object should have two properties `level` and `logger`. * From 74e7c38d323b72256f2e8a9d456dbcb6cca2451b Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 10 Jun 2025 09:52:19 +0200 Subject: [PATCH 05/13] vector type and struct transformer, bolt 6.0 --- README.md | 52 +-------- .../src/bolt/bolt-protocol-v6x0.js | 39 +++++++ .../bolt/bolt-protocol-v6x0.transformer.js | 110 ++++++++++++++++++ packages/bolt-connection/src/bolt/create.js | 9 ++ .../bolt-connection/src/bolt/handshake.js | 2 +- .../src/packstream/packstream-v1.js | 11 +- packages/core/src/index.ts | 6 +- packages/core/src/internal/constants.ts | 2 + packages/core/src/vector.ts | 76 ++++++++++++ .../bolt/bolt-protocol-v6x0.js | 39 +++++++ .../bolt/bolt-protocol-v6x0.transformer.js | 110 ++++++++++++++++++ .../lib/bolt-connection/bolt/create.js | 9 ++ .../lib/bolt-connection/bolt/handshake.js | 2 +- .../packstream/packstream-v1.js | 11 +- packages/neo4j-driver-deno/lib/core/index.ts | 6 +- .../lib/core/internal/constants.ts | 2 + packages/neo4j-driver-deno/lib/core/vector.ts | 78 +++++++++++++ packages/neo4j-driver/src/index.js | 14 ++- .../neo4j-driver/test/vector-examples.test.js | 38 +----- .../testkit-backend/src/feature/common.js | 1 + 20 files changed, 508 insertions(+), 109 deletions(-) create mode 100644 packages/bolt-connection/src/bolt/bolt-protocol-v6x0.js create mode 100644 packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js create mode 100644 packages/core/src/vector.ts create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/core/vector.ts diff --git a/README.md b/README.md index 868a7b074..ba6d40ddc 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,11 @@ The 6.0 release of the JavaScript driver will introduce the ability to read and write Vectors to the database. These are single type arrays with inner types of int 8/16/32/64 and float 32/64. -To facilitate ease of use and good integration with AI packages in JavaScript, Vectors will be represented by JavaScript TypedArrays. - -This forces 2 changes to the driver API, which we would like feedback on before they are set in stone: -1. **Minor** You can no longer pass a TypedArray, such as a Float32Array, to the driver and get it translated to a list of numbers by the driver. To send a TypedArray as a regular list, you will need to use Array.from() -2. **Major** Raw bytes can no longer be sent using an Int8Array, which was the correct way to send them before. Bytes will now be sent and recieved as ArrayBuffers. These are the object that is wrapper by a Int8Array, so it is not a complex code change, but existing code that is used to read or write raw bytes **WILL** break. - -UPDATED NOTE: To make the migration simpler, a config option `useVectorTypes` has been introduced that's enabled by default. Disabling this option will avoid both of these breaking changes, at the cost of not supporting vector types. +To facilitate ease of use and good integration with AI packages in JavaScript, Vectors will be represented by JavaScript TypedArrays, wrapped inside a new Vector type much like Integers. A number of pieces of example code can be found in the Vector types examples test file [here](./packages/neo4j-driver/test/vector-examples.test.js) -The Vector type is not yet supported on the Bolt Protocol, so in this preview they are actually sent and stored as Lists of Float64. This means that any vector written with this preview will not actually be stored as a vector, and when read from the database it will again be a list of numbers. - -NOTE: The vector test files have mock lines to convert the returned lists into the vectors they will be in the final version. +Vector types are usuable from bolt version 6.0 and forward, which no current server currently supports, so this can not be tested against any live Neo4j Database at this time. ## Example code @@ -23,48 +15,14 @@ NOTE: The vector test files have mock lines to convert the returned lists into t ```Javascript const driver = neo4j.driver(uri, sharedNeo4j.authToken) await driver.executeQuery('CREATE (p:Product) SET p.embeddings = $embeddings', { - embeddings: Float32Array.from([0, 1, 2, 3]), //Typed arrays can be created from a regular list of Numbers + embeddings: neo4j.vector(Float32Array.from([0, 1, 2, 3])), //Typed arrays can be created from a regular list of Numbers }) const res = await driver.executeQuery('MATCH (p:Product) RETURN p.embeddings as embeddings') let vector = res.records[0].get('embeddings') - console.log(vector[3]) //3 - - await driver.close() -``` - -### Read and Write Bytes -```Javascript - const driver = neo4j.driver(uri, sharedNeo4j.authToken) - const byteWriter = Int8Array.from([0, 1, 2, 3]) - await driver.executeQuery('CREATE (p:Product) SET p.bytes = $bytes', { - //bytes: byteWriter #This was the proper way to send bytes before, but this will send an Int8 vector in the proposed API - bytes: byteWriter.buffer //This is the new way to send bytes - }) - const res = await driver.executeQuery('MATCH (p:Product) RETURN p.bytes as bytes') - - let bytes = res.records[0].get('bytes') //In the old API, this would now be an Int8Array, but is now an Arraybuffer - bytes = Int8Array.from(bytes) //This converts the object into an Int8Array, able to be used as before. - - console.log(bytes[3]) //3 - - await driver.close() -``` - -### Disable vector types -```Javascript - const driver = neo4j.driver(uri, sharedNeo4j.authToken, {useVectorTypes: false}) - const byteWriter = Int8Array.from([0, 1, 2, 3]) - const typedArray = Int32Array.from([0, 1, 2, 3]) - await driver.executeQuery('CREATE (p:Product) SET p.bytes = $bytes, p.arr = $array', { - bytes: byteWriter //With vector types disabled this is once again how to send bytes - array: typedArray - }) - const res = await driver.executeQuery('MATCH (p:Product) RETURN p.bytes as bytes, p.arr as array') - - let bytes = res.records[0].get('bytes') //as in the old API this is now an Int8Array. - let bytes = res.records[0].get('bytes') //As in the old API this is now a list of Numbers. + console.log(vector.typedArray[3]) //3 + console.log(vector.type) //FLOAT32 await driver.close() ``` diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.js b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.js new file mode 100644 index 000000000..9db026f1a --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import BoltProtocolV5x8 from './bolt-protocol-v5x8' + +import transformersFactories from './bolt-protocol-v6x0.transformer' +import Transformer from './transformer' + +import { internal } from 'neo4j-driver-core' + +const { + constants: { BOLT_PROTOCOL_V6_0 } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x8 { + get version () { + return BOLT_PROTOCOL_V6_0 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } +} diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js new file mode 100644 index 000000000..a76a2b3c8 --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js @@ -0,0 +1,110 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import v5x8 from './bolt-protocol-v5x8.transformer' +import { TypeTransformer } from './transformer' +import { structure } from '../packstream' +import { Vector, newError } from 'neo4j-driver-core' +const VECTOR = 0x56 +const FLOAT_32 = 0xc6 +const FLOAT_64 = 0xc1 +const INT_8 = 0xc8 +const INT_16 = 0xc9 +const INT_32 = 0xca +const INT_64 = 0xcb + +function createVectorTransformer () { + return new TypeTransformer({ + signature: VECTOR, + isTypeInstance: object => object instanceof Vector, + toStructure: vector => { + const startTime = new Date().getTime() + const dataview = new DataView(vector.typedArray.byteLength) + let set + let typeMarker + if (vector.type === 'INT8') { + typeMarker = Uint8Array.from([INT_8]) + set = dataview.setUint8 + } else if (vector.type === 'INT16') { + typeMarker = Uint8Array.from([INT_16]) + set = dataview.setUint16 + } else if (vector.type === 'INT32') { + typeMarker = Uint8Array.from([INT_32]) + set = dataview.setUint32 + } else if (vector.type === 'INT64') { + typeMarker = Uint8Array.from([INT_64]) + set = dataview.setUint64 + } else if (vector.type === 'FLOAT32') { + typeMarker = Uint8Array.from([FLOAT_32]) + set = dataview.setFloat32 + } else if (vector.type === 'FLOAT64') { + typeMarker = Uint8Array.from([FLOAT_64]) + set = dataview.setFloat64 + } else { + throw newError('Vector is of unsupported type') + } + for (let i = 0; i < vector.typedArray.length; i++) { + set(i * vector.typedArray.BYTES_PER_ELEMENT, vector.typedArray[i]) + } + const struct = new structure.Structure(VECTOR, [typeMarker, Uint8Array.from(dataview.buffer)]) + console.debug(`Packing vector took ${new Date().getTime() - startTime}ms`) + return struct + }, + fromStructure: structure => { + const typeMarker = structure.fields[0][0] + const byteArray = structure.fields[1] + const dataview = new DataView(byteArray.length) + let typedArray + let set + let resultArray + if (typeMarker === INT_8) { + return Int8Array.from(byteArray.buffer) + } if (typeMarker === INT_16) { + typedArray = Int16Array.from(byteArray.buffer) + resultArray = Int16Array.from(dataview.buffer) + set = dataview.setInt16 + } if (typeMarker === INT_32) { + typedArray = Int32Array.from(byteArray.buffer) + resultArray = Int32Array.from(dataview.buffer) + set = dataview.setInt32 + } if (typeMarker === INT_64) { + typedArray = BigInt64Array.from(byteArray.buffer) + resultArray = BigInt64Array.from(dataview.buffer) + set = dataview.setBigInt64 + } if (typeMarker === FLOAT_32) { + typedArray = Float32Array.from(byteArray.buffer) + resultArray = Float32Array.from(dataview.buffer) + set = dataview.setFloat32 + } if (typeMarker === FLOAT_64) { + typedArray = Float64Array.from(byteArray.buffer) + resultArray = Float64Array.from(dataview.buffer) + set = dataview.setFloat64 + } else { + throw newError('Recieved Vector of unknown type') + } + for (let i = 0; i < typedArray.length; i++) { + set(i * typedArray.BYTES_PER_ELEMENT, typedArray[i]) + } + return new Vector(resultArray) + } + }) +} + +export default { + ...v5x8, + createVectorTransformer +} diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index ac855a1ff..bacb963b3 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -33,6 +33,7 @@ import BoltProtocolV5x5 from './bolt-protocol-v5x5' import BoltProtocolV5x6 from './bolt-protocol-v5x6' import BoltProtocolV5x7 from './bolt-protocol-v5x7' import BoltProtocolV5x8 from './bolt-protocol-v5x8' +import BoltProtocolV6x0 from './bolt-protocol-v6x0' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel' import ResponseHandler from './response-handler' @@ -268,6 +269,14 @@ function createProtocol ( log, onProtocolError, serversideRouting) + case 6.0: + return new BoltProtocolV6x0(server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting) default: throw newError('Unknown Bolt protocol version: ' + version) } diff --git a/packages/bolt-connection/src/bolt/handshake.js b/packages/bolt-connection/src/bolt/handshake.js index 7f1d2a1c4..4354654cf 100644 --- a/packages/bolt-connection/src/bolt/handshake.js +++ b/packages/bolt-connection/src/bolt/handshake.js @@ -19,7 +19,7 @@ import { alloc } from '../channel' import { newError } from 'neo4j-driver-core' const BOLT_MAGIC_PREAMBLE = 0x6060b017 -const AVAILABLE_BOLT_PROTOCOLS = ['5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0'] // bolt protocols the client will accept, ordered by preference +const AVAILABLE_BOLT_PROTOCOLS = ['6.0', '5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0'] // bolt protocols the client will accept, ordered by preference const DESIRED_CAPABILITES = 0 function version (major, minor) { diff --git a/packages/bolt-connection/src/packstream/packstream-v1.js b/packages/bolt-connection/src/packstream/packstream-v1.js index 3d1bfd93b..8a18663e7 100644 --- a/packages/bolt-connection/src/packstream/packstream-v1.js +++ b/packages/bolt-connection/src/packstream/packstream-v1.js @@ -94,17 +94,8 @@ class Packer { return () => this.packInteger(int(x)) } else if (isInt(x)) { return () => this.packInteger(x) - } else if (x.BYTES_PER_ELEMENT != null && this._useVectorTypes) { - return () => { - this.packListHeader(x.length) - for (let i = 0; i < x.length; i++) { - this.packable(x[i] === undefined ? null : x[i], dehydrateStruct)() - } - } - } else if (x instanceof Int8Array && this._useVectorTypes) { + } else if (x instanceof Int8Array) { return () => this.packBytes(x) - } else if (x instanceof ArrayBuffer) { - return () => this.packBytes(new Int8Array(x)) } else if (x instanceof Array) { return () => { this.packListHeader(x.length) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a0950cf37..d2d1300b2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -101,6 +101,7 @@ import * as json from './json' import resultTransformers, { ResultTransformer } from './result-transformers' import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate' import * as internal from './internal' // todo: removed afterwards +import Vector, { VectorType, vector } from './vector' /** * Object containing string constants representing predefined {@link Neo4jError} codes. @@ -263,7 +264,10 @@ export { notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + Vector, + VectorType, + vector } export type { diff --git a/packages/core/src/internal/constants.ts b/packages/core/src/internal/constants.ts index b45034da9..7403677c3 100644 --- a/packages/core/src/internal/constants.ts +++ b/packages/core/src/internal/constants.ts @@ -40,6 +40,7 @@ const BOLT_PROTOCOL_V5_5: number = 5.5 const BOLT_PROTOCOL_V5_6: number = 5.6 const BOLT_PROTOCOL_V5_7: number = 5.7 const BOLT_PROTOCOL_V5_8: number = 5.8 +const BOLT_PROTOCOL_V6_0: number = 6.0 const TELEMETRY_APIS = { MANAGED_TRANSACTION: 0, @@ -76,5 +77,6 @@ export { BOLT_PROTOCOL_V5_6, BOLT_PROTOCOL_V5_7, BOLT_PROTOCOL_V5_8, + BOLT_PROTOCOL_V6_0, TELEMETRY_APIS } diff --git a/packages/core/src/vector.ts b/packages/core/src/vector.ts new file mode 100644 index 000000000..738f66f9d --- /dev/null +++ b/packages/core/src/vector.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { newError } from './error' + +export enum VectorType { + 'INT8', + 'INT16', + 'INT32', + 'INT64', + 'FLOAT32', + 'FLOAT64', + +} + +/** + * A wrapper class for JavaScript TypedArrays that makes the driver send them as a Vector type to the database. + * @access public + * @exports Vector + * @class A Integer class for representing a 64 bit two's-complement integer value. + * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray The TypedArray to convert to a vector + * + * @constructor + * + */ +export default class Vector { + typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array + type: VectorType + constructor (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array) { + if (typedArray instanceof Int8Array) { + this.type = VectorType.INT8 + } + if (typedArray instanceof Int16Array) { + this.type = VectorType.INT16 + } + if (typedArray instanceof Int32Array) { + this.type = VectorType.INT32 + } + if (typedArray instanceof BigInt64Array) { + this.type = VectorType.INT64 + } + if (typedArray instanceof Float32Array) { + this.type = VectorType.FLOAT32 + } + if (typedArray instanceof Float64Array) { + this.type = VectorType.FLOAT64 + } else { + throw newError('The neo4j Vector class is a wrapper for TypedArrays') + } + this.typedArray = typedArray + } +} + +/** + * Cast a TypedArray to a {@link Vector} + * @access public + * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray - The value to use. + * @return {Vector} - The Neo4j Vector ready to be used as a query parameter + */ +export function vector (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array): Vector { + return new Vector(typedArray) +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.js new file mode 100644 index 000000000..be4e7456b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import BoltProtocolV5x8 from './bolt-protocol-v5x8.js' + +import transformersFactories from './bolt-protocol-v6x0.transformer.js' +import Transformer from './transformer.js' + +import { internal } from '../../core/index.ts' + +const { + constants: { BOLT_PROTOCOL_V6_0 } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x8 { + get version () { + return BOLT_PROTOCOL_V6_0 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js new file mode 100644 index 000000000..f71179f59 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js @@ -0,0 +1,110 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import v5x8 from './bolt-protocol-v5x8.transformer.js' +import { TypeTransformer } from './transformer.js' +import { structure } from '../packstream/index.js' +import { Vector, newError } from '../../core/index.ts' +const VECTOR = 0x56 +const FLOAT_32 = 0xc6 +const FLOAT_64 = 0xc1 +const INT_8 = 0xc8 +const INT_16 = 0xc9 +const INT_32 = 0xca +const INT_64 = 0xcb + +function createVectorTransformer () { + return new TypeTransformer({ + signature: VECTOR, + isTypeInstance: object => object instanceof Vector, + toStructure: vector => { + const startTime = new Date().getTime() + const dataview = new DataView(vector.typedArray.byteLength) + let set + let typeMarker + if (vector.type === 'INT8') { + typeMarker = Uint8Array.from([INT_8]) + set = dataview.setUint8 + } else if (vector.type === 'INT16') { + typeMarker = Uint8Array.from([INT_16]) + set = dataview.setUint16 + } else if (vector.type === 'INT32') { + typeMarker = Uint8Array.from([INT_32]) + set = dataview.setUint32 + } else if (vector.type === 'INT64') { + typeMarker = Uint8Array.from([INT_64]) + set = dataview.setUint64 + } else if (vector.type === 'FLOAT32') { + typeMarker = Uint8Array.from([FLOAT_32]) + set = dataview.setFloat32 + } else if (vector.type === 'FLOAT64') { + typeMarker = Uint8Array.from([FLOAT_64]) + set = dataview.setFloat64 + } else { + throw newError('Vector is of unsupported type') + } + for (let i = 0; i < vector.typedArray.length; i++) { + set(i * vector.typedArray.BYTES_PER_ELEMENT, vector.typedArray[i]) + } + const struct = new structure.Structure(VECTOR, [typeMarker, Uint8Array.from(dataview.buffer)]) + console.debug(`Packing vector took ${new Date().getTime() - startTime}ms`) + return struct + }, + fromStructure: structure => { + const typeMarker = structure.fields[0][0] + const byteArray = structure.fields[1] + const dataview = new DataView(byteArray.length) + let typedArray + let set + let resultArray + if (typeMarker === INT_8) { + return Int8Array.from(byteArray.buffer) + } if (typeMarker === INT_16) { + typedArray = Int16Array.from(byteArray.buffer) + resultArray = Int16Array.from(dataview.buffer) + set = dataview.setInt16 + } if (typeMarker === INT_32) { + typedArray = Int32Array.from(byteArray.buffer) + resultArray = Int32Array.from(dataview.buffer) + set = dataview.setInt32 + } if (typeMarker === INT_64) { + typedArray = BigInt64Array.from(byteArray.buffer) + resultArray = BigInt64Array.from(dataview.buffer) + set = dataview.setBigInt64 + } if (typeMarker === FLOAT_32) { + typedArray = Float32Array.from(byteArray.buffer) + resultArray = Float32Array.from(dataview.buffer) + set = dataview.setFloat32 + } if (typeMarker === FLOAT_64) { + typedArray = Float64Array.from(byteArray.buffer) + resultArray = Float64Array.from(dataview.buffer) + set = dataview.setFloat64 + } else { + throw newError('Recieved Vector of unknown type') + } + for (let i = 0; i < typedArray.length; i++) { + set(i * typedArray.BYTES_PER_ELEMENT, typedArray[i]) + } + return new Vector(resultArray) + } + }) +} + +export default { + ...v5x8, + createVectorTransformer +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js index 55f42ec04..f93f8f36c 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -33,6 +33,7 @@ import BoltProtocolV5x5 from './bolt-protocol-v5x5.js' import BoltProtocolV5x6 from './bolt-protocol-v5x6.js' import BoltProtocolV5x7 from './bolt-protocol-v5x7.js' import BoltProtocolV5x8 from './bolt-protocol-v5x8.js' +import BoltProtocolV6x0 from './bolt-protocol-v6x0.js' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel/index.js' import ResponseHandler from './response-handler.js' @@ -268,6 +269,14 @@ function createProtocol ( log, onProtocolError, serversideRouting) + case 6.0: + return new BoltProtocolV6x0(server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting) default: throw newError('Unknown Bolt protocol version: ' + version) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js index ddf9bccca..adcf1624f 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js @@ -19,7 +19,7 @@ import { alloc } from '../channel/index.js' import { newError } from '../../core/index.ts' const BOLT_MAGIC_PREAMBLE = 0x6060b017 -const AVAILABLE_BOLT_PROTOCOLS = ['5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0'] // bolt protocols the client will accept, ordered by preference +const AVAILABLE_BOLT_PROTOCOLS = ['6.0', '5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0'] // bolt protocols the client will accept, ordered by preference const DESIRED_CAPABILITES = 0 function version (major, minor) { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js index b2a31effd..cbab3c94c 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js @@ -94,17 +94,8 @@ class Packer { return () => this.packInteger(int(x)) } else if (isInt(x)) { return () => this.packInteger(x) - } else if (x.BYTES_PER_ELEMENT != null && this._useVectorTypes) { - return () => { - this.packListHeader(x.length) - for (let i = 0; i < x.length; i++) { - this.packable(x[i] === undefined ? null : x[i], dehydrateStruct)() - } - } - } else if (x instanceof Int8Array && this._useVectorTypes) { + } else if (x instanceof Int8Array) { return () => this.packBytes(x) - } else if (x instanceof ArrayBuffer) { - return () => this.packBytes(new Int8Array(x)) } else if (x instanceof Array) { return () => { this.packListHeader(x.length) diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 3a341708b..efd9cb299 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -101,6 +101,7 @@ import * as json from './json.ts' import resultTransformers, { ResultTransformer } from './result-transformers.ts' import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate.ts' import * as internal from './internal/index.ts' +import Vector, {VectorType, vector} from './vector.ts' /** * Object containing string constants representing predefined {@link Neo4jError} codes. @@ -263,7 +264,10 @@ export { notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + Vector, + VectorType, + vector } export type { diff --git a/packages/neo4j-driver-deno/lib/core/internal/constants.ts b/packages/neo4j-driver-deno/lib/core/internal/constants.ts index b45034da9..7403677c3 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/constants.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/constants.ts @@ -40,6 +40,7 @@ const BOLT_PROTOCOL_V5_5: number = 5.5 const BOLT_PROTOCOL_V5_6: number = 5.6 const BOLT_PROTOCOL_V5_7: number = 5.7 const BOLT_PROTOCOL_V5_8: number = 5.8 +const BOLT_PROTOCOL_V6_0: number = 6.0 const TELEMETRY_APIS = { MANAGED_TRANSACTION: 0, @@ -76,5 +77,6 @@ export { BOLT_PROTOCOL_V5_6, BOLT_PROTOCOL_V5_7, BOLT_PROTOCOL_V5_8, + BOLT_PROTOCOL_V6_0, TELEMETRY_APIS } diff --git a/packages/neo4j-driver-deno/lib/core/vector.ts b/packages/neo4j-driver-deno/lib/core/vector.ts new file mode 100644 index 000000000..6bbb64345 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/vector.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { newError } from './error.ts' + +export enum VectorType { + "INT8", + "INT16", + "INT32", + "INT64", + "FLOAT32", + "FLOAT64", + +} + +/** + * A wrapper class for JavaScript TypedArrays that makes the driver send them as a Vector type to the database. + * @access public + * @exports Vector + * @class A Integer class for representing a 64 bit two's-complement integer value. + * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray The TypedArray to convert to a vector + * + * @constructor + * + */ +export default class Vector { + typedArray : Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array + type: VectorType + constructor(typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array ){ + if(typedArray instanceof Int8Array) { + this.type = VectorType.INT8 + } + if(typedArray instanceof Int16Array) { + this.type = VectorType.INT16 + } + if(typedArray instanceof Int32Array) { + this.type = VectorType.INT32 + } + if(typedArray instanceof BigInt64Array) { + this.type = VectorType.INT64 + } + if(typedArray instanceof Float32Array) { + this.type = VectorType.FLOAT32 + } + if(typedArray instanceof Float64Array) { + this.type = VectorType.FLOAT64 + } + else { + throw newError("The neo4j Vector class is a wrapper for TypedArrays") + } + this.typedArray = typedArray + } +} + + +/** + * Cast a TypedArray to a {@link Vector} + * @access public + * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray - The value to use. + * @return {Vector} - The Neo4j Vector ready to be used as a query parameter + */ +export function vector(typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array ) : Vector { + return new Vector(typedArray) +} diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 911ad9fcd..1330ad597 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -78,7 +78,9 @@ import { notificationFilterMinimumSeverityLevel, staticAuthTokenManager, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + Vector, + vector } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -282,7 +284,8 @@ const types = { LocalDateTime, LocalTime, Time, - Integer + Integer, + Vector } /** @@ -402,7 +405,8 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + vector } export { @@ -474,6 +478,8 @@ export { notificationFilterDisabledCategory, notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + vector, + Vector } export default forExport diff --git a/packages/neo4j-driver/test/vector-examples.test.js b/packages/neo4j-driver/test/vector-examples.test.js index 1d9494c23..d04d9d587 100644 --- a/packages/neo4j-driver/test/vector-examples.test.js +++ b/packages/neo4j-driver/test/vector-examples.test.js @@ -42,45 +42,15 @@ describe('#integration vector type api suggestion', () => { const bufferWriter = Uint8Array.from([1, 1]) await driver.executeQuery('CREATE (p:Product) SET p.vector_from_array = $vector_from_array, p.vector_from_buffer = $vector_from_buffer', { - vector_from_array: Float32Array.from([1, 2, 3, 4]), // Typed arrays can be created from a regular list of Numbers - vector_from_buffer: new Uint8Array(bufferWriter.buffer) // Or from a bytebuffer + vector_from_array: neo4j.vector(Float32Array.from([1, 2, 3, 4])), // Typed arrays can be created from a regular list of Numbers + vector_from_buffer: neo4j.vector(new Uint8Array(bufferWriter.buffer)) // Or from a bytebuffer }) const res = await driver.executeQuery('MATCH (p:Product) RETURN p.vector_from_array as arrayVector, p.vector_from_buffer as bufferVector') - let arrayVec = res.records[0].get('arrayVector') - let bufferVec = res.records[0].get('bufferVector') - - // THE FOLLOWING 2 LINES ARE HERE TO EMULATE THE FINISHED PROPOSED API, WOULD NOT BE NEEDED IN THE FINISHED PRODUCT - arrayVec = Float32Array.from(arrayVec) - bufferVec = Uint8Array.from(bufferVec) - // END OF MOCK LINES + const arrayVec = res.records[0].get('arrayVector').typedArray + const bufferVec = res.records[0].get('bufferVector').typedArray expect(arrayVec[0]).toBe(1) expect(bufferVec[1]).toBe(1) }) - - it('write and read bytes', async () => { - const driver = driverGlobal - - const bufferWriter = Int8Array.from([1, 1]) - await driver.executeQuery('CREATE (p:Product) SET p.bytes = $bytes', { - bytes: bufferWriter.buffer // New way to write and read bytes, as Int8Arrays are now interpreted as Vector - }) - const res = await driver.executeQuery('MATCH (p:Product) RETURN p.bytes as bytes') - const bytes = res.records[0].get('bytes') - - expect(new Int8Array(bytes)).toEqual(bufferWriter) - }) - - it('write TypedArray as List', async () => { - const driver = driverGlobal - - const float32 = Float32Array.from([1, 1]) - await driver.executeQuery('CREATE (p:Product) SET p.arr = $arr', { - arr: Array.from(float32) // converts the TypedArray to a standard array. - }) - const res = await driver.executeQuery('MATCH (p:Product) RETURN p.arr as arr') - - expect(Float32Array.from(res.records[0].get('arr'))).toEqual(float32) - }) }) diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index aa847e062..e30a8a80e 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -28,6 +28,7 @@ const features = [ 'Feature:Bolt:5.6', 'Feature:Bolt:5.7', 'Feature:Bolt:5.8', + 'Feature:Bolt:6.0', 'Feature:Bolt:HandshakeManifestV1', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', From 34e513228153c5df79d9cfc976e06b763a5f0070 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:02:24 +0200 Subject: [PATCH 06/13] remove feature bolt 6.0 --- packages/testkit-backend/src/feature/common.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index e30a8a80e..aa847e062 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -28,7 +28,6 @@ const features = [ 'Feature:Bolt:5.6', 'Feature:Bolt:5.7', 'Feature:Bolt:5.8', - 'Feature:Bolt:6.0', 'Feature:Bolt:HandshakeManifestV1', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', From 0707d3f9719c21d4025c5e834bdb2d1c9a4dec26 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:13:36 +0200 Subject: [PATCH 07/13] remove useVectorTypes --- .../src/bolt/bolt-protocol-v1.js | 13 ++-- .../src/bolt/bolt-protocol-v2.js | 4 +- packages/bolt-connection/src/bolt/create.js | 4 +- .../src/connection/connection-channel.js | 1 - .../src/packstream/packstream-v1.js | 9 +-- .../src/packstream/packstream-v2.js | 4 +- packages/core/src/types.ts | 13 ---- .../bolt-connection/bolt/bolt-protocol-v1.js | 13 ++-- .../bolt-connection/bolt/bolt-protocol-v2.js | 4 +- .../lib/bolt-connection/bolt/create.js | 4 +- .../connection/connection-channel.js | 1 - .../packstream/packstream-v1.js | 9 +-- .../packstream/packstream-v2.js | 4 +- packages/neo4j-driver-deno/lib/core/index.ts | 2 +- packages/neo4j-driver-deno/lib/core/types.ts | 13 ---- packages/neo4j-driver-deno/lib/core/vector.ts | 68 +++++++++---------- 16 files changed, 60 insertions(+), 106 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js index 98ca40511..d04b33b4e 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js @@ -68,7 +68,6 @@ export default class BoltProtocol { * @param {Object} packstreamConfig Packstream configuration * @param {boolean} packstreamConfig.disableLosslessIntegers if this connection should convert all received integers to native JS numbers. * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. - * @param {boolean} packstreamConfig.useVectorTypes if this connection should support vector types and treat TypedArras as vectors. * @param {CreateResponseHandler} createResponseHandler Function which creates the response handler * @param {Logger} log the logger * @param {OnProtocolError} onProtocolError handles protocol errors @@ -76,21 +75,21 @@ export default class BoltProtocol { constructor ( server, chunker, - { disableLosslessIntegers, useBigInt, useVectorTypes } = {}, + { disableLosslessIntegers, useBigInt } = {}, createResponseHandler = () => null, log, onProtocolError ) { this._server = server || {} this._chunker = chunker - this._packer = this._createPacker(chunker, useVectorTypes) - this._unpacker = this._createUnpacker(disableLosslessIntegers, useBigInt, useVectorTypes) + this._packer = this._createPacker(chunker) + this._unpacker = this._createUnpacker(disableLosslessIntegers, useBigInt) this._responseHandler = createResponseHandler(this) this._log = log this._onProtocolError = onProtocolError this._fatalError = null this._lastMessageSignature = null - this._config = { disableLosslessIntegers, useBigInt, useVectorTypes } + this._config = { disableLosslessIntegers, useBigInt } } get transformer () { @@ -489,8 +488,8 @@ export default class BoltProtocol { return new v1.Packer(chunker) } - _createUnpacker (disableLosslessIntegers, useBigInt, useVectorTypes) { - return new v1.Unpacker(disableLosslessIntegers, useBigInt, useVectorTypes) + _createUnpacker (disableLosslessIntegers, useBigInt) { + return new v1.Unpacker(disableLosslessIntegers, useBigInt) } /** diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v2.js b/packages/bolt-connection/src/bolt/bolt-protocol-v2.js index 3c35ba590..d7c0e930b 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v2.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v2.js @@ -29,8 +29,8 @@ export default class BoltProtocol extends BoltProtocolV1 { return new v2.Packer(chunker) } - _createUnpacker (disableLosslessIntegers, useBigInt, useVectorTypes) { - return new v2.Unpacker(disableLosslessIntegers, useBigInt, useVectorTypes) + _createUnpacker (disableLosslessIntegers, useBigInt) { + return new v2.Unpacker(disableLosslessIntegers, useBigInt) } get transformer () { diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index bacb963b3..18891e197 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -50,7 +50,6 @@ import ResponseHandler from './response-handler' * @param {ResponseHandler~Observer} config.observer Observer * @param {boolean} config.disableLosslessIntegers Disable the lossless integers * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. - * @param {boolean} packstreamConfig.useVectorTypes if this connection should support vector types and treat TypedArras as vectors. * @param {boolean} config.serversideRouting It's using server side routing */ export default function create ({ @@ -60,7 +59,6 @@ export default function create ({ channel, disableLosslessIntegers, useBigInt, - useVectorTypes, serversideRouting, server, // server info log, @@ -96,7 +94,7 @@ export default function create ({ version, server, chunker, - { disableLosslessIntegers, useBigInt, useVectorTypes }, + { disableLosslessIntegers, useBigInt }, serversideRouting, createResponseHandler, observer.onProtocolError.bind(observer), diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index ec6df1a3e..b57542462 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -69,7 +69,6 @@ export function createChannelConnection ( dechunker, disableLosslessIntegers: config.disableLosslessIntegers, useBigInt: config.useBigInt, - useVectorTypes: config.useVectorTypes, serversideRouting, server: conn.server, log: conn.logger, diff --git a/packages/bolt-connection/src/packstream/packstream-v1.js b/packages/bolt-connection/src/packstream/packstream-v1.js index 8a18663e7..6416f0868 100644 --- a/packages/bolt-connection/src/packstream/packstream-v1.js +++ b/packages/bolt-connection/src/packstream/packstream-v1.js @@ -324,10 +324,9 @@ class Unpacker { * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint */ - constructor (disableLosslessIntegers = false, useBigInt = false, useVectorTypes = true) { + constructor (disableLosslessIntegers = false, useBigInt = false) { this._disableLosslessIntegers = disableLosslessIntegers this._useBigInt = useBigInt - this._useVectorTypes = useVectorTypes } unpack (buffer, hydrateStructure = functional.identity) { @@ -487,11 +486,7 @@ class Unpacker { for (let i = 0; i < size; i++) { value[i] = buffer.readInt8() } - if (this._useVectorTypes) { - return value.buffer - } else { - return value - } + return value } _unpackMap (marker, markerHigh, markerLow, buffer, hydrateStructure) { diff --git a/packages/bolt-connection/src/packstream/packstream-v2.js b/packages/bolt-connection/src/packstream/packstream-v2.js index 5d9a52f63..b083264d5 100644 --- a/packages/bolt-connection/src/packstream/packstream-v2.js +++ b/packages/bolt-connection/src/packstream/packstream-v2.js @@ -29,7 +29,7 @@ export class Unpacker extends v1.Unpacker { * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint */ - constructor (disableLosslessIntegers = false, useBigInt = false, useVectorTypes = true) { - super(disableLosslessIntegers, useBigInt, useVectorTypes) + constructor (disableLosslessIntegers = false, useBigInt = false) { + super(disableLosslessIntegers, useBigInt) } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 56a2219ae..8f60394a3 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -77,7 +77,6 @@ export class Config { disableLosslessIntegers?: boolean notificationFilter?: NotificationFilter useBigInt?: boolean - useVectorTypes?: boolean logging?: LoggingConfig resolver?: (address: string) => string[] | Promise userAgent?: string @@ -263,18 +262,6 @@ export class Config { */ this.useBigInt = false - /** - * Enables Vector types being sent over bolt, and makes all TypedArrays be interpreted as vectors - * - * **Warning:** This makes TypedArrays be sent as Vectors rather than Lists, and requires bytes be sent as ArrayBuffers rather than Int8Arrays - * If migrating from 5.x drivers and vector types are not needed, this can be disabled to make migration simpler. - * - * **Default**: ```true``` - * - * @type {boolean|undefined} - */ - this.useVectorTypes = true - /** * Specify the logging configuration for the driver. Object should have two properties `level` and `logger`. * diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js index 0ffd415f3..a83f32f5a 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js @@ -68,7 +68,6 @@ export default class BoltProtocol { * @param {Object} packstreamConfig Packstream configuration * @param {boolean} packstreamConfig.disableLosslessIntegers if this connection should convert all received integers to native JS numbers. * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. - * @param {boolean} packstreamConfig.useVectorTypes if this connection should support vector types and treat TypedArras as vectors. * @param {CreateResponseHandler} createResponseHandler Function which creates the response handler * @param {Logger} log the logger * @param {OnProtocolError} onProtocolError handles protocol errors @@ -76,21 +75,21 @@ export default class BoltProtocol { constructor ( server, chunker, - { disableLosslessIntegers, useBigInt, useVectorTypes } = {}, + { disableLosslessIntegers, useBigInt } = {}, createResponseHandler = () => null, log, onProtocolError ) { this._server = server || {} this._chunker = chunker - this._packer = this._createPacker(chunker, useVectorTypes) - this._unpacker = this._createUnpacker(disableLosslessIntegers, useBigInt, useVectorTypes) + this._packer = this._createPacker(chunker) + this._unpacker = this._createUnpacker(disableLosslessIntegers, useBigInt) this._responseHandler = createResponseHandler(this) this._log = log this._onProtocolError = onProtocolError this._fatalError = null this._lastMessageSignature = null - this._config = { disableLosslessIntegers, useBigInt, useVectorTypes } + this._config = { disableLosslessIntegers, useBigInt } } get transformer () { @@ -489,8 +488,8 @@ export default class BoltProtocol { return new v1.Packer(chunker) } - _createUnpacker (disableLosslessIntegers, useBigInt, useVectorTypes) { - return new v1.Unpacker(disableLosslessIntegers, useBigInt, useVectorTypes) + _createUnpacker (disableLosslessIntegers, useBigInt) { + return new v1.Unpacker(disableLosslessIntegers, useBigInt) } /** diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js index cfb4fc666..d0e6757f2 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js @@ -29,8 +29,8 @@ export default class BoltProtocol extends BoltProtocolV1 { return new v2.Packer(chunker) } - _createUnpacker (disableLosslessIntegers, useBigInt, useVectorTypes) { - return new v2.Unpacker(disableLosslessIntegers, useBigInt, useVectorTypes) + _createUnpacker (disableLosslessIntegers, useBigInt) { + return new v2.Unpacker(disableLosslessIntegers, useBigInt) } get transformer () { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js index f93f8f36c..cea9b1dd4 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -50,7 +50,6 @@ import ResponseHandler from './response-handler.js' * @param {ResponseHandler~Observer} config.observer Observer * @param {boolean} config.disableLosslessIntegers Disable the lossless integers * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. - * @param {boolean} packstreamConfig.useVectorTypes if this connection should support vector types and treat TypedArras as vectors. * @param {boolean} config.serversideRouting It's using server side routing */ export default function create ({ @@ -60,7 +59,6 @@ export default function create ({ channel, disableLosslessIntegers, useBigInt, - useVectorTypes, serversideRouting, server, // server info log, @@ -96,7 +94,7 @@ export default function create ({ version, server, chunker, - { disableLosslessIntegers, useBigInt, useVectorTypes }, + { disableLosslessIntegers, useBigInt }, serversideRouting, createResponseHandler, observer.onProtocolError.bind(observer), diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 3e2ff44d1..7fcf4689c 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -69,7 +69,6 @@ export function createChannelConnection ( dechunker, disableLosslessIntegers: config.disableLosslessIntegers, useBigInt: config.useBigInt, - useVectorTypes: config.useVectorTypes, serversideRouting, server: conn.server, log: conn.logger, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js index cbab3c94c..8730501aa 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js @@ -324,10 +324,9 @@ class Unpacker { * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint */ - constructor (disableLosslessIntegers = false, useBigInt = false, useVectorTypes = true) { + constructor (disableLosslessIntegers = false, useBigInt = false) { this._disableLosslessIntegers = disableLosslessIntegers this._useBigInt = useBigInt - this._useVectorTypes = useVectorTypes } unpack (buffer, hydrateStructure = functional.identity) { @@ -487,11 +486,7 @@ class Unpacker { for (let i = 0; i < size; i++) { value[i] = buffer.readInt8() } - if (this._useVectorTypes) { - return value.buffer - } else { - return value - } + return value } _unpackMap (marker, markerHigh, markerLow, buffer, hydrateStructure) { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js index 33d0c78e8..19e480919 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js @@ -29,7 +29,7 @@ export class Unpacker extends v1.Unpacker { * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint */ - constructor (disableLosslessIntegers = false, useBigInt = false, useVectorTypes = true) { - super(disableLosslessIntegers, useBigInt, useVectorTypes) + constructor (disableLosslessIntegers = false, useBigInt = false) { + super(disableLosslessIntegers, useBigInt) } } diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index efd9cb299..06db9cba1 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -101,7 +101,7 @@ import * as json from './json.ts' import resultTransformers, { ResultTransformer } from './result-transformers.ts' import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate.ts' import * as internal from './internal/index.ts' -import Vector, {VectorType, vector} from './vector.ts' +import Vector, { VectorType, vector } from './vector.ts' /** * Object containing string constants representing predefined {@link Neo4jError} codes. diff --git a/packages/neo4j-driver-deno/lib/core/types.ts b/packages/neo4j-driver-deno/lib/core/types.ts index 989b0d202..7cdc1e747 100644 --- a/packages/neo4j-driver-deno/lib/core/types.ts +++ b/packages/neo4j-driver-deno/lib/core/types.ts @@ -77,7 +77,6 @@ export class Config { disableLosslessIntegers?: boolean notificationFilter?: NotificationFilter useBigInt?: boolean - useVectorTypes?: boolean logging?: LoggingConfig resolver?: (address: string) => string[] | Promise userAgent?: string @@ -263,18 +262,6 @@ export class Config { */ this.useBigInt = false - /** - * Enables Vector types being sent over bolt, and makes all TypedArrays be interpreted as vectors - * - * **Warning:** This makes TypedArrays be sent as Vectors rather than Lists, and requires bytes be sent as ArrayBuffers rather than Int8Arrays - * If migrating from 5.x drivers and vector types are not needed, this can be disabled to make migration simpler. - * - * **Default**: ```true``` - * - * @type {boolean|undefined} - */ - this.useVectorTypes = true - /** * Specify the logging configuration for the driver. Object should have two properties `level` and `logger`. * diff --git a/packages/neo4j-driver-deno/lib/core/vector.ts b/packages/neo4j-driver-deno/lib/core/vector.ts index 6bbb64345..0c63ed173 100644 --- a/packages/neo4j-driver-deno/lib/core/vector.ts +++ b/packages/neo4j-driver-deno/lib/core/vector.ts @@ -18,12 +18,12 @@ import { newError } from './error.ts' export enum VectorType { - "INT8", - "INT16", - "INT32", - "INT64", - "FLOAT32", - "FLOAT64", + 'INT8', + 'INT16', + 'INT32', + 'INT64', + 'FLOAT32', + 'FLOAT64', } @@ -35,44 +35,42 @@ export enum VectorType { * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray The TypedArray to convert to a vector * * @constructor - * + * */ export default class Vector { - typedArray : Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array - type: VectorType - constructor(typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array ){ - if(typedArray instanceof Int8Array) { - this.type = VectorType.INT8 - } - if(typedArray instanceof Int16Array) { - this.type = VectorType.INT16 - } - if(typedArray instanceof Int32Array) { - this.type = VectorType.INT32 - } - if(typedArray instanceof BigInt64Array) { - this.type = VectorType.INT64 - } - if(typedArray instanceof Float32Array) { - this.type = VectorType.FLOAT32 - } - if(typedArray instanceof Float64Array) { - this.type = VectorType.FLOAT64 - } - else { - throw newError("The neo4j Vector class is a wrapper for TypedArrays") - } - this.typedArray = typedArray + typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array + type: VectorType + constructor (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array) { + if (typedArray instanceof Int8Array) { + this.type = VectorType.INT8 + } + if (typedArray instanceof Int16Array) { + this.type = VectorType.INT16 + } + if (typedArray instanceof Int32Array) { + this.type = VectorType.INT32 + } + if (typedArray instanceof BigInt64Array) { + this.type = VectorType.INT64 } + if (typedArray instanceof Float32Array) { + this.type = VectorType.FLOAT32 + } + if (typedArray instanceof Float64Array) { + this.type = VectorType.FLOAT64 + } else { + throw newError('The neo4j Vector class is a wrapper for TypedArrays') + } + this.typedArray = typedArray + } } - /** * Cast a TypedArray to a {@link Vector} * @access public * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray - The value to use. * @return {Vector} - The Neo4j Vector ready to be used as a query parameter */ -export function vector(typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array ) : Vector { - return new Vector(typedArray) +export function vector (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array): Vector { + return new Vector(typedArray) } From 0d603c8671eae1e8097a99852a52b0e3512c54f4 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:27:57 +0200 Subject: [PATCH 08/13] 6.0 bolt with vector support passing tests --- .../bolt/bolt-protocol-v6x0.transformer.js | 118 +- .../bolt-protocol-v6x0.test.js.snap | 61 + .../test/bolt/bolt-protocol-v6x0.test.js | 1606 +++++++++++++++++ packages/core/src/index.ts | 4 +- packages/core/src/vector.ts | 49 +- .../bolt/bolt-protocol-v6x0.transformer.js | 118 +- packages/neo4j-driver-deno/lib/core/index.ts | 4 +- packages/neo4j-driver-deno/lib/core/vector.ts | 44 +- 8 files changed, 1851 insertions(+), 153 deletions(-) create mode 100644 packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v6x0.test.js.snap create mode 100644 packages/bolt-connection/test/bolt/bolt-protocol-v6x0.test.js diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js index a76a2b3c8..daa532d34 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js @@ -32,72 +32,84 @@ function createVectorTransformer () { signature: VECTOR, isTypeInstance: object => object instanceof Vector, toStructure: vector => { - const startTime = new Date().getTime() - const dataview = new DataView(vector.typedArray.byteLength) + const dataview = new DataView(new ArrayBuffer(vector.typedArray.byteLength)) let set let typeMarker - if (vector.type === 'INT8') { - typeMarker = Uint8Array.from([INT_8]) - set = dataview.setUint8 - } else if (vector.type === 'INT16') { - typeMarker = Uint8Array.from([INT_16]) - set = dataview.setUint16 - } else if (vector.type === 'INT32') { - typeMarker = Uint8Array.from([INT_32]) - set = dataview.setUint32 - } else if (vector.type === 'INT64') { - typeMarker = Uint8Array.from([INT_64]) - set = dataview.setUint64 - } else if (vector.type === 'FLOAT32') { - typeMarker = Uint8Array.from([FLOAT_32]) - set = dataview.setFloat32 - } else if (vector.type === 'FLOAT64') { - typeMarker = Uint8Array.from([FLOAT_64]) - set = dataview.setFloat64 - } else { - throw newError('Vector is of unsupported type') + switch (vector.type) { + case 'INT8': + typeMarker = Uint8Array.from([INT_8]) + set = dataview.setUint8.bind(dataview) + break + case 'INT16': + typeMarker = Uint8Array.from([INT_16]) + set = dataview.setUint16.bind(dataview) + break + case 'INT32': + typeMarker = Uint8Array.from([INT_32]) + set = dataview.setUint32.bind(dataview) + break + case 'INT64': + typeMarker = Uint8Array.from([INT_64]) + set = dataview.setBigInt64.bind(dataview) + break + case 'FLOAT32': + typeMarker = Uint8Array.from([FLOAT_32]) + set = dataview.setFloat32.bind(dataview) + break + case 'FLOAT64': + typeMarker = Uint8Array.from([FLOAT_64]) + set = dataview.setFloat64.bind(dataview) + break + default: + throw newError(`Vector is of unsupported type ${vector.type}`) } for (let i = 0; i < vector.typedArray.length; i++) { set(i * vector.typedArray.BYTES_PER_ELEMENT, vector.typedArray[i]) } - const struct = new structure.Structure(VECTOR, [typeMarker, Uint8Array.from(dataview.buffer)]) - console.debug(`Packing vector took ${new Date().getTime() - startTime}ms`) + const struct = new structure.Structure(VECTOR, [typeMarker, new Int8Array(dataview.buffer)]) return struct }, fromStructure: structure => { const typeMarker = structure.fields[0][0] - const byteArray = structure.fields[1] - const dataview = new DataView(byteArray.length) - let typedArray + const arrayBuffer = structure.fields[1] + const setview = new DataView(new ArrayBuffer(arrayBuffer.byteLength)) + const getview = new DataView(arrayBuffer.buffer) + let get let set let resultArray - if (typeMarker === INT_8) { - return Int8Array.from(byteArray.buffer) - } if (typeMarker === INT_16) { - typedArray = Int16Array.from(byteArray.buffer) - resultArray = Int16Array.from(dataview.buffer) - set = dataview.setInt16 - } if (typeMarker === INT_32) { - typedArray = Int32Array.from(byteArray.buffer) - resultArray = Int32Array.from(dataview.buffer) - set = dataview.setInt32 - } if (typeMarker === INT_64) { - typedArray = BigInt64Array.from(byteArray.buffer) - resultArray = BigInt64Array.from(dataview.buffer) - set = dataview.setBigInt64 - } if (typeMarker === FLOAT_32) { - typedArray = Float32Array.from(byteArray.buffer) - resultArray = Float32Array.from(dataview.buffer) - set = dataview.setFloat32 - } if (typeMarker === FLOAT_64) { - typedArray = Float64Array.from(byteArray.buffer) - resultArray = Float64Array.from(dataview.buffer) - set = dataview.setFloat64 - } else { - throw newError('Recieved Vector of unknown type') + switch (typeMarker) { + case INT_8: + return new Vector(Int8Array.from(arrayBuffer)) + case INT_16: + resultArray = new Int16Array(setview.buffer) + get = getview.getInt16.bind(getview) + set = setview.setInt16.bind(setview) + break + case INT_32: + resultArray = new Int32Array(setview.buffer) + get = getview.getInt32.bind(getview) + set = setview.setInt32.bind(setview) + break + case INT_64: + resultArray = new BigInt64Array(setview.buffer) + get = getview.getBigInt64.bind(getview) + set = setview.setBigInt64.bind(setview) + break + case FLOAT_32: + resultArray = new Float32Array(setview.buffer) + get = getview.getFloat32.bind(getview) + set = setview.setFloat32.bind(setview) + break + case FLOAT_64: + resultArray = new Float64Array(setview.buffer) + get = getview.getFloat64.bind(getview) + set = setview.setFloat64.bind(setview) + break + default: + throw newError(`Recieved Vector of unknown type ${typeMarker}`) } - for (let i = 0; i < typedArray.length; i++) { - set(i * typedArray.BYTES_PER_ELEMENT, typedArray[i]) + for (let i = 0; i < arrayBuffer.length; i += resultArray.BYTES_PER_ELEMENT) { + set(i, get(i), true) } return new Vector(resultArray) } diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v6x0.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v6x0.test.js.snap new file mode 100644 index 000000000..a56e2c5af --- /dev/null +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v6x0.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; + +exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; + +exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; + +exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Date with more fields) 1`] = `"Wrong struct size for Date, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Duration with less fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Duration with more fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalDateTime with less fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalDateTime with more fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalTime with less fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalTime with more fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Node with less fields) 1`] = `"Wrong struct size for Node, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Node with more fields) 1`] = `"Wrong struct size for Node, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Path with less fields) 1`] = `"Wrong struct size for Path, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Path with more fields) 1`] = `"Wrong struct size for Path, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point with less fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point with more fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point3D with less fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point3D with more fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Relationship with less fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 5"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Relationship with more fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 9"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Time with less fields) 1`] = `"Wrong struct size for Time, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Time with more fileds) 1`] = `"Wrong struct size for Time, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (UnboundRelationship with less fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (UnboundRelationship with more fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 5"`; diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v6x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v6x0.test.js new file mode 100644 index 000000000..afc7fbf61 --- /dev/null +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v6x0.test.js @@ -0,0 +1,1606 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BoltProtocolV6x0 from '../../src/bolt/bolt-protocol-v6x0' +import RequestMessage from '../../src/bolt/request-message' +import { v2, structure } from '../../src/packstream' +import utils from '../test-utils' +import { LoginObserver, RouteObserver } from '../../src/bolt/stream-observers' +import fc from 'fast-check' +import { + Date, + DateTime, + Duration, + LocalDateTime, + LocalTime, + Path, + PathSegment, + Point, + Relationship, + Time, + UnboundRelationship, + Node, + internal, + vector, + json +} from 'neo4j-driver-core' + +import { alloc } from '../../src/channel' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' + +const WRITE = 'WRITE' + +const { + txConfig: { TxConfig }, + bookmarks: { Bookmarks }, + logger: { Logger }, + temporalUtil +} = internal + +describe('#unit BoltProtocolV6x0', () => { + beforeEach(() => { + expect.extend(utils.matchers) + }) + + telemetryBehaviour.protocolSupportsTelemetry(newProtocol) + + it('should enrich error metadata', () => { + const protocol = new BoltProtocolV6x0() + const enrichedData = protocol.enrichErrorMetadata({ neo4j_code: 'hello', diagnostic_record: {} }) + expect(enrichedData.code).toBe('hello') + expect(enrichedData.diagnostic_record.OPERATION).toBe('') + expect(enrichedData.diagnostic_record.OPERATION_CODE).toBe('0') + expect(enrichedData.diagnostic_record.CURRENT_SCHEMA).toBe('/') + }) + + it('should request routing information', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + const routingContext = { someContextParam: 'value' } + const databaseName = 'name' + + const observer = protocol.requestRoutingInformation({ + routingContext, + databaseName + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.routeV4x4(routingContext, [], { databaseName, impersonatedUser: null }) + ) + expect(protocol.observers).toEqual([observer]) + expect(observer).toEqual(expect.any(RouteObserver)) + expect(protocol.flushes).toEqual([true]) + }) + + it('should request routing information sending bookmarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + const routingContext = { someContextParam: 'value' } + const listOfBookmarks = ['a', 'b', 'c'] + const bookmarks = new Bookmarks(listOfBookmarks) + const databaseName = 'name' + + const observer = protocol.requestRoutingInformation({ + routingContext, + databaseName, + sessionContext: { bookmarks } + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.routeV4x4(routingContext, listOfBookmarks, { databaseName, impersonatedUser: null }) + ) + expect(protocol.observers).toEqual([observer]) + expect(observer).toEqual(expect.any(RouteObserver)) + expect(protocol.flushes).toEqual([true]) + }) + + it('should run a query', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it('should run a with impersonated user', () => { + const database = 'testdb' + const impersonatedUser = 'the impostor' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it('should begin a transaction', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should begin a transaction with impersonated user', () => { + const database = 'testdb' + const impersonatedUser = 'the impostor' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE, impersonatedUser }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should return correct bolt version number', () => { + const protocol = new BoltProtocolV6x0(null, null, false) + + expect(protocol.version).toBe(6.0) + }) + + it('should update metadata', () => { + const metadata = { t_first: 1, t_last: 2, db_hits: 3, some_other_key: 4 } + const protocol = new BoltProtocolV6x0(null, null, false) + + const transformedMetadata = protocol.transformMetadata(metadata) + + expect(transformedMetadata).toEqual({ + result_available_after: 1, + result_consumed_after: 2, + db_hits: 3, + some_other_key: 4 + }) + }) + + it('should initialize connection', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const boltAgent = { + product: 'neo4j-javascript/5.28', + platform: 'netbsd 1.1.1; Some arch', + languageDetails: 'Node/16.0.1 (v8 1.7.0)' + } + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, boltAgent, authToken }) + + protocol.verifyMessageCount(2) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x3(clientName, boltAgent) + ) + expect(protocol.messages[1]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers.length).toBe(2) + + // hello observer + const helloObserver = protocol.observers[0] + expect(helloObserver).toBeInstanceOf(LoginObserver) + expect(helloObserver).not.toBe(observer) + + // login observer + const loginObserver = protocol.observers[1] + expect(loginObserver).toBeInstanceOf(LoginObserver) + expect(loginObserver).toBe(observer) + + expect(protocol.flushes).toEqual([false, true]) + }) + + it.each([ + 'javascript-driver/6.0.0', + '', + undefined, + null + ])('should always use the user agent set by the user', (userAgent) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const boltAgent = { + product: 'neo4j-javascript/5.28', + platform: 'netbsd 1.1.1; Some arch', + languageDetails: 'Node/16.0.1 (v8 1.7.0)' + } + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent, boltAgent, authToken }) + + protocol.verifyMessageCount(2) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x3(userAgent, boltAgent) + ) + expect(protocol.messages[1]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers.length).toBe(2) + + // hello observer + const helloObserver = protocol.observers[0] + expect(helloObserver).toBeInstanceOf(LoginObserver) + expect(helloObserver).not.toBe(observer) + + // login observer + const loginObserver = protocol.observers[1] + expect(loginObserver).toBeInstanceOf(LoginObserver) + expect(loginObserver).toBe(observer) + + expect(protocol.flushes).toEqual([false, true]) + }) + + it.each( + [true, false] + )('should logon to the server [flush=%s]', (flush) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.logon({ authToken, flush }) + + protocol.verifyMessageCount(1) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([flush]) + }) + + it.each( + [true, false] + )('should logoff from the server [flush=%s]', (flush) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.logoff({ flush }) + + protocol.verifyMessageCount(1) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.logoff() + ) + + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([flush]) + }) + + it('should begin a transaction', () => { + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, mode: WRITE }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should commit', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.commitTransaction() + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage(RequestMessage.commit()) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should rollback', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.rollbackTransaction() + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage(RequestMessage.rollback()) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should support logoff', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + + expect(protocol.supportsReAuth).toBe(true) + }) + + describe('unpacker configuration', () => { + test.each([ + [false, false], + [false, true], + [true, false], + [true, true] + ])( + 'should create unpacker with disableLosslessIntegers=%p and useBigInt=%p', + (disableLosslessIntegers, useBigInt) => { + const protocol = new BoltProtocolV6x0(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) + + describe('notificationFilter', () => { + notificationFilterBehaviour.shouldSupportGqlNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldSupportGqlNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldSupportGqlNotificationFilterOnRun(newProtocol) + }) + + describe('watermarks', () => { + it('.run() should configure watermarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = utils.spyProtocolWrite( + new BoltProtocolV6x0(recorder, null, false) + ) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + const observer = protocol.run(query, parameters, { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + lowRecordWatermark: 100, + highRecordWatermark: 200 + }) + + expect(observer._lowRecordWatermark).toEqual(100) + expect(observer._highRecordWatermark).toEqual(200) + }) + }) + + describe('packstream', () => { + it('should configure v2 packer', () => { + const protocol = new BoltProtocolV6x0(null, null, false) + expect(protocol.packer()).toBeInstanceOf(v2.Packer) + }) + + it('should configure v2 unpacker', () => { + const protocol = new BoltProtocolV6x0(null, null, false) + expect(protocol.unpacker()).toBeInstanceOf(v2.Unpacker) + }) + }) + + describe('.packable()', () => { + it.each([ + ['Node', new Node(1, ['a'], { a: 'b' }, 'c')], + ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], + ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], + ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] + ])('should resultant function not pack graph types (%s)', (_, graphType) => { + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + null, + false + ) + + const packable = protocol.packable(graphType) + + expect(packable).toThrowErrorMatchingSnapshot() + }) + + it.each([ + ['Duration', new Duration(1, 1, 1, 1)], + ['LocalTime', new LocalTime(1, 1, 1, 1)], + ['Time', new Time(1, 1, 1, 1, 1)], + ['Date', new Date(1, 1, 1)], + ['LocalDateTime', new LocalDateTime(1, 1, 1, 1, 1, 1, 1)], + [ + 'DateTimeWithZoneOffset', + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneOffset / 1978', + new DateTime(1978, 12, 16, 10, 5, 59, 128000987, -150 * 60) + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CET', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CEST', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 1 * 60 * 60, 'Europe/Berlin') + ], + ['Point2D', new Point(1, 1, 1)], + ['Point3D', new Point(1, 1, 1, 1)] + ])('should pack spatial types and temporal types (%s)', (_, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked).toEqual(object) + }) + + it.each([ + ['Int8', vector(Int8Array.from([1, 2, 3]))], + ['Int16', vector(Int16Array.from([1, 2, 3]))], + ['Int32', vector(Int32Array.from([1, 2, 3]))], + ['Int64', vector(BigInt64Array.from([BigInt(1), BigInt(2), BigInt(3)]))], + ['Float32', vector(Float32Array.from([1, 2, 3]))], + ['Float64', vector(Float64Array.from([1, 2, 3]))] + ])('should pack vectors (%s)', (_, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + {} + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(json.stringify(unpacked)).toEqual(json.stringify(object)) + }) + + it.each([ + [ + 'DateTimeWithZoneId / Australia', + new DateTime(2022, 6, 15, 15, 21, 18, 183_000_000, undefined, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId', + new DateTime(2022, 6, 22, 15, 21, 18, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CEST', + new DateTime(2022, 3, 27, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CEST', + new DateTime(2022, 3, 27, 0, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CEST', + new DateTime(2022, 3, 27, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CEST', + new DateTime(2022, 3, 27, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CET', + new DateTime(2022, 10, 30, 2, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CET', + new DateTime(2022, 10, 30, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CET', + new DateTime(2022, 10, 30, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CET', + new DateTime(2022, 10, 30, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn summer time', + new DateTime(2018, 11, 4, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn summer time', + new DateTime(2018, 11, 4, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn summer time', + new DateTime(2018, 11, 5, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn summer time', + new DateTime(2018, 11, 5, 2, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn winter time', + new DateTime(2019, 2, 17, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn winter time', + new DateTime(2019, 2, 17, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn winter time', + new DateTime(2019, 2, 18, 0, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn winter time', + new DateTime(2019, 2, 18, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Istanbul', + new DateTime(1978, 12, 16, 12, 35, 59, 128000987, undefined, 'Europe/Istanbul') + ], + [ + 'DateTimeWithZoneId / Istanbul', + new DateTime(2020, 6, 15, 4, 30, 0, 183_000_000, undefined, 'Pacific/Honolulu') + ], + [ + 'DateWithWithZoneId / Berlin before common era', + new DateTime(-2020, 6, 15, 4, 30, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateWithWithZoneId / Max Date', + new DateTime(99_999, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Kiritimati') + ], + [ + 'DateWithWithZoneId / Min Date', + new DateTime(-99_999, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Samoa') + ], + [ + 'DateWithWithZoneId / Ambiguous date between 00 and 99', + new DateTime(50, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Samoa') + ] + ])('should pack and unpack DateTimeWithZoneId and without offset (%s)', (_, object) => { + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + + it('should pack and unpack DateTimeWithOffset', () => { + fc.assert( + fc.property( + fc.date({ + min: temporalUtil.newDate(utils.MIN_UTC_IN_MS + utils.ONE_DAY_IN_MS), + max: temporalUtil.newDate(utils.MAX_UTC_IN_MS - utils.ONE_DAY_IN_MS) + }), + fc.integer({ min: 0, max: 999_999 }), + utils.arbitraryTimeZoneId(), + (date, nanoseconds, timeZoneId) => { + const object = new DateTime( + date.getUTCFullYear(), + date.getUTCMonth() + 1, + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds() * 1_000_000 + nanoseconds, + undefined, + timeZoneId + ) + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + ) + }) + + it('should pack and unpack DateTimeWithZoneIdAndNoOffset', () => { + fc.assert( + fc.property(fc.date(), date => { + const object = DateTime.fromStandardDate(date) + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + expect(unpacked).toEqual(object) + }) + ) + }) + }) + + describe('.unpack()', () => { + it.each([ + [ + 'Node', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }, 'elementId']), + new Node(1, ['a'], { c: 'd' }, 'elementId') + ], + [ + 'Relationship', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }, 'elementId', 'node1', 'node2']), + new Relationship(1, 2, 3, '4', { 5: 6 }, 'elementId', 'node1', 'node2') + ], + [ + 'UnboundRelationship', + new structure.Structure(0x72, [1, '2', { 3: 4 }, 'elementId']), + new UnboundRelationship(1, '2', { 3: 4 }, 'elementId') + ], + [ + 'Path', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }, 'node1']), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }, 'node2']), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }, 'node3']) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'reltype1', { 4: '5' }, 'rel1', 'node1', 'node2']), + new structure.Structure(0x52, [5, 4, 2, 'reltype2', { 6: 7 }, 'rel2', 'node2', 'node3']) + ], + [1, 1, 2, 2] + ] + ), + new Path( + new Node(1, ['2'], { 3: '4' }, 'node1'), + new Node(2, ['3'], { 4: '5' }, 'node3'), + [ + new PathSegment( + new Node(1, ['2'], { 3: '4' }, 'node1'), + new Relationship(3, 1, 4, 'reltype1', { 4: '5' }, 'rel1', 'node1', 'node2'), + new Node(4, ['5'], { 6: 7 }, 'node2') + ), + new PathSegment( + new Node(4, ['5'], { 6: 7 }, 'node2'), + new Relationship(5, 4, 2, 'reltype2', { 6: 7 }, 'rel2', 'node2', 'node3'), + new Node(2, ['3'], { 4: '5' }, 'node3') + ) + ] + ) + ] + ])('should unpack graph types (%s)', (_, struct, graphObject) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + false + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(graphObject) + }) + + it.each([ + [ + 'Node with less fields', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }]) + ], + [ + 'Node with more fields', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }, '1', 'b']) + ], + [ + 'Relationship with less fields', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }]) + ], + [ + 'Relationship with more fields', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }, '1', '2', '3', '4']) + ], + [ + 'UnboundRelationship with less fields', + new structure.Structure(0x72, [1, '2', { 3: 4 }]) + ], + [ + 'UnboundRelationship with more fields', + new structure.Structure(0x72, [1, '2', { 3: 4 }, '1', '2']) + ], + [ + 'Path with less fields', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }]), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }]), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }]) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'rel1', { 4: '5' }]), + new structure.Structure(0x52, [5, 4, 2, 'rel2', { 6: 7 }]) + ] + ] + ) + ], + [ + 'Path with more fields', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }]), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }]), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }]) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'rel1', { 4: '5' }]), + new structure.Structure(0x52, [5, 4, 2, 'rel2', { 6: 7 }]) + ], + [1, 1, 2, 2], + 'a' + ] + ) + ], + [ + 'Point with less fields', + new structure.Structure(0x58, [1, 2]) + ], + [ + 'Point with more fields', + new structure.Structure(0x58, [1, 2, 3, 4]) + ], + [ + 'Point3D with less fields', + new structure.Structure(0x59, [1, 2, 3]) + ], + + [ + 'Point3D with more fields', + new structure.Structure(0x59, [1, 2, 3, 4, 6]) + ], + [ + 'Duration with less fields', + new structure.Structure(0x45, [1, 2, 3]) + ], + [ + 'Duration with more fields', + new structure.Structure(0x45, [1, 2, 3, 4, 5]) + ], + [ + 'LocalTime with less fields', + new structure.Structure(0x74, []) + ], + [ + 'LocalTime with more fields', + new structure.Structure(0x74, [1, 2]) + ], + [ + 'Time with less fields', + new structure.Structure(0x54, [1]) + ], + [ + 'Time with more fileds', + new structure.Structure(0x54, [1, 2, 3]) + ], + [ + 'Date with less fields', + new structure.Structure(0x44, []) + ], + [ + 'Date with more fields', + new structure.Structure(0x44, [1, 2]) + ], + [ + 'LocalDateTime with less fields', + new structure.Structure(0x64, [1]) + ], + [ + 'LocalDateTime with more fields', + new structure.Structure(0x64, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneOffset with less fields', + new structure.Structure(0x49, [1, 2]) + ], + [ + 'DateTimeWithZoneOffset with more fields', + new structure.Structure(0x49, [1, 2, 3, 4]) + ], + [ + 'DateTimeWithZoneId with less fields', + new structure.Structure(0x69, [1, 2]) + ], + [ + 'DateTimeWithZoneId with more fields', + new structure.Structure(0x69, [1, 2, 'America/Sao Paulo', 'Brasil']) + ] + ])('should not unpack with wrong size (%s)', (_, struct) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + false + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(() => unpacked instanceof structure.Structure).toThrowErrorMatchingSnapshot() + }) + + it.each([ + [ + 'Point', + new structure.Structure(0x58, [1, 2, 3]), + new Point(1, 2, 3) + ], + [ + 'Point3D', + new structure.Structure(0x59, [1, 2, 3, 4]), + new Point(1, 2, 3, 4) + ], + [ + 'Duration', + new structure.Structure(0x45, [1, 2, 3, 4]), + new Duration(1, 2, 3, 4) + ], + [ + 'LocalTime', + new structure.Structure(0x74, [1]), + new LocalTime(0, 0, 0, 1) + ], + [ + 'Time', + new structure.Structure(0x54, [1, 2]), + new Time(0, 0, 0, 1, 2) + ], + [ + 'Date', + new structure.Structure(0x44, [1]), + new Date(1970, 1, 2) + ], + [ + 'LocalDateTime', + new structure.Structure(0x64, [1, 2]), + new LocalDateTime(1970, 1, 1, 0, 0, 1, 2) + ], + [ + 'DateTimeWithZoneOffset', + new structure.Structure(0x49, [ + 1655212878, 183_000_000, 120 * 60 + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneOffset / 1978', + new structure.Structure(0x49, [ + 282659759, 128000987, -150 * 60 + ]), + new DateTime(1978, 12, 16, 10, 5, 59, 128000987, -150 * 60) + ], + [ + 'DateTimeWithZoneId', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Australia', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Australia/Eucla' + ]), + new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, 8 * 60 * 60 + 45 * 60, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId / Honolulu', + new structure.Structure(0x69, [ + 1592231400, 183_000_000, 'Pacific/Honolulu' + ]), + new DateTime(2020, 6, 15, 4, 30, 0, 183_000_000, -10 * 60 * 60, 'Pacific/Honolulu') + ], + [ + 'DateTimeWithZoneId / Midnight', + new structure.Structure(0x69, [ + 1685397950, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2023, 5, 30, 0, 5, 50, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ] + ])('should unpack spatial types and temporal types (%s)', (_, struct, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset/0x46', + new structure.Structure(0x46, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x66', + new structure.Structure(0x66, [1, 2, 'America/Sao_Paulo']) + ] + ])('should unpack deprecated temporal types as unknown structs (%s)', (_, struct) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(struct) + }) + }) + + describe('result metadata enrichment', () => { + it('run should configure BoltProtocolV6x0._enrichMetadata as enrichMetadata', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + + expect(observer._enrichMetadata).toBe(protocol._enrichMetadata) + }) + + describe('BoltProtocolV6x0._enrichMetadata', () => { + const protocol = newProtocol() + + it('should handle empty metadata', () => { + const metadata = protocol._enrichMetadata({}) + + expect(metadata).toEqual({}) + }) + + it('should handle metadata with random objects', () => { + const metadata = protocol._enrichMetadata({ + a: 1133, + b: 345 + }) + + expect(metadata).toEqual({ + a: 1133, + b: 345 + }) + }) + + it('should handle metadata not change notifications ', () => { + const metadata = protocol._enrichMetadata({ + a: 1133, + b: 345, + notifications: [ + { + severity: 'WARNING', + category: 'HINT' + } + ] + }) + + expect(metadata).toEqual({ + a: 1133, + b: 345, + notifications: [ + { + severity: 'WARNING', + category: 'HINT' + } + ] + }) + }) + + it.each([ + [null, null], + [undefined, undefined], + [[], []], + [statusesWithDiagnosticRecord(null, null), statusesWithDiagnosticRecord(null, null)], + [statusesWithDiagnosticRecord(undefined, undefined), statusesWithDiagnosticRecord({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + }, + { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + })], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' } + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G', + _position: { + offset: 1, + line: 2, + column: 3 + } + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G', + _position: { + offset: 1, + line: 2, + column: 3 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null, + _position: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null, + _position: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: undefined, + OPERATION_CODE: undefined, + CURRENT_SCHEMA: undefined, + _status_parameters: undefined, + _severity: undefined, + _classification: undefined, + _position: undefined + }), + statusesWithDiagnosticRecord({ + OPERATION: undefined, + OPERATION_CODE: undefined, + CURRENT_SCHEMA: undefined, + _status_parameters: undefined, + _severity: undefined, + _classification: undefined, + _position: undefined + }) + ], + [ + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + title: 'Mitt title', + diagnostic_record: { + _classification: 'SOME', + _severity: 'INFORMATION' + } + }], + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + title: 'Mitt title', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _classification: 'SOME', + _severity: 'INFORMATION' + } + }] + ], + [ + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + description: 'description', + title: 'Mitt title', + diagnostic_record: { + _classification: 'SOME', + _severity: 'INFORMATION' + } + }], + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + title: 'Mitt title', + description: 'description', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _classification: 'SOME', + _severity: 'INFORMATION' + } + }] + ], + [ + [{ + gql_status: '03N33', + status_description: 'info: description', + description: 'description' + }], + [{ + gql_status: '03N33', + status_description: 'info: description', + description: 'description', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + } + }] + ] + ])('should handle statuses (%o) ', (statuses, expectedStatuses) => { + const metadata = protocol._enrichMetadata({ + a: 1133, + b: 345, + statuses + }) + + expect(metadata).toEqual({ + a: 1133, + b: 345, + statuses: expectedStatuses + }) + }) + }) + + function statusesWithDiagnosticRecord (...diagnosticRecords) { + return diagnosticRecords.map(diagnosticRecord => { + return { + gql_status: '00000', + status_description: 'note: successful completion', + diagnostic_record: diagnosticRecord + } + }) + } + }) + + function newProtocol (recorder) { + return new BoltProtocolV6x0(recorder, null, false, undefined, undefined, () => {}) + } +}) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d2d1300b2..226dc9ad9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -266,7 +266,6 @@ export { clientCertificateProviders, resolveCertificateProvider, Vector, - VectorType, vector } @@ -298,7 +297,8 @@ export type { ClientCertificate, ClientCertificateProvider, ClientCertificateProviders, - RotatingClientCertificateProvider + RotatingClientCertificateProvider, + VectorType } export default forExport diff --git a/packages/core/src/vector.ts b/packages/core/src/vector.ts index 738f66f9d..d1714645f 100644 --- a/packages/core/src/vector.ts +++ b/packages/core/src/vector.ts @@ -17,15 +17,21 @@ import { newError } from './error' -export enum VectorType { - 'INT8', - 'INT16', - 'INT32', - 'INT64', - 'FLOAT32', - 'FLOAT64', +type EnumRecord = { [key in T]: key } +export type VectorType = 'INT8' | 'INT16' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLOAT64' +/** + * @typedef {'INT8' | 'INT16' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLOAT64'} VectorType + */ +const vectorTypes: EnumRecord = { + INT8: 'INT8', + INT16: 'INT16', + INT32: 'INT32', + INT64: 'INT64', + FLOAT32: 'FLOAT32', + FLOAT64: 'FLOAT64' } +Object.freeze(vectorTypes) /** * A wrapper class for JavaScript TypedArrays that makes the driver send them as a Vector type to the database. @@ -42,24 +48,19 @@ export default class Vector { type: VectorType constructor (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array) { if (typedArray instanceof Int8Array) { - this.type = VectorType.INT8 - } - if (typedArray instanceof Int16Array) { - this.type = VectorType.INT16 - } - if (typedArray instanceof Int32Array) { - this.type = VectorType.INT32 - } - if (typedArray instanceof BigInt64Array) { - this.type = VectorType.INT64 - } - if (typedArray instanceof Float32Array) { - this.type = VectorType.FLOAT32 - } - if (typedArray instanceof Float64Array) { - this.type = VectorType.FLOAT64 + this.type = vectorTypes.INT8 + } else if (typedArray instanceof Int16Array) { + this.type = vectorTypes.INT16 + } else if (typedArray instanceof Int32Array) { + this.type = vectorTypes.INT32 + } else if (typedArray instanceof BigInt64Array) { + this.type = vectorTypes.INT64 + } else if (typedArray instanceof Float32Array) { + this.type = vectorTypes.FLOAT32 + } else if (typedArray instanceof Float64Array) { + this.type = vectorTypes.FLOAT64 } else { - throw newError('The neo4j Vector class is a wrapper for TypedArrays') + throw newError(`The neo4j Vector class is a wrapper for TypedArrays. got ${typeof typedArray}`) } this.typedArray = typedArray } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js index f71179f59..c57ba99aa 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js @@ -32,72 +32,84 @@ function createVectorTransformer () { signature: VECTOR, isTypeInstance: object => object instanceof Vector, toStructure: vector => { - const startTime = new Date().getTime() - const dataview = new DataView(vector.typedArray.byteLength) + const dataview = new DataView(new ArrayBuffer(vector.typedArray.byteLength)) let set let typeMarker - if (vector.type === 'INT8') { - typeMarker = Uint8Array.from([INT_8]) - set = dataview.setUint8 - } else if (vector.type === 'INT16') { - typeMarker = Uint8Array.from([INT_16]) - set = dataview.setUint16 - } else if (vector.type === 'INT32') { - typeMarker = Uint8Array.from([INT_32]) - set = dataview.setUint32 - } else if (vector.type === 'INT64') { - typeMarker = Uint8Array.from([INT_64]) - set = dataview.setUint64 - } else if (vector.type === 'FLOAT32') { - typeMarker = Uint8Array.from([FLOAT_32]) - set = dataview.setFloat32 - } else if (vector.type === 'FLOAT64') { - typeMarker = Uint8Array.from([FLOAT_64]) - set = dataview.setFloat64 - } else { - throw newError('Vector is of unsupported type') + switch (vector.type) { + case 'INT8': + typeMarker = Uint8Array.from([INT_8]) + set = dataview.setUint8.bind(dataview) + break + case 'INT16': + typeMarker = Uint8Array.from([INT_16]) + set = dataview.setUint16.bind(dataview) + break + case 'INT32': + typeMarker = Uint8Array.from([INT_32]) + set = dataview.setUint32.bind(dataview) + break + case 'INT64': + typeMarker = Uint8Array.from([INT_64]) + set = dataview.setBigInt64.bind(dataview) + break + case 'FLOAT32': + typeMarker = Uint8Array.from([FLOAT_32]) + set = dataview.setFloat32.bind(dataview) + break + case 'FLOAT64': + typeMarker = Uint8Array.from([FLOAT_64]) + set = dataview.setFloat64.bind(dataview) + break + default: + throw newError(`Vector is of unsupported type ${vector.type}`) } for (let i = 0; i < vector.typedArray.length; i++) { set(i * vector.typedArray.BYTES_PER_ELEMENT, vector.typedArray[i]) } - const struct = new structure.Structure(VECTOR, [typeMarker, Uint8Array.from(dataview.buffer)]) - console.debug(`Packing vector took ${new Date().getTime() - startTime}ms`) + const struct = new structure.Structure(VECTOR, [typeMarker, new Int8Array(dataview.buffer)]) return struct }, fromStructure: structure => { const typeMarker = structure.fields[0][0] - const byteArray = structure.fields[1] - const dataview = new DataView(byteArray.length) - let typedArray + const arrayBuffer = structure.fields[1] + const setview = new DataView(new ArrayBuffer(arrayBuffer.byteLength)) + const getview = new DataView(arrayBuffer.buffer) + let get let set let resultArray - if (typeMarker === INT_8) { - return Int8Array.from(byteArray.buffer) - } if (typeMarker === INT_16) { - typedArray = Int16Array.from(byteArray.buffer) - resultArray = Int16Array.from(dataview.buffer) - set = dataview.setInt16 - } if (typeMarker === INT_32) { - typedArray = Int32Array.from(byteArray.buffer) - resultArray = Int32Array.from(dataview.buffer) - set = dataview.setInt32 - } if (typeMarker === INT_64) { - typedArray = BigInt64Array.from(byteArray.buffer) - resultArray = BigInt64Array.from(dataview.buffer) - set = dataview.setBigInt64 - } if (typeMarker === FLOAT_32) { - typedArray = Float32Array.from(byteArray.buffer) - resultArray = Float32Array.from(dataview.buffer) - set = dataview.setFloat32 - } if (typeMarker === FLOAT_64) { - typedArray = Float64Array.from(byteArray.buffer) - resultArray = Float64Array.from(dataview.buffer) - set = dataview.setFloat64 - } else { - throw newError('Recieved Vector of unknown type') + switch (typeMarker) { + case INT_8: + return new Vector(Int8Array.from(arrayBuffer)) + case INT_16: + resultArray = new Int16Array(setview.buffer) + get = getview.getInt16.bind(getview) + set = setview.setInt16.bind(setview) + break + case INT_32: + resultArray = new Int32Array(setview.buffer) + get = getview.getInt32.bind(getview) + set = setview.setInt32.bind(setview) + break + case INT_64: + resultArray = new BigInt64Array(setview.buffer) + get = getview.getBigInt64.bind(getview) + set = setview.setBigInt64.bind(setview) + break + case FLOAT_32: + resultArray = new Float32Array(setview.buffer) + get = getview.getFloat32.bind(getview) + set = setview.setFloat32.bind(setview) + break + case FLOAT_64: + resultArray = new Float64Array(setview.buffer) + get = getview.getFloat64.bind(getview) + set = setview.setFloat64.bind(setview) + break + default: + throw newError(`Recieved Vector of unknown type ${typeMarker}`) } - for (let i = 0; i < typedArray.length; i++) { - set(i * typedArray.BYTES_PER_ELEMENT, typedArray[i]) + for (let i = 0; i < arrayBuffer.length; i += resultArray.BYTES_PER_ELEMENT) { + set(i, get(i), true) } return new Vector(resultArray) } diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 06db9cba1..135e19afb 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -266,7 +266,6 @@ export { clientCertificateProviders, resolveCertificateProvider, Vector, - VectorType, vector } @@ -298,7 +297,8 @@ export type { ClientCertificate, ClientCertificateProvider, ClientCertificateProviders, - RotatingClientCertificateProvider + RotatingClientCertificateProvider, + VectorType } export default forExport diff --git a/packages/neo4j-driver-deno/lib/core/vector.ts b/packages/neo4j-driver-deno/lib/core/vector.ts index 0c63ed173..51b833905 100644 --- a/packages/neo4j-driver-deno/lib/core/vector.ts +++ b/packages/neo4j-driver-deno/lib/core/vector.ts @@ -17,15 +17,21 @@ import { newError } from './error.ts' -export enum VectorType { - 'INT8', - 'INT16', - 'INT32', - 'INT64', - 'FLOAT32', - 'FLOAT64', +type EnumRecord = { [key in T]: key } +export type VectorType = 'INT8' | 'INT16' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLOAT64' +/** + * @typedef {'INT8' | 'INT16' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLOAT64'} VectorType + */ +const vectorTypes : EnumRecord= { + INT8: 'INT8', + INT16: 'INT16', + INT32: 'INT32', + INT64: 'INT64', + FLOAT32: 'FLOAT32', + FLOAT64: 'FLOAT64', } +Object.freeze(vectorTypes) /** * A wrapper class for JavaScript TypedArrays that makes the driver send them as a Vector type to the database. @@ -42,24 +48,24 @@ export default class Vector { type: VectorType constructor (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array) { if (typedArray instanceof Int8Array) { - this.type = VectorType.INT8 + this.type = vectorTypes.INT8 } - if (typedArray instanceof Int16Array) { - this.type = VectorType.INT16 + else if (typedArray instanceof Int16Array) { + this.type = vectorTypes.INT16 } - if (typedArray instanceof Int32Array) { - this.type = VectorType.INT32 + else if (typedArray instanceof Int32Array) { + this.type = vectorTypes.INT32 } - if (typedArray instanceof BigInt64Array) { - this.type = VectorType.INT64 + else if (typedArray instanceof BigInt64Array) { + this.type = vectorTypes.INT64 } - if (typedArray instanceof Float32Array) { - this.type = VectorType.FLOAT32 + else if (typedArray instanceof Float32Array) { + this.type = vectorTypes.FLOAT32 } - if (typedArray instanceof Float64Array) { - this.type = VectorType.FLOAT64 + else if (typedArray instanceof Float64Array) { + this.type = vectorTypes.FLOAT64 } else { - throw newError('The neo4j Vector class is a wrapper for TypedArrays') + throw newError(`The neo4j Vector class is a wrapper for TypedArrays. got ${typeof typedArray}`) } this.typedArray = typedArray } From f08f8bec9746180c6538aa5faa183516ea5b29a2 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:51:56 +0200 Subject: [PATCH 09/13] deno sync --- packages/neo4j-driver-deno/lib/core/vector.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/vector.ts b/packages/neo4j-driver-deno/lib/core/vector.ts index 51b833905..89c9ff391 100644 --- a/packages/neo4j-driver-deno/lib/core/vector.ts +++ b/packages/neo4j-driver-deno/lib/core/vector.ts @@ -23,13 +23,13 @@ export type VectorType = 'INT8' | 'INT16' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLO /** * @typedef {'INT8' | 'INT16' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLOAT64'} VectorType */ -const vectorTypes : EnumRecord= { +const vectorTypes: EnumRecord = { INT8: 'INT8', INT16: 'INT16', INT32: 'INT32', INT64: 'INT64', FLOAT32: 'FLOAT32', - FLOAT64: 'FLOAT64', + FLOAT64: 'FLOAT64' } Object.freeze(vectorTypes) @@ -49,20 +49,15 @@ export default class Vector { constructor (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array) { if (typedArray instanceof Int8Array) { this.type = vectorTypes.INT8 - } - else if (typedArray instanceof Int16Array) { + } else if (typedArray instanceof Int16Array) { this.type = vectorTypes.INT16 - } - else if (typedArray instanceof Int32Array) { + } else if (typedArray instanceof Int32Array) { this.type = vectorTypes.INT32 - } - else if (typedArray instanceof BigInt64Array) { + } else if (typedArray instanceof BigInt64Array) { this.type = vectorTypes.INT64 - } - else if (typedArray instanceof Float32Array) { + } else if (typedArray instanceof Float32Array) { this.type = vectorTypes.FLOAT32 - } - else if (typedArray instanceof Float64Array) { + } else if (typedArray instanceof Float64Array) { this.type = vectorTypes.FLOAT64 } else { throw newError(`The neo4j Vector class is a wrapper for TypedArrays. got ${typeof typedArray}`) From 5cb2f7a41e453f9c5a58c49d64c545a1c216db2c Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 13 Jun 2025 09:04:00 +0200 Subject: [PATCH 10/13] fix for big endian machines --- .../src/bolt/bolt-protocol-v6x0.transformer.js | 10 +++++++++- .../bolt/bolt-protocol-v6x0.transformer.js | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js index daa532d34..12b239e22 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js @@ -70,6 +70,7 @@ function createVectorTransformer () { return struct }, fromStructure: structure => { + const isLittleEndian = checkLittleEndian() const typeMarker = structure.fields[0][0] const arrayBuffer = structure.fields[1] const setview = new DataView(new ArrayBuffer(arrayBuffer.byteLength)) @@ -109,13 +110,20 @@ function createVectorTransformer () { throw newError(`Recieved Vector of unknown type ${typeMarker}`) } for (let i = 0; i < arrayBuffer.length; i += resultArray.BYTES_PER_ELEMENT) { - set(i, get(i), true) + set(i, get(i), isLittleEndian) } return new Vector(resultArray) } }) } +function checkLittleEndian () { + const dataview = new DataView(new ArrayBuffer(2)) + dataview.setInt16(0, 1000, true) + const typeArray = new Int16Array(dataview.buffer) + return typeArray[0] === 1000 +} + export default { ...v5x8, createVectorTransformer diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js index c57ba99aa..c03e05a10 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js @@ -70,6 +70,7 @@ function createVectorTransformer () { return struct }, fromStructure: structure => { + const isLittleEndian = checkLittleEndian() const typeMarker = structure.fields[0][0] const arrayBuffer = structure.fields[1] const setview = new DataView(new ArrayBuffer(arrayBuffer.byteLength)) @@ -109,13 +110,20 @@ function createVectorTransformer () { throw newError(`Recieved Vector of unknown type ${typeMarker}`) } for (let i = 0; i < arrayBuffer.length; i += resultArray.BYTES_PER_ELEMENT) { - set(i, get(i), true) + set(i, get(i), isLittleEndian) } return new Vector(resultArray) } }) } +function checkLittleEndian () { + const dataview = new DataView(new ArrayBuffer(2)) + dataview.setInt16(0, 1000, true) + const typeArray = new Int16Array(dataview.buffer) + return typeArray[0] === 1000 +} + export default { ...v5x8, createVectorTransformer From 4fcbfd6e7aebc15a2582724a4b51588a2d1c3622 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 13 Jun 2025 10:38:33 +0200 Subject: [PATCH 11/13] only run example test if Bolt 6.0 is active --- .../neo4j-driver/test/vector-examples.test.js | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/neo4j-driver/test/vector-examples.test.js b/packages/neo4j-driver/test/vector-examples.test.js index d04d9d587..602dbff95 100644 --- a/packages/neo4j-driver/test/vector-examples.test.js +++ b/packages/neo4j-driver/test/vector-examples.test.js @@ -20,10 +20,17 @@ import sharedNeo4j from './internal/shared-neo4j' describe('#integration vector type api suggestion', () => { let driverGlobal + let protocolVersion const uri = `bolt://${sharedNeo4j.hostnameWithBoltPort}` beforeAll(async () => { driverGlobal = neo4j.driver(uri, sharedNeo4j.authToken) + const tmpDriver = neo4j.driver( + `bolt://${sharedNeo4j.hostnameWithBoltPort}`, + sharedNeo4j.authToken + ) + protocolVersion = await sharedNeo4j.cleanupAndGetProtocolVersion(tmpDriver) + await tmpDriver.close() }) beforeEach(async () => { @@ -38,19 +45,21 @@ describe('#integration vector type api suggestion', () => { }) it('write and read vectors', async () => { - const driver = driverGlobal + if (protocolVersion >= 6.0) { + const driver = driverGlobal - const bufferWriter = Uint8Array.from([1, 1]) - await driver.executeQuery('CREATE (p:Product) SET p.vector_from_array = $vector_from_array, p.vector_from_buffer = $vector_from_buffer', { - vector_from_array: neo4j.vector(Float32Array.from([1, 2, 3, 4])), // Typed arrays can be created from a regular list of Numbers - vector_from_buffer: neo4j.vector(new Uint8Array(bufferWriter.buffer)) // Or from a bytebuffer - }) - const res = await driver.executeQuery('MATCH (p:Product) RETURN p.vector_from_array as arrayVector, p.vector_from_buffer as bufferVector') + const bufferWriter = Uint8Array.from([1, 1]) + await driver.executeQuery('CREATE (p:Product) SET p.vector_from_array = $vector_from_array, p.vector_from_buffer = $vector_from_buffer', { + vector_from_array: neo4j.vector(Float32Array.from([1, 2, 3, 4])), // Typed arrays can be created from a regular list of Numbers + vector_from_buffer: neo4j.vector(new Uint8Array(bufferWriter.buffer)) // Or from a bytebuffer + }) + const res = await driver.executeQuery('MATCH (p:Product) RETURN p.vector_from_array as arrayVector, p.vector_from_buffer as bufferVector') - const arrayVec = res.records[0].get('arrayVector').typedArray - const bufferVec = res.records[0].get('bufferVector').typedArray + const arrayVec = res.records[0].get('arrayVector').typedArray + const bufferVec = res.records[0].get('bufferVector').typedArray - expect(arrayVec[0]).toBe(1) - expect(bufferVec[1]).toBe(1) + expect(arrayVec[0]).toBe(1) + expect(bufferVec[1]).toBe(1) + } }) }) From da524fa81174c73541144e420f50ff1fd71fe540 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 13 Jun 2025 13:46:39 +0200 Subject: [PATCH 12/13] small change to vector class --- README.md | 2 +- packages/core/src/vector.ts | 12 ++++++++---- packages/neo4j-driver-deno/lib/core/vector.ts | 12 ++++++++---- packages/neo4j-driver/test/vector-examples.test.js | 4 ++-- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ba6d40ddc..e04bf85c3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Vector types are usuable from bolt version 6.0 and forward, which no current ser let vector = res.records[0].get('embeddings') - console.log(vector.typedArray[3]) //3 + console.log(vector.toTypedArray()) //Float32Array[0, 1, 2, 3] console.log(vector.type) //FLOAT32 await driver.close() diff --git a/packages/core/src/vector.ts b/packages/core/src/vector.ts index d1714645f..ad3c121af 100644 --- a/packages/core/src/vector.ts +++ b/packages/core/src/vector.ts @@ -43,10 +43,10 @@ Object.freeze(vectorTypes) * @constructor * */ -export default class Vector { - typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array +export default class Vector { + typedArray: K type: VectorType - constructor (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array) { + constructor (typedArray: K) { if (typedArray instanceof Int8Array) { this.type = vectorTypes.INT8 } else if (typedArray instanceof Int16Array) { @@ -64,6 +64,10 @@ export default class Vector { } this.typedArray = typedArray } + + toTypedArray (): K { + return this.typedArray + } } /** @@ -72,6 +76,6 @@ export default class Vector { * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray - The value to use. * @return {Vector} - The Neo4j Vector ready to be used as a query parameter */ -export function vector (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array): Vector { +export function vector (typedArray: K): Vector { return new Vector(typedArray) } diff --git a/packages/neo4j-driver-deno/lib/core/vector.ts b/packages/neo4j-driver-deno/lib/core/vector.ts index 89c9ff391..f9ae358e3 100644 --- a/packages/neo4j-driver-deno/lib/core/vector.ts +++ b/packages/neo4j-driver-deno/lib/core/vector.ts @@ -43,10 +43,10 @@ Object.freeze(vectorTypes) * @constructor * */ -export default class Vector { - typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array +export default class Vector { + typedArray: K type: VectorType - constructor (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array) { + constructor (typedArray: K) { if (typedArray instanceof Int8Array) { this.type = vectorTypes.INT8 } else if (typedArray instanceof Int16Array) { @@ -64,6 +64,10 @@ export default class Vector { } this.typedArray = typedArray } + + toTypedArray(): K { + return this.typedArray + } } /** @@ -72,6 +76,6 @@ export default class Vector { * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray - The value to use. * @return {Vector} - The Neo4j Vector ready to be used as a query parameter */ -export function vector (typedArray: Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array): Vector { +export function vector (typedArray: K): Vector { return new Vector(typedArray) } diff --git a/packages/neo4j-driver/test/vector-examples.test.js b/packages/neo4j-driver/test/vector-examples.test.js index 602dbff95..e6d500a0a 100644 --- a/packages/neo4j-driver/test/vector-examples.test.js +++ b/packages/neo4j-driver/test/vector-examples.test.js @@ -55,8 +55,8 @@ describe('#integration vector type api suggestion', () => { }) const res = await driver.executeQuery('MATCH (p:Product) RETURN p.vector_from_array as arrayVector, p.vector_from_buffer as bufferVector') - const arrayVec = res.records[0].get('arrayVector').typedArray - const bufferVec = res.records[0].get('bufferVector').typedArray + const arrayVec = res.records[0].get('arrayVector').toTypedArray() + const bufferVec = res.records[0].get('bufferVector').toTypedArray() expect(arrayVec[0]).toBe(1) expect(bufferVec[1]).toBe(1) From 506979f2e867ef470716454349c036540ccb99f7 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:05:21 +0200 Subject: [PATCH 13/13] deno sync --- packages/neo4j-driver-deno/lib/core/vector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/neo4j-driver-deno/lib/core/vector.ts b/packages/neo4j-driver-deno/lib/core/vector.ts index f9ae358e3..4de021d9e 100644 --- a/packages/neo4j-driver-deno/lib/core/vector.ts +++ b/packages/neo4j-driver-deno/lib/core/vector.ts @@ -65,7 +65,7 @@ export default class Vector