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.
Files changed (58) hide show
  1. package/.github/workflows/ci.yml +5 -3
  2. package/.husky/pre-commit +1 -0
  3. package/README.md +0 -1
  4. package/dist/src/authorization/move-errors-to-output-nodes.js +1 -0
  5. package/dist/src/authorization/move-errors-to-output-nodes.js.map +1 -1
  6. package/dist/src/authorization/transformers/traversal.js +13 -2
  7. package/dist/src/authorization/transformers/traversal.js.map +1 -1
  8. package/dist/src/cruddl-version.js +1 -1
  9. package/dist/src/database/arangodb/aql-generator.js +856 -185
  10. package/dist/src/database/arangodb/aql-generator.js.map +1 -1
  11. package/dist/src/database/arangodb/arangodb-adapter.js +30 -10
  12. package/dist/src/database/arangodb/arangodb-adapter.js.map +1 -1
  13. package/dist/src/database/arangodb/config.js +3 -8
  14. package/dist/src/database/arangodb/config.js.map +1 -1
  15. package/dist/src/database/arangodb/traversal-helpers.d.ts +10 -0
  16. package/dist/src/database/arangodb/traversal-helpers.js +52 -0
  17. package/dist/src/database/arangodb/traversal-helpers.js.map +1 -0
  18. package/dist/src/database/inmemory/inmemory-adapter.js +3 -7
  19. package/dist/src/database/inmemory/inmemory-adapter.js.map +1 -1
  20. package/dist/src/database/inmemory/js-generator.js +75 -55
  21. package/dist/src/database/inmemory/js-generator.js.map +1 -1
  22. package/dist/src/execution/runtime-errors.d.ts +1 -0
  23. package/dist/src/execution/runtime-errors.js +3 -3
  24. package/dist/src/execution/runtime-errors.js.map +1 -1
  25. package/dist/src/model/implementation/indices.js +17 -1
  26. package/dist/src/model/implementation/indices.js.map +1 -1
  27. package/dist/src/query-tree/queries.d.ts +102 -7
  28. package/dist/src/query-tree/queries.js +77 -10
  29. package/dist/src/query-tree/queries.js.map +1 -1
  30. package/dist/src/schema/schema-builder.js +5 -1
  31. package/dist/src/schema/schema-builder.js.map +1 -1
  32. package/dist/src/schema-generation/field-nodes.d.ts +10 -5
  33. package/dist/src/schema-generation/field-nodes.js +32 -4
  34. package/dist/src/schema-generation/field-nodes.js.map +1 -1
  35. package/dist/src/schema-generation/filter-augmentation.js +0 -1
  36. package/dist/src/schema-generation/filter-augmentation.js.map +1 -1
  37. package/dist/src/schema-generation/flex-search-post-filter-augmentation.js +0 -1
  38. package/dist/src/schema-generation/flex-search-post-filter-augmentation.js.map +1 -1
  39. package/dist/src/schema-generation/order-by-and-pagination-augmentation.js +41 -22
  40. package/dist/src/schema-generation/order-by-and-pagination-augmentation.js.map +1 -1
  41. package/dist/src/schema-generation/output-type-generator.js +5 -3
  42. package/dist/src/schema-generation/output-type-generator.js.map +1 -1
  43. package/dist/src/schema-generation/query-node-object-type/context.d.ts +1 -1
  44. package/dist/src/schema-generation/query-node-object-type/context.js +1 -1
  45. package/dist/src/schema-generation/query-node-object-type/query-node-generator.js +37 -3
  46. package/dist/src/schema-generation/query-node-object-type/query-node-generator.js.map +1 -1
  47. package/dist/src/schema-generation/root-field-helper.d.ts +3 -23
  48. package/dist/src/schema-generation/root-field-helper.js +9 -109
  49. package/dist/src/schema-generation/root-field-helper.js.map +1 -1
  50. package/dist/src/schema-generation/utils/filtering.d.ts +1 -2
  51. package/dist/src/schema-generation/utils/filtering.js +28 -6
  52. package/dist/src/schema-generation/utils/filtering.js.map +1 -1
  53. package/dist/src/schema-generation/utils/relations.js +0 -1
  54. package/dist/src/schema-generation/utils/relations.js.map +1 -1
  55. package/dist/src/utils/util-types.d.ts +6 -0
  56. package/knip.json +6 -1
  57. package/package.json +6 -11
  58. 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
- newNestedContextWithVariableMapping(variableNode, aqlVariable) {
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
- * The AQLFragment for the variable will be available via getVariable().
84
+ * If aqlFrag is omitted, a new AQLVariable will be created using the name of the variableNode.
84
85
  *
85
- * @param {VariableQueryNode} variableNode the variable as referenced in the query tree
86
- * @returns {QueryContext} the nested context
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
- introduceVariable(variableNode) {
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
- const variable = new aql_1.AQLVariable(variableNode.label);
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.newNestedContextWithVariableMapping(resultVariable, resultVar);
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.introduceVariable(assignmentNode.variableNode);
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 parenthesizeList(...content) {
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.parenthesizeList = parenthesizeList;
249
- function parenthesizeObject(...content) {
250
- return (0, aql_1.aql) `FIRST${parenthesizeList(...content)}`;
250
+ aqlExt.subquery = subquery;
251
+ function firstOfSubquery(...content) {
252
+ return (0, aql_1.aql) `FIRST${subquery(...content)}`;
251
253
  }
252
- aqlExt.parenthesizeObject = parenthesizeObject;
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.introduceVariable(node.variableNode);
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.parenthesizeObject((0, aql_1.aql) `LET ${tmpVar} = ${processNode(node.variableValueNode, newContext)}`, (0, aql_1.aql) `RETURN ${processNode(node.resultNode, newContext)}`);
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
- .introduceVariable(node.itemVariable)
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.parenthesizeList((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)}`);
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.introduceVariable(node.itemVariable);
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.introduceVariable(assignmentNode.variableNode);
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
- return aqlExt.parenthesizeList((0, aql_1.aql) `FOR ${itemVar}`, generateInClauseWithFilterAndOrderAndLimit({ node, context, itemContext, itemVar }), ...variableAssignments, (0, aql_1.aql) `RETURN ${processNode(innerNode, itemProjectionContext)}`);
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
- filterDanglingEdges = (0, aql_1.aql) `FILTER ${itemVar} != null`;
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
- let limitClause;
400
- if (node.maxCount != undefined) {
401
- if (node.skip === 0) {
402
- limitClause = (0, aql_1.aql) `LIMIT ${node.maxCount}`;
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
- limitClause = (0, aql_1.aql) `LIMIT ${node.skip}, ${node.maxCount}`;
420
+ return (0, aql_1.aql) `LIMIT ${skip}, ${maxCount}`;
406
421
  }
407
422
  }
408
- else if (node.skip > 0) {
409
- limitClause = (0, aql_1.aql) `LIMIT ${node.skip}, ${Number.MAX_SAFE_INTEGER}`;
423
+ else if (skip > 0) {
424
+ return (0, aql_1.aql) `LIMIT ${skip}, ${Number.MAX_SAFE_INTEGER}`;
410
425
  }
411
426
  else {
412
- limitClause = (0, aql_1.aql) ``;
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.introduceVariableAlias(node.itemVariable, entityVar);
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.parenthesizeObject((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}`);
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
- return aqlExt[isList ? 'parenthesizeList' : 'parenthesizeObject']((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
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.introduceVariable(node.dictionaryVar);
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.parenthesizeList(
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.parenthesizeList((0, aql_1.aql) `FOR ${indexVar}`,
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.parenthesizeList((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}] ]`);
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.parenthesizeList((0, aql_1.aql) `UPSERT {
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.parenthesizeList((0, aql_1.aql) `UPSERT {
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
- // need to wrap this in a subquery because ANY is not possible as first token of an expression node in AQL
1021
- return aqlExt.parenthesizeList((0, aql_1.aql) `FOR ${tmpVar}`, (0, aql_1.aql) `IN ${getSimpleFollowEdgeFragment(node, context)}`, (0, aql_1.aql) `FILTER ${tmpVar} != null`, (0, aql_1.aql) `RETURN ${tmpVar}`);
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
- const sourceFrag = processNode(node.sourceEntityNode, context);
1025
- const fieldDepth = node.fieldSegments.filter((s) => s.isListSegment).length;
1026
- if (node.relationSegments.length) {
1027
- let mapFrag;
1028
- let remainingDepth = fieldDepth;
1029
- if (node.fieldSegments.length) {
1030
- // if we have both, it might be beneficial to do the field traversal within the mapping node
1031
- // because it may allow ArangoDB to figure out that only one particular field is of interest, and e.g.
1032
- // discard the root entities earlier
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
- mapFrag = (nodeFrag) => getFieldTraversalFragmentWithoutFlattening(node.fieldSegments, nodeFrag);
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
- if (node.captureRootEntity) {
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
- // traversal requires real ids
1061
- let fixedSourceFrag = sourceFrag;
1062
- if (node.entitiesIdentifierKind === query_tree_1.EntitiesIdentifierKind.ID) {
1063
- if (node.sourceIsList) {
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
- const frag = getRelationTraversalFragment({
1071
- segments: node.relationSegments,
1072
- sourceFrag: fixedSourceFrag,
1073
- sourceIsList: node.sourceIsList,
1074
- alwaysProduceList: node.alwaysProduceList,
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.captureRootEntity) {
1087
- // doesn't make sense (and isn't possible) to capture the root entity if we're not even crossing root entities
1088
- throw new Error(`captureRootEntity without relationSegments detected`);
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
- if (node.sourceIsList) {
1091
- // don't need, don't bother
1092
- throw new Error(`sourceIsList without relationSegments detected`);
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
- if (node.alwaysProduceList) {
1095
- // don't need, don't bother
1096
- throw new Error(`alwaysProduceList without relationSegments detected`);
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
- // should normally not occur
1103
- return sourceFrag;
1612
+ throw new Error(`Expected at least one field segment`);
1104
1613
  }
1105
- // flatten 1 less than the fieldDepth:
1106
- // - no list segments -> evaluate to the object
1107
- // - one list segment -> evaluate to the list, so no flattening
1108
- // - two list segments -> needs flattening once to get one list
1109
- return getFlattenFrag(getFieldTraversalFragmentWithoutFlattening(node.fieldSegments, sourceFrag), fieldDepth - 1);
1110
- });
1111
- function getRelationTraversalFragment({ segments, sourceFrag, sourceIsList, alwaysProduceList, mapFrag, context, }) {
1112
- if (!segments.length) {
1113
- return sourceFrag;
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 nodeVar = aql_1.aql.variable(`node`);
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.introduceVariableAlias(segment.vertexFilterVariable, nodeVar);
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.introduceVariableAlias(segment.vertexFilterVariable, vertexInPathFrag);
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 (segment.isListSegment || (alwaysProduceList && segmentIndex === segments.length - 1)) {
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
- const lastSegment = segments[segments.length - 1];
1179
- // remove dangling edges, unless we already did because the last segment wasn't a list segment (see above, we add the FILTER there)
1180
- if (lastSegment.isListSegment) {
1181
- forFragments.push((0, aql_1.aql) `FILTER ${currentObjectFrag} != null`);
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 aqlExt[returnList ? 'parenthesizeList' : 'parenthesizeObject'](sourceIsList ? (0, aql_1.aql) `FOR ${sourceEntityVar} IN ${sourceFrag}` : (0, aql_1.aql) ``, ...forFragments, (0, aql_1.aql) `RETURN ${returnFrag}`);
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 getFieldTraversalFragmentWithoutFlattening(segments, sourceFrag) {
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
- // the array expansion operator [*] does two useful things:
1206
- // - it performs the next field access basically as .map(o => o.fieldName).
1207
- // - it converts non-lists to lists (important so that if we flatten afterwards, we don't include NULL lists
1208
- // the latter is why we also add the [*] at the end of the expression, which might look strange in the AQL.
1209
- frag = (0, aql_1.aql) `${frag}[*]`;
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.parenthesizeObject((0, aql_1.aql) `INSERT ${processNode(node.objectNode, context)} IN ${getCollectionForType(node.rootEntityType, AccessType.WRITE, context)}`, (0, aql_1.aql) `RETURN NEW._key`);
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.parenthesizeList((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`);
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.introduceVariable(node.currentEntityVariable);
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.parenthesizeList((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`);
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 countVar = aql_1.aql.variable(`count`);
1258
- return aqlExt[node.resultValue === query_tree_1.DeleteEntitiesResultValue.OLD_ENTITIES
1259
- ? 'parenthesizeList'
1260
- : 'parenthesizeObject']((0, aql_1.aql) `FOR ${entityVar}`, (0, aql_1.aql) `${generateInClause(node.listNode, context, entityVar)}`, (0, aql_1.aql) `REMOVE ${entityFrag}`, (0, aql_1.aql) `IN ${getCollectionForType(node.rootEntityType, AccessType.WRITE, context)}`, optionsFrag, node.resultValue === query_tree_1.DeleteEntitiesResultValue.OLD_ENTITIES
1261
- ? (0, aql_1.aql) `RETURN OLD`
1262
- : aql_1.aql.lines((0, aql_1.aql) `COLLECT WITH COUNT INTO ${countVar}`, (0, aql_1.aql) `RETURN ${countVar}`));
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.parenthesizeList((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
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.parenthesizeList(node.edgeFilter.fromIDsNode
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.parenthesizeList((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)}`);
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
  }