diff --git a/README.md b/README.md index b176bd13c..2630be65a 100644 --- a/README.md +++ b/README.md @@ -503,3 +503,29 @@ var driver = neo4j.driver( { disableLosslessIntegers: true } ) ``` + +#### Writing and reading Vectors + +Neo4j supports storing vector embeddings in a dedicated vector type. Sending large lists with the driver will result in significant overhead as each value will be transmitted with type information, so the 6.0.0 release of the driver introduced the Neo4j Vector type. + +The Vector type supports signed integers of 8, 16, 32 and 64 bits, and floats of 32 and 64 bits. The Vector type is a wrapper for JavaScript TypedArrays of those types. + +To create a neo4j Vector in your code, do the following: + +```javascript +var neo4j = require('neo4j-driver') + +var typedArray = Float32Array.from([1, 2, 3]) //this is how to convert a regular array of numbers into a TypedArray, useful if you handle vectors as regular arrays in your code + +var neo4jVector = neo4j.vector(typedArray) //this creates a neo4j Vector of type Float32, containing the values [1, 2, 3] + +driver.executeQuery('CREATE (n {embeddings: $myVectorParam})', { myVectorParam: neo4jVector }) +``` + +To access the data in a retrieved Vector you can do the following: + +```javascript +var retrievedTypedArray = neo4jVector.asTypedArray() //This will return a TypedArray of the same type as the Vector + +var retrievedArray = Array.from(retrievedTypedArray) //This will convert the TypedArray to a regular array of Numbers. (Not safe for Int64 arrays) +``` \ No newline at end of file diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v1.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v1.transformer.js index e0dc42e41..6dcd7f90c 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v1.transformer.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v1.transformer.js @@ -23,7 +23,8 @@ import { UnboundRelationship, Path, toNumber, - PathSegment + PathSegment, + Vector } from 'neo4j-driver-core' import { structure } from '../packstream' @@ -43,6 +44,8 @@ const UNBOUND_RELATIONSHIP_STRUCT_SIZE = 3 const PATH = 0x50 const PATH_STRUCT_SIZE = 3 +const VECTOR = 0x56 + /** * Creates the Node Transformer * @returns {TypeTransformer} @@ -177,9 +180,27 @@ function createPathTransformer () { }) } +/** + * Creates a typeTransformer that throws errors if vectors are transmitted. + * @returns {TypeTransformer} + */ +function createVectorTransformer () { + return new TypeTransformer({ + signature: VECTOR, + isTypeInstance: object => object instanceof Vector, + toStructure: _ => { + throw newError('Sending vector types require server and driver to be communicating with Bolt protocol 6.0 or later. Please update your database version.') + }, + fromStructure: _ => { + throw newError('Server tried to send Vector object, but server and driver are communicating on a version of the Bolt protocol that does not support vectors.') + } + }) +} + export default { createNodeTransformer, createRelationshipTransformer, createUnboundRelationshipTransformer, - createPathTransformer + createPathTransformer, + createVectorTransformer } 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..c9ccd8578 --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js @@ -0,0 +1,138 @@ +/** + * 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 + +const typeToTypeMarker = { + INT8: INT_8, + INT16: INT_16, + INT32: INT_32, + INT64: INT_64, + FLOAT32: FLOAT_32, + FLOAT64: FLOAT_64 +} + +function createVectorTransformer () { + return new TypeTransformer({ + signature: VECTOR, + isTypeInstance: object => object instanceof Vector, + toStructure: vector => { + const typeMarker = typeToTypeMarker[vector.getType()] + if (typeMarker === undefined) { + throw newError(`Vector object has unknown type: ${vector.getType()}`) + } + const buffer = fixBufferEndianness(typeMarker, vector.asTypedArray().buffer) + const struct = new structure.Structure(VECTOR, [Int8Array.from([typeMarker]), new Int8Array(buffer)]) + return struct + }, + fromStructure: structure => { + const typeMarker = Uint8Array.from(structure.fields[0])[0] + const byteArray = structure.fields[1] + const buffer = fixBufferEndianness(typeMarker, byteArray.buffer) + switch (typeMarker) { + case INT_8: + return new Vector(new Int8Array(buffer)) + case INT_16: + return new Vector(new Int16Array(buffer)) + case INT_32: + return new Vector(new Int32Array(buffer)) + case INT_64: + return new Vector(new BigInt64Array(buffer)) + case FLOAT_32: + return new Vector(new Float32Array(buffer)) + case FLOAT_64: + return new Vector(new Float64Array(buffer)) + default: + throw newError(`Received Vector structure with unsupported type marker: ${typeMarker}`) + } + } + }) +} + +function fixBufferEndianness (typeMarker, buffer) { + const isLittleEndian = checkLittleEndian() + if (isLittleEndian) { + const setview = new DataView(new ArrayBuffer(buffer.byteLength)) + // we want exact byte accuracy, so we cannot simply get the value from the typed array + const getview = new DataView(buffer) + let set + let get + let elementSize + switch (typeMarker) { + case INT_8: + elementSize = 1 + set = setview.setInt8.bind(setview) + get = getview.getInt8.bind(getview) + break + case INT_16: + elementSize = 2 + set = setview.setInt16.bind(setview) + get = getview.getInt16.bind(getview) + break + case INT_32: + elementSize = 4 + set = setview.setInt32.bind(setview) + get = getview.getInt32.bind(getview) + break + case INT_64: + elementSize = 8 + set = setview.setBigInt64.bind(setview) + get = getview.getBigInt64.bind(getview) + break + case FLOAT_32: + elementSize = 4 + set = setview.setInt32.bind(setview) + get = getview.getInt32.bind(getview) + break + case FLOAT_64: + elementSize = 8 + set = setview.setBigInt64.bind(setview) + get = getview.getBigInt64.bind(getview) + break + default: + throw newError(`Vector is of unsupported type ${typeMarker}`) + } + for (let i = 0; i < buffer.byteLength; i += elementSize) { + set(i, get(i, isLittleEndian)) + } + return setview.buffer + } else { + return buffer + } +} + +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/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index 1b8f792a5..18891e197 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' @@ -266,6 +267,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/test/bolt/__snapshots__/bolt-protocol-v1.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v1.test.js.snap index 905cc191a..988131b2e 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v1.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v1.test.js.snap @@ -1,13 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#unit BoltProtocolV1 .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 BoltProtocolV1 .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 BoltProtocolV1 .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 BoltProtocolV1 .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 BoltProtocolV1 .packable() should pack types introduced afterwards as Map (Date) 1`] = ` { "day": 1, @@ -86,6 +78,16 @@ exports[`#unit BoltProtocolV1 .packable() should pack types introduced afterward } `; +exports[`#unit BoltProtocolV1 .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 BoltProtocolV1 .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 BoltProtocolV1 .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 BoltProtocolV1 .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 BoltProtocolV1 .unpack() should error out of unpacking Vectors 1`] = `"Server tried to send Vector object, but server and driver are communicating on a version of the Bolt protocol that does not support vectors."`; + exports[`#unit BoltProtocolV1 .unpack() should not unpack graph types with wrong size(Node with less fields) 1`] = `"Wrong struct size for Node, expected 3 but was 2"`; exports[`#unit BoltProtocolV1 .unpack() should not unpack graph types with wrong size(Node with more fields) 1`] = `"Wrong struct size for Node, expected 3 but was 4"`; 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-v1.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js index b08fc90bf..26742391a 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js @@ -30,7 +30,9 @@ import { Relationship, Time, UnboundRelationship, - Node + Node, + newError, + Vector } from 'neo4j-driver-core' import utils from '../test-utils' import { LoginObserver } from '../../src/bolt/stream-observers' @@ -444,6 +446,19 @@ describe('#unit BoltProtocolV1', () => { expect(unpacked).toBeInstanceOf(Object) expect(unpacked).toMatchSnapshot() }) + + it('should error out when trying to pack a Vector', () => { + const object = new Vector(Int16Array.from([1, 2, 3])) + const buffer = alloc(256) + const protocol = new BoltProtocolV1( + new utils.MessageRecordingConnection(), + buffer, + false + ) + const packable = protocol.packable(object) + expect(packable).toThrow(newError('Sending vector types require server and driver to be communicating with Bolt protocol 6.0 or later. Please update your database version.')) + buffer.reset() + }) }) describe('.unpack()', () => { @@ -621,6 +636,24 @@ describe('#unit BoltProtocolV1', () => { const unpacked = protocol.unpack(buffer) expect(unpacked).toEqual(struct) }) + + it('should error out of unpacking Vectors', () => { + const struct = new structure.Structure(0x56, [Int8Array.from([0xc8]), Int8Array.from([1, 2, 3])]) + const buffer = alloc(256) + const protocol = new BoltProtocolV1( + 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() + }) }) describe('Bolt v5.1', () => { 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 a0950cf37..226dc9ad9 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,9 @@ export { notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + Vector, + vector } export type { @@ -294,7 +297,8 @@ export type { ClientCertificate, ClientCertificateProvider, ClientCertificateProviders, - RotatingClientCertificateProvider + RotatingClientCertificateProvider, + VectorType } export default forExport 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..e6d1ef799 --- /dev/null +++ b/packages/core/src/vector.ts @@ -0,0 +1,93 @@ +/** + * 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' + +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. + * @access public + * @exports Vector + * @class A Vector class that wraps a JavaScript TypedArray to enable writing/reading the Neo4j Vector type. + * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray The TypedArray to convert to a vector + * + * @constructor + * + */ +export default class Vector { + _typedArray: K + _type: VectorType + constructor (typedArray: K) { + if (typedArray instanceof Int8Array) { + 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. got ${typeof typedArray}`) + } + this._typedArray = typedArray + } + + /** + * Converts the Vector back to a typedArray + * @returns {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} - a TypedArray of the Vectors type. + */ + asTypedArray (): K { + return this._typedArray + } + + /** + * Gets the type of the Vector + * @returns {VectorType} - The type of the vector, corresponding to the type of the wrapped TypedArray. + */ + getType (): VectorType { + return this._type + } +} + +/** + * 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: K): Vector { + return new Vector(typedArray) +} diff --git a/packages/core/test/vector-type.test.ts b/packages/core/test/vector-type.test.ts new file mode 100644 index 000000000..0a03c63fc --- /dev/null +++ b/packages/core/test/vector-type.test.ts @@ -0,0 +1,35 @@ +/** + * 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 { vector } from '../src' + +describe('Vector', () => { + describe('vector', () => { + it.each([ + ['Int8Array', Int8Array.from([0]), 'INT8'], + ['Int16Array', Int16Array.from([0]), 'INT16'], + ['Int32Array', Int32Array.from([0]), 'INT32'], + ['BigInt64Array', BigInt64Array.from([BigInt(0)]), 'INT64'], + ['Float32Array', Float32Array.from([0]), 'FLOAT32'], + ['Float64Array', Float64Array.from([0]), 'FLOAT64'] + ])('should create vector from (%s)', (_, typedArray, expectedType) => { + const vec = vector(typedArray) + expect(vec.getType()).toEqual(expectedType) + expect(vec.asTypedArray()).toEqual(typedArray) + }) + }) +}) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.transformer.js index 9c7797ead..e0268b081 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.transformer.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.transformer.js @@ -23,7 +23,8 @@ import { UnboundRelationship, Path, toNumber, - PathSegment + PathSegment, + Vector } from '../../core/index.ts' import { structure } from '../packstream/index.js' @@ -43,6 +44,8 @@ const UNBOUND_RELATIONSHIP_STRUCT_SIZE = 3 const PATH = 0x50 const PATH_STRUCT_SIZE = 3 +const VECTOR = 0x56 + /** * Creates the Node Transformer * @returns {TypeTransformer} @@ -177,9 +180,27 @@ function createPathTransformer () { }) } +/** + * Creates a typeTransformer that throws errors if vectors are transmitted. + * @returns {TypeTransformer} + */ +function createVectorTransformer () { + return new TypeTransformer({ + signature: VECTOR, + isTypeInstance: object => object instanceof Vector, + toStructure: _ => { + throw newError('Sending vector types require server and driver to be communicating with Bolt protocol 6.0 or later. Please update your database version.') + }, + fromStructure: _ => { + throw newError('Server tried to send Vector object, but server and driver are communicating on a version of the Bolt protocol that does not support vectors.') + } + }) +} + export default { createNodeTransformer, createRelationshipTransformer, createUnboundRelationshipTransformer, - createPathTransformer + createPathTransformer, + createVectorTransformer } 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..c0e0d16b2 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js @@ -0,0 +1,138 @@ +/** + * 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 + +const typeToTypeMarker = { + INT8: INT_8, + INT16: INT_16, + INT32: INT_32, + INT64: INT_64, + FLOAT32: FLOAT_32, + FLOAT64: FLOAT_64 +} + +function createVectorTransformer () { + return new TypeTransformer({ + signature: VECTOR, + isTypeInstance: object => object instanceof Vector, + toStructure: vector => { + const typeMarker = typeToTypeMarker[vector.getType()] + if (typeMarker === undefined) { + throw newError(`Vector object has unknown type: ${vector.getType()}`) + } + const buffer = fixBufferEndianness(typeMarker, vector.asTypedArray().buffer) + const struct = new structure.Structure(VECTOR, [Int8Array.from([typeMarker]), new Int8Array(buffer)]) + return struct + }, + fromStructure: structure => { + const typeMarker = Uint8Array.from(structure.fields[0])[0] + const byteArray = structure.fields[1] + const buffer = fixBufferEndianness(typeMarker, byteArray.buffer) + switch (typeMarker) { + case INT_8: + return new Vector(new Int8Array(buffer)) + case INT_16: + return new Vector(new Int16Array(buffer)) + case INT_32: + return new Vector(new Int32Array(buffer)) + case INT_64: + return new Vector(new BigInt64Array(buffer)) + case FLOAT_32: + return new Vector(new Float32Array(buffer)) + case FLOAT_64: + return new Vector(new Float64Array(buffer)) + default: + throw newError(`Received Vector structure with unsupported type marker: ${typeMarker}`) + } + } + }) +} + +function fixBufferEndianness (typeMarker, buffer) { + const isLittleEndian = checkLittleEndian() + if (isLittleEndian) { + const setview = new DataView(new ArrayBuffer(buffer.byteLength)) + // we want exact byte accuracy, so we cannot simply get the value from the typed array + const getview = new DataView(buffer) + let set + let get + let elementSize + switch (typeMarker) { + case INT_8: + elementSize = 1 + set = setview.setInt8.bind(setview) + get = getview.getInt8.bind(getview) + break + case INT_16: + elementSize = 2 + set = setview.setInt16.bind(setview) + get = getview.getInt16.bind(getview) + break + case INT_32: + elementSize = 4 + set = setview.setInt32.bind(setview) + get = getview.getInt32.bind(getview) + break + case INT_64: + elementSize = 8 + set = setview.setBigInt64.bind(setview) + get = getview.getBigInt64.bind(getview) + break + case FLOAT_32: + elementSize = 4 + set = setview.setInt32.bind(setview) + get = getview.getInt32.bind(getview) + break + case FLOAT_64: + elementSize = 8 + set = setview.setBigInt64.bind(setview) + get = getview.getBigInt64.bind(getview) + break + default: + throw newError(`Vector is of unsupported type ${typeMarker}`) + } + for (let i = 0; i < buffer.byteLength; i += elementSize) { + set(i, get(i, isLittleEndian)) + } + return setview.buffer + } else { + return buffer + } +} + +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/create.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js index 188e8c6bd..cea9b1dd4 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' @@ -266,6 +267,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/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 3a341708b..135e19afb 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,9 @@ export { notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + Vector, + vector } export type { @@ -294,7 +297,8 @@ export type { ClientCertificate, ClientCertificateProvider, ClientCertificateProviders, - RotatingClientCertificateProvider + RotatingClientCertificateProvider, + VectorType } export default forExport 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..ba8c636e4 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/vector.ts @@ -0,0 +1,93 @@ +/** + * 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' + +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. + * @access public + * @exports Vector + * @class A Vector class that wraps a JavaScript TypedArray to enable writing/reading the Neo4j Vector type. + * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray The TypedArray to convert to a vector + * + * @constructor + * + */ +export default class Vector { + _typedArray: K + _type: VectorType + constructor (typedArray: K) { + if (typedArray instanceof Int8Array) { + 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. got ${typeof typedArray}`) + } + this._typedArray = typedArray + } + + /** + * Converts the Vector back to a typedArray + * @returns {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} - a TypedArray of the Vectors type. + */ + asTypedArray (): K { + return this._typedArray + } + + /** + * Gets the type of the Vector + * @returns {VectorType} - The type of the vector, corresponding to the type of the wrapped TypedArray. + */ + getType (): VectorType { + return this._type + } +} + +/** + * 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: K): Vector { + return new Vector(typedArray) +} diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 41f775d59..bc07fae11 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -108,7 +108,10 @@ import { ClientCertificateProviders, RotatingClientCertificateProvider, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + vector, + VectorType, + Vector } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { DirectConnectionProvider, RoutingConnectionProvider } from './bolt-connection/index.js' @@ -207,6 +210,7 @@ function driver ( routing = true break default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unknown scheme: ${parsedUrl.scheme ?? 'null'}`) } @@ -259,6 +263,7 @@ function driver ( routingContext: parsedUrl.query }) } else { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!isEmptyObjectOrNull(parsedUrl.query)) { throw new Error( `Parameters are not supported with none routed scheme. Given URL: '${url}'` @@ -440,7 +445,9 @@ const forExport = { notificationFilterDisabledCategory, notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + Vector, + vector } export { @@ -511,7 +518,8 @@ export { notificationFilterDisabledCategory, notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + vector } export type { QueryResult, @@ -542,6 +550,8 @@ export type { ClientCertificate, ClientCertificateProvider, ClientCertificateProviders, - RotatingClientCertificateProvider + RotatingClientCertificateProvider, + Vector, + VectorType } export default forExport diff --git a/packages/neo4j-driver-lite/README.md b/packages/neo4j-driver-lite/README.md index 0c5c069cf..d3b3d41a9 100644 --- a/packages/neo4j-driver-lite/README.md +++ b/packages/neo4j-driver-lite/README.md @@ -390,3 +390,29 @@ var driver = neo4j.driver( { disableLosslessIntegers: true } ) ``` + +#### Writing and reading Vectors + +Neo4j supports storing vector embeddings in a dedicated vector type. Sending large lists with the driver will result in significant overhead as each value will be transmitted with type information, so the 6.0.0 release of the driver introduced the Neo4j Vector type. + +The Vector type supports signed integers of 8, 16, 32 and 64 bits, and floats of 32 and 64 bits. The Vector type is a wrapper for JavaScript TypedArrays of those types. + +To create a neo4j Vector in your code, do the following: + +```javascript +var neo4j = require('neo4j-driver') + +var typedArray = Float32Array.from([1, 2, 3]) //this is how to convert a regular array of numbers into a TypedArray, useful if you handle vectors as regular arrays in your code + +var neo4jVector = neo4j.vector(typedArray) //this creates a neo4j Vector of type Float32, containing the values [1, 2, 3] + +driver.executeQuery('CREATE (n {embeddings: $myVectorParam})', { myVectorParam: neo4jVector }) +``` + +To access the data in a retrieved Vector you can do the following: + +```javascript +var retrievedTypedArray = neo4jVector.asTypedArray() //This will return a TypedArray of the same type as the Vector + +var retrievedArray = Array.from(retrievedTypedArray) //This will convert the TypedArray to a regular array of Numbers. (Not safe for Int64 arrays) +``` \ No newline at end of file diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index 2e078aa48..fa1bbae2a 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -108,7 +108,10 @@ import { ClientCertificateProviders, RotatingClientCertificateProvider, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + vector, + VectorType, + Vector } from 'neo4j-driver-core' import { DirectConnectionProvider, RoutingConnectionProvider } from 'neo4j-driver-bolt-connection' @@ -206,6 +209,7 @@ function driver ( routing = true break default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unknown scheme: ${parsedUrl.scheme ?? 'null'}`) } @@ -258,6 +262,7 @@ function driver ( routingContext: parsedUrl.query }) } else { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!isEmptyObjectOrNull(parsedUrl.query)) { throw new Error( `Parameters are not supported with none routed scheme. Given URL: '${url}'` @@ -439,7 +444,9 @@ const forExport = { notificationFilterDisabledCategory, notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + Vector, + vector } export { @@ -510,7 +517,8 @@ export { notificationFilterDisabledCategory, notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + vector } export type { QueryResult, @@ -541,6 +549,8 @@ export type { ClientCertificate, ClientCertificateProvider, ClientCertificateProviders, - RotatingClientCertificateProvider + RotatingClientCertificateProvider, + Vector, + VectorType } export default forExport diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 911ad9fcd..fb3affcba 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -78,7 +78,10 @@ import { notificationFilterMinimumSeverityLevel, staticAuthTokenManager, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + Vector, + VectorType, + vector } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -282,7 +285,9 @@ const types = { LocalDateTime, LocalTime, Time, - Integer + Integer, + Vector, + VectorType } /** @@ -402,7 +407,8 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + vector } export { @@ -474,6 +480,9 @@ export { notificationFilterDisabledCategory, notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + vector, + Vector, + VectorType } export default forExport diff --git a/packages/neo4j-driver/test/vector-type.test.js b/packages/neo4j-driver/test/vector-type.test.js new file mode 100644 index 000000000..d2fcad60a --- /dev/null +++ b/packages/neo4j-driver/test/vector-type.test.js @@ -0,0 +1,65 @@ +/** + * 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', () => { + 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 () => { + 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 () => { + 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 arrayVec = res.records[0].get('arrayVector').asTypedArray() + const bufferVec = res.records[0].get('bufferVector').asTypedArray() + + expect(arrayVec[0]).toBe(1) + expect(bufferVec[1]).toBe(1) + } + }) +}) diff --git a/packages/testkit-backend/src/cypher-native-binders.js b/packages/testkit-backend/src/cypher-native-binders.js index a0d8fd61d..22272007b 100644 --- a/packages/testkit-backend/src/cypher-native-binders.js +++ b/packages/testkit-backend/src/cypher-native-binders.js @@ -176,6 +176,13 @@ export default function CypherNativeBinders (neo4j) { }) } + if (x.asTypedArray != null) { + const dtype = typeToDType[x.getType()] + const buffer = fixBufferEndianness(dtype, x.asTypedArray().buffer) + const data = toHexString(new Uint8Array(buffer)) + return structResponse('CypherVector', { dtype, data }) + } + // If all failed, interpret as a map const map = {} for (const [key, value] of Object.entries(x)) { @@ -284,6 +291,26 @@ export default function CypherNativeBinders (neo4j) { } } throw new Error(`Unknown Point system '${data.system}'`) + case 'CypherVector': { + const arrayBuffer = toByteArray(data.data) + const buffer = fixBufferEndianness(data.dtype, arrayBuffer.buffer) + switch (data.dtype) { + case 'i8': + return neo4j.vector(new Int8Array(buffer)) + case 'i16': + return neo4j.vector(new Int16Array(buffer)) + case 'i32': + return neo4j.vector(new Int32Array(buffer)) + case 'i64': + return neo4j.vector(new BigInt64Array(buffer)) + case 'f32': + return neo4j.vector(new Float32Array(buffer)) + case 'f64': + return neo4j.vector(new Float64Array(buffer)) + default: + throw new Error('Unknown Inner Vector type ' + data.dtype) + } + } } console.log(`Type ${name} is not handle by cypherToNative`, c) const err = 'Unable to convert ' + c + ' to native type' @@ -314,6 +341,90 @@ export default function CypherNativeBinders (neo4j) { } } + function toHexString (byteArray) { + let string = '' + for (let i = 0; i < byteArray.length; i++) { + string += (('0' + byteArray[i].toString(16) + ' ').slice(-3)) + } + return string.slice(0, -1) + } + + function toByteArray (hexString) { + const result = [] + for (let i = 0; i < hexString.length; i += 3) { + result.push(parseInt(hexString.substr(i, i + 2), 16)) + } + return Uint8Array.from(result) + } + + const typeToDType = { + INT8: 'i8', + INT16: 'i16', + INT32: 'i32', + INT64: 'i64', + FLOAT32: 'f32', + FLOAT64: 'f64' + } + + function fixBufferEndianness (typeMarker, buffer) { + const isLittleEndian = checkLittleEndian() + if (isLittleEndian) { + const setview = new DataView(new ArrayBuffer(buffer.byteLength)) + // we want exact byte accuracy, so we cannot simply get the value from the typed array + const getview = new DataView(buffer) + let set + let get + let elementSize + switch (typeMarker) { + case 'i8': + elementSize = 1 + set = setview.setInt8.bind(setview) + get = getview.getInt8.bind(getview) + break + case 'i16': + elementSize = 2 + set = setview.setInt16.bind(setview) + get = getview.getInt16.bind(getview) + break + case 'i32': + elementSize = 4 + set = setview.setInt32.bind(setview) + get = getview.getInt32.bind(getview) + break + case 'i64': + elementSize = 8 + set = setview.setBigInt64.bind(setview) + get = getview.getBigInt64.bind(getview) + break + case 'f32': + elementSize = 4 + set = setview.setInt32.bind(setview) + get = getview.getInt32.bind(getview) + break + case 'f64': + elementSize = 8 + set = setview.setBigInt64.bind(setview) + get = getview.getBigInt64.bind(getview) + break + default: + throw new Error(`Vector is of unsupported type ${typeMarker}`) + } + for (let i = 0; i < buffer.byteLength; i += elementSize) { + set(i, get(i, isLittleEndian)) + } + return setview.buffer + } else { + return buffer + } + } + + function checkLittleEndian () { + const dataview = new DataView(new ArrayBuffer(2)) + dataview.setInt16(0, 1000, true) + const typeArray = new Int16Array(dataview.buffer) + return typeArray[0] === 1000 + } + this.valueResponse = valueResponse this.objectToCypher = objectToCypher this.objectToNative = objectToNative diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index aa847e062..a619d6531 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', @@ -42,6 +43,7 @@ const features = [ 'Feature:API:Session:NotificationsConfig', 'Feature:API:Summary:GqlStatusObjects', 'Feature:API:Liveness.Check', + 'Feature:API:Type.Vector', 'Optimization:AuthPipelining', 'Optimization:EagerTransactionBegin', 'Optimization:ExecuteQueryPipelining',