diff --git a/CHANGELOG b/CHANGELOG index bf40939b74..b3dd3865ba 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ v1.4 ------ +* changed AQL COLLECT to use a stable sort, so any previous SORT order is preserved + * issue #547: Javascript error in the web interface * issue #550: Make AQL graph functions support key in addition to id diff --git a/arangod/Ahuacatl/ahuacatl-codegen.c b/arangod/Ahuacatl/ahuacatl-codegen.c index a86c242f74..59b7fc34ba 100644 --- a/arangod/Ahuacatl/ahuacatl-codegen.c +++ b/arangod/Ahuacatl/ahuacatl-codegen.c @@ -538,7 +538,8 @@ static void EndScope (TRI_aql_codegen_js_t* const generator) { static TRI_aql_codegen_register_t CreateSortFunction (TRI_aql_codegen_js_t* const generator, const TRI_aql_node_t* const node, - const size_t elementIndex) { + const size_t elementIndex, + const bool stable) { TRI_aql_node_t* list; TRI_aql_codegen_scope_t* scope; TRI_aql_codegen_register_t functionIndex = IncFunction(generator); @@ -561,12 +562,22 @@ static TRI_aql_codegen_register_t CreateSortFunction (TRI_aql_codegen_js_t* cons for (i = 0; i < n; ++i) { TRI_aql_node_t* element = TRI_AQL_NODE_MEMBER(list, i); - scope->_prefix = "l"; + if (stable) { + scope->_prefix = "l[1]"; + } + else { + scope->_prefix = "l"; + } ScopeOutput(generator, "lhs = "); ProcessNode(generator, TRI_AQL_NODE_MEMBER(element, elementIndex)); ScopeOutput(generator, ";\n"); - scope->_prefix = "r"; + if (stable) { + scope->_prefix = "r[1]"; + } + else { + scope->_prefix = "r"; + } ScopeOutput(generator, "rhs = "); ProcessNode(generator, TRI_AQL_NODE_MEMBER(element, elementIndex)); ScopeOutput(generator, ";\n"); @@ -584,9 +595,16 @@ static TRI_aql_codegen_register_t CreateSortFunction (TRI_aql_codegen_js_t* cons } ScopeOutput(generator, "}\n"); } + + if (stable) { + // sort order determined by previous index position (stable sort) + ScopeOutput(generator, "return l[0] - r[0];\n"); + } + else { + // return 0 if all elements are equal + ScopeOutput(generator, "return 0;\n"); + } - // return 0 if all elements are equal - ScopeOutput(generator, "return 0;\n"); ScopeOutput(generator, "}\n"); // finish scope @@ -2019,7 +2037,7 @@ static void ProcessSort (TRI_aql_codegen_js_t* const generator, // } CloseLoops(generator); - functionIndex = CreateSortFunction(generator, node, 0); + functionIndex = CreateSortFunction(generator, node, 0, false); // now apply actual sorting ScopeOutput(generator, "aql.SORT("); @@ -2080,7 +2098,7 @@ static void ProcessCollect (TRI_aql_codegen_js_t* const generator, CloseLoops(generator); // sort function - sortFunctionIndex = CreateSortFunction(generator, node, 1); + sortFunctionIndex = CreateSortFunction(generator, node, 1, true); // group function groupFunctionIndex = CreateGroupFunction(generator, node); diff --git a/js/actions/api-cursor.js b/js/actions/api-cursor.js index a1d370ceb4..646a8b84f9 100644 --- a/js/actions/api-cursor.js +++ b/js/actions/api-cursor.js @@ -281,7 +281,29 @@ function POST_api_cursor(req, res) { /// /// Valid request for next batch: /// -/// @verbinclude api-cursor-create-for-limit-return-cont +/// @EXAMPLE_ARANGOSH_RUN{RestCursorForLimitReturnCont} +/// var url = "/_api/cursor"; +/// var cn = "products"; +/// db._drop(cn); +/// db._create(cn); +/// +/// db.products.save({"hello1":"world1"}); +/// db.products.save({"hello2":"world1"}); +/// db.products.save({"hello3":"world1"}); +/// db.products.save({"hello4":"world1"}); +/// db.products.save({"hello5":"world1"}); +/// +/// var url = "/_api/cursor"; +/// var body = '{ "query" : "FOR p IN products LIMIT 5 RETURN p", "count" : true, "batchSize" : 2 }'; +/// var response = logCurlRequest('POST', url, body); +/// +/// var body = response.body.replace(/\\/g, ''); +/// var _id = JSON.parse(body).id; +/// response = logCurlRequest('PUT', url + '/' + _id, ''); +/// assert(response.code === 200); +/// +/// logJsonResponse(response); +/// @END_EXAMPLE_ARANGOSH_RUN /// /// Missing identifier /// @@ -367,7 +389,30 @@ function PUT_api_cursor (req, res) { /// /// @EXAMPLES /// -/// @verbinclude api-cursor-delete +/// @EXAMPLE_ARANGOSH_RUN{RestCursorDelete} +/// var url = "/_api/cursor"; +/// var cn = "products"; +/// db._drop(cn); +/// db._create(cn); +/// +/// db.products.save({"hello1":"world1"}); +/// db.products.save({"hello2":"world1"}); +/// db.products.save({"hello3":"world1"}); +/// db.products.save({"hello4":"world1"}); +/// db.products.save({"hello5":"world1"}); +/// +/// var url = "/_api/cursor"; +/// var body = '{ "query" : "FOR p IN products LIMIT 5 RETURN p", "count" : true, "batchSize" : 2 }'; +/// var response = logCurlRequest('POST', url, body); +/// logJsonResponse(response); +/// var body = response.body.replace(/\\/g, ''); +/// var _id = JSON.parse(body).id; +/// response = logCurlRequest('DELETE', url + '/' + _id); +/// +/// assert(response.code === 202); +/// +/// logJsonResponse(response); +/// @END_EXAMPLE_ARANGOSH_RUN //////////////////////////////////////////////////////////////////////////////// function DELETE_api_cursor(req, res) { diff --git a/js/actions/api-graph.js b/js/actions/api-graph.js index 428d4a86e6..87e5b99b23 100644 --- a/js/actions/api-graph.js +++ b/js/actions/api-graph.js @@ -682,7 +682,7 @@ function get_graph_vertex (req, res, g) { /// var url = "/_api/graph/graph/vertex/v1"; /// var response = logCurlRequest('DELETE', url); /// -/// //assert(response.code === 202); +/// assert(response.code === 202); /// /// logJsonResponse(response); /// db._drop("edges"); @@ -1194,7 +1194,7 @@ function post_graph_all_vertices (req, res, g) { /// body += '[] }}'; /// var response = logCurlRequest('POST', url, body); /// -/// //assert(response.code === 201); +/// assert(response.code === 201); /// logJsonResponse(response); /// db._drop("edges"); /// db._drop("vertices"); diff --git a/js/actions/api-simple.js b/js/actions/api-simple.js index 2acb4ced03..36f4bbc24a 100644 --- a/js/actions/api-simple.js +++ b/js/actions/api-simple.js @@ -785,7 +785,7 @@ actions.defineHttp({ /// /// var response = logCurlRequest('PUT', url, body); /// -/// //assert(response.code === 201); +/// assert(response.code === 201); /// /// logJsonResponse(response); /// db._drop(cn); @@ -806,7 +806,7 @@ actions.defineHttp({ /// /// var response = logCurlRequest('PUT', url, body); /// -/// //assert(response.code === 201); +/// assert(response.code === 201); /// /// logJsonResponse(response); /// db._drop(cn); @@ -827,7 +827,7 @@ actions.defineHttp({ /// /// var response = logCurlRequest('PUT', url, body); /// -/// //assert(response.code === 201); +/// assert(response.code === 201); /// /// logJsonResponse(response); /// db._drop(cn); @@ -936,7 +936,7 @@ actions.defineHttp({ /// /// var response = logCurlRequest('PUT', url, body); /// -/// //assert(response.code === 201); +/// assert(response.code === 200); /// /// logJsonResponse(response); /// db._drop(cn); @@ -957,7 +957,7 @@ actions.defineHttp({ /// /// var response = logCurlRequest('PUT', url, body); /// -/// //assert(response.code === 201); +/// assert(response.code === 404); /// /// logJsonResponse(response); /// db._drop(cn); diff --git a/js/client/modules/org/arangodb/graph.js b/js/client/modules/org/arangodb/graph.js index c1789f6d27..ed29a2e586 100644 --- a/js/client/modules/org/arangodb/graph.js +++ b/js/client/modules/org/arangodb/graph.js @@ -30,6 +30,7 @@ var arangodb = require("org/arangodb"); var arangosh = require("org/arangodb/arangosh"); +var is = require("org/arangodb/is"); var ArangoQueryCursor = require("org/arangodb/arango-query-cursor").ArangoQueryCursor; @@ -438,8 +439,7 @@ Graph.prototype.addEdge = function (out_vertex, in_vertex, id, label, data) { if (data === null || typeof data !== "object") { params = {}; - } - else { + } else { params = data._shallowCopy || {}; } @@ -448,6 +448,10 @@ Graph.prototype.addEdge = function (out_vertex, in_vertex, id, label, data) { params._to = in_vertex._properties._key; params.$label = label; + if (is.notExisty(params.$label) && is.existy(data) && is.existy(data.$label)) { + params.$label = data.$label; + } + requestResult = this._connection.POST("/_api/graph/" + encodeURIComponent(this._properties._key) + "/edge", JSON.stringify(params)); diff --git a/js/common/modules/org/arangodb/is.js b/js/common/modules/org/arangodb/is.js new file mode 100644 index 0000000000..6d05288d0f --- /dev/null +++ b/js/common/modules/org/arangodb/is.js @@ -0,0 +1,59 @@ +/*jslint indent: 2, nomen: true, maxlen: 100, white: true, plusplus: true, eqeq: true */ +/*global require, exports */ + +//////////////////////////////////////////////////////////////////////////////// +/// @brief Check if something is something +/// +/// @file +/// +/// DISCLAIMER +/// +/// Copyright 2010-2012 triagens GmbH, Cologne, Germany +/// +/// 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. +/// +/// Copyright holder is triAGENS GmbH, Cologne, Germany +/// +/// @author Dr. Frank Celler, Lucas Dohmen +/// @author Copyright 2011-2012, triAGENS GmbH, Cologne, Germany +//////////////////////////////////////////////////////////////////////////////// + +// Check if a value is not undefined or null +var existy = function (x) { + "use strict"; + // This is != on purpose to also check for undefined + return x != null; +}; + +// Check if a value is undefined or null +var notExisty = function (x) { + "use strict"; + return !existy(x); +}; + +// Check if a value is existy and not false +var truthy = function (x) { + "use strict"; + return (x !== false) && existy(x); +}; + +// Check if a value is not truthy +var falsy = function (x) { + "use strict"; + return !truthy(x); +}; + +exports.existy = existy; +exports.notExisty = notExisty; +exports.truthy = truthy; +exports.falsy = falsy; diff --git a/js/common/tests/shell-graph.js b/js/common/tests/shell-graph.js index 7638b83adc..b36a28ceaa 100644 --- a/js/common/tests/shell-graph.js +++ b/js/common/tests/shell-graph.js @@ -250,6 +250,19 @@ function GraphBasicsSuite() { assertEqual("testValue", edge.getProperty("testProperty")); }, + testAddEdgeWithLabelSetViaData : function () { + var v1, + v2, + edge; + + v1 = graph.addVertex("vertex1"); + v2 = graph.addVertex("vertex2"); + + edge = graph.addEdge(v1, v2, null, null, {"$label": "test"}); + + assertEqual("test", edge.getLabel()); + }, + //////////////////////////////////////////////////////////////////////////////// /// @brief change a property //////////////////////////////////////////////////////////////////////////////// diff --git a/js/server/modules/org/arangodb/ahuacatl.js b/js/server/modules/org/arangodb/ahuacatl.js index 5bbe093b2d..7afa1e44b8 100644 --- a/js/server/modules/org/arangodb/ahuacatl.js +++ b/js/server/modules/org/arangodb/ahuacatl.js @@ -2104,12 +2104,17 @@ function GROUP (value, sortFunction, groupFunction, into) { return [ ]; } - SORT(value, sortFunction); + var augmented = [ ], i; + for (i = 0; i < n; ++i) { + augmented.push([ i, value[i] ]); + } - var result = [ ], currentGroup, oldGroup, i; + SORT(augmented, sortFunction); + + var result = [ ], currentGroup, oldGroup; for (i = 0; i < n; ++i) { - var row = value[i]; + var row = augmented[i][1]; var groupValue = groupFunction(row); if (RELATIONAL_UNEQUAL(oldGroup, groupValue)) { diff --git a/js/server/tests/ahuacatl-queries-simple.js b/js/server/tests/ahuacatl-queries-simple.js index 38174293a8..18b8267b8e 100644 --- a/js/server/tests/ahuacatl-queries-simple.js +++ b/js/server/tests/ahuacatl-queries-simple.js @@ -945,6 +945,90 @@ function ahuacatlQuerySimpleTestSuite () { actual = getQueryResults("FOR i IN [ 1 ] RETURN { a: -1 -3, b: 2 - 0 }"); assertEqual([ { a: -4, b: 2 } ], actual); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief stable sort after COLLECT +//////////////////////////////////////////////////////////////////////////////// + + testStableSort1: function () { + var data = [ + { "city" : "london", "order" : 4 }, + { "city" : "paris", "order" : 4 }, + { "city" : "london", "order" : 2 }, + { "city" : "paris", "order" : 3 }, + { "city" : "new york", "order" : 2 }, + { "city" : "london", "order" : 3 }, + { "city" : "new york", "order" : 4 }, + { "city" : "new york", "order" : 3 }, + { "city" : "paris", "order" : 1 }, + { "city" : "new york", "order" : 1 }, + { "city" : "paris", "order" : 2 }, + { "city" : "london", "order" : 1 } + ]; + + var actual, expected; + + expected = [ + { "city" : "london", "orders" : [ 4, 3, 2, 1 ] }, + { "city" : "new york", "orders" : [ 4, 3, 2, 1 ] }, + { "city" : "paris", "orders" : [ 4, 3, 2, 1 ] } + ]; + + actual = getQueryResults("FOR value IN " + JSON.stringify(data) + " SORT value.order DESC COLLECT city = value.city INTO orders RETURN { city: city, orders: orders[*].value.order }"); + assertEqual(expected, actual); + + expected = [ + { "city" : "london", "orders" : [ 1, 2, 3, 4 ] }, + { "city" : "new york", "orders" : [ 1, 2, 3, 4 ] }, + { "city" : "paris", "orders" : [ 1, 2, 3, 4 ] } + ]; + + actual = getQueryResults("FOR value IN " + JSON.stringify(data) + " SORT value.order ASC COLLECT city = value.city INTO orders RETURN { city: city, orders: orders[*].value.order }"); + assertEqual(expected, actual); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief stable sort after COLLECT +//////////////////////////////////////////////////////////////////////////////// + + testStableSort2: function () { + var data = [ + { "city" : "london", "order" : 4 }, + { "city" : "paris", "order" : 4 }, + { "city" : "london", "order" : 2 }, + { "city" : "paris", "order" : 3 }, + { "city" : "new york", "order" : 2 }, + { "city" : "london", "order" : 3 }, + { "city" : "new york", "order" : 4 }, + { "city" : "new york", "order" : 3 }, + { "city" : "paris", "order" : 1 }, + { "city" : "new york", "order" : 1 }, + { "city" : "paris", "order" : 2 }, + { "city" : "london", "order" : 1 } + ]; + + var actual, expected; + + expected = [ + { "order" : 1, "cities" : [ "paris", "new york", "london" ] }, + { "order" : 2, "cities" : [ "paris", "new york", "london" ] }, + { "order" : 3, "cities" : [ "paris", "new york", "london" ] }, + { "order" : 4, "cities" : [ "paris", "new york", "london" ] } + ]; + + actual = getQueryResults("FOR value IN " + JSON.stringify(data) + " SORT value.city DESC COLLECT order = value.order INTO cities RETURN { order: order, cities: cities[*].value.city }"); + assertEqual(expected, actual); + + expected = [ + { "order" : 4, "cities" : [ "london", "new york", "paris" ] }, + { "order" : 3, "cities" : [ "london", "new york", "paris" ] }, + { "order" : 2, "cities" : [ "london", "new york", "paris" ] }, + { "order" : 1, "cities" : [ "london", "new york", "paris" ] } + ]; + + actual = getQueryResults("FOR value IN " + JSON.stringify(data) + " SORT value.city ASC COLLECT order = value.order INTO cities SORT order DESC RETURN { order: order, cities: cities[*].value.city }"); + assertEqual(expected, actual); } };