cruddl 4.2.3 → 5.0.0-0.deploytest.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +5 -3
- package/.husky/pre-commit +1 -0
- package/README.md +0 -1
- package/dist/src/authorization/move-errors-to-output-nodes.js +1 -0
- package/dist/src/authorization/move-errors-to-output-nodes.js.map +1 -1
- package/dist/src/authorization/transformers/traversal.js +13 -2
- package/dist/src/authorization/transformers/traversal.js.map +1 -1
- package/dist/src/cruddl-version.js +1 -1
- package/dist/src/database/arangodb/aql-generator.js +856 -185
- package/dist/src/database/arangodb/aql-generator.js.map +1 -1
- package/dist/src/database/arangodb/arangodb-adapter.js +30 -10
- package/dist/src/database/arangodb/arangodb-adapter.js.map +1 -1
- package/dist/src/database/arangodb/config.js +3 -8
- package/dist/src/database/arangodb/config.js.map +1 -1
- package/dist/src/database/arangodb/traversal-helpers.d.ts +10 -0
- package/dist/src/database/arangodb/traversal-helpers.js +52 -0
- package/dist/src/database/arangodb/traversal-helpers.js.map +1 -0
- package/dist/src/database/inmemory/inmemory-adapter.js +3 -7
- package/dist/src/database/inmemory/inmemory-adapter.js.map +1 -1
- package/dist/src/database/inmemory/js-generator.js +75 -55
- package/dist/src/database/inmemory/js-generator.js.map +1 -1
- package/dist/src/execution/runtime-errors.d.ts +1 -0
- package/dist/src/execution/runtime-errors.js +3 -3
- package/dist/src/execution/runtime-errors.js.map +1 -1
- package/dist/src/model/implementation/indices.js +17 -1
- package/dist/src/model/implementation/indices.js.map +1 -1
- package/dist/src/query-tree/queries.d.ts +102 -7
- package/dist/src/query-tree/queries.js +77 -10
- package/dist/src/query-tree/queries.js.map +1 -1
- package/dist/src/schema/schema-builder.js +5 -1
- package/dist/src/schema/schema-builder.js.map +1 -1
- package/dist/src/schema-generation/field-nodes.d.ts +10 -5
- package/dist/src/schema-generation/field-nodes.js +32 -4
- package/dist/src/schema-generation/field-nodes.js.map +1 -1
- package/dist/src/schema-generation/filter-augmentation.js +0 -1
- package/dist/src/schema-generation/filter-augmentation.js.map +1 -1
- package/dist/src/schema-generation/flex-search-post-filter-augmentation.js +0 -1
- package/dist/src/schema-generation/flex-search-post-filter-augmentation.js.map +1 -1
- package/dist/src/schema-generation/order-by-and-pagination-augmentation.js +41 -22
- package/dist/src/schema-generation/order-by-and-pagination-augmentation.js.map +1 -1
- package/dist/src/schema-generation/output-type-generator.js +5 -3
- package/dist/src/schema-generation/output-type-generator.js.map +1 -1
- package/dist/src/schema-generation/query-node-object-type/context.d.ts +1 -1
- package/dist/src/schema-generation/query-node-object-type/context.js +1 -1
- package/dist/src/schema-generation/query-node-object-type/query-node-generator.js +37 -3
- package/dist/src/schema-generation/query-node-object-type/query-node-generator.js.map +1 -1
- package/dist/src/schema-generation/root-field-helper.d.ts +3 -23
- package/dist/src/schema-generation/root-field-helper.js +9 -109
- package/dist/src/schema-generation/root-field-helper.js.map +1 -1
- package/dist/src/schema-generation/utils/filtering.d.ts +1 -2
- package/dist/src/schema-generation/utils/filtering.js +28 -6
- package/dist/src/schema-generation/utils/filtering.js.map +1 -1
- package/dist/src/schema-generation/utils/relations.js +0 -1
- package/dist/src/schema-generation/utils/relations.js.map +1 -1
- package/dist/src/utils/util-types.d.ts +6 -0
- package/knip.json +6 -1
- package/package.json +6 -11
- package/tsconfig.json +1 -1
|
@@ -15,6 +15,7 @@ const aql_1 = require("./aql");
|
|
|
15
15
|
const arango_basics_1 = require("./arango-basics");
|
|
16
16
|
const arango_search_helpers_1 = require("./schema-migration/arango-search-helpers");
|
|
17
17
|
const execution_options_1 = require("../../execution/execution-options");
|
|
18
|
+
const traversal_helpers_1 = require("./traversal-helpers");
|
|
18
19
|
var AccessType;
|
|
19
20
|
(function (AccessType) {
|
|
20
21
|
/**
|
|
@@ -67,7 +68,7 @@ class QueryContext {
|
|
|
67
68
|
* @param variableNode the variable token as it is referenced in the query tree
|
|
68
69
|
* @param aqlVariable the variable token as it will be available within the AQL fragment
|
|
69
70
|
*/
|
|
70
|
-
|
|
71
|
+
newNestedContextWithVariableBinding(variableNode, aqlVariable) {
|
|
71
72
|
const newContext = new QueryContext(this.options);
|
|
72
73
|
newContext.variableMap = new Map(this.variableMap);
|
|
73
74
|
newContext.variableMap.set(variableNode, aqlVariable);
|
|
@@ -80,27 +81,21 @@ class QueryContext {
|
|
|
80
81
|
/**
|
|
81
82
|
* Creates a new QueryContext that is identical to this one but has one additional variable binding
|
|
82
83
|
*
|
|
83
|
-
*
|
|
84
|
+
* If aqlFrag is omitted, a new AQLVariable will be created using the name of the variableNode.
|
|
84
85
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
86
|
+
* For aqlFrag, you can specify an AQLVariable or a different AQLFragment (e.g. a field access).
|
|
87
|
+
* Do not specify complex AQL expressions because they would be repeated for every use of the
|
|
88
|
+
* variable.
|
|
89
|
+
*
|
|
90
|
+
* @param variableNode the variable as referenced in the query tree
|
|
91
|
+
* @param aqlFrag the fragment representing the variable in AQL
|
|
92
|
+
* @returns the new context
|
|
87
93
|
*/
|
|
88
|
-
|
|
94
|
+
bindVariable(variableNode, aqlFrag = new aql_1.AQLVariable(variableNode.label)) {
|
|
89
95
|
if (this.variableMap.has(variableNode)) {
|
|
90
96
|
throw new Error(`Variable ${variableNode} is introduced twice`);
|
|
91
97
|
}
|
|
92
|
-
|
|
93
|
-
return this.newNestedContextWithVariableMapping(variableNode, variable);
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Creates a new QueryContext that is identical to this one but has one additional variable binding
|
|
97
|
-
*
|
|
98
|
-
* @param variableNode the variable as referenced in the query tree
|
|
99
|
-
* @param existingVariable a variable that has been previously introduced with introduceVariable() and fetched by getVariable
|
|
100
|
-
* @returns {QueryContext} the nested context
|
|
101
|
-
*/
|
|
102
|
-
introduceVariableAlias(variableNode, existingVariable) {
|
|
103
|
-
return this.newNestedContextWithVariableMapping(variableNode, existingVariable);
|
|
98
|
+
return this.newNestedContextWithVariableBinding(variableNode, aqlFrag);
|
|
104
99
|
}
|
|
105
100
|
/**
|
|
106
101
|
* Creates a new QueryContext that includes an additional transaction step and adds resultVariable to the scope
|
|
@@ -118,7 +113,7 @@ class QueryContext {
|
|
|
118
113
|
let newContext;
|
|
119
114
|
if (resultVariable) {
|
|
120
115
|
resultVar = new aql_1.AQLQueryResultVariable(resultVariable.label);
|
|
121
|
-
newContext = this.
|
|
116
|
+
newContext = this.newNestedContextWithVariableBinding(resultVariable, resultVar);
|
|
122
117
|
}
|
|
123
118
|
else {
|
|
124
119
|
resultVar = undefined;
|
|
@@ -170,6 +165,10 @@ class QueryContext {
|
|
|
170
165
|
if (!variable) {
|
|
171
166
|
throw new Error(`Variable ${variableNode.toString()} is used but not introduced`);
|
|
172
167
|
}
|
|
168
|
+
// we're returning an AQLFragment as AQLVariable
|
|
169
|
+
// it can be a non-variable fragment (e.g. a simple field access) if we used bindVariable
|
|
170
|
+
// typescript only allows it because there are no private fields in AQLVariable
|
|
171
|
+
// TODO introduce a better way to introduce a VariableQueryNode as AQLVariable, then make this here return AQLFragment
|
|
173
172
|
return variable;
|
|
174
173
|
}
|
|
175
174
|
getPreExecuteQueries() {
|
|
@@ -207,7 +206,7 @@ function createAQLCompoundQuery(node, resultVariable, resultValidator, context)
|
|
|
207
206
|
const variableAssignmentNodes = [];
|
|
208
207
|
node = (0, utils_1.extractVariableAssignments)(node, variableAssignmentNodes);
|
|
209
208
|
for (const assignmentNode of variableAssignmentNodes) {
|
|
210
|
-
context = context.
|
|
209
|
+
context = context.bindVariable(assignmentNode.variableNode);
|
|
211
210
|
const tmpVar = context.getVariable(assignmentNode.variableNode);
|
|
212
211
|
variableAssignments.push((0, aql_1.aql) `LET ${tmpVar} = ${processNode(assignmentNode.variableValueNode, context)}`);
|
|
213
212
|
}
|
|
@@ -233,6 +232,9 @@ var aqlExt;
|
|
|
233
232
|
(function (aqlExt) {
|
|
234
233
|
function safeJSONKey(key) {
|
|
235
234
|
if (aql_1.aql.isSafeIdentifier(key)) {
|
|
235
|
+
// TODO meta fields are currently not considered safe because of the leading underscore
|
|
236
|
+
// think about if we should generally allow leading underscores in isSafeIdentifier
|
|
237
|
+
// or just allow them here (here it would definitely be safe)
|
|
236
238
|
// we could always collide with a (future) keyword, so use "name" syntax instead of identifier
|
|
237
239
|
// ("" looks more natural than `` in json keys)
|
|
238
240
|
return (0, aql_1.aql) `${aql_1.aql.string(key)}`;
|
|
@@ -242,14 +244,14 @@ var aqlExt;
|
|
|
242
244
|
}
|
|
243
245
|
}
|
|
244
246
|
aqlExt.safeJSONKey = safeJSONKey;
|
|
245
|
-
function
|
|
247
|
+
function subquery(...content) {
|
|
246
248
|
return aql_1.aql.lines((0, aql_1.aql) `(`, aql_1.aql.indent(aql_1.aql.lines(...content)), (0, aql_1.aql) `)`);
|
|
247
249
|
}
|
|
248
|
-
aqlExt.
|
|
249
|
-
function
|
|
250
|
-
return (0, aql_1.aql) `FIRST${
|
|
250
|
+
aqlExt.subquery = subquery;
|
|
251
|
+
function firstOfSubquery(...content) {
|
|
252
|
+
return (0, aql_1.aql) `FIRST${subquery(...content)}`;
|
|
251
253
|
}
|
|
252
|
-
aqlExt.
|
|
254
|
+
aqlExt.firstOfSubquery = firstOfSubquery;
|
|
253
255
|
})(aqlExt || (aqlExt = {}));
|
|
254
256
|
const processors = new Map();
|
|
255
257
|
function register(type, processor) {
|
|
@@ -298,11 +300,11 @@ register(query_tree_1.VariableQueryNode, (node, context) => {
|
|
|
298
300
|
return context.getVariable(node);
|
|
299
301
|
});
|
|
300
302
|
register(query_tree_1.VariableAssignmentQueryNode, (node, context) => {
|
|
301
|
-
const newContext = context.
|
|
303
|
+
const newContext = context.bindVariable(node.variableNode);
|
|
302
304
|
const tmpVar = newContext.getVariable(node.variableNode);
|
|
303
305
|
// note that we have to know statically if the context var is a list or an object
|
|
304
306
|
// assuming object here because lists are not needed currently
|
|
305
|
-
return aqlExt.
|
|
307
|
+
return aqlExt.firstOfSubquery((0, aql_1.aql) `LET ${tmpVar} = ${processNode(node.variableValueNode, newContext)}`, (0, aql_1.aql) `RETURN ${processNode(node.resultNode, newContext)}`);
|
|
306
308
|
});
|
|
307
309
|
register(query_tree_1.WithPreExecutionQueryNode, (node, context) => {
|
|
308
310
|
let currentContext = context;
|
|
@@ -358,14 +360,14 @@ register(query_tree_1.RevisionQueryNode, (node, context) => {
|
|
|
358
360
|
});
|
|
359
361
|
register(flex_search_2.FlexSearchQueryNode, (node, context) => {
|
|
360
362
|
let itemContext = context
|
|
361
|
-
.
|
|
363
|
+
.bindVariable(node.itemVariable)
|
|
362
364
|
.withExtension(inFlexSearchFilterSymbol, true);
|
|
363
365
|
const viewName = (0, arango_search_helpers_1.getFlexSearchViewNameForRootEntity)(node.rootEntityType);
|
|
364
366
|
context.addCollectionAccess(viewName, AccessType.EXPLICIT_READ);
|
|
365
|
-
return aqlExt.
|
|
367
|
+
return aqlExt.subquery((0, aql_1.aql) `FOR ${itemContext.getVariable(node.itemVariable)}`, (0, aql_1.aql) `IN ${aql_1.aql.collection(viewName)}`, (0, aql_1.aql) `SEARCH ${processNode(node.flexFilterNode, itemContext)}`, node.isOptimisationsDisabled ? (0, aql_1.aql) `OPTIONS { conditionOptimization: 'none' }` : (0, aql_1.aql) ``, (0, aql_1.aql) `RETURN ${itemContext.getVariable(node.itemVariable)}`);
|
|
366
368
|
});
|
|
367
369
|
register(query_tree_1.TransformListQueryNode, (node, context) => {
|
|
368
|
-
let itemContext = context.
|
|
370
|
+
let itemContext = context.bindVariable(node.itemVariable);
|
|
369
371
|
const itemVar = itemContext.getVariable(node.itemVariable);
|
|
370
372
|
let itemProjectionContext = itemContext;
|
|
371
373
|
// move LET statements up
|
|
@@ -376,11 +378,16 @@ register(query_tree_1.TransformListQueryNode, (node, context) => {
|
|
|
376
378
|
const variableAssignmentNodes = [];
|
|
377
379
|
innerNode = (0, utils_1.extractVariableAssignments)(innerNode, variableAssignmentNodes);
|
|
378
380
|
for (const assignmentNode of variableAssignmentNodes) {
|
|
379
|
-
itemProjectionContext = itemProjectionContext.
|
|
381
|
+
itemProjectionContext = itemProjectionContext.bindVariable(assignmentNode.variableNode);
|
|
380
382
|
const tmpVar = itemProjectionContext.getVariable(assignmentNode.variableNode);
|
|
381
383
|
variableAssignments.push((0, aql_1.aql) `LET ${tmpVar} = ${processNode(assignmentNode.variableValueNode, itemProjectionContext)}`);
|
|
382
384
|
}
|
|
383
|
-
|
|
385
|
+
// TODO aql-perf: set maxProjects to a value > 5?
|
|
386
|
+
// The reduce-extraction-to-projection optimization is crucial to reduce memory usage of queries
|
|
387
|
+
// over large root entities if only some of the fields are queried. The default is to only apply
|
|
388
|
+
// it if 5 or less fields are selected. We probably want to increase this limit
|
|
389
|
+
// (also applies to FollowEdgeQueryNode and TraversalQueryNode)
|
|
390
|
+
return aqlExt.subquery((0, aql_1.aql) `FOR ${itemVar}`, generateInClauseWithFilterAndOrderAndLimit({ node, context, itemContext, itemVar }), ...variableAssignments, (0, aql_1.aql) `RETURN ${processNode(innerNode, itemProjectionContext)}`);
|
|
384
391
|
});
|
|
385
392
|
/**
|
|
386
393
|
* Generates an IN... clause for a TransformListQueryNode to be used within a query / subquery (FOR ... IN ...)
|
|
@@ -390,37 +397,42 @@ function generateInClauseWithFilterAndOrderAndLimit({ node, context, itemVar, it
|
|
|
390
397
|
let filterDanglingEdges = (0, aql_1.aql) ``;
|
|
391
398
|
if (node.listNode instanceof query_tree_1.FollowEdgeQueryNode) {
|
|
392
399
|
list = getSimpleFollowEdgeFragment(node.listNode, context);
|
|
393
|
-
|
|
400
|
+
// using $var._key != null instead of $var != null because the latter prevents ArangoDB
|
|
401
|
+
// from applying the reduce-extraction-to-projection optimization
|
|
402
|
+
filterDanglingEdges = (0, aql_1.aql) `FILTER ${itemVar}._key != null`;
|
|
394
403
|
}
|
|
395
404
|
else {
|
|
396
405
|
list = processNode(node.listNode, context);
|
|
397
406
|
}
|
|
398
407
|
let filter = (0, utils_1.simplifyBooleans)(node.filterNode);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
408
|
+
return aql_1.aql.lines((0, aql_1.aql) `IN ${list}`, filter instanceof query_tree_1.ConstBoolQueryNode && filter.value
|
|
409
|
+
? (0, aql_1.aql) ``
|
|
410
|
+
: (0, aql_1.aql) `FILTER ${processNode(filter, itemContext)}`, filterDanglingEdges, generateSortAQL(node.orderBy, itemContext), generateLimitClause(node) ?? (0, aql_1.aql) ``);
|
|
411
|
+
}
|
|
412
|
+
function generateLimitClause({ skip = 0, maxCount }) {
|
|
413
|
+
// Todo use something like aql.integer() which validates the number is an integer and within range
|
|
414
|
+
// (that way, we don't have so many bound parameters)
|
|
415
|
+
if (maxCount != undefined) {
|
|
416
|
+
if (skip === 0) {
|
|
417
|
+
return (0, aql_1.aql) `LIMIT ${maxCount}`;
|
|
403
418
|
}
|
|
404
419
|
else {
|
|
405
|
-
|
|
420
|
+
return (0, aql_1.aql) `LIMIT ${skip}, ${maxCount}`;
|
|
406
421
|
}
|
|
407
422
|
}
|
|
408
|
-
else if (
|
|
409
|
-
|
|
423
|
+
else if (skip > 0) {
|
|
424
|
+
return (0, aql_1.aql) `LIMIT ${skip}, ${Number.MAX_SAFE_INTEGER}`;
|
|
410
425
|
}
|
|
411
426
|
else {
|
|
412
|
-
|
|
427
|
+
return undefined;
|
|
413
428
|
}
|
|
414
|
-
return aql_1.aql.lines((0, aql_1.aql) `IN ${list}`, filter instanceof query_tree_1.ConstBoolQueryNode && filter.value
|
|
415
|
-
? (0, aql_1.aql) ``
|
|
416
|
-
: (0, aql_1.aql) `FILTER ${processNode(filter, itemContext)}`, filterDanglingEdges, generateSortAQL(node.orderBy, itemContext), limitClause);
|
|
417
429
|
}
|
|
418
430
|
/**
|
|
419
431
|
* Generates an IN... clause for a list to be used within a query / subquery (FOR ... IN ...)
|
|
420
432
|
*/
|
|
421
433
|
function generateInClause(node, context, entityVar) {
|
|
422
434
|
if (node instanceof query_tree_1.TransformListQueryNode && node.innerNode === node.itemVariable) {
|
|
423
|
-
const itemContext = context.
|
|
435
|
+
const itemContext = context.bindVariable(node.itemVariable, entityVar);
|
|
424
436
|
return generateInClauseWithFilterAndOrderAndLimit({
|
|
425
437
|
node,
|
|
426
438
|
itemContext,
|
|
@@ -444,7 +456,7 @@ register(query_tree_1.CountQueryNode, (node, context) => {
|
|
|
444
456
|
// note that ArangoDB's inline-subqueries rule optimizes for the case where listNode is a TransformList again.
|
|
445
457
|
const itemVar = aql_1.aql.variable('item');
|
|
446
458
|
const countVar = aql_1.aql.variable('count');
|
|
447
|
-
return aqlExt.
|
|
459
|
+
return aqlExt.firstOfSubquery((0, aql_1.aql) `FOR ${itemVar}`, (0, aql_1.aql) `IN ${processNode(node.listNode, context)}`, (0, aql_1.aql) `COLLECT WITH COUNT INTO ${countVar}`, (0, aql_1.aql) `RETURN ${countVar}`);
|
|
448
460
|
});
|
|
449
461
|
register(query_tree_1.AggregationQueryNode, (node, context) => {
|
|
450
462
|
const itemVar = aql_1.aql.variable('item');
|
|
@@ -550,7 +562,6 @@ register(query_tree_1.AggregationQueryNode, (node, context) => {
|
|
|
550
562
|
break;
|
|
551
563
|
// these should also remove NULL values by definition
|
|
552
564
|
case model_1.AggregationOperator.DISTINCT:
|
|
553
|
-
// use COLLECT a = a instead of RETURN DISTINCT to be able to sort
|
|
554
565
|
distinct = true;
|
|
555
566
|
filterFrag = (0, aql_1.aql) `${itemVar} != null`;
|
|
556
567
|
isList = true;
|
|
@@ -563,27 +574,34 @@ register(query_tree_1.AggregationQueryNode, (node, context) => {
|
|
|
563
574
|
default:
|
|
564
575
|
throw new Error(`Unsupported aggregator: ${node.aggregationOperator}`);
|
|
565
576
|
}
|
|
566
|
-
|
|
577
|
+
const subqueryFrag = aqlExt.subquery((0, aql_1.aql) `FOR ${itemVar}`, (0, aql_1.aql) `IN ${processNode(node.listNode, context)}`, filterFrag ? (0, aql_1.aql) `FILTER ${filterFrag}` : (0, aql_1.aql) ``, sort ? (0, aql_1.aql) `SORT ${itemVar}` : (0, aql_1.aql) ``, aggregationFunction
|
|
567
578
|
? (0, aql_1.aql) `COLLECT AGGREGATE ${aggregationVar} = ${aggregationFunction}(${itemFrag})`
|
|
568
579
|
: distinct
|
|
569
580
|
? (0, aql_1.aql) `COLLECT ${aggregationVar} = ${itemFrag}`
|
|
570
581
|
: (0, aql_1.aql) ``, (0, aql_1.aql) `RETURN ${resultFragment}`);
|
|
582
|
+
if (isList) {
|
|
583
|
+
return subqueryFrag;
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
// generates FIRST(...) because subqueryFrag has parentheses
|
|
587
|
+
return (0, aql_1.aql) `FIRST${subqueryFrag}`;
|
|
588
|
+
}
|
|
571
589
|
});
|
|
572
590
|
register(query_tree_1.UpdateChildEntitiesQueryNode, (node, context) => {
|
|
573
591
|
const itemsVar = aql_1.aql.variable('items');
|
|
574
592
|
const itemsWithIndexVar = aql_1.aql.variable('itemsWithIndex');
|
|
575
|
-
const childContext = context.
|
|
593
|
+
const childContext = context.bindVariable(node.dictionaryVar);
|
|
576
594
|
const dictVar = childContext.getVariable(node.dictionaryVar);
|
|
577
595
|
const updatedDictVar = aql_1.aql.variable('updatedDict');
|
|
578
596
|
const itemVar = aql_1.aql.variable('item');
|
|
579
597
|
const indexVar = aql_1.aql.variable('indexVar');
|
|
580
|
-
return aqlExt.
|
|
598
|
+
return aqlExt.subquery(
|
|
581
599
|
// could be a complex expression, and we're using it multiple times -> store in a variable
|
|
582
600
|
(0, aql_1.aql) `LET ${itemsVar} = ${processNode(node.originalList, context)}`,
|
|
583
601
|
// add a __index property to each item so we can sort by this later
|
|
584
602
|
// regular field names cannot start with an underscore, so we're safe to use __index as a
|
|
585
603
|
// temporary property to store the index of the child entity in the list
|
|
586
|
-
(0, aql_1.aql) `LET ${itemsWithIndexVar} = ${aqlExt.
|
|
604
|
+
(0, aql_1.aql) `LET ${itemsWithIndexVar} = ${aqlExt.subquery((0, aql_1.aql) `FOR ${indexVar}`,
|
|
587
605
|
// 0..-1 would evaluate to [0, -1], so the ZIP would complain because the right side
|
|
588
606
|
// has more entries (2) than the left (0). RANGE() behaves the same
|
|
589
607
|
(0, aql_1.aql) `IN LENGTH(${itemsVar}) > 0 ? 0..(LENGTH(${itemsVar}) - 1) : []`, (0, aql_1.aql) `RETURN MERGE(NTH(${itemsVar}, ${indexVar}), { __index: ${indexVar} })`)}`,
|
|
@@ -610,9 +628,10 @@ register(query_tree_1.MergeObjectsQueryNode, (node, context) => {
|
|
|
610
628
|
return (0, aql_1.aql) `MERGE(${objectsFragment})`;
|
|
611
629
|
});
|
|
612
630
|
register(query_tree_1.ObjectEntriesQueryNode, (node, context) => {
|
|
631
|
+
// TODO aql-perf: use array inline expression
|
|
613
632
|
const objectVar = aql_1.aql.variable('object');
|
|
614
633
|
const keyVar = aql_1.aql.variable('key');
|
|
615
|
-
return aqlExt.
|
|
634
|
+
return aqlExt.subquery((0, aql_1.aql) `LET ${objectVar} = ${processNode(node.objectNode, context)}`, (0, aql_1.aql) `FOR ${keyVar} IN IS_DOCUMENT(${objectVar}) ? ATTRIBUTES(${objectVar}) : []`, (0, aql_1.aql) `RETURN [ ${keyVar}, ${objectVar}[${keyVar}] ]`);
|
|
616
635
|
});
|
|
617
636
|
register(query_tree_1.FirstOfListQueryNode, (node, context) => {
|
|
618
637
|
return (0, aql_1.aql) `FIRST(${processNode(node.listNode, context)})`;
|
|
@@ -759,7 +778,7 @@ function getBillingInput(node, key, context, currentTimestamp) {
|
|
|
759
778
|
}
|
|
760
779
|
register(query_tree_1.CreateBillingEntityQueryNode, (node, context) => {
|
|
761
780
|
const currentTimestamp = context.options.clock.getCurrentTimestamp();
|
|
762
|
-
return aqlExt.
|
|
781
|
+
return aqlExt.subquery((0, aql_1.aql) `UPSERT {
|
|
763
782
|
key: ${node.key},
|
|
764
783
|
type: ${node.rootEntityTypeName}
|
|
765
784
|
}`, (0, aql_1.aql) `INSERT {
|
|
@@ -774,7 +793,7 @@ register(query_tree_1.CreateBillingEntityQueryNode, (node, context) => {
|
|
|
774
793
|
register(query_tree_1.ConfirmForBillingQueryNode, (node, context) => {
|
|
775
794
|
const key = processNode(node.keyNode, context);
|
|
776
795
|
const currentTimestamp = context.options.clock.getCurrentTimestamp();
|
|
777
|
-
return aqlExt.
|
|
796
|
+
return aqlExt.subquery((0, aql_1.aql) `UPSERT {
|
|
778
797
|
key: ${key},
|
|
779
798
|
type: ${node.rootEntityTypeName}
|
|
780
799
|
}`, (0, aql_1.aql) `INSERT {
|
|
@@ -1017,109 +1036,683 @@ register(query_tree_1.EntitiesQueryNode, (node, context) => {
|
|
|
1017
1036
|
});
|
|
1018
1037
|
register(query_tree_1.FollowEdgeQueryNode, (node, context) => {
|
|
1019
1038
|
const tmpVar = aql_1.aql.variable('node');
|
|
1020
|
-
|
|
1021
|
-
|
|
1039
|
+
return aqlExt.subquery((0, aql_1.aql) `FOR ${tmpVar}`, (0, aql_1.aql) `IN ${getSimpleFollowEdgeFragment(node, context)}`,
|
|
1040
|
+
// filter out dangling edges (edges that point to non-existing entities)
|
|
1041
|
+
// using $var._key != null instead of $var != null because the latter prevents ArangodB
|
|
1042
|
+
// from applying the reduce-extraction-to-projection optimization
|
|
1043
|
+
(0, aql_1.aql) `FILTER ${tmpVar}._key != null`, (0, aql_1.aql) `RETURN ${tmpVar}`);
|
|
1022
1044
|
});
|
|
1023
1045
|
register(query_tree_1.TraversalQueryNode, (node, context) => {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
//
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
if (node.captureRootEntity) {
|
|
1034
|
-
if (fieldDepth === 0) {
|
|
1035
|
-
// fieldSegments.length && fieldDepth === 0 means we only traverse through entity extensions
|
|
1036
|
-
// actually, shouldn't really occur because a collect path can't end with an entity extension and
|
|
1037
|
-
// value objects don't capture root entities
|
|
1038
|
-
// however, we can easily implement this so let's do it
|
|
1039
|
-
mapFrag = (nodeFrag) => (0, aql_1.aql) `{ obj: ${getFieldTraversalFragmentWithoutFlattening(node.fieldSegments, nodeFrag)}, root: ${nodeFrag}) }`;
|
|
1040
|
-
}
|
|
1041
|
-
else {
|
|
1042
|
-
// the result of getFieldTraversalFragmentWithoutFlattening() now is a list, so we need to iterate
|
|
1043
|
-
// over it. if the depth is > 1, we need to flatten the deeper ones so we can do one FOR loop over them
|
|
1044
|
-
// we still return a list, so we just reduce the depth to 1 and not to 0
|
|
1045
|
-
const entityVar = aql_1.aql.variable('entity');
|
|
1046
|
-
mapFrag = (rootEntityFrag) => aqlExt.parenthesizeList((0, aql_1.aql) `FOR ${entityVar} IN ${getFlattenFrag(getFieldTraversalFragmentWithoutFlattening(node.fieldSegments, rootEntityFrag), fieldDepth - 1)}`, (0, aql_1.aql) `RETURN { obj: ${entityVar}, root: ${rootEntityFrag} }`);
|
|
1047
|
-
remainingDepth = 1;
|
|
1048
|
-
}
|
|
1046
|
+
// We have a lot of different methods here for different cases
|
|
1047
|
+
// The AQL looks very different depending on the case, and separate methods are easiser to
|
|
1048
|
+
// understand than lots of conditionals in one big method
|
|
1049
|
+
if (node.relationSegments.length && node.fieldSegments.length) {
|
|
1050
|
+
if (!node.fieldSegments.some((f) => f.isListSegment)) {
|
|
1051
|
+
// non-list field segments are similar to no field segments at all because we just
|
|
1052
|
+
// append a simple field path to the root variable
|
|
1053
|
+
if (node.resultIsList) {
|
|
1054
|
+
return processTraversalWithListRelationSegmentsAndNonListFieldSegments(node, context);
|
|
1049
1055
|
}
|
|
1050
1056
|
else {
|
|
1051
|
-
|
|
1057
|
+
// We currently don't allow non-list @collect paths, so this is never executed
|
|
1058
|
+
// Keep it for nonetheless because we might allow it in the future or use
|
|
1059
|
+
// TraversalQueryNode for other purposes
|
|
1060
|
+
return processTraversalWithNonListRelationSegmentsAndNonListFieldSegments(node, context);
|
|
1052
1061
|
}
|
|
1053
1062
|
}
|
|
1063
|
+
if (node.innerNode && (0, traversal_helpers_1.mightGenerateSubquery)(node.innerNode)) {
|
|
1064
|
+
// cannot have subqueries within array expansions, so we need to use a subquery here
|
|
1065
|
+
return processTraversalWithRelationAndListFieldSegmentsUsingSubquery(node, context);
|
|
1066
|
+
}
|
|
1067
|
+
// these both use array expansion expressions
|
|
1068
|
+
if (node.orderBy.isUnordered()) {
|
|
1069
|
+
return processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWithoutSort(node, context);
|
|
1070
|
+
}
|
|
1054
1071
|
else {
|
|
1055
|
-
|
|
1056
|
-
// doesn't make sense to capture the root entity if we're returning the root entities
|
|
1057
|
-
throw new Error(`captureRootEntity without fieldSegments detected`);
|
|
1058
|
-
}
|
|
1072
|
+
return processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWithSort(node, context);
|
|
1059
1073
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
if (node.
|
|
1063
|
-
|
|
1064
|
-
fixedSourceFrag = getFullIDFromKeysFragment(sourceFrag, node.relationSegments[0].relationSide.sourceType);
|
|
1065
|
-
}
|
|
1066
|
-
else {
|
|
1067
|
-
fixedSourceFrag = getFullIDFromKeyFragment(sourceFrag, node.relationSegments[0].relationSide.sourceType);
|
|
1068
|
-
}
|
|
1074
|
+
}
|
|
1075
|
+
else if (node.relationSegments.length) {
|
|
1076
|
+
if (node.resultIsList) {
|
|
1077
|
+
return processTraversalWithOnlyRelationSegmentsAsList(node, context);
|
|
1069
1078
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
mapFrag,
|
|
1076
|
-
context,
|
|
1077
|
-
});
|
|
1078
|
-
if (node.relationSegments.some((s) => s.isListSegment) || node.sourceIsList) {
|
|
1079
|
-
// if the relation contains a list segment, getRelationTraversalFragment will return a list
|
|
1080
|
-
// if we already returned lists within the mapFrag (-> current value of remainingDepth), we need to add that
|
|
1081
|
-
remainingDepth++;
|
|
1079
|
+
else {
|
|
1080
|
+
// We currently don't allow non-list @collect paths, so this is never executed
|
|
1081
|
+
// Keep it for nonetheless because we might allow it in the future or use
|
|
1082
|
+
// TraversalQueryNode for other purposes
|
|
1083
|
+
return processTraversalWithOnlyRelationSegmentsNoList(node, context);
|
|
1082
1084
|
}
|
|
1083
|
-
// flatten 1 less than the depth, see below
|
|
1084
|
-
return getFlattenFrag(frag, remainingDepth - 1);
|
|
1085
1085
|
}
|
|
1086
|
-
if (node.
|
|
1087
|
-
//
|
|
1088
|
-
|
|
1086
|
+
else if (node.fieldSegments.length) {
|
|
1087
|
+
// In the simple case, we can use an array expansion expression instead of a subquery
|
|
1088
|
+
// - SORT is not supported by array expressions (documented)
|
|
1089
|
+
// - subqueries in array expressions currently cause an internal error in arangodb (3.12.6)
|
|
1090
|
+
if (node.orderBy.isUnordered() &&
|
|
1091
|
+
(!node.innerNode || !(0, traversal_helpers_1.mightGenerateSubquery)(node.innerNode))) {
|
|
1092
|
+
return processTraversalWithOnlyFieldSegmentsUsingArrayExpansion(node, context);
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
return processTraversalWithOnlyFieldSegmentsUsingSubquery(node, context);
|
|
1096
|
+
}
|
|
1089
1097
|
}
|
|
1090
|
-
|
|
1091
|
-
// don't need,
|
|
1092
|
-
throw new Error(`
|
|
1098
|
+
else {
|
|
1099
|
+
// don't need this case, so better guard against it
|
|
1100
|
+
throw new Error(`TraversalQueryNode must have at least one segment`);
|
|
1093
1101
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1102
|
+
});
|
|
1103
|
+
/**
|
|
1104
|
+
* Produces:
|
|
1105
|
+
*
|
|
1106
|
+
* FIRST(
|
|
1107
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1108
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1109
|
+
* FOR result IN OUTBOUND hop2_target
|
|
1110
|
+
* RETURN innerNode(result)
|
|
1111
|
+
* )
|
|
1112
|
+
*
|
|
1113
|
+
* or, if preserveNullValues is true and hop2_target is a to-1 relation:
|
|
1114
|
+
*
|
|
1115
|
+
* FIRST(
|
|
1116
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1117
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1118
|
+
* LET result = FIRST(FOR node3 IN OUTBOUND hop2_target RETURNN node3)
|
|
1119
|
+
* RETURN innerNode(result)
|
|
1120
|
+
* )
|
|
1121
|
+
*/
|
|
1122
|
+
function processTraversalWithOnlyRelationSegmentsNoList(node, context) {
|
|
1123
|
+
if (node.fieldSegments.length > 0) {
|
|
1124
|
+
throw new Error(`Did not expect any field segments`);
|
|
1125
|
+
}
|
|
1126
|
+
if (node.filterNode ||
|
|
1127
|
+
!node.orderBy.isUnordered() ||
|
|
1128
|
+
node.skip !== undefined ||
|
|
1129
|
+
node.maxCount !== undefined) {
|
|
1130
|
+
throw new Error(`Cannot have filter, orderBy, skip or maxCount on non-list relation traversal`);
|
|
1131
|
+
}
|
|
1132
|
+
const innerContext = context.bindVariable(node.itemVariable);
|
|
1133
|
+
const itemVar = innerContext.getVariable(node.itemVariable);
|
|
1134
|
+
const forStatementsFrag = getRelationTraversalForStatements({
|
|
1135
|
+
node,
|
|
1136
|
+
innermostItemVar: itemVar,
|
|
1137
|
+
preserveNullValues: node.preserveNullValues,
|
|
1138
|
+
context,
|
|
1139
|
+
});
|
|
1140
|
+
return aqlExt.firstOfSubquery(forStatementsFrag, (0, aql_1.aql) `RETURN ${node.innerNode ? processNode(node.innerNode, innerContext) : itemVar}`);
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Produces:
|
|
1144
|
+
*
|
|
1145
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1146
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1147
|
+
* FOR item IN OUTBOUND hop2_target
|
|
1148
|
+
* FILTER filterExpr(item)
|
|
1149
|
+
* SORT item.field1 ASC
|
|
1150
|
+
* LIMIT skip, maxCount
|
|
1151
|
+
* RETURN innerNode(item)
|
|
1152
|
+
*
|
|
1153
|
+
* or, if preserveNullValues is true and hop2_target is a to-1 relation:
|
|
1154
|
+
*
|
|
1155
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1156
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1157
|
+
* LET item = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
|
|
1158
|
+
* FILTER filterExpr(item)
|
|
1159
|
+
* SORT item.field1 ASC
|
|
1160
|
+
* LIMIT skip, maxCount
|
|
1161
|
+
* RETURN innerNode(item)
|
|
1162
|
+
*/
|
|
1163
|
+
function processTraversalWithOnlyRelationSegmentsAsList(node, context) {
|
|
1164
|
+
if (node.fieldSegments.length > 0) {
|
|
1165
|
+
throw new Error(`Did not expect any field segments`);
|
|
1166
|
+
}
|
|
1167
|
+
// note: this is the only variant where sourceIsList and alwaysProduceList is supported
|
|
1168
|
+
// (used in getPreEntityRemovalStatementsForRelationSide())
|
|
1169
|
+
// - sourceIsList is handled in getRelationTraversalForStatements()
|
|
1170
|
+
// - alwaysProduceList is automatically handled because we always RETURN a list here
|
|
1171
|
+
// we could refactor the single usage so it does not use a TraversalQueryNode in the first place
|
|
1172
|
+
const itemVar = aql_1.aql.variable(`node`);
|
|
1173
|
+
const innerContext = context.bindVariable(node.itemVariable, itemVar);
|
|
1174
|
+
const forStatementsFrag = getRelationTraversalForStatements({
|
|
1175
|
+
node,
|
|
1176
|
+
innermostItemVar: itemVar,
|
|
1177
|
+
context,
|
|
1178
|
+
preserveNullValues: node.preserveNullValues,
|
|
1179
|
+
});
|
|
1180
|
+
return aqlExt.subquery(forStatementsFrag, node.filterNode ? (0, aql_1.aql) `FILTER ${processNode(node.filterNode, context)}` : (0, aql_1.aql) ``,
|
|
1181
|
+
// yes, we can SORT and LIMIT like this even if there are multiple FOR statements
|
|
1182
|
+
// because there is one result set for the cross product of all FOR statements
|
|
1183
|
+
// see https://docs.arangodb.com/3.12/aql/high-level-operations/for/#usage
|
|
1184
|
+
generateSortAQL(node.orderBy, innerContext), generateLimitClause(node) ?? (0, aql_1.aql) ``, (0, aql_1.aql) `RETURN ${node.innerNode ? processNode(node.innerNode, innerContext) : itemVar}`);
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Produces:
|
|
1188
|
+
*
|
|
1189
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1190
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1191
|
+
* FOR item IN OUTBOUND hop2_target
|
|
1192
|
+
* FILTER filterExpr(item.fieldSegment1.fieldSegment2)
|
|
1193
|
+
* SORT item.fieldSegment1.fieldSegment2.sortField ASC
|
|
1194
|
+
* LIMIT skip, maxCount
|
|
1195
|
+
* RETURN innerNode(item.fieldSegment1.fieldSegment2, { root: node2 })
|
|
1196
|
+
*
|
|
1197
|
+
* or, if preserveNullValues is true and hop2_target is a to-1 relation:
|
|
1198
|
+
*
|
|
1199
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1200
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1201
|
+
* LET item = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
|
|
1202
|
+
* FILTER filterExpr(item.fieldSegment1.fieldSegment2)
|
|
1203
|
+
* SORT item.fieldSegment1.fieldSegment2.sortField ASC
|
|
1204
|
+
* LIMIT skip, maxCount
|
|
1205
|
+
* RETURN innerNode(item.fieldSegment1.fieldSegment2, { root: node2 })
|
|
1206
|
+
*/
|
|
1207
|
+
function processTraversalWithListRelationSegmentsAndNonListFieldSegments(node, context) {
|
|
1208
|
+
if (!node.relationSegments.some((f) => f.isListSegment)) {
|
|
1209
|
+
throw new Error(`Expected at least one relation list segment`);
|
|
1210
|
+
}
|
|
1211
|
+
if (node.fieldSegments.some((f) => f.isListSegment)) {
|
|
1212
|
+
throw new Error(`Did not expect any field list segments`);
|
|
1213
|
+
}
|
|
1214
|
+
// this is very similar to processTraversalWithOnlyRelationSegmentsAsList(),
|
|
1215
|
+
// but instead of using the rootVar in filter, sort and mapping, we use the field traversal result
|
|
1216
|
+
const rootVar = aql_1.aql.variable(`root`);
|
|
1217
|
+
const forStatementsFrag = getRelationTraversalForStatements({
|
|
1218
|
+
node,
|
|
1219
|
+
innermostItemVar: rootVar,
|
|
1220
|
+
context,
|
|
1221
|
+
preserveNullValues: node.preserveNullValues,
|
|
1222
|
+
});
|
|
1223
|
+
const fieldTraversalFrag = getFieldTraversalFragment({
|
|
1224
|
+
segments: node.fieldSegments,
|
|
1225
|
+
sourceFrag: rootVar,
|
|
1226
|
+
});
|
|
1227
|
+
const innerContext = context.bindVariable(node.itemVariable, fieldTraversalFrag);
|
|
1228
|
+
// note: we don't filter out NULL values even if preserveNullValues is false because that's currently
|
|
1229
|
+
// only a flag for performance - actually filtering out NULLs is done by a surrounding AggregationQueryNode
|
|
1230
|
+
// TODO aql-perf remove this note once the NULL filtering / preserving has moved out of AggregationQueryNode
|
|
1231
|
+
return aqlExt.subquery(forStatementsFrag, node.filterNode ? (0, aql_1.aql) `FILTER ${processNode(node.filterNode, innerContext)}` : (0, aql_1.aql) ``, generateSortAQL(node.orderBy, innerContext), generateLimitClause(node) ?? (0, aql_1.aql) ``, (0, aql_1.aql) `RETURN ${node.innerNode ? processNode(node.innerNode, innerContext) : fieldTraversalFrag}`);
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Produces:
|
|
1235
|
+
*
|
|
1236
|
+
* FIRST(
|
|
1237
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1238
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1239
|
+
* FOR item IN OUTBOUND hop2_target
|
|
1240
|
+
* RETURN innerNode(item.fieldSegment1.fieldSegment2, { root: node2 })
|
|
1241
|
+
* )
|
|
1242
|
+
*
|
|
1243
|
+
* or, if preserveNullValues is true and hop2_target is a to-1 relation:
|
|
1244
|
+
*
|
|
1245
|
+
* FIRST(
|
|
1246
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1247
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1248
|
+
* LET item = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
|
|
1249
|
+
* RETURN innerNode(item.fieldSegment1.fieldSegment2, { root: node2 })
|
|
1250
|
+
* )
|
|
1251
|
+
*/
|
|
1252
|
+
function processTraversalWithNonListRelationSegmentsAndNonListFieldSegments(node, context) {
|
|
1253
|
+
if (node.fieldSegments.some((f) => f.isListSegment)) {
|
|
1254
|
+
throw new Error(`Did not expect any field list segments`);
|
|
1255
|
+
}
|
|
1256
|
+
if (node.relationSegments.some((f) => f.isListSegment)) {
|
|
1257
|
+
throw new Error(`Did not expect any relation list segments`);
|
|
1258
|
+
}
|
|
1259
|
+
if (node.filterNode ||
|
|
1260
|
+
!node.orderBy.isUnordered() ||
|
|
1261
|
+
node.skip !== undefined ||
|
|
1262
|
+
node.maxCount !== undefined) {
|
|
1263
|
+
throw new Error(`Cannot have filter, orderBy, skip or maxCount on non-list traversal`);
|
|
1264
|
+
}
|
|
1265
|
+
// this is very similar to processTraversalWithOnlyRelationSegmentsNoList(),
|
|
1266
|
+
// but instead of using the rootVar in mapping, we use the field traversal result
|
|
1267
|
+
const rootVar = aql_1.aql.variable(`root`);
|
|
1268
|
+
const forStatementsFrag = getRelationTraversalForStatements({
|
|
1269
|
+
node,
|
|
1270
|
+
innermostItemVar: rootVar,
|
|
1271
|
+
preserveNullValues: node.preserveNullValues,
|
|
1272
|
+
context,
|
|
1273
|
+
});
|
|
1274
|
+
const fieldTraversalFrag = getFieldTraversalFragment({
|
|
1275
|
+
segments: node.fieldSegments,
|
|
1276
|
+
sourceFrag: rootVar,
|
|
1277
|
+
});
|
|
1278
|
+
const innerContext = context.bindVariable(node.itemVariable, fieldTraversalFrag);
|
|
1279
|
+
return aqlExt.firstOfSubquery(forStatementsFrag, (0, aql_1.aql) `RETURN ${node.innerNode ? processNode(node.innerNode, innerContext) : fieldTraversalFrag}`);
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Produces:
|
|
1283
|
+
*
|
|
1284
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1285
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1286
|
+
* FOR root IN OUTBOUND hop2_target
|
|
1287
|
+
* FOR item IN root.fieldSegment1[*].fieldSegment2[** FILTER filterExpr(CURRENT)][*]
|
|
1288
|
+
* SORT item.sortValues[0]
|
|
1289
|
+
* LIMIT skip, maxCount
|
|
1290
|
+
* RETURN innerNode(item, { root })
|
|
1291
|
+
*
|
|
1292
|
+
* or, if preserveNullValues is true and hop2_target is a to-1 relation:
|
|
1293
|
+
*
|
|
1294
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1295
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1296
|
+
* LET root = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
|
|
1297
|
+
* FOR item IN root.fieldSegment1[*].fieldSegment2[** FILTER filterExpr(CURRENT)][*]
|
|
1298
|
+
* SORT item.sortValues[0]
|
|
1299
|
+
* LIMIT skip, maxCount
|
|
1300
|
+
* RETURN innerNode(item, { root })
|
|
1301
|
+
*/
|
|
1302
|
+
function processTraversalWithRelationAndListFieldSegmentsUsingSubquery(node, context) {
|
|
1303
|
+
if (!node.relationSegments.length || !node.fieldSegments) {
|
|
1304
|
+
throw new Error(`Expected both relation and field segments`);
|
|
1305
|
+
}
|
|
1306
|
+
if (!node.fieldSegments.some((s) => s.isListSegment)) {
|
|
1307
|
+
throw new Error(`Expected at least one list field segment`);
|
|
1308
|
+
}
|
|
1309
|
+
if (!node.resultIsList) {
|
|
1310
|
+
throw new Error(`Cannot have sort on non-list traversal`);
|
|
1311
|
+
}
|
|
1312
|
+
// this will hold the node of the innermost relation traversal
|
|
1313
|
+
const rootVar = aql_1.aql.variable('root');
|
|
1314
|
+
const forStatementsFrag = getRelationTraversalForStatements({
|
|
1315
|
+
node,
|
|
1316
|
+
innermostItemVar: rootVar,
|
|
1317
|
+
preserveNullValues: node.preserveNullValues,
|
|
1318
|
+
context,
|
|
1319
|
+
});
|
|
1320
|
+
const filterNode = node.filterNode;
|
|
1321
|
+
const innerFilterFrag = filterNode
|
|
1322
|
+
? (itemFrag) => {
|
|
1323
|
+
// don't provide rootEntityVariable
|
|
1324
|
+
// (if we want to filter on root, it should happen outside already)
|
|
1325
|
+
const innerContext = context.bindVariable(node.itemVariable, itemFrag);
|
|
1326
|
+
return processNode(filterNode, innerContext);
|
|
1327
|
+
}
|
|
1328
|
+
: undefined;
|
|
1329
|
+
const fieldTraversalFrag = getFieldTraversalFragment({
|
|
1330
|
+
segments: node.fieldSegments,
|
|
1331
|
+
sourceFrag: rootVar,
|
|
1332
|
+
filterFrag: innerFilterFrag,
|
|
1333
|
+
});
|
|
1334
|
+
const itemVar = aql_1.aql.variable(`item`);
|
|
1335
|
+
const innerContext = context
|
|
1336
|
+
.bindVariable(node.itemVariable, itemVar)
|
|
1337
|
+
.bindVariable(node.rootEntityVariable, rootVar);
|
|
1338
|
+
// The fieldTraversalFrag will produce a list, so a simple RETURN mapFrag would result in nested lists
|
|
1339
|
+
// -> we iterate over the items again to flatten the lists (the FOR ${itemVar} ...)
|
|
1340
|
+
return aqlExt.subquery((0, aql_1.aql) `${forStatementsFrag}`, (0, aql_1.aql) `FOR ${itemVar} IN ${fieldTraversalFrag}`, generateSortAQL(node.orderBy, innerContext), generateLimitClause(node) ?? (0, aql_1.aql) ``, (0, aql_1.aql) `RETURN ${node.innerNode ? processNode(node.innerNode, innerContext) : itemVar}`);
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Produces this if the result of the relation traversal is a single item:
|
|
1344
|
+
*
|
|
1345
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1346
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1347
|
+
* FOR root IN OUTBOUND hop2_target
|
|
1348
|
+
* FOR item IN root.fieldSegment1[*].fieldSegment2[**][*
|
|
1349
|
+
* FILTER filterExpr(CURRENT)
|
|
1350
|
+
* LIMIT skip, maxCount
|
|
1351
|
+
* RETURN innerNode(CURRENT, { root })
|
|
1352
|
+
* ]
|
|
1353
|
+
* RETURN item
|
|
1354
|
+
*
|
|
1355
|
+
* or, if the result of the relation traversal is a list:
|
|
1356
|
+
*
|
|
1357
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1358
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1359
|
+
* FOR root IN OUTBOUND hop2_target
|
|
1360
|
+
* FOR item IN root.fieldSegment1[*].fieldSegment2[**][*
|
|
1361
|
+
* FILTER filterExpr(CURRENT)
|
|
1362
|
+
* RETURN innerNode(CURRENT, { root })
|
|
1363
|
+
* ]
|
|
1364
|
+
* LIMIT skip, maxCount
|
|
1365
|
+
* RETURN item
|
|
1366
|
+
*
|
|
1367
|
+
* ---
|
|
1368
|
+
*
|
|
1369
|
+
* or, if preserveNullValues is true and hop2_target is a to-1 relation:
|
|
1370
|
+
*
|
|
1371
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1372
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1373
|
+
* LET root = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
|
|
1374
|
+
* FOR item IN root.fieldSegment1[*].fieldSegment2[**][*
|
|
1375
|
+
* FILTER filterExpr(CURRENT)
|
|
1376
|
+
* LIMIT skip, maxCount
|
|
1377
|
+
* RETURN innerNode(CURRENT, { root })
|
|
1378
|
+
* ]
|
|
1379
|
+
* RETURN item
|
|
1380
|
+
*
|
|
1381
|
+
* or, if the result of the relation traversal is a list:
|
|
1382
|
+
*
|
|
1383
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1384
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1385
|
+
* LET root = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
|
|
1386
|
+
* FOR item IN root.fieldSegment1[*].fieldSegment2[**][*
|
|
1387
|
+
* FILTER filterExpr(CURRENT)
|
|
1388
|
+
* RETURN innerNode(CURRENT, { root })
|
|
1389
|
+
* ]
|
|
1390
|
+
* LIMIT skip, maxCount
|
|
1391
|
+
* RETURN item
|
|
1392
|
+
*/
|
|
1393
|
+
function processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWithoutSort(node, context) {
|
|
1394
|
+
if (!node.relationSegments.length || !node.fieldSegments) {
|
|
1395
|
+
throw new Error(`Expected both relation and field segments`);
|
|
1396
|
+
}
|
|
1397
|
+
if (!node.fieldSegments.some((s) => s.isListSegment)) {
|
|
1398
|
+
throw new Error(`Expected at least one list field segment`);
|
|
1399
|
+
}
|
|
1400
|
+
if (!node.orderBy.isUnordered()) {
|
|
1401
|
+
throw new Error(`Did not expect orderBy clauses`);
|
|
1402
|
+
}
|
|
1403
|
+
// this will hold the node of the innermost relation traversal
|
|
1404
|
+
const rootVar = aql_1.aql.variable('root');
|
|
1405
|
+
const forStatementsFrag = getRelationTraversalForStatements({
|
|
1406
|
+
node,
|
|
1407
|
+
innermostItemVar: rootVar,
|
|
1408
|
+
preserveNullValues: node.preserveNullValues,
|
|
1409
|
+
context,
|
|
1410
|
+
});
|
|
1411
|
+
const innerNode = node.innerNode;
|
|
1412
|
+
const innerMapFrag = innerNode
|
|
1413
|
+
? (itemFrag) => {
|
|
1414
|
+
const innerContext = context
|
|
1415
|
+
.bindVariable(node.itemVariable, itemFrag)
|
|
1416
|
+
.bindVariable(node.rootEntityVariable, rootVar);
|
|
1417
|
+
return processNode(innerNode, innerContext);
|
|
1418
|
+
}
|
|
1419
|
+
: undefined;
|
|
1420
|
+
// We can do the LIMIT within the field traversal's mapping function using an array expansion,
|
|
1421
|
+
// but only if the relation traversal yields at most one result (i.e. if it only follows 1:1 relations)
|
|
1422
|
+
const lastRelationSegment = node.relationSegments[node.relationSegments.length - 1];
|
|
1423
|
+
let limitArgs;
|
|
1424
|
+
let innerLimitArgs;
|
|
1425
|
+
if (lastRelationSegment.resultIsList) {
|
|
1426
|
+
limitArgs = {
|
|
1427
|
+
skip: node.skip,
|
|
1428
|
+
maxCount: node.maxCount,
|
|
1429
|
+
};
|
|
1430
|
+
innerLimitArgs = {};
|
|
1431
|
+
}
|
|
1432
|
+
else {
|
|
1433
|
+
limitArgs = {};
|
|
1434
|
+
innerLimitArgs = {
|
|
1435
|
+
skip: node.skip,
|
|
1436
|
+
maxCount: node.maxCount,
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
let innerFilterFrag;
|
|
1440
|
+
const filterNode = node.filterNode;
|
|
1441
|
+
if (filterNode) {
|
|
1442
|
+
innerFilterFrag = (itemFrag) => {
|
|
1443
|
+
// don't map rootEntityVariable
|
|
1444
|
+
// (if we want to filter on root, it should happen outside already)
|
|
1445
|
+
const innerContext = context.bindVariable(node.itemVariable, itemFrag);
|
|
1446
|
+
return processNode(filterNode, innerContext);
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
const fieldTraversalFrag = getFieldTraversalFragment({
|
|
1450
|
+
segments: node.fieldSegments,
|
|
1451
|
+
sourceFrag: rootVar,
|
|
1452
|
+
mapFrag: innerMapFrag,
|
|
1453
|
+
filterFrag: innerFilterFrag,
|
|
1454
|
+
...innerLimitArgs,
|
|
1455
|
+
});
|
|
1456
|
+
// the fieldTraversalFrag will produce a list, so a simple RETURN mapFrag would result in nested lists
|
|
1457
|
+
// -> we iterate over the items again to flatten the lists
|
|
1458
|
+
// Note: If the relation traversal only consists of 1:1, we could theoretically use something like this:
|
|
1459
|
+
// LET root = FIRST(FOR obj IN OUTBOUND source edge_collection RETURN obj)
|
|
1460
|
+
// RETURN root.children
|
|
1461
|
+
// however, that would prevent the reduce-extraction-to-projection optimization
|
|
1462
|
+
// (because we would access the whole root object)
|
|
1463
|
+
// -> it's better to produce the same structure as we do for 1:n relation traversals:
|
|
1464
|
+
// FOR root IN OUTBOUND source edge_collection
|
|
1465
|
+
// FOR item IN root.children
|
|
1466
|
+
// RETURN item
|
|
1467
|
+
// (could also use FLATTEN(), but since we're already using nested FORs, this is probably cleaner)
|
|
1468
|
+
const itemVar = aql_1.aql.variable(`item`);
|
|
1469
|
+
return aqlExt.subquery((0, aql_1.aql) `${forStatementsFrag}`, (0, aql_1.aql) `FOR ${itemVar} IN ${fieldTraversalFrag}`, generateLimitClause(limitArgs) ?? (0, aql_1.aql) ``, (0, aql_1.aql) `RETURN ${itemVar}`);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Produces:
|
|
1473
|
+
*
|
|
1474
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1475
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1476
|
+
* FOR root IN OUTBOUND hop2_target
|
|
1477
|
+
* FOR item IN root.fieldSegment1[*].fieldSegment2[**][*
|
|
1478
|
+
* FILTER filterExpr(CURRENT)
|
|
1479
|
+
* RETURN {
|
|
1480
|
+
* value: innerNode(CURRENT, { root }),
|
|
1481
|
+
* sortValues: [
|
|
1482
|
+
* CURRENT.sortField1,
|
|
1483
|
+
* CURRENT.sortField2
|
|
1484
|
+
* ]
|
|
1485
|
+
* }
|
|
1486
|
+
* ]
|
|
1487
|
+
* SORT item.sortValues[0] ASC, item.sortValues[1] DESC
|
|
1488
|
+
* LIMIT skip, maxCount
|
|
1489
|
+
* RETURN item.value
|
|
1490
|
+
*
|
|
1491
|
+
* or, if preserveNullValues is true and hop2_target is a to-1 relation:
|
|
1492
|
+
*
|
|
1493
|
+
* FOR node1 IN OUTBOUND source_hop1
|
|
1494
|
+
* FOR node2 in OUTBOUND hop1_hop2
|
|
1495
|
+
* LET root = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
|
|
1496
|
+
* FOR item IN root.fieldSegment1[*].fieldSegment2[**][*
|
|
1497
|
+
* FILTER filterExpr(CURRENT)
|
|
1498
|
+
* RETURN {
|
|
1499
|
+
* value: innerNode(CURRENT, { root }),
|
|
1500
|
+
* sortValues: [
|
|
1501
|
+
* CURRENT.sortField1,
|
|
1502
|
+
* CURRENT.sortField2
|
|
1503
|
+
* ]
|
|
1504
|
+
* }
|
|
1505
|
+
* ]
|
|
1506
|
+
* SORT item.sortValues[0] ASC, item.sortValues[1] DESC
|
|
1507
|
+
* LIMIT skip, maxCount
|
|
1508
|
+
* RETURN item.value
|
|
1509
|
+
*/
|
|
1510
|
+
function processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWithSort(node, context) {
|
|
1511
|
+
if (!node.relationSegments.length || !node.fieldSegments) {
|
|
1512
|
+
throw new Error(`Expected both relation and field segments`);
|
|
1513
|
+
}
|
|
1514
|
+
if (!node.fieldSegments.some((s) => s.isListSegment)) {
|
|
1515
|
+
throw new Error(`Expected at least one list field segment`);
|
|
1516
|
+
}
|
|
1517
|
+
if (!node.resultIsList) {
|
|
1518
|
+
throw new Error(`Cannot have sort on non-list traversal`);
|
|
1519
|
+
}
|
|
1520
|
+
if (node.orderBy.isUnordered()) {
|
|
1521
|
+
throw new Error(`Expected orderBy clauses`);
|
|
1522
|
+
}
|
|
1523
|
+
// this will hold the node of the innermost relation traversal
|
|
1524
|
+
const rootVar = aql_1.aql.variable('root');
|
|
1525
|
+
const forStatementsFrag = getRelationTraversalForStatements({
|
|
1526
|
+
node,
|
|
1527
|
+
innermostItemVar: rootVar,
|
|
1528
|
+
preserveNullValues: node.preserveNullValues,
|
|
1529
|
+
context,
|
|
1530
|
+
});
|
|
1531
|
+
/*
|
|
1532
|
+
A simple way to implement this would be to first SORT, and then map:
|
|
1533
|
+
|
|
1534
|
+
FOR v_root1 IN 1..1 INBOUND v_order1 @@deliveries_order
|
|
1535
|
+
FOR v_item1 IN v_root1.`deliveryContents`[*].`items`[**]
|
|
1536
|
+
SORT v_item1.`itemNumber`
|
|
1537
|
+
RETURN {
|
|
1538
|
+
"itemNumber": v_item1.`itemNumber`
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
(this is done by processTraversalWithRelationAndListFieldSegmentsUsingSubquery())
|
|
1542
|
+
|
|
1543
|
+
Instead, we map first, then SORT:
|
|
1544
|
+
|
|
1545
|
+
FOR v_root1 IN 1..1 INBOUND v_order1 @@deliveries_order
|
|
1546
|
+
FOR v_item1 IN v_root1.`deliveryContents`[*].`items`[** RETURN {
|
|
1547
|
+
value: {
|
|
1548
|
+
"itemNumber": CURRENT.`itemNumber`
|
|
1549
|
+
},
|
|
1550
|
+
sortValues: [
|
|
1551
|
+
CURRENT.`itemNumber`
|
|
1552
|
+
]
|
|
1553
|
+
}]
|
|
1554
|
+
SORT v_item1.sortValues[0]
|
|
1555
|
+
RETURN v_item1.value
|
|
1556
|
+
|
|
1557
|
+
This seems to reduce the memory consumption by up to factor 2
|
|
1558
|
+
if there are many / large fields in the items that are not needed at all
|
|
1559
|
+
(because sorting probably copies the whole item into some temporary structure)
|
|
1560
|
+
*/
|
|
1561
|
+
// we sort after mapping roots to items, so we need to preserve the sort values that are based on the item
|
|
1562
|
+
// -> we produce items like this: { value: ..., sortValues: [...] }
|
|
1563
|
+
const innerMapFrag = (itemFrag) => {
|
|
1564
|
+
const innerContext = context
|
|
1565
|
+
.bindVariable(node.itemVariable, itemFrag)
|
|
1566
|
+
.bindVariable(node.rootEntityVariable, rootVar);
|
|
1567
|
+
const valueFrag = node.innerNode ? processNode(node.innerNode, innerContext) : itemFrag;
|
|
1568
|
+
return aql_1.aql.lines((0, aql_1.aql) `{`, aql_1.aql.indent(aql_1.aql.lines((0, aql_1.aql) `value: ${valueFrag},`, (0, aql_1.aql) `sortValues: [`, aql_1.aql.indent(aql_1.aql.join(node.orderBy.clauses.map((c) => processNode(c.valueNode, innerContext)), (0, aql_1.aql) `,\n`)), (0, aql_1.aql) `]`)), (0, aql_1.aql) `}`);
|
|
1569
|
+
};
|
|
1570
|
+
const filterNode = node.filterNode;
|
|
1571
|
+
const innerFilterFrag = filterNode
|
|
1572
|
+
? (itemFrag) => {
|
|
1573
|
+
// don't provide rootEntityVariable
|
|
1574
|
+
// (if we want to filter on root, it should happen outside already)
|
|
1575
|
+
const innerContext = context.bindVariable(node.itemVariable, itemFrag);
|
|
1576
|
+
return processNode(filterNode, innerContext);
|
|
1577
|
+
}
|
|
1578
|
+
: undefined;
|
|
1579
|
+
const fieldTraversalFrag = getFieldTraversalFragment({
|
|
1580
|
+
segments: node.fieldSegments,
|
|
1581
|
+
sourceFrag: rootVar,
|
|
1582
|
+
mapFrag: innerMapFrag,
|
|
1583
|
+
filterFrag: innerFilterFrag,
|
|
1584
|
+
});
|
|
1585
|
+
// holds the { value, sortValues }
|
|
1586
|
+
const itemVar = aql_1.aql.variable(`item`);
|
|
1587
|
+
const clauseFrags = node.orderBy.clauses.map((clause, index) => (0, aql_1.aql) `${itemVar}.sortValues[${aql_1.aql.integer(index)}]${dirAQL(clause.direction)}`);
|
|
1588
|
+
// The fieldTraversalFrag will produce a list, so a simple RETURN mapFrag would result in nested lists
|
|
1589
|
+
// -> we iterate over the items again to flatten the lists (the FOR ${itemVar} ...)
|
|
1590
|
+
return aqlExt.subquery((0, aql_1.aql) `${forStatementsFrag}`, (0, aql_1.aql) `FOR ${itemVar} IN ${fieldTraversalFrag}`, (0, aql_1.aql) `SORT ${aql_1.aql.join(clauseFrags, (0, aql_1.aql) `, `)}`, generateLimitClause(node) ?? (0, aql_1.aql) ``, (0, aql_1.aql) `RETURN ${itemVar}.value`);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Produces:
|
|
1594
|
+
*
|
|
1595
|
+
* source.listFieldSegment1[*].listFieldSegment2[**][*
|
|
1596
|
+
* FILTER filterExpr(CURRENT.nonListFieldSegment)
|
|
1597
|
+
* LIMIT skip, maxCount
|
|
1598
|
+
* RETURN innerNode(CURRENT.nonListFieldSegment)
|
|
1599
|
+
* ]
|
|
1600
|
+
*/
|
|
1601
|
+
function processTraversalWithOnlyFieldSegmentsUsingArrayExpansion(node, context) {
|
|
1602
|
+
if (node.relationSegments.length) {
|
|
1603
|
+
throw new Error(`Expected no relation segments`);
|
|
1097
1604
|
}
|
|
1098
1605
|
if (node.entitiesIdentifierKind !== query_tree_1.EntitiesIdentifierKind.ENTITY) {
|
|
1099
1606
|
throw new Error(`Only ENTITY identifiers supported without relationSegments`);
|
|
1100
1607
|
}
|
|
1608
|
+
if (!node.orderBy.isUnordered()) {
|
|
1609
|
+
throw new Error(`Did not expect orderBy clauses`);
|
|
1610
|
+
}
|
|
1101
1611
|
if (!node.fieldSegments.length) {
|
|
1102
|
-
|
|
1103
|
-
return sourceFrag;
|
|
1612
|
+
throw new Error(`Expected at least one field segment`);
|
|
1104
1613
|
}
|
|
1105
|
-
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1614
|
+
const sourceFrag = processNode(node.sourceEntityNode, context);
|
|
1615
|
+
// no SORT clause, so we can put everything into array expansions
|
|
1616
|
+
// This is more efficient than using subqueries.
|
|
1617
|
+
const innerNode = node.innerNode;
|
|
1618
|
+
const mapFrag = innerNode
|
|
1619
|
+
? (itemFrag) => processNode(innerNode, context.bindVariable(node.itemVariable, itemFrag))
|
|
1620
|
+
: undefined;
|
|
1621
|
+
const filterNode = node.filterNode;
|
|
1622
|
+
const filterFrag = filterNode
|
|
1623
|
+
? (itemFrag) => processNode(filterNode, context.bindVariable(node.itemVariable, itemFrag))
|
|
1624
|
+
: undefined;
|
|
1625
|
+
// if there are no list segments, this will just be a simple path access
|
|
1626
|
+
// -> will naturally be either a list or not, depending on what's needed
|
|
1627
|
+
return getFieldTraversalFragment({
|
|
1628
|
+
segments: node.fieldSegments,
|
|
1629
|
+
sourceFrag,
|
|
1630
|
+
mapFrag,
|
|
1631
|
+
filterFrag,
|
|
1632
|
+
skip: node.skip,
|
|
1633
|
+
maxCount: node.maxCount,
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Produces:
|
|
1638
|
+
*
|
|
1639
|
+
* FOR item IN source.listFieldSegment1[*].listFieldSegment2[**].nonListFieldSegment
|
|
1640
|
+
* FILTER filterExpr(item)
|
|
1641
|
+
* SORT item.sortField1 ASC, item.sortField1 DESC
|
|
1642
|
+
* LIMIT skip, maxCount
|
|
1643
|
+
* RETURN innerNode(item)
|
|
1644
|
+
*/
|
|
1645
|
+
function processTraversalWithOnlyFieldSegmentsUsingSubquery(node, context) {
|
|
1646
|
+
if (node.relationSegments.length) {
|
|
1647
|
+
throw new Error(`Expected no relation segments`);
|
|
1648
|
+
}
|
|
1649
|
+
if (node.entitiesIdentifierKind !== query_tree_1.EntitiesIdentifierKind.ENTITY) {
|
|
1650
|
+
throw new Error(`Only ENTITY identifiers supported without relationSegments`);
|
|
1651
|
+
}
|
|
1652
|
+
if (!node.fieldSegments.length) {
|
|
1653
|
+
throw new Error(`Expected at least one field segment`);
|
|
1654
|
+
}
|
|
1655
|
+
if (!node.resultIsList) {
|
|
1656
|
+
// this branch would always create a list due to the subquery
|
|
1657
|
+
throw new Error(`Cannot have orderBy on non-list field traversal`);
|
|
1658
|
+
}
|
|
1659
|
+
const sourceFrag = processNode(node.sourceEntityNode, context);
|
|
1660
|
+
// need to use a subquery because array inline expressions don't support SORT
|
|
1661
|
+
// this also means we can't do the innerNode mapping in inline expressions using [* RETURN ...]
|
|
1662
|
+
// because otherwise we could not access the sort fields outside
|
|
1663
|
+
// theoretically we could map the sort fields like we do in the relation + field traversal case,
|
|
1664
|
+
// but we wouldn't gain anything from this here
|
|
1665
|
+
// -> a simple subquery
|
|
1666
|
+
// TODO aql-perf: we could use the { value, sortValues } approach here as well to reduce memory consumption
|
|
1667
|
+
const fieldTraversalFrag = getFieldTraversalFragment({
|
|
1668
|
+
segments: node.fieldSegments,
|
|
1669
|
+
sourceFrag,
|
|
1670
|
+
});
|
|
1671
|
+
const itemVar = aql_1.aql.variable('item');
|
|
1672
|
+
const innerContext = context.bindVariable(node.itemVariable, itemVar);
|
|
1673
|
+
const returnValueFrag = node.innerNode ? processNode(node.innerNode, innerContext) : itemVar;
|
|
1674
|
+
// filter in the subquery instead of in getFieldTraversalFragment() because the filter
|
|
1675
|
+
// expression might use subqueries
|
|
1676
|
+
const filterFrag = node.filterNode
|
|
1677
|
+
? (0, aql_1.aql) `FILTER ${processNode(node.filterNode, innerContext)}`
|
|
1678
|
+
: (0, aql_1.aql) ``;
|
|
1679
|
+
return aqlExt.subquery((0, aql_1.aql) `FOR ${itemVar}`, (0, aql_1.aql) `IN ${fieldTraversalFrag}`, filterFrag, generateSortAQL(node.orderBy, innerContext), generateLimitClause(node) ?? (0, aql_1.aql) ``, (0, aql_1.aql) `RETURN ${returnValueFrag}`);
|
|
1680
|
+
}
|
|
1681
|
+
function getRelationTraversalForStatements({ node, innermostItemVar, context, preserveNullValues = false, }) {
|
|
1682
|
+
if (!node.relationSegments.length) {
|
|
1683
|
+
throw new Error(`Expected at least one relation segment`);
|
|
1684
|
+
}
|
|
1685
|
+
const segments = node.relationSegments;
|
|
1686
|
+
const sourceIsList = node.sourceIsList;
|
|
1687
|
+
const plainSourceFrag = processNode(node.sourceEntityNode, context);
|
|
1688
|
+
// traversal requires real ids
|
|
1689
|
+
let sourceFrag;
|
|
1690
|
+
if (node.entitiesIdentifierKind === query_tree_1.EntitiesIdentifierKind.ID) {
|
|
1691
|
+
if (node.sourceIsList) {
|
|
1692
|
+
sourceFrag = getFullIDFromKeysFragment(plainSourceFrag, node.relationSegments[0].relationSide.sourceType);
|
|
1693
|
+
}
|
|
1694
|
+
else {
|
|
1695
|
+
sourceFrag = getFullIDFromKeyFragment(plainSourceFrag, node.relationSegments[0].relationSide.sourceType);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
else {
|
|
1699
|
+
sourceFrag = plainSourceFrag;
|
|
1114
1700
|
}
|
|
1115
|
-
// ArangoDB 3.4.5 introduced PRUNE which also supports IS_SAME_COLLECTION so we may be able to use just one
|
|
1116
|
-
// traversal which lists all affected edge collections and prunes on the path in the future.
|
|
1117
|
-
const forFragments = [];
|
|
1118
1701
|
const sourceEntityVar = aql_1.aql.variable(`sourceEntity`);
|
|
1119
1702
|
let currentObjectFrag = sourceIsList ? sourceEntityVar : sourceFrag;
|
|
1703
|
+
const forFragments = sourceIsList
|
|
1704
|
+
? [(0, aql_1.aql) `FOR ${sourceEntityVar} IN ${sourceFrag}`]
|
|
1705
|
+
: [];
|
|
1706
|
+
// ArangoDB 3.4.5 introduced PRUNE which also supports IS_SAME_COLLECTION so we may be able to use just one
|
|
1707
|
+
// traversal which lists all affected edge collections and prunes on the path in the future.
|
|
1120
1708
|
let segmentIndex = 0;
|
|
1709
|
+
const lastListSegmentIndex = segments.findLastIndex((s) => s.isListSegment);
|
|
1121
1710
|
for (const segment of segments) {
|
|
1122
|
-
const
|
|
1711
|
+
const isLastSegment = segmentIndex === segments.length - 1;
|
|
1712
|
+
// if there is no list segment, lastListSegmentIndex is -1, so needsNullableVar is true for all segments
|
|
1713
|
+
const needsNullableVar = preserveNullValues && segmentIndex > lastListSegmentIndex; // see below
|
|
1714
|
+
// the caller can specify the result var because they will need it to work with it
|
|
1715
|
+
const nodeVar = isLastSegment && !needsNullableVar ? innermostItemVar : aql_1.aql.variable(`node`);
|
|
1123
1716
|
const edgeVar = aql_1.aql.variable(`edge`);
|
|
1124
1717
|
const pathVar = aql_1.aql.variable(`path`);
|
|
1125
1718
|
const dir = segment.relationSide.isFromSide ? (0, aql_1.aql) `OUTBOUND` : (0, aql_1.aql) `INBOUND`;
|
|
@@ -1129,7 +1722,7 @@ function getRelationTraversalFragment({ segments, sourceFrag, sourceIsList, alwa
|
|
|
1129
1722
|
if (!segment.vertexFilterVariable) {
|
|
1130
1723
|
throw new Error(`vertexFilter is set, but vertexFilterVariable is not`);
|
|
1131
1724
|
}
|
|
1132
|
-
const filterContext = context.
|
|
1725
|
+
const filterContext = context.bindVariable(segment.vertexFilterVariable, nodeVar);
|
|
1133
1726
|
// PRUNE to stop on a node that has to be filtered out (only necessary for traversals > 1 path length)
|
|
1134
1727
|
// however, PRUNE only seems to be a performance feature and is not reliably evaluated
|
|
1135
1728
|
// (e.g. it's not when using COLLECT with distinct for some reason), so we need to add a path filter
|
|
@@ -1142,7 +1735,7 @@ function getRelationTraversalFragment({ segments, sourceFrag, sourceIsList, alwa
|
|
|
1142
1735
|
throw new Error(`Unsupported filter pattern for graph traversal`);
|
|
1143
1736
|
}
|
|
1144
1737
|
const vertexInPathFrag = (0, aql_1.aql) `${pathVar}.vertices[*]`;
|
|
1145
|
-
const pathFilterContext = context.
|
|
1738
|
+
const pathFilterContext = context.bindVariable(segment.vertexFilterVariable, vertexInPathFrag);
|
|
1146
1739
|
const lhsFrag = processNode(segment.vertexFilter.lhs, pathFilterContext);
|
|
1147
1740
|
const opFrag = getAQLOperator(segment.vertexFilter.operator);
|
|
1148
1741
|
if (!opFrag) {
|
|
@@ -1158,68 +1751,138 @@ function getRelationTraversalFragment({ segments, sourceFrag, sourceIsList, alwa
|
|
|
1158
1751
|
}
|
|
1159
1752
|
}
|
|
1160
1753
|
const traversalFrag = (0, aql_1.aql) `FOR ${nodeVar}, ${edgeVar}, ${pathVar} IN ${segment.minDepth}..${segment.maxDepth} ${dir} ${currentObjectFrag} ${getCollectionForRelation(segment.relationSide.relation, AccessType.EXPLICIT_READ, context)}${pruneFrag}${filterFrag}`;
|
|
1161
|
-
if
|
|
1754
|
+
// if we just put one FOR after the other, we never get NULL values:
|
|
1755
|
+
// consider Consignment has-many Delivery has-one Order
|
|
1756
|
+
// LET consignment = ...
|
|
1757
|
+
// FOR delivery IN OUTBOUND consignment consignment_deliveries
|
|
1758
|
+
// FOR order IN OUTBOUND delivery delivery_order
|
|
1759
|
+
// RETURN order
|
|
1760
|
+
// The second FOR would simply not find any orders, so the whole subquery would be [].
|
|
1761
|
+
// This is ok most of the time because regular lists in cruddl never have NULL values
|
|
1762
|
+
// but for some aggregations, we need to count NULL values (-> preserveNullValues is true)
|
|
1763
|
+
// to not break these cases, we use a LET statement. This is inefficient though as it
|
|
1764
|
+
// retrieves the whole document and prevents reduce-extraction-to-projection optimizations
|
|
1765
|
+
// TODO aql-perf we should refactor those cases to embed the aggregation in the traversal
|
|
1766
|
+
if (needsNullableVar) {
|
|
1767
|
+
// to ignore dangling edges, add a FILTER though
|
|
1768
|
+
// (if there was one dangling edge and one real edge collected, we should use the real one)
|
|
1769
|
+
// using $var._key != null instead of $var != null because the latter prevents ArangoDB
|
|
1770
|
+
// from applying the reduce-extraction-to-projection optimization
|
|
1771
|
+
const nullableVar = isLastSegment ? innermostItemVar : aql_1.aql.variable(`nullableNode`);
|
|
1772
|
+
forFragments.push((0, aql_1.aql) `LET ${nullableVar} = FIRST(${traversalFrag} FILTER ${nodeVar}._key != NULL RETURN ${nodeVar})`);
|
|
1773
|
+
currentObjectFrag = nullableVar;
|
|
1774
|
+
}
|
|
1775
|
+
else {
|
|
1162
1776
|
// this is simple - we can just push one FOR statement after the other
|
|
1163
1777
|
forFragments.push(traversalFrag);
|
|
1164
1778
|
currentObjectFrag = nodeVar;
|
|
1165
1779
|
}
|
|
1166
|
-
else {
|
|
1167
|
-
// if this is not a list, we need to preserve NULL values
|
|
1168
|
-
// (actually, we don't in some cases, but we need to figure out when)
|
|
1169
|
-
// to preserve null values, we need to use FIRST
|
|
1170
|
-
// to ignore dangling edges, add a FILTER though (if there was one dangling edge and one real edge collected, we should use the real one)
|
|
1171
|
-
const nullableVar = aql_1.aql.variable(`nullableNode`);
|
|
1172
|
-
forFragments.push((0, aql_1.aql) `LET ${nullableVar} = FIRST(${traversalFrag} FILTER ${nodeVar} != null RETURN ${nodeVar})`);
|
|
1173
|
-
currentObjectFrag = nullableVar;
|
|
1174
|
-
}
|
|
1175
1780
|
context.addCollectionAccess((0, arango_basics_1.getCollectionNameForRootEntity)(segment.relationSide.targetType), AccessType.IMPLICIT_READ);
|
|
1176
1781
|
segmentIndex++;
|
|
1177
1782
|
}
|
|
1178
|
-
|
|
1179
|
-
//
|
|
1180
|
-
if
|
|
1181
|
-
|
|
1783
|
+
// each FOR automatically removes NULLs of the previous step (just how FOR works)
|
|
1784
|
+
// This does not apply to the last FOR though
|
|
1785
|
+
// - if it's not a list, this is correct: we would want the NULL values there
|
|
1786
|
+
// (e.g. deliveries.order should have as many NULLs as there are deliveries without an order)
|
|
1787
|
+
// dangling edges are already filtered out above in the LET (needsNullableVar)
|
|
1788
|
+
// - if it's a list, we shouldn't have any NULLs because we don't have "relation to NULL"s
|
|
1789
|
+
// in case of dangling edges however (i.e. edge without corresponding document)
|
|
1790
|
+
// we could generate NULLS. We generally ignore those in cruddl. -> filter them out
|
|
1791
|
+
// preserveNullValues is handled above (the dangling edge filter is in the LET there)
|
|
1792
|
+
if (segments[segments.length - 1].isListSegment) {
|
|
1793
|
+
// using $var._key != null instead of $var != null because the latter prevents ArangoDB
|
|
1794
|
+
// from applying the reduce-extraction-to-projection optimization
|
|
1795
|
+
forFragments.push((0, aql_1.aql) `FILTER ${currentObjectFrag}._key != null`);
|
|
1182
1796
|
}
|
|
1183
|
-
const returnFrag = mapFrag ? mapFrag(currentObjectFrag) : currentObjectFrag;
|
|
1184
|
-
const returnList = lastSegment.resultIsList || sourceIsList || alwaysProduceList;
|
|
1185
1797
|
// make sure we don't return a list with one element
|
|
1186
|
-
return
|
|
1187
|
-
}
|
|
1188
|
-
function getFlattenFrag(listFrag, depth) {
|
|
1189
|
-
if (depth <= 0) {
|
|
1190
|
-
return listFrag;
|
|
1191
|
-
}
|
|
1192
|
-
if (depth === 1) {
|
|
1193
|
-
return (0, aql_1.aql) `${listFrag}[**]`;
|
|
1194
|
-
}
|
|
1195
|
-
return (0, aql_1.aql) `FLATTEN(${listFrag}, ${aql_1.aql.integer(depth)})`;
|
|
1798
|
+
return aql_1.aql.lines(...forFragments);
|
|
1196
1799
|
}
|
|
1197
|
-
function
|
|
1800
|
+
function getFieldTraversalFragment({ segments, sourceFrag, mapFrag, filterFrag, skip = 0, maxCount, }) {
|
|
1198
1801
|
if (!segments.length) {
|
|
1199
1802
|
return sourceFrag;
|
|
1200
1803
|
}
|
|
1804
|
+
if ((mapFrag || filterFrag || maxCount !== undefined || skip > 0) &&
|
|
1805
|
+
!segments.some((s) => s.isListSegment)) {
|
|
1806
|
+
throw new Error(`Cannot have map, filter or limit on field traversal without list segments`);
|
|
1807
|
+
}
|
|
1808
|
+
const limitFrag = generateLimitClause({ skip, maxCount });
|
|
1809
|
+
// We use array inline expressions instead of FOR ... IN ... RETURN ... because this reduces the number of
|
|
1810
|
+
// execution nodes which would need to pass the data between them. Inline expressions are more efficient.
|
|
1811
|
+
//
|
|
1812
|
+
// We flatten on the go
|
|
1813
|
+
// -> source[*].field1[**].field2[**]
|
|
1814
|
+
// (instead of (source[*].field1[*].field2[*])[***})
|
|
1815
|
+
// because this allows us to put a LIMIT on the last item and have it apply to the list as a whole.
|
|
1816
|
+
// Every access "into" an array requires a [*] or [**] operator because otherwise you would just get NULL
|
|
1817
|
+
//
|
|
1818
|
+
// The first array access uses [*], further ones use [**] because using [*] again would result in nested arrays.
|
|
1819
|
+
// source[*].prop is equivalent to source.map(o => o.prop)
|
|
1820
|
+
// source[**] is equivalent to source.flat()
|
|
1821
|
+
// source[**].prop is equivalent to source.flat().map(o => o.prop)
|
|
1822
|
+
// (there is no direct equivalent of flatMap())
|
|
1823
|
+
//
|
|
1824
|
+
// If there are non-list segments in the middle, those can just be put without an operator:
|
|
1825
|
+
// source.items[*].extension.children[**].extension.children[**]
|
|
1826
|
+
// (equivalent to source.items.map(o => o.extension.children).flatMap(o => o.extension.children))
|
|
1827
|
+
//
|
|
1828
|
+
// [*] treats NULL as an empty list, even though this is undocumented:
|
|
1829
|
+
// https://docs.arangodb.com/stable/aql/operators/#array-expansion
|
|
1830
|
+
// > It is required that the expanded variable is an array.
|
|
1831
|
+
// however, the code that handles it looks very intentional (if not array, return empty array)
|
|
1832
|
+
// https://github.com/arangodb/arangodb/blob/b1f655a12d80b6f44d3384919cb707019bd00c3a/arangod/Aql/Expression.cpp#L1685
|
|
1833
|
+
// -> it is unlikely to be changed.
|
|
1834
|
+
//
|
|
1835
|
+
// The same does not apply to flattening however: while (NULL)[**] == [] just as (NULL)[*] == [],
|
|
1836
|
+
// ([NULL])[**] == [ NULL ] (so it just does not flatten, but keep the null)
|
|
1837
|
+
// we need to treat the NULL item as an empty list (e.g. an uninitialized child entity field)
|
|
1838
|
+
// -> we need to use [*][**] instead of just [**]
|
|
1839
|
+
//
|
|
1840
|
+
// We also append [**] at the end if there were two list segments because we still need to flatten then
|
|
1841
|
+
// If there was only one list segment, we append [*] at the end because it converts NULL to an empty list
|
|
1201
1842
|
let frag = sourceFrag;
|
|
1843
|
+
let index = 0;
|
|
1844
|
+
let hasSeenListSegment = false;
|
|
1845
|
+
const lastListSegmentIndex = segments.findLastIndex((s) => s.isListSegment);
|
|
1202
1846
|
for (const segment of segments) {
|
|
1203
1847
|
frag = (0, aql_1.aql) `${frag}${getPropertyAccessFragment(segment.field.name)}`;
|
|
1204
1848
|
if (segment.isListSegment) {
|
|
1205
|
-
//
|
|
1206
|
-
//
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1849
|
+
// use ** to flatten / flatMap if this is a nested list
|
|
1850
|
+
// always use * after to convert NULLs to empty lists
|
|
1851
|
+
const operatorPrefixFrag = hasSeenListSegment ? (0, aql_1.aql) `[*][**` : (0, aql_1.aql) `[*`;
|
|
1852
|
+
const operatorSuffixFrag = (0, aql_1.aql) `]`;
|
|
1853
|
+
if (index === lastListSegmentIndex) {
|
|
1854
|
+
// If there is a non-list segment at the end, we stop the regular expression at the last list segment
|
|
1855
|
+
// and instead put the non-list segments into the RETURN / FILTER
|
|
1856
|
+
// (e.g. source.items[*].children[** FILTER CURRENT.extension.field > 0 RETURN CURRENT.extension)
|
|
1857
|
+
const remainingSegments = segments.slice(index + 1);
|
|
1858
|
+
const itemAccessFrag = remainingSegments.reduce((currentFrag, seg) => (0, aql_1.aql) `${currentFrag ?? (0, aql_1.aql) ``}${getPropertyAccessFragment(seg.field.name)}`, undefined);
|
|
1859
|
+
const itemFrag = (0, aql_1.aql) `CURRENT${itemAccessFrag ?? (0, aql_1.aql) ``}`;
|
|
1860
|
+
// if there are remaining segments to access, and we don't have a map, just append the item
|
|
1861
|
+
// access at the end: source.items[*].field
|
|
1862
|
+
const suffixFrag = itemAccessFrag && !mapFrag ? itemAccessFrag : (0, aql_1.aql) ``;
|
|
1863
|
+
const returnExprFrag = mapFrag ? (0, aql_1.aql) ` RETURN ${mapFrag(itemFrag)}` : (0, aql_1.aql) ``;
|
|
1864
|
+
const filterExprFrag = filterFrag ? (0, aql_1.aql) ` FILTER ${filterFrag(itemFrag)}` : (0, aql_1.aql) ``;
|
|
1865
|
+
const limitExprFrag = limitFrag ? (0, aql_1.aql) ` ${limitFrag}` : (0, aql_1.aql) ``;
|
|
1866
|
+
return (0, aql_1.aql) `${frag}${operatorPrefixFrag}${filterExprFrag}${limitExprFrag}${returnExprFrag}${operatorSuffixFrag}${suffixFrag}`;
|
|
1867
|
+
}
|
|
1868
|
+
else {
|
|
1869
|
+
frag = (0, aql_1.aql) `${frag}${operatorPrefixFrag}${operatorSuffixFrag}`;
|
|
1870
|
+
}
|
|
1871
|
+
hasSeenListSegment = true;
|
|
1210
1872
|
}
|
|
1873
|
+
index++;
|
|
1211
1874
|
}
|
|
1212
1875
|
return frag;
|
|
1213
1876
|
}
|
|
1214
1877
|
register(query_tree_1.CreateEntityQueryNode, (node, context) => {
|
|
1215
|
-
return aqlExt.
|
|
1878
|
+
return aqlExt.firstOfSubquery((0, aql_1.aql) `INSERT ${processNode(node.objectNode, context)} IN ${getCollectionForType(node.rootEntityType, AccessType.WRITE, context)}`, (0, aql_1.aql) `RETURN NEW._key`);
|
|
1216
1879
|
});
|
|
1217
1880
|
register(query_tree_1.CreateEntitiesQueryNode, (node, context) => {
|
|
1218
1881
|
const entityVar = aql_1.aql.variable('entity');
|
|
1219
|
-
return aqlExt.
|
|
1882
|
+
return aqlExt.subquery((0, aql_1.aql) `FOR ${entityVar} IN ${processNode(node.objectsNode, context)}`, (0, aql_1.aql) `INSERT ${entityVar} IN ${getCollectionForType(node.rootEntityType, AccessType.WRITE, context)}`, (0, aql_1.aql) `RETURN NEW._key`);
|
|
1220
1883
|
});
|
|
1221
1884
|
register(query_tree_1.UpdateEntitiesQueryNode, (node, context) => {
|
|
1222
|
-
const newContext = context.
|
|
1885
|
+
const newContext = context.bindVariable(node.currentEntityVariable);
|
|
1223
1886
|
const entityVar = newContext.getVariable(node.currentEntityVariable);
|
|
1224
1887
|
let entityFrag;
|
|
1225
1888
|
let options;
|
|
@@ -1235,7 +1898,7 @@ register(query_tree_1.UpdateEntitiesQueryNode, (node, context) => {
|
|
|
1235
1898
|
entityFrag = entityVar;
|
|
1236
1899
|
options = (0, aql_1.aql) `{ mergeObjects: false }`;
|
|
1237
1900
|
}
|
|
1238
|
-
return aqlExt.
|
|
1901
|
+
return aqlExt.subquery((0, aql_1.aql) `FOR ${entityVar}`, (0, aql_1.aql) `IN ${processNode(node.listNode, context)}`, (0, aql_1.aql) `UPDATE ${entityFrag}`, (0, aql_1.aql) `WITH ${updateFrag}`, (0, aql_1.aql) `IN ${getCollectionForType(node.rootEntityType, AccessType.WRITE, context)}`, (0, aql_1.aql) `OPTIONS ${options}`, (0, aql_1.aql) `RETURN NEW._key`);
|
|
1239
1902
|
});
|
|
1240
1903
|
register(query_tree_1.DeleteEntitiesQueryNode, (node, context) => {
|
|
1241
1904
|
const entityVar = aql_1.aql.variable((0, utils_2.decapitalize)(node.rootEntityType.name));
|
|
@@ -1254,16 +1917,24 @@ register(query_tree_1.DeleteEntitiesQueryNode, (node, context) => {
|
|
|
1254
1917
|
entityFrag = entityVar;
|
|
1255
1918
|
optionsFrag = (0, aql_1.aql) ``;
|
|
1256
1919
|
}
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1920
|
+
const commonFrags = [
|
|
1921
|
+
(0, aql_1.aql) `FOR ${entityVar}`,
|
|
1922
|
+
(0, aql_1.aql) `${generateInClause(node.listNode, context, entityVar)}`,
|
|
1923
|
+
(0, aql_1.aql) `REMOVE ${entityFrag}`,
|
|
1924
|
+
(0, aql_1.aql) `IN ${getCollectionForType(node.rootEntityType, AccessType.WRITE, context)}`,
|
|
1925
|
+
optionsFrag,
|
|
1926
|
+
];
|
|
1927
|
+
if (node.resultValue === query_tree_1.DeleteEntitiesResultValue.OLD_ENTITIES) {
|
|
1928
|
+
return aqlExt.subquery(...commonFrags, (0, aql_1.aql) `RETURN OLD`);
|
|
1929
|
+
}
|
|
1930
|
+
else {
|
|
1931
|
+
const countVar = aql_1.aql.variable(`count`);
|
|
1932
|
+
return aqlExt.firstOfSubquery(...commonFrags, (0, aql_1.aql) `COLLECT WITH COUNT INTO ${countVar}`, (0, aql_1.aql) `RETURN ${countVar}`);
|
|
1933
|
+
}
|
|
1263
1934
|
});
|
|
1264
1935
|
register(query_tree_1.AddEdgesQueryNode, (node, context) => {
|
|
1265
1936
|
const edgeVar = aql_1.aql.variable('edge');
|
|
1266
|
-
return aqlExt.
|
|
1937
|
+
return aqlExt.subquery((0, aql_1.aql) `FOR ${edgeVar}`, (0, aql_1.aql) `IN [ ${aql_1.aql.join(node.edges.map((edge) => formatEdge(node.relation, edge, context)), (0, aql_1.aql) `, `)} ]`, (0, aql_1.aql) `UPSERT { _from: ${edgeVar}._from, _to: ${edgeVar}._to }`, // need to unpack avoid dynamic property names in UPSERT example filter
|
|
1267
1938
|
(0, aql_1.aql) `INSERT ${edgeVar}`, (0, aql_1.aql) `UPDATE {}`, (0, aql_1.aql) `IN ${getCollectionForRelation(node.relation, AccessType.WRITE, context)}`);
|
|
1268
1939
|
});
|
|
1269
1940
|
register(query_tree_1.RemoveEdgesQueryNode, (node, context) => {
|
|
@@ -1283,7 +1954,7 @@ register(query_tree_1.RemoveEdgesQueryNode, (node, context) => {
|
|
|
1283
1954
|
else {
|
|
1284
1955
|
edgeFilter = (0, aql_1.aql) ``;
|
|
1285
1956
|
}
|
|
1286
|
-
return aqlExt.
|
|
1957
|
+
return aqlExt.subquery(node.edgeFilter.fromIDsNode
|
|
1287
1958
|
? (0, aql_1.aql) `FOR ${fromVar} IN ${getFullIDsFromKeysNode(node.edgeFilter.fromIDsNode, node.relation.fromType, context)}`
|
|
1288
1959
|
: (0, aql_1.aql) ``, node.edgeFilter.toIDsNode
|
|
1289
1960
|
? (0, aql_1.aql) `FOR ${toVar} IN ${getFullIDsFromKeysNode(node.edgeFilter.toIDsNode, node.relation.toType, context)}`
|
|
@@ -1291,7 +1962,7 @@ register(query_tree_1.RemoveEdgesQueryNode, (node, context) => {
|
|
|
1291
1962
|
});
|
|
1292
1963
|
register(query_tree_1.SetEdgeQueryNode, (node, context) => {
|
|
1293
1964
|
const edgeVar = aql_1.aql.variable('edge');
|
|
1294
|
-
return aqlExt.
|
|
1965
|
+
return aqlExt.subquery((0, aql_1.aql) `UPSERT ${formatEdge(node.relation, node.existingEdge, context)}`, (0, aql_1.aql) `INSERT ${formatEdge(node.relation, node.newEdge, context)}`, (0, aql_1.aql) `UPDATE ${formatEdge(node.relation, node.newEdge, context)}`, (0, aql_1.aql) `IN ${getCollectionForRelation(node.relation, AccessType.WRITE, context)}`);
|
|
1295
1966
|
});
|
|
1296
1967
|
/**
|
|
1297
1968
|
* Gets an aql fragment that evaluates to a string of the format "collectionName/objectKey", given a query node that
|
|
@@ -1376,16 +2047,16 @@ function getAQLOperator(op) {
|
|
|
1376
2047
|
return undefined;
|
|
1377
2048
|
}
|
|
1378
2049
|
}
|
|
2050
|
+
function dirAQL(dir) {
|
|
2051
|
+
if (dir == query_tree_1.OrderDirection.DESCENDING) {
|
|
2052
|
+
return (0, aql_1.aql) ` DESC`;
|
|
2053
|
+
}
|
|
2054
|
+
return (0, aql_1.aql) ``;
|
|
2055
|
+
}
|
|
1379
2056
|
function generateSortAQL(orderBy, context) {
|
|
1380
2057
|
if (orderBy.isUnordered()) {
|
|
1381
2058
|
return (0, aql_1.aql) ``;
|
|
1382
2059
|
}
|
|
1383
|
-
function dirAQL(dir) {
|
|
1384
|
-
if (dir == query_tree_1.OrderDirection.DESCENDING) {
|
|
1385
|
-
return (0, aql_1.aql) ` DESC`;
|
|
1386
|
-
}
|
|
1387
|
-
return (0, aql_1.aql) ``;
|
|
1388
|
-
}
|
|
1389
2060
|
const clauses = orderBy.clauses.map((cl) => (0, aql_1.aql) `(${processNode(cl.valueNode, context)}) ${dirAQL(cl.direction)}`);
|
|
1390
2061
|
return (0, aql_1.aql) `SORT ${aql_1.aql.join(clauses, (0, aql_1.aql) `, `)}`;
|
|
1391
2062
|
}
|