This is an automated email from the ASF dual-hosted git repository. spmallette pushed a commit to branch TINKERPOP-1959-tp33 in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit ef34c12c7bf84cc812db8fe55bb09d594c84cc35 Author: Matthew Allen <[email protected]> AuthorDate: Thu Aug 23 15:48:36 2018 +0100 Initial Commit for TINKERPOP-1959 solution. --- .../glv/GraphTraversalSource.template | 13 +++ gremlin-javascript/glv/TraversalSource.template | 16 +++- .../lib/driver/driver-remote-connection.js | 29 ++++-- .../lib/driver/remote-connection.js | 28 +++++- .../gremlin-javascript/lib/process/bytecode.js | 69 +++++++++++++- .../lib/process/graph-traversal.js | 13 +++ .../gremlin-javascript/lib/process/traversal.js | 16 +++- .../test/integration/traversal-test.js | 18 ++++ .../gremlin-javascript/test/unit/eval-test.js | 104 +++++++++++++++++++++ .../gremlin/server/gremlin-server-integration.yaml | 1 + 10 files changed, 291 insertions(+), 16 deletions(-) diff --git a/gremlin-javascript/glv/GraphTraversalSource.template b/gremlin-javascript/glv/GraphTraversalSource.template index 58be16c..3f8e715 100644 --- a/gremlin-javascript/glv/GraphTraversalSource.template +++ b/gremlin-javascript/glv/GraphTraversalSource.template @@ -104,6 +104,19 @@ class GraphTraversal extends Traversal { return this; } <% } %> + + + /** + * Send a Gremlin-Groovy script to the server. If a script is not passed in + * then the bytecode instructions will be converted to a script and sent. + * @param {string} gremlinScript The script to send to server + * @param {array} bindings Map of bindings + * @param {*} options Options to configure the script sending + */ + eval(script, bindings) { + this.bytecode.addStep('eval', [ script, bindings ]); + return this._applyStrategies().then(() => this._getNext()); + } } function callOnEmptyTraversal(fnName, args) { diff --git a/gremlin-javascript/glv/TraversalSource.template b/gremlin-javascript/glv/TraversalSource.template index 6965110..6afe868 100644 --- a/gremlin-javascript/glv/TraversalSource.template +++ b/gremlin-javascript/glv/TraversalSource.template @@ -79,15 +79,29 @@ class Traversal { } /** + * Send a Gremlin-Groovy script to the server. If a script is not passed in + * then the bytecode instructions will be converted to a script and sent. + * @param {string} gremlinScript The script to send to server + * @param {array} bindings Map of bindings + * @param {*} options Options to configure the script sending + */ + eval(script, bindings) { + this.bytecode.addStep('eval', [ script, bindings ]); + return this._applyStrategies().then(() => this._getNext()); + } + + /** * Synchronous iterator of traversers including * @private */ _getNext() { while (this.traversers && this._traversersIteratorIndex < this.traversers.length) { let traverser = this.traversers[this._traversersIteratorIndex]; - if (traverser.bulk > 0) { + if (traverser.bulk && traverser.bulk > 0) { traverser.bulk--; return { value: traverser.object, done: false }; + } else if (traverser.bulk === undefined) { + return { value: traverser, done: true } } this._traversersIteratorIndex++; } diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/driver-remote-connection.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/driver-remote-connection.js index ef0242c..fb66aae 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/driver-remote-connection.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/driver-remote-connection.js @@ -107,7 +107,7 @@ class DriverRemoteConnection extends RemoteConnection { } /** @override */ - submit(bytecode, op, args, requestId) { + submit(bytecode, op, args, requestId, processor) { return this.open().then(() => new Promise((resolve, reject) => { if (requestId === null || requestId === undefined) { requestId = utils.getUuid(); @@ -116,27 +116,38 @@ class DriverRemoteConnection extends RemoteConnection { result: null }; } - const message = bufferFromString(this._header + JSON.stringify(this._getRequest(requestId, bytecode, op, args))); + + const message = bufferFromString(this._header + JSON.stringify(this._getRequest(requestId, bytecode, op, args, processor))); this._ws.send(message); })); } - _getRequest(id, bytecode, op, args) { + _getRequest(id, bytecode, op, args, processor) { if (args) { args = this._adaptArgs(args); } - + return ({ 'requestId': { '@type': 'g:UUID', '@value': id }, 'op': op || 'bytecode', - 'processor': 'traversal', - 'args': args || { - 'gremlin': this._writer.adaptObject(bytecode), - 'aliases': { 'g': this.traversalSource } - } + // if using op eval need to ensure processor stays unset if caller didn't set it. + 'processor': (!processor && op !== 'eval') ? 'traversal' : processor, + 'args': this._getArgs(args || { + 'gremlin': this._writer.adaptObject(bytecode) + }, + op + ) }); } + _getArgs(args, op) { + if (args.aliases === undefined) { + args.aliases = { 'g': this.traversalSource }; + } + + return args; + } + _handleMessage(data) { const response = this._reader.read(JSON.parse(data.toString())); if (response.requestId === null || response.requestId === undefined) { diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/remote-connection.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/remote-connection.js index be6f962..d93d89b 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/remote-connection.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/remote-connection.js @@ -34,11 +34,12 @@ class RemoteConnection { * @abstract * @param {Bytecode} bytecode * @param {String} op Operation to perform, defaults to bytecode. - * @param {Object} args The arguments for the operation. Defaults to an associative array containing values for "aliases" and "gremlin" keyss. + * @param {Object} args The arguments for the operation. Defaults to an associative array containing values for "aliases" and "gremlin" keys. * @param {String} requestId A requestId for the current request. If none provided then a requestId is generated internally. + * @param {String} processor The processor to use on the connection. * @returns {Promise} */ - submit(bytecode, op, args, requestId) { + submit(bytecode, op, args, requestId, processor) { throw new Error('submit() was not implemented'); }; } @@ -66,7 +67,28 @@ class RemoteStrategy extends TraversalStrategy { if (traversal.traversers) { return Promise.resolve(); } - return this.connection.submit(traversal.getBytecode()).then(function (remoteTraversal) { + + let instructions = traversal.getBytecode(); + let op = 'bytecode'; + let processor = 'traversal'; + let args = null; + + // check if the last instruction is an eval statement + const bytecode = traversal.getBytecode(); + if (bytecode.stepInstructions.length && bytecode.stepInstructions[bytecode.stepInstructions.length-1][0] === 'eval') { + const script = traversal.getBytecode().toScript(); + op = 'eval'; + processor = ''; + args = { + 'gremlin': script.script, + 'bindings': script.bindings, + 'language': 'gremlin-groovy', + 'accept': 'application/json', + }; + instructions = null; + } + + return this.connection.submit(instructions, op, args, null, processor).then(function (remoteTraversal) { traversal.sideEffects = remoteTraversal.sideEffects; traversal.traversers = remoteTraversal.traversers; }); diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/bytecode.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/bytecode.js index 0f5ba15..fe96568 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/bytecode.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/bytecode.js @@ -91,8 +91,73 @@ class Bytecode { (this.stepInstructions.length > 0 ? JSON.stringify(this.stepInstructions) : '') ); } -} + /** + * Returns a script representations of the step instructions that can be used by standard eval operation. + * @returns {Object} An object containing a script string and bindings map. + */ + toScript() { + let bindings = {}; + let script = 'g'; + let length = this.stepInstructions.length; + + // if eval was passed a script then simply execute the given script. + if (this.stepInstructions[length - 1][0] === 'eval' + && this.stepInstructions[length - 1][1] !== undefined + && this.stepInstructions[length - 1][1] !== null + ) { + return { + script: this.stepInstructions[length - 1][1], + bindings: this.stepInstructions[length - 1][2] + } + } + + if (this.stepInstructions[length - 1][0] === 'eval') { + this.stepInstructions.pop(); + length = this.stepInstructions.length; + } + // build the script from the glv instructions. + let paramIdx = 1; + for (let i = 0; i < length; i++) { + const params = this.stepInstructions[i].slice(1); + script = script + '.' + this.stepInstructions[i][0] + '('; + + if (params.length) { + for (let k = 0; k < params.length; k++) { + if (k > 0) { + script = script + ', '; + } + + if (Object(params[k]) === params[k]) { + script = script + params[k].toString(); + } else { + const prop = `p${paramIdx++}`; + script = script + prop; + + if (typeof params[k] === 'number') { + if (Number.isInteger(params[k])) { + bindings[prop] = Number.parseInt(params[k]); + } else { + bindings[prop] = Number.parseFloat(params[k]); + } + } else if (params[k] === undefined) { + bindings[prop] = null; + } else { + bindings[prop] = params[k]; + } + } + } + } + + script = script + ')'; + } + + return { + script, + bindings + }; + } +} -module.exports = Bytecode; \ No newline at end of file +module.exports = Bytecode; diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js index edeb2cb..aa0259b 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js @@ -1132,6 +1132,19 @@ class GraphTraversal extends Traversal { return this; } + + + /** + * Send a Gremlin-Groovy script to the server. If a script is not passed in + * then the bytecode instructions will be converted to a script and sent. + * @param {string} gremlinScript The script to send to server + * @param {array} bindings Map of bindings + * @param {*} options Options to configure the script sending + */ + eval(script, bindings) { + this.bytecode.addStep('eval', [ script, bindings ]); + return this._applyStrategies().then(() => this._getNext()); + } } function callOnEmptyTraversal(fnName, args) { diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal.js index d39ccf0..d8a0761 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal.js @@ -79,15 +79,29 @@ class Traversal { } /** + * Send a Gremlin-Groovy script to the server. If a script is not passed in + * then the bytecode instructions will be converted to a script and sent. + * @param {string} gremlinScript The script to send to server + * @param {array} bindings Map of bindings + * @param {*} options Options to configure the script sending + */ + eval(script, bindings) { + this.bytecode.addStep('eval', [ script, bindings ]); + return this._applyStrategies().then(() => this._getNext()); + } + + /** * Synchronous iterator of traversers including * @private */ _getNext() { while (this.traversers && this._traversersIteratorIndex < this.traversers.length) { let traverser = this.traversers[this._traversersIteratorIndex]; - if (traverser.bulk > 0) { + if (traverser.bulk && traverser.bulk > 0) { traverser.bulk--; return { value: traverser.object, done: false }; + } else if (traverser.bulk === undefined) { + return { value: traverser, done: true } } this._traversersIteratorIndex++; } diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js index 920d998..9d5af16 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js @@ -66,4 +66,22 @@ describe('Traversal', function () { }); }); }); + describe('#eval()', function() { + it('should submit the traversal as a script and return a result', function() { + var g = new Graph().traversal().withRemote(connection); + return g.V().count().eval().then(function (item) { + assert.ok(item); + assert.strictEqual(item.done, true); + assert.strictEqual(typeof item.value, 'number'); + }); + }); + + it('should submit a script and bindings and return a result', function() { + var g = new Graph().traversal().withRemote(connection); + return g.V().eval('g.V().has(\'name\', name)', { name: 'marko' }).then(function (item) { + assert.ok(item); + assert.ok(item.value instanceof Vertex); + }); + }); + }); }); \ No newline at end of file diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/eval-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/eval-test.js new file mode 100644 index 0000000..c3c87fb --- /dev/null +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/eval-test.js @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +'use strict'; + +const assert = require('assert'); +const expect = require('chai').expect; +const graph = require('../../lib/structure/graph'); +const t = require('../../lib/process/traversal'); +const TraversalStrategies = require('../../lib/process/traversal-strategy').TraversalStrategies; +const Bytecode = require('../../lib/process/bytecode'); + +describe('Traversal', function () { + + describe('#getBytecode#toScript()', function () { + it('should add steps and produce valid script representation', function () { + const g = new graph.Graph().traversal(); + const script = g.V().out('created').getBytecode().toScript(); + assert.ok(script); + assert.strictEqual(script.script, 'g.V().out(p1)'); + }); + + it('should add steps and produce valid script representation with parameter bindings', function () { + const g = new graph.Graph().traversal(); + const script = g.addV('name', 'Lilac').getBytecode().toScript(); + assert.ok(script); + assert.strictEqual(script.script, 'g.addV(p1, p2)'); + assert.ok(script.bindings); + assert.deepStrictEqual(script.bindings, { p1: 'name', p2: 'Lilac' }); + }); + + it('should add steps containing enum and produce valid script representation', function () { + const g = new graph.Graph().traversal(); + const script = g.V().order().by('age', t.order.decr).getBytecode().toScript(); + assert.ok(script); + assert.strictEqual(script.script, 'g.V().order().by(p1, decr)'); + }); + + it('should add steps containing a predicate and produce valid script representation', function () { + const g = new graph.Graph().traversal(); + const script = g.V().hasLabel('person').has('age', t.P.gt(30)).getBytecode().toScript(); + assert.ok(script); + assert.strictEqual(script.script, 'g.V().hasLabel(p1).has(p2, gt(30))'); + }); + + it('should take a script and return that script along with passed in bindings', function () { + const g = new graph.Graph().traversal(); + const bytecode = g.addV('name', 'Lilac').getBytecode(); + const script = bytecode.addStep('eval', [ + 'g.addV(\'name\', name).property(\'created\', date)', + { name: 'Lilac', created: '2018-01-01T00:00:00.000z' } + ]).toScript(); + assert.ok(script); + assert.strictEqual(script.script, 'g.addV(\'name\', name).property(\'created\', date)'); + assert.deepStrictEqual(script.bindings, { name: 'Lilac', created: '2018-01-01T00:00:00.000z' }); + }); + }); + + describe('#eval()', function () { + it('should apply the strategies and return a Promise with the iterator item', function () { + const strategyMock = { + apply: function (traversal) { + traversal.traversers = [ new t.Traverser(1, 1), new t.Traverser(2, 1) ]; + return Promise.resolve(); + } + }; + const strategies = new TraversalStrategies(); + strategies.addStrategy(strategyMock); + const traversal = new t.Traversal(null, strategies, new Bytecode()); + return traversal.eval(null, null, null) + .then(function (item) { + assert.strictEqual(item.value, 1); + assert.strictEqual(item.done, false); + return traversal.eval(null, null, null); + }) + .then(function (item) { + assert.strictEqual(item.value, 2); + assert.strictEqual(item.done, false); + return traversal.eval(null, null, null); + }) + .then(function (item) { + assert.strictEqual(item.value, null); + assert.strictEqual(item.done, true); + return traversal.eval(null, null, null); + }); + }); + }); +}); diff --git a/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml b/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml index 4e6cf9a..f4d508a 100644 --- a/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml +++ b/gremlin-server/src/test/resources/org/apache/tinkerpop/gremlin/server/gremlin-server-integration.yaml @@ -42,6 +42,7 @@ serializers: - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3d0] }} processors: - { className: org.apache.tinkerpop.gremlin.server.op.session.SessionOpProcessor, config: { sessionTimeout: 28800000 }} + - { className: org.apache.tinkerpop.gremlin.server.op.standard.StandardOpProcessor, config: { maxParameters: 64 }} metrics: { slf4jReporter: {enabled: true, interval: 180000}} strictTransactionManagement: false
