diff --git a/src/v1/driver.js b/src/v1/driver.js index 4dd03c593..771120761 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -23,6 +23,7 @@ import {connect} from './internal/connector'; import StreamObserver from './internal/stream-observer'; import {newError, SERVICE_UNAVAILABLE} from './error'; import {DirectConnectionProvider} from './internal/connection-providers'; +import Bookmark from './internal/bookmark'; const READ = 'READ', WRITE = 'WRITE'; /** @@ -115,13 +116,14 @@ class Driver { * made available for others to use. * * @param {string} [mode=WRITE] the access mode of this session, allowed values are {@link READ} and {@link WRITE}. - * @param {string} [bookmark=null] the initial reference to some previous transaction. Value is optional and - * absence indicates that that the bookmark does not exist or is unknown. + * @param {string|string[]} [bookmarkOrBookmarks=null] the initial reference or references to some previous + * transactions. Value is optional and absence indicates that that the bookmarks do not exist or are unknown. * @return {Session} new session. */ - session(mode, bookmark) { + session(mode, bookmarkOrBookmarks) { const sessionMode = Driver._validateSessionMode(mode); const connectionProvider = this._getOrCreateConnectionProvider(); + const bookmark = new Bookmark(bookmarkOrBookmarks); return this._createSession(sessionMode, connectionProvider, bookmark, this._config); } diff --git a/src/v1/internal/bookmark.js b/src/v1/internal/bookmark.js new file mode 100644 index 000000000..ccb350130 --- /dev/null +++ b/src/v1/internal/bookmark.js @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2002-2017 "Neo Technology,"," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * 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 * as util from './util'; + +const BOOKMARK_KEY = 'bookmark'; +const BOOKMARKS_KEY = 'bookmarks'; +const BOOKMARK_PREFIX = 'neo4j:bookmark:v1:tx'; + +const UNKNOWN_BOOKMARK_VALUE = -1; + +export default class Bookmark { + + /** + * @constructor + * @param {string|string[]} values single bookmark as string or multiple bookmarks as a string array. + */ + constructor(values) { + this._values = asStringArray(values); + this._maxValue = maxBookmark(this._values); + } + + /** + * Check if the given bookmark is meaningful and can be send to the database. + * @return {boolean} returns true bookmark has a value, false otherwise. + */ + isEmpty() { + return this._maxValue === null; + } + + /** + * Get maximum value of this bookmark as string. + * @return {string|null} the maximum value or null if it is not defined. + */ + maxBookmarkAsString() { + return this._maxValue; + } + + /** + * Get this bookmark as an object for begin transaction call. + * @return {object} the value of this bookmark as object. + */ + asBeginTransactionParameters() { + if (this.isEmpty()) { + return {}; + } + + // Driver sends {bookmark: "max", bookmarks: ["one", "two", "max"]} instead of simple + // {bookmarks: ["one", "two", "max"]} for backwards compatibility reasons. Old servers can only accept single + // bookmark that is why driver has to parse and compare given list of bookmarks. This functionality will + // eventually be removed. + return { + [BOOKMARK_KEY]: this._maxValue, + [BOOKMARKS_KEY]: this._values + }; + } +} + +/** + * Converts given value to an array. + * @param {string|string[]} [value=undefined] argument to convert. + * @return {string[]} value converted to an array. + */ +function asStringArray(value) { + if (!value) { + return []; + } + + if (util.isString(value)) { + return [value]; + } + + if (Array.isArray(value)) { + const result = []; + for (let i = 0; i < value.length; i++) { + const element = value[i]; + if (!util.isString(element)) { + throw new TypeError(`Bookmark should be a string, given: '${element}'`); + } + result.push(element); + } + return result; + } + + throw new TypeError(`Bookmark should either be a string or a string array, given: '${value}'`); +} + +/** + * Find latest bookmark in the given array of bookmarks. + * @param {string[]} bookmarks array of bookmarks. + * @return {string|null} latest bookmark value. + */ +function maxBookmark(bookmarks) { + if (!bookmarks || bookmarks.length === 0) { + return null; + } + + let maxBookmark = bookmarks[0]; + let maxValue = bookmarkValue(maxBookmark); + + for (let i = 1; i < bookmarks.length; i++) { + const bookmark = bookmarks[i]; + const value = bookmarkValue(bookmark); + + if (value > maxValue) { + maxBookmark = bookmark; + maxValue = value; + } + } + + return maxBookmark; +} + +/** + * Calculate numeric value for the given bookmark. + * @param {string} bookmark argument to get numeric value for. + * @return {number} value of the bookmark. + */ +function bookmarkValue(bookmark) { + if (bookmark && bookmark.indexOf(BOOKMARK_PREFIX) === 0) { + const result = parseInt(bookmark.substring(BOOKMARK_PREFIX.length)); + return result ? result : UNKNOWN_BOOKMARK_VALUE; + } + return UNKNOWN_BOOKMARK_VALUE; +} diff --git a/src/v1/internal/util.js b/src/v1/internal/util.js index e7fd5c66e..50811e23a 100644 --- a/src/v1/internal/util.js +++ b/src/v1/internal/util.js @@ -116,6 +116,7 @@ function trimAndVerify(string, name, url) { export { isEmptyObjectOrNull, + isString, assertString, parseScheme, parseUrl, diff --git a/src/v1/session.js b/src/v1/session.js index 74770a936..5ff5f13e6 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -24,6 +24,7 @@ import {assertString} from './internal/util'; import ConnectionHolder from './internal/connection-holder'; import Driver, {READ, WRITE} from './driver'; import TransactionExecutor from './internal/transaction-executor'; +import Bookmark from './internal/bookmark'; /** * A Session instance is used for handling the connection and @@ -37,7 +38,7 @@ class Session { * @constructor * @param {string} mode the default access mode for this session. * @param {ConnectionProvider} connectionProvider - the connection provider to acquire connections from. - * @param {string} [bookmark=undefined] - the initial bookmark for this session. + * @param {Bookmark} bookmark - the initial bookmark for this session. * @param {Object} [config={}] - this driver configuration. */ constructor(mode, connectionProvider, bookmark, config) { @@ -94,21 +95,17 @@ class Session { * * While a transaction is open the session cannot be used to run statements outside the transaction. * - * @param {string} bookmark - a reference to a previous transaction. DEPRECATED: This parameter is deprecated in - * favour of {@link Driver#session} that accepts an initial bookmark. Session will ensure that all nested - * transactions are chained with bookmarks to guarantee causal consistency. + * @param {string|string[]} [bookmarkOrBookmarks=null] - reference or references to some previous transactions. + * DEPRECATED: This parameter is deprecated in favour of {@link Driver#session} that accepts an initial bookmark. + * Session will ensure that all nested transactions are chained with bookmarks to guarantee causal consistency. * @returns {Transaction} - New Transaction */ - beginTransaction(bookmark) { - return this._beginTransaction(this._mode, bookmark); + beginTransaction(bookmarkOrBookmarks) { + this._updateBookmark(new Bookmark(bookmarkOrBookmarks)); + return this._beginTransaction(this._mode); } - _beginTransaction(accessMode, bookmark) { - if (bookmark) { - assertString(bookmark, 'Bookmark'); - this._updateBookmark(bookmark); - } - + _beginTransaction(accessMode) { if (this._hasTx) { throw newError('You cannot begin a transaction on a session with an open transaction; ' + 'either run from within the transaction or use a different session.'); @@ -128,10 +125,10 @@ class Session { /** * Return the bookmark received following the last completed {@link Transaction}. * - * @return a reference to a previous transaction + * @return {string|null} a reference to a previous transaction */ lastBookmark() { - return this._lastBookmark; + return this._lastBookmark.maxBookmarkAsString(); } /** @@ -170,13 +167,18 @@ class Session { _runTransaction(accessMode, transactionWork) { return this._transactionExecutor.execute( - () => this._beginTransaction(accessMode, this.lastBookmark()), + () => this._beginTransaction(accessMode), transactionWork ); } + /** + * Update value of the last bookmark. + * @param {Bookmark} newBookmark the new bookmark. + * @private + */ _updateBookmark(newBookmark) { - if (newBookmark) { + if (newBookmark && !newBookmark.isEmpty()) { this._lastBookmark = newBookmark; } } diff --git a/src/v1/transaction.js b/src/v1/transaction.js index c4c0fa7e4..1c1ac34b3 100644 --- a/src/v1/transaction.js +++ b/src/v1/transaction.js @@ -20,6 +20,7 @@ import StreamObserver from './internal/stream-observer'; import Result from './result'; import {assertString} from './internal/util'; import {EMPTY_CONNECTION_HOLDER} from './internal/connection-holder'; +import Bookmark from './internal/bookmark'; /** * Represents a transaction in the Neo4j database. @@ -31,27 +32,23 @@ class Transaction { * @constructor * @param {ConnectionHolder} connectionHolder - the connection holder to get connection from. * @param {function()} onClose - Function to be called when transaction is committed or rolled back. - * @param errorTransformer callback use to transform error - * @param bookmark optional bookmark - * @param onBookmark callback invoked when new bookmark is produced + * @param {function(error: Error): Error} errorTransformer callback use to transform error. + * @param {Bookmark} bookmark bookmark for transaction begin. + * @param {function(bookmark: Bookmark)} onBookmark callback invoked when new bookmark is produced. */ constructor(connectionHolder, onClose, errorTransformer, bookmark, onBookmark) { this._connectionHolder = connectionHolder; - let streamObserver = new _TransactionStreamObserver(this); - let params = {}; - if (bookmark) { - params = {bookmark: bookmark}; - } + const streamObserver = new _TransactionStreamObserver(this); this._connectionHolder.getConnection(streamObserver).then(conn => { - conn.run('BEGIN', params, streamObserver); + conn.run('BEGIN', bookmark.asBeginTransactionParameters(), streamObserver); conn.pullAll(streamObserver); }).catch(error => streamObserver.onError(error)); this._state = _states.ACTIVE; this._onClose = onClose; this._errorTransformer = errorTransformer; - this._onBookmark = onBookmark || (() => {}); + this._onBookmark = onBookmark; } /** @@ -149,7 +146,7 @@ class _TransactionStreamObserver extends StreamObserver { onCompleted(meta) { super.onCompleted(meta); - const bookmark = meta.bookmark; + const bookmark = new Bookmark(meta.bookmark); this._tx._onBookmark(bookmark); } } diff --git a/test/internal/bookmark.test.js b/test/internal/bookmark.test.js new file mode 100644 index 000000000..7b406c65b --- /dev/null +++ b/test/internal/bookmark.test.js @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2002-2017 "Neo Technology,"," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * 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 Bookmark from '../../src/v1/internal/bookmark'; + +describe('Bookmark', () => { + + it('should be possible to construct bookmark from string', () => { + const bookmark = new Bookmark('neo4j:bookmark:v1:tx412'); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:tx412'); + }); + + it('should be possible to construct bookmark from string array', () => { + const bookmark = new Bookmark(['neo4j:bookmark:v1:tx1', 'neo4j:bookmark:v1:tx2', 'neo4j:bookmark:v1:tx3']); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:tx3'); + }); + + it('should be possible to construct bookmark from null', () => { + const bookmark = new Bookmark(null); + + expect(bookmark.isEmpty()).toBeTruthy(); + expect(bookmark.maxBookmarkAsString()).toBeNull(); + }); + + it('should be possible to construct bookmark from undefined', () => { + const bookmark = new Bookmark(undefined); + + expect(bookmark.isEmpty()).toBeTruthy(); + expect(bookmark.maxBookmarkAsString()).toBeNull(); + }); + + it('should be possible to construct bookmark from an empty string', () => { + const bookmark = new Bookmark(''); + + expect(bookmark.isEmpty()).toBeTruthy(); + expect(bookmark.maxBookmarkAsString()).toBeNull(); + }); + + it('should be possible to construct bookmark from empty array', () => { + const bookmark = new Bookmark([]); + + expect(bookmark.isEmpty()).toBeTruthy(); + expect(bookmark.maxBookmarkAsString()).toBeNull(); + }); + + it('should not be possible to construct bookmark from object', () => { + expect(() => new Bookmark({})).toThrowError(TypeError); + expect(() => new Bookmark({bookmark: 'neo4j:bookmark:v1:tx1'})).toThrowError(TypeError); + }); + + it('should not be possible to construct bookmark from number array', () => { + expect(() => new Bookmark([1, 2, 3])).toThrowError(TypeError); + }); + + it('should not be possible to construct bookmark from mixed array', () => { + expect(() => new Bookmark(['neo4j:bookmark:v1:tx1', 2, 'neo4j:bookmark:v1:tx3'])).toThrowError(TypeError); + }); + + it('should keep unparsable bookmark', () => { + const bookmark = new Bookmark('neo4j:bookmark:v1:txWrong'); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:txWrong'); + }); + + it('should skip unparsable bookmarks', () => { + const bookmark = new Bookmark(['neo4j:bookmark:v1:tx42', 'neo4j:bookmark:v1:txWrong', 'neo4j:bookmark:v1:tx4242']); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:tx4242'); + }); + + it('should turn into empty transaction params when empty', () => { + const bookmark = new Bookmark(null); + + expect(bookmark.isEmpty()).toBeTruthy(); + expect(bookmark.asBeginTransactionParameters()).toEqual({}); + }); + + it('should turn into transaction params when represents single bookmark', () => { + const bookmark = new Bookmark('neo4j:bookmark:v1:tx142'); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.asBeginTransactionParameters()).toEqual({ + bookmark: 'neo4j:bookmark:v1:tx142', + bookmarks: ['neo4j:bookmark:v1:tx142'] + }); + }); + + it('should turn into transaction params when represents multiple bookmarks', () => { + const bookmark = new Bookmark( + ['neo4j:bookmark:v1:tx1', 'neo4j:bookmark:v1:tx3', 'neo4j:bookmark:v1:tx42', 'neo4j:bookmark:v1:tx5'] + ); + + expect(bookmark.isEmpty()).toBeFalsy(); + expect(bookmark.asBeginTransactionParameters()).toEqual({ + bookmark: 'neo4j:bookmark:v1:tx42', + bookmarks: ['neo4j:bookmark:v1:tx1', 'neo4j:bookmark:v1:tx3', 'neo4j:bookmark:v1:tx42', 'neo4j:bookmark:v1:tx5'] + }); + }); + +}); diff --git a/test/resources/boltkit/multiple_bookmarks.script b/test/resources/boltkit/multiple_bookmarks.script new file mode 100644 index 000000000..5e7344103 --- /dev/null +++ b/test/resources/boltkit/multiple_bookmarks.script @@ -0,0 +1,16 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx94", "bookmarks": ["neo4j:bookmark:v1:tx5", "neo4j:bookmark:v1:tx29", "neo4j:bookmark:v1:tx94", "neo4j:bookmark:v1:tx56", "neo4j:bookmark:v1:tx16", "neo4j:bookmark:v1:tx68"]} + PULL_ALL +S: SUCCESS {} + SUCCESS {} +C: RUN "CREATE (n {name:'Bob'})" {} + PULL_ALL +S: SUCCESS {} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx95"} +C: RUN "COMMIT" {} + PULL_ALL +S: SUCCESS {} + SUCCESS {} diff --git a/test/resources/boltkit/read_tx_with_bookmarks.script b/test/resources/boltkit/read_tx_with_bookmarks.script index a48ee65f9..cb22ffba3 100644 --- a/test/resources/boltkit/read_tx_with_bookmarks.script +++ b/test/resources/boltkit/read_tx_with_bookmarks.script @@ -2,7 +2,7 @@ !: AUTO RESET !: AUTO PULL_ALL -C: RUN "BEGIN" {"bookmark": "OldBookmark"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {} SUCCESS {} @@ -11,7 +11,7 @@ C: RUN "MATCH (n) RETURN n.name AS name" {} S: SUCCESS {"fields": ["name"]} RECORD ["Bob"] RECORD ["Alice"] - SUCCESS {"bookmark": "NewBookmark"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} diff --git a/test/resources/boltkit/write_read_tx_with_bookmark_override.script b/test/resources/boltkit/write_read_tx_with_bookmark_override.script index 889b5d324..238d55b09 100644 --- a/test/resources/boltkit/write_read_tx_with_bookmark_override.script +++ b/test/resources/boltkit/write_read_tx_with_bookmark_override.script @@ -2,19 +2,19 @@ !: AUTO RESET !: AUTO PULL_ALL -C: RUN "BEGIN" {"bookmark": "BookmarkA"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {} SUCCESS {} C: RUN "CREATE (n {name:'Bob'})" {} PULL_ALL S: SUCCESS {} - SUCCESS {"bookmark": "BookmarkB"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} SUCCESS {} -C: RUN "BEGIN" {"bookmark": "BookmarkOverride"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx99", "bookmarks": ["neo4j:bookmark:v1:tx99"]} PULL_ALL S: SUCCESS {} SUCCESS {} @@ -22,7 +22,7 @@ C: RUN "MATCH (n) RETURN n.name AS name" {} PULL_ALL S: SUCCESS {"fields": ["name"]} RECORD ["Bob"] - SUCCESS {"bookmark": "BookmarkC"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx424242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} diff --git a/test/resources/boltkit/write_read_tx_with_bookmarks.script b/test/resources/boltkit/write_read_tx_with_bookmarks.script index e8207f887..fc4a18e3a 100644 --- a/test/resources/boltkit/write_read_tx_with_bookmarks.script +++ b/test/resources/boltkit/write_read_tx_with_bookmarks.script @@ -2,19 +2,19 @@ !: AUTO RESET !: AUTO PULL_ALL -C: RUN "BEGIN" {"bookmark": "BookmarkA"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {} SUCCESS {} C: RUN "CREATE (n {name:'Bob'})" {} PULL_ALL S: SUCCESS {} - SUCCESS {"bookmark": "BookmarkB"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} SUCCESS {} -C: RUN "BEGIN" {"bookmark": "BookmarkB"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx4242", "bookmarks": ["neo4j:bookmark:v1:tx4242"]} PULL_ALL S: SUCCESS {} SUCCESS {} @@ -22,7 +22,7 @@ C: RUN "MATCH (n) RETURN n.name AS name" {} PULL_ALL S: SUCCESS {"fields": ["name"]} RECORD ["Bob"] - SUCCESS {"bookmark": "BookmarkC"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx424242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} diff --git a/test/resources/boltkit/write_tx_with_bookmarks.script b/test/resources/boltkit/write_tx_with_bookmarks.script index 08247a9e2..82269cb37 100644 --- a/test/resources/boltkit/write_tx_with_bookmarks.script +++ b/test/resources/boltkit/write_tx_with_bookmarks.script @@ -2,14 +2,14 @@ !: AUTO RESET !: AUTO PULL_ALL -C: RUN "BEGIN" {"bookmark": "OldBookmark"} +C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {} SUCCESS {} C: RUN "CREATE (n {name:'Bob'})" {} PULL_ALL S: SUCCESS {} - SUCCESS {"bookmark": "NewBookmark"} + SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {} diff --git a/test/v1/direct.driver.boltkit.it.js b/test/v1/direct.driver.boltkit.it.js index 781f78df3..ce03ba837 100644 --- a/test/v1/direct.driver.boltkit.it.js +++ b/test/v1/direct.driver.boltkit.it.js @@ -17,8 +17,8 @@ * limitations under the License. */ -import neo4j from '../../lib/v1'; -import {READ, WRITE} from '../../lib/v1/driver'; +import neo4j from '../../src/v1'; +import {READ, WRITE} from '../../src/v1/driver'; import boltkit from './boltkit'; import sharedNeo4j from '../internal/shared-neo4j'; @@ -62,7 +62,7 @@ describe('direct driver', () => { kit.run(() => { const driver = createDriver(); - const session = driver.session(READ, 'OldBookmark'); + const session = driver.session(READ, 'neo4j:bookmark:v1:tx42'); const tx = session.beginTransaction(); tx.run('MATCH (n) RETURN n.name AS name').then(result => { const records = result.records; @@ -71,7 +71,7 @@ describe('direct driver', () => { expect(records[1].get('name')).toEqual('Alice'); tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('NewBookmark'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); session.close(() => { driver.close(); @@ -96,14 +96,14 @@ describe('direct driver', () => { kit.run(() => { const driver = createDriver(); - const session = driver.session(WRITE, 'OldBookmark'); + const session = driver.session(WRITE, 'neo4j:bookmark:v1:tx42'); const tx = session.beginTransaction(); tx.run('CREATE (n {name:\'Bob\'})').then(result => { const records = result.records; expect(records.length).toEqual(0); tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('NewBookmark'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); session.close(() => { driver.close(); @@ -128,14 +128,14 @@ describe('direct driver', () => { kit.run(() => { const driver = createDriver(); - const session = driver.session(WRITE, 'BookmarkA'); + const session = driver.session(WRITE, 'neo4j:bookmark:v1:tx42'); const writeTx = session.beginTransaction(); writeTx.run('CREATE (n {name:\'Bob\'})').then(result => { const records = result.records; expect(records.length).toEqual(0); writeTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkB'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); const readTx = session.beginTransaction(); readTx.run('MATCH (n) RETURN n.name AS name').then(result => { @@ -144,7 +144,7 @@ describe('direct driver', () => { expect(records[0].get('name')).toEqual('Bob'); readTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkC'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242'); session.close(() => { driver.close(); @@ -171,23 +171,23 @@ describe('direct driver', () => { kit.run(() => { const driver = createDriver(); - const session = driver.session(WRITE, 'BookmarkA'); + const session = driver.session(WRITE, 'neo4j:bookmark:v1:tx42'); const writeTx = session.beginTransaction(); writeTx.run('CREATE (n {name:\'Bob\'})').then(result => { const records = result.records; expect(records.length).toEqual(0); writeTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkB'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); - const readTx = session.beginTransaction('BookmarkOverride'); + const readTx = session.beginTransaction('neo4j:bookmark:v1:tx99'); readTx.run('MATCH (n) RETURN n.name AS name').then(result => { const records = result.records; expect(records.length).toEqual(1); expect(records[0].get('name')).toEqual('Bob'); readTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkC'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242'); session.close(() => { driver.close(); @@ -215,14 +215,14 @@ describe('direct driver', () => { kit.run(() => { const driver = createDriver(); - const session = driver.session(WRITE, 'BookmarkA'); + const session = driver.session(WRITE, 'neo4j:bookmark:v1:tx42'); const writeTx = session.beginTransaction(); writeTx.run('CREATE (n {name:\'Bob\'})').then(result => { const records = result.records; expect(records.length).toEqual(0); writeTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkB'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); const readTx = session.beginTransaction(null); readTx.run('MATCH (n) RETURN n.name AS name').then(result => { @@ -231,7 +231,7 @@ describe('direct driver', () => { expect(records[0].get('name')).toEqual('Bob'); readTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkC'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242'); session.close(() => { driver.close(); diff --git a/test/v1/routing.driver.boltkit.it.js b/test/v1/routing.driver.boltkit.it.js index ebc97eb15..769cb22a1 100644 --- a/test/v1/routing.driver.boltkit.it.js +++ b/test/v1/routing.driver.boltkit.it.js @@ -1072,10 +1072,10 @@ describe('routing driver', () => { const driver = newDriver('bolt+routing://127.0.0.1:9001'); const session = driver.session(); - const tx = session.beginTransaction('OldBookmark'); + const tx = session.beginTransaction('neo4j:bookmark:v1:tx42'); tx.run('CREATE (n {name:\'Bob\'})').then(() => { tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('NewBookmark'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); session.close(); driver.close(); @@ -1093,11 +1093,11 @@ describe('routing driver', () => { }); it('should send initial bookmark wihtout access mode', done => { - testWriteSessionWithAccessModeAndBookmark(null, 'OldBookmark', done); + testWriteSessionWithAccessModeAndBookmark(null, 'neo4j:bookmark:v1:tx42', done); }); it('should use write session mode and initial bookmark', done => { - testWriteSessionWithAccessModeAndBookmark(WRITE, 'OldBookmark', done); + testWriteSessionWithAccessModeAndBookmark(WRITE, 'neo4j:bookmark:v1:tx42', done); }); it('should use read session mode and initial bookmark', done => { @@ -1113,7 +1113,7 @@ describe('routing driver', () => { kit.run(() => { const driver = newDriver('bolt+routing://127.0.0.1:9001'); - const session = driver.session(READ, 'OldBookmark'); + const session = driver.session(READ, 'neo4j:bookmark:v1:tx42'); const tx = session.beginTransaction(); tx.run('MATCH (n) RETURN n.name AS name').then(result => { const records = result.records; @@ -1122,7 +1122,7 @@ describe('routing driver', () => { expect(records[1].get('name')).toEqual('Alice'); tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('NewBookmark'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); session.close(); driver.close(); @@ -1152,11 +1152,11 @@ describe('routing driver', () => { kit.run(() => { const driver = newDriver('bolt+routing://127.0.0.1:9001'); - const session = driver.session(null, 'BookmarkA'); + const session = driver.session(null, 'neo4j:bookmark:v1:tx42'); const writeTx = session.beginTransaction(); writeTx.run('CREATE (n {name:\'Bob\'})').then(() => { writeTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkB'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); const readTx = session.beginTransaction(); readTx.run('MATCH (n) RETURN n.name AS name').then(result => { @@ -1165,7 +1165,7 @@ describe('routing driver', () => { expect(records[0].get('name')).toEqual('Bob'); readTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('BookmarkC'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242'); session.close(); driver.close(); @@ -1858,6 +1858,38 @@ describe('routing driver', () => { }); }); + it('should send multiple bookmarks', done => { + const kit = new boltkit.BoltKit(); + const router = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9010); + const writer = kit.start('./test/resources/boltkit/multiple_bookmarks.script', 9007); + + kit.run(() => { + const driver = newDriver('bolt+routing://127.0.0.1:9010'); + + const bookmarks = ['neo4j:bookmark:v1:tx5', 'neo4j:bookmark:v1:tx29', 'neo4j:bookmark:v1:tx94', + 'neo4j:bookmark:v1:tx56', 'neo4j:bookmark:v1:tx16', 'neo4j:bookmark:v1:tx68']; + const session = driver.session(WRITE, bookmarks); + const tx = session.beginTransaction(); + + tx.run(`CREATE (n {name:'Bob'})`).then(() => { + tx.commit().then(() => { + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx95'); + + session.close(); + driver.close(); + + router.exit(code1 => { + writer.exit(code2 => { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + function moveNextDateNow30SecondsForward() { const currentTime = Date.now(); hijackNextDateNowCall(currentTime + 30 * 1000 + 1); @@ -1880,7 +1912,7 @@ describe('routing driver', () => { const tx = session.beginTransaction(); tx.run('CREATE (n {name:\'Bob\'})').then(() => { tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('NewBookmark'); + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242'); session.close(); driver.close(); diff --git a/test/v1/session.test.js b/test/v1/session.test.js index de4a2daf5..daf6a0dc2 100644 --- a/test/v1/session.test.js +++ b/test/v1/session.test.js @@ -24,6 +24,7 @@ import {READ} from '../../src/v1/driver'; import {SingleConnectionProvider} from '../../src/v1/internal/connection-providers'; import FakeConnection from '../internal/fake-connection'; import sharedNeo4j from '../internal/shared-neo4j'; +import _ from 'lodash'; describe('session', () => { @@ -434,9 +435,9 @@ describe('session', () => { it('should fail nicely for illegal bookmark', () => { expect(() => session.beginTransaction({})).toThrowError(TypeError); + expect(() => session.beginTransaction({foo: 'bar'})).toThrowError(TypeError); expect(() => session.beginTransaction(42)).toThrowError(TypeError); - expect(() => session.beginTransaction([])).toThrowError(TypeError); - expect(() => session.beginTransaction(['bookmark'])).toThrowError(TypeError); + expect(() => session.beginTransaction([42.0, 42.0])).toThrowError(TypeError); }); it('should allow creation of a ' + neo4j.session.READ + ' session', done => { @@ -943,6 +944,27 @@ describe('session', () => { }); }); + it('should send multiple bookmarks', done => { + if (!serverIs31OrLater(done)) { + return; + } + + const nodeCount = 17; + const bookmarkPromises = _.range(nodeCount).map(() => runQueryAndGetBookmark(driver)); + + Promise.all(bookmarkPromises).then(bookmarks => { + expect(_.uniq(bookmarks).length > 1).toBeTruthy(); + bookmarks.forEach(bookmark => expect(_.isString(bookmark)).toBeTruthy()); + + const session = driver.session(READ, bookmarks); + session.run('MATCH (n) RETURN count(n)').then(result => { + const count = result.records[0].get(0).toInt(); + expect(count).toEqual(nodeCount); + session.close(() => done()); + }); + }); + }); + function serverIs31OrLater(done) { // lazy way of checking the version number // if server has been set we know it is at least 3.1 @@ -1004,4 +1026,21 @@ describe('session', () => { }); }); } + + function runQueryAndGetBookmark(driver) { + const session = driver.session(); + const tx = session.beginTransaction(); + + return new Promise((resolve, reject) => { + tx.run('CREATE ()').then(() => { + tx.commit().then(() => { + const bookmark = session.lastBookmark(); + session.close(() => { + resolve(bookmark); + }); + }).catch(error => reject(error)); + }).catch(error => reject(error)); + }); + } + }); diff --git a/test/v1/transaction.test.js b/test/v1/transaction.test.js index c69d44ae7..baa5309b2 100644 --- a/test/v1/transaction.test.js +++ b/test/v1/transaction.test.js @@ -233,7 +233,7 @@ describe('transaction', () => { } const tx = session.beginTransaction(); - expect(session.lastBookmark()).not.toBeDefined(); + expect(session.lastBookmark()).toBeNull(); tx.run("CREATE (:TXNode1)").then(() => { tx.run("CREATE (:TXNode2)").then(() => { tx.commit().then(() => { @@ -249,7 +249,7 @@ describe('transaction', () => { return; } - expect(session.lastBookmark()).not.toBeDefined(); + expect(session.lastBookmark()).toBeNull(); const tx1 = session.beginTransaction(); tx1.run('CREATE ()').then(() => { @@ -282,7 +282,7 @@ describe('transaction', () => { return; } - expect(session.lastBookmark()).not.toBeDefined(); + expect(session.lastBookmark()).toBeNull(); const tx1 = session.beginTransaction(); tx1.run('CREATE ()').then(() => {