cruddl 5.0.0-alpha.1 → 5.0.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 (38) hide show
  1. package/.github/workflows/ci.yml +7 -4
  2. package/dist/src/cruddl-version.js +1 -1
  3. package/dist/src/database/arangodb/aql-generator.d.ts +1 -1
  4. package/dist/src/database/arangodb/aql-generator.js +228 -65
  5. package/dist/src/database/arangodb/aql-generator.js.map +1 -1
  6. package/dist/src/database/arangodb/schema-migration/arango-search-helpers.js +17 -3
  7. package/dist/src/database/arangodb/schema-migration/arango-search-helpers.js.map +1 -1
  8. package/dist/src/database/arangodb/traversal-helpers.d.ts +11 -8
  9. package/dist/src/database/arangodb/traversal-helpers.js +67 -24
  10. package/dist/src/database/arangodb/traversal-helpers.js.map +1 -1
  11. package/dist/src/database/inmemory/js-generator.js +6 -1
  12. package/dist/src/database/inmemory/js-generator.js.map +1 -1
  13. package/dist/src/query-tree/queries.js +2 -2
  14. package/dist/src/query-tree/queries.js.map +1 -1
  15. package/dist/src/query-tree/utils/extract-variable-assignments.d.ts +6 -10
  16. package/dist/src/query-tree/utils/extract-variable-assignments.js +33 -28
  17. package/dist/src/query-tree/utils/extract-variable-assignments.js.map +1 -1
  18. package/dist/src/query-tree/utils/index.d.ts +1 -0
  19. package/dist/src/query-tree/utils/index.js +1 -0
  20. package/dist/src/query-tree/utils/index.js.map +1 -1
  21. package/dist/src/query-tree/utils/referenced-variables.d.ts +3 -0
  22. package/dist/src/query-tree/utils/referenced-variables.js +18 -0
  23. package/dist/src/query-tree/utils/referenced-variables.js.map +1 -0
  24. package/dist/src/query-tree/variables.d.ts +19 -0
  25. package/dist/src/query-tree/variables.js +24 -1
  26. package/dist/src/query-tree/variables.js.map +1 -1
  27. package/dist/src/schema-generation/field-nodes.d.ts +12 -0
  28. package/dist/src/schema-generation/field-nodes.js +21 -3
  29. package/dist/src/schema-generation/field-nodes.js.map +1 -1
  30. package/dist/src/schema-generation/output-type-generator.js +4 -0
  31. package/dist/src/schema-generation/output-type-generator.js.map +1 -1
  32. package/dist/src/schema-generation/query-node-object-type/definition.d.ts +5 -0
  33. package/dist/src/schema-generation/query-node-object-type/definition.js.map +1 -1
  34. package/dist/src/schema-generation/query-node-object-type/query-node-generator.js +11 -6
  35. package/dist/src/schema-generation/query-node-object-type/query-node-generator.js.map +1 -1
  36. package/dist/src/utils/visitor.d.ts +10 -0
  37. package/dist/src/utils/visitor.js.map +1 -1
  38. package/package.json +1 -1
@@ -5,7 +5,7 @@ jobs:
5
5
  runs-on: ubuntu-latest
6
6
  strategy:
7
7
  matrix:
8
- arango-image: ['arangodb:3.12']
8
+ arango-image: ['arangodb:3.12', 'arangodb:3.12.6']
9
9
  node-version: [20.x, 22.x, 24.x, 25.x]
10
10
  steps:
11
11
  - uses: actions/checkout@v2
@@ -26,11 +26,14 @@ jobs:
26
26
  if: startsWith(github.ref, 'refs/tags/v')
27
27
  runs-on: ubuntu-latest
28
28
  needs: test
29
+ permissions:
30
+ id-token: write
31
+ contents: read
29
32
  steps:
30
- - uses: actions/checkout@v2
31
- - uses: actions/setup-node@v1
33
+ - uses: actions/checkout@v4
34
+ - uses: actions/setup-node@v4
32
35
  with:
33
- node-version: 22.x
36
+ node-version: '24'
34
37
  registry-url: https://registry.npmjs.org/
35
38
  - run: npm ci
36
39
  - run: npm publish --tag next
@@ -3,5 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CRUDDL_VERSION = void 0;
4
4
  // do not modify - this is parsed and changed by the build script
5
5
  // explicitly annotating with "string" so typescript won't infer a string literal type
6
- exports.CRUDDL_VERSION = '5.0.0-alpha.1';
6
+ exports.CRUDDL_VERSION = '5.0.0';
7
7
  //# sourceMappingURL=cruddl-version.js.map
@@ -1,7 +1,7 @@
1
+ import { Clock, IDGenerator } from '../../execution/execution-options';
1
2
  import { QueryNode } from '../../query-tree';
2
3
  import { FlexSearchTokenizable } from '../database-adapter';
3
4
  import { AQLCompoundQuery, AQLFragment } from './aql';
4
- import { Clock, IDGenerator } from '../../execution/execution-options';
5
5
  export interface QueryGenerationOptions {
6
6
  /**
7
7
  * An interface to determine the current date/time
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getAQLQuery = getAQLQuery;
4
4
  exports.generateTokenizationQuery = generateTokenizationQuery;
5
+ const execution_options_1 = require("../../execution/execution-options");
5
6
  const model_1 = require("../../model");
6
7
  const flex_search_1 = require("../../model/implementation/flex-search");
7
8
  const query_tree_1 = require("../../query-tree");
@@ -14,7 +15,6 @@ const like_helpers_1 = require("../like-helpers");
14
15
  const aql_1 = require("./aql");
15
16
  const arango_basics_1 = require("./arango-basics");
16
17
  const arango_search_helpers_1 = require("./schema-migration/arango-search-helpers");
17
- const execution_options_1 = require("../../execution/execution-options");
18
18
  const traversal_helpers_1 = require("./traversal-helpers");
19
19
  var AccessType;
20
20
  (function (AccessType) {
@@ -299,6 +299,10 @@ register(query_tree_1.ConcatListsQueryNode, (node, context) => {
299
299
  register(query_tree_1.VariableQueryNode, (node, context) => {
300
300
  return context.getVariable(node);
301
301
  });
302
+ register(query_tree_1.HoistableQueryNode, (node, context) => {
303
+ // if we process a HoistableQueryNode here, the node did not get hoisted, but that's fine too
304
+ return processNode(node.node, context);
305
+ });
302
306
  register(query_tree_1.VariableAssignmentQueryNode, (node, context) => {
303
307
  const newContext = context.bindVariable(node.variableNode);
304
308
  const tmpVar = newContext.getVariable(node.variableNode);
@@ -367,27 +371,60 @@ register(flex_search_2.FlexSearchQueryNode, (node, context) => {
367
371
  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)}`);
368
372
  });
369
373
  register(query_tree_1.TransformListQueryNode, (node, context) => {
370
- let itemContext = context.bindVariable(node.itemVariable);
371
- const itemVar = itemContext.getVariable(node.itemVariable);
372
- let itemProjectionContext = itemContext;
373
374
  // move LET statements up
374
375
  // they often occur for value objects / entity extensions
375
376
  // this avoids the FIRST() and the subquery which reduces load on the AQL query optimizer
376
- let variableAssignments = [];
377
+ const hoistedAssignments = [];
378
+ const loopScopedAssignmentNodes = [];
379
+ const loopScopedVariables = new Set();
380
+ let currentContext = context;
377
381
  let innerNode = node.innerNode;
378
382
  const variableAssignmentNodes = [];
379
383
  innerNode = (0, utils_1.extractVariableAssignments)(innerNode, variableAssignmentNodes);
380
384
  for (const assignmentNode of variableAssignmentNodes) {
385
+ const referencedVariables = (0, utils_1.getReferencedVariables)(assignmentNode.variableValueNode);
386
+ const referencesItemVariable = referencedVariables.has(node.itemVariable);
387
+ let referencesLoopScopedVariable = false;
388
+ for (const variableNode of loopScopedVariables) {
389
+ if (referencedVariables.has(variableNode)) {
390
+ referencesLoopScopedVariable = true;
391
+ break;
392
+ }
393
+ }
394
+ if (!referencesItemVariable && !referencesLoopScopedVariable) {
395
+ currentContext = currentContext.bindVariable(assignmentNode.variableNode);
396
+ const tmpVar = currentContext.getVariable(assignmentNode.variableNode);
397
+ // ArangoDB will try to move the hoisted variable down to their usages again, even
398
+ // though that increases memory usage because it requires the subquery to hold the whole
399
+ // source (root) variable. NOEVAL() forces the optimizer to keep the variable where it's
400
+ // declared
401
+ hoistedAssignments.push((0, aql_1.aql) `LET ${tmpVar} = NOEVAL(${processNode(assignmentNode.variableValueNode, currentContext)})`);
402
+ }
403
+ else {
404
+ loopScopedAssignmentNodes.push(assignmentNode);
405
+ loopScopedVariables.add(assignmentNode.variableNode);
406
+ }
407
+ }
408
+ let itemContext = currentContext.bindVariable(node.itemVariable);
409
+ const itemVar = itemContext.getVariable(node.itemVariable);
410
+ let itemProjectionContext = itemContext;
411
+ const loopScopedAssignments = [];
412
+ for (const assignmentNode of loopScopedAssignmentNodes) {
381
413
  itemProjectionContext = itemProjectionContext.bindVariable(assignmentNode.variableNode);
382
414
  const tmpVar = itemProjectionContext.getVariable(assignmentNode.variableNode);
383
- variableAssignments.push((0, aql_1.aql) `LET ${tmpVar} = ${processNode(assignmentNode.variableValueNode, itemProjectionContext)}`);
415
+ loopScopedAssignments.push((0, aql_1.aql) `LET ${tmpVar} = ${processNode(assignmentNode.variableValueNode, itemProjectionContext)}`);
384
416
  }
385
417
  // TODO aql-perf: set maxProjects to a value > 5?
386
418
  // The reduce-extraction-to-projection optimization is crucial to reduce memory usage of queries
387
419
  // over large root entities if only some of the fields are queried. The default is to only apply
388
420
  // it if 5 or less fields are selected. We probably want to increase this limit
389
421
  // (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)}`);
422
+ return aqlExt.subquery(...hoistedAssignments, (0, aql_1.aql) `FOR ${itemVar}`, generateInClauseWithFilterAndOrderAndLimit({
423
+ node,
424
+ context: currentContext,
425
+ itemContext,
426
+ itemVar,
427
+ }), ...loopScopedAssignments, (0, aql_1.aql) `RETURN ${processNode(innerNode, itemProjectionContext)}`);
391
428
  });
392
429
  /**
393
430
  * Generates an IN... clause for a TransformListQueryNode to be used within a query / subquery (FOR ... IN ...)
@@ -1060,7 +1097,7 @@ register(query_tree_1.TraversalQueryNode, (node, context) => {
1060
1097
  return processTraversalWithNonListRelationSegmentsAndNonListFieldSegments(node, context);
1061
1098
  }
1062
1099
  }
1063
- if (node.innerNode && (0, traversal_helpers_1.mightGenerateSubquery)(node.innerNode)) {
1100
+ if (!(0, traversal_helpers_1.supportedAsArrayExpansion)(node, { skipTopLevelChecks: true })) {
1064
1101
  // cannot have subqueries within array expansions, so we need to use a subquery here
1065
1102
  return processTraversalWithRelationAndListFieldSegmentsUsingSubquery(node, context);
1066
1103
  }
@@ -1087,8 +1124,7 @@ register(query_tree_1.TraversalQueryNode, (node, context) => {
1087
1124
  // In the simple case, we can use an array expansion expression instead of a subquery
1088
1125
  // - SORT is not supported by array expressions (documented)
1089
1126
  // - 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))) {
1127
+ if ((0, traversal_helpers_1.supportedAsArrayExpansion)(node)) {
1092
1128
  return processTraversalWithOnlyFieldSegmentsUsingArrayExpansion(node, context);
1093
1129
  }
1094
1130
  else {
@@ -1100,14 +1136,109 @@ register(query_tree_1.TraversalQueryNode, (node, context) => {
1100
1136
  throw new Error(`TraversalQueryNode must have at least one segment`);
1101
1137
  }
1102
1138
  });
1139
+ /**
1140
+ * Extracts three kinds of VariableAssignmentQueryNode from the traversal's innerNode
1141
+ *
1142
+ * Variables that depend on nested traversals etc. are not extracted.
1143
+ */
1144
+ function extractTraversalAssignments(node) {
1145
+ if (!node.innerNode) {
1146
+ return {
1147
+ innerNode: undefined,
1148
+ traversalIndependentAssignments: [],
1149
+ rootScopedAssignments: [],
1150
+ itemScopedAssignments: [],
1151
+ };
1152
+ }
1153
+ const extractedAssignments = [];
1154
+ const processedInnerNode = (0, utils_1.extractVariableAssignments)(node.innerNode, extractedAssignments);
1155
+ const traversalIndependentAssignments = [];
1156
+ const rootScopedAssignments = [];
1157
+ const itemScopedAssignments = [];
1158
+ const rootScopedVariables = [node.rootEntityVariable];
1159
+ const itemScopedVariables = [node.itemVariable];
1160
+ for (const assignmentNode of extractedAssignments) {
1161
+ const referencedVariables = (0, utils_1.getReferencedVariables)(assignmentNode.variableValueNode);
1162
+ if (itemScopedVariables.some((v) => referencedVariables.has(v))) {
1163
+ itemScopedAssignments.push(assignmentNode);
1164
+ itemScopedVariables.push(assignmentNode.variableNode);
1165
+ }
1166
+ else if (rootScopedVariables.some((v) => referencedVariables.has(v))) {
1167
+ rootScopedAssignments.push(assignmentNode);
1168
+ rootScopedVariables.push(assignmentNode.variableNode);
1169
+ }
1170
+ else {
1171
+ traversalIndependentAssignments.push(assignmentNode);
1172
+ }
1173
+ }
1174
+ return {
1175
+ innerNode: processedInnerNode,
1176
+ traversalIndependentAssignments,
1177
+ rootScopedAssignments,
1178
+ itemScopedAssignments,
1179
+ };
1180
+ }
1181
+ /**
1182
+ * Extracts variable assignments from the traversal's innerNode and produces LET statements
1183
+ */
1184
+ function extractTraversalAssignmentsAsAql({ node, context, rootVar, itemVar, wrapRootVarsInNoEval = true, }) {
1185
+ // TODO aql-perf: can there also be VariableAssignments in filterNode? If yes, should we hoist them?
1186
+ const { innerNode: processedInnerNode, traversalIndependentAssignments, rootScopedAssignments, itemScopedAssignments, } = extractTraversalAssignments(node);
1187
+ if (rootScopedAssignments.length > 0 && !rootVar) {
1188
+ throw new Error('Found variable assignments that depend on the root variable, but the current traversal variant does not support them.');
1189
+ }
1190
+ if (itemScopedAssignments.length > 0 && !itemVar) {
1191
+ throw new Error('Found variable assignments that depend on the loop variable, but the current traversal variant does not support them.');
1192
+ }
1193
+ const { fragments: independentAssignmentFrags, context: baseContext } = buildAssignmentFragments(traversalIndependentAssignments, context, {
1194
+ wrapWithNoEval: true,
1195
+ });
1196
+ const rootBaseContext = rootVar
1197
+ ? baseContext.bindVariable(node.rootEntityVariable, rootVar)
1198
+ : baseContext;
1199
+ const { fragments: rootAssignmentFrags, context: rootContext } = buildAssignmentFragments(rootScopedAssignments, rootBaseContext, {
1200
+ wrapWithNoEval: wrapRootVarsInNoEval,
1201
+ });
1202
+ const itemBaseContext = itemVar
1203
+ ? rootContext.bindVariable(node.itemVariable, itemVar)
1204
+ : rootContext;
1205
+ const { fragments: itemAssignmentFrags, context: itemContext } = buildAssignmentFragments(itemScopedAssignments, itemBaseContext);
1206
+ const innerFrag = itemVar && processedInnerNode
1207
+ ? processNode(processedInnerNode, itemContext)
1208
+ : (itemVar ?? (0, aql_1.aql) `NULL`);
1209
+ return {
1210
+ processedInnerNode,
1211
+ innerFrag,
1212
+ independentAssignmentFrags,
1213
+ rootAssignmentFrags,
1214
+ itemAssignmentFrags,
1215
+ rootContext,
1216
+ itemBaseContext,
1217
+ itemContext,
1218
+ };
1219
+ }
1220
+ function buildAssignmentFragments(assignments, startContext, { wrapWithNoEval = false } = {}) {
1221
+ let currentContext = startContext;
1222
+ const fragments = [];
1223
+ for (const assignmentNode of assignments) {
1224
+ currentContext = currentContext.bindVariable(assignmentNode.variableNode);
1225
+ const tmpVar = currentContext.getVariable(assignmentNode.variableNode);
1226
+ const valueFrag = processNode(assignmentNode.variableValueNode, currentContext);
1227
+ fragments.push(wrapWithNoEval
1228
+ ? (0, aql_1.aql) `LET ${tmpVar} = NOEVAL(${valueFrag})`
1229
+ : (0, aql_1.aql) `LET ${tmpVar} = ${valueFrag}`);
1230
+ }
1231
+ return { fragments, context: currentContext };
1232
+ }
1103
1233
  /**
1104
1234
  * Produces:
1105
1235
  *
1106
1236
  * FIRST(
1107
1237
  * FOR node1 IN OUTBOUND source_hop1
1108
1238
  * FOR node2 in OUTBOUND hop1_hop2
1109
- * FOR result IN OUTBOUND hop2_target
1110
- * RETURN innerNode(result)
1239
+ * FOR item IN OUTBOUND hop2_target
1240
+ * LET extractedVar = variableValueExpr(item)
1241
+ * RETURN innerNode(item)
1111
1242
  * )
1112
1243
  *
1113
1244
  * or, if preserveNullValues is true and hop2_target is a to-1 relation:
@@ -1115,8 +1246,9 @@ register(query_tree_1.TraversalQueryNode, (node, context) => {
1115
1246
  * FIRST(
1116
1247
  * FOR node1 IN OUTBOUND source_hop1
1117
1248
  * FOR node2 in OUTBOUND hop1_hop2
1118
- * LET result = FIRST(FOR node3 IN OUTBOUND hop2_target RETURNN node3)
1119
- * RETURN innerNode(result)
1249
+ * LET item = FIRST(FOR node3 IN OUTBOUND hop2_target RETURNN node3)
1250
+ * LET extractedVar = variableValueExpr(item)
1251
+ * RETURN innerNode(item)
1120
1252
  * )
1121
1253
  */
1122
1254
  function processTraversalWithOnlyRelationSegmentsNoList(node, context) {
@@ -1129,15 +1261,15 @@ function processTraversalWithOnlyRelationSegmentsNoList(node, context) {
1129
1261
  node.maxCount !== undefined) {
1130
1262
  throw new Error(`Cannot have filter, orderBy, skip or maxCount on non-list relation traversal`);
1131
1263
  }
1132
- const innerContext = context.bindVariable(node.itemVariable);
1133
- const itemVar = innerContext.getVariable(node.itemVariable);
1264
+ const itemVar = aql_1.aql.variable(node.itemVariable.label ?? 'item');
1134
1265
  const forStatementsFrag = getRelationTraversalForStatements({
1135
1266
  node,
1136
1267
  innermostItemVar: itemVar,
1137
1268
  preserveNullValues: node.preserveNullValues,
1138
1269
  context,
1139
1270
  });
1140
- return aqlExt.firstOfSubquery(forStatementsFrag, (0, aql_1.aql) `RETURN ${node.innerNode ? processNode(node.innerNode, innerContext) : itemVar}`);
1271
+ const { independentAssignmentFrags, itemAssignmentFrags, innerFrag } = extractTraversalAssignmentsAsAql({ node, context, itemVar });
1272
+ return aqlExt.firstOfSubquery(...independentAssignmentFrags, forStatementsFrag, ...itemAssignmentFrags, (0, aql_1.aql) `RETURN ${innerFrag}`);
1141
1273
  }
1142
1274
  /**
1143
1275
  * Produces:
@@ -1145,6 +1277,7 @@ function processTraversalWithOnlyRelationSegmentsNoList(node, context) {
1145
1277
  * FOR node1 IN OUTBOUND source_hop1
1146
1278
  * FOR node2 in OUTBOUND hop1_hop2
1147
1279
  * FOR item IN OUTBOUND hop2_target
1280
+ * LET extractedVar = variableValueExpr(item)
1148
1281
  * FILTER filterExpr(item)
1149
1282
  * SORT item.field1 ASC
1150
1283
  * LIMIT skip, maxCount
@@ -1155,6 +1288,7 @@ function processTraversalWithOnlyRelationSegmentsNoList(node, context) {
1155
1288
  * FOR node1 IN OUTBOUND source_hop1
1156
1289
  * FOR node2 in OUTBOUND hop1_hop2
1157
1290
  * LET item = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
1291
+ * LET extractedVar = variableValueExpr(item)
1158
1292
  * FILTER filterExpr(item)
1159
1293
  * SORT item.field1 ASC
1160
1294
  * LIMIT skip, maxCount
@@ -1170,39 +1304,43 @@ function processTraversalWithOnlyRelationSegmentsAsList(node, context) {
1170
1304
  // - alwaysProduceList is automatically handled because we always RETURN a list here
1171
1305
  // we could refactor the single usage so it does not use a TraversalQueryNode in the first place
1172
1306
  const itemVar = aql_1.aql.variable(`node`);
1173
- const innerContext = context.bindVariable(node.itemVariable, itemVar);
1174
1307
  const forStatementsFrag = getRelationTraversalForStatements({
1175
1308
  node,
1176
1309
  innermostItemVar: itemVar,
1177
1310
  context,
1178
1311
  preserveNullValues: node.preserveNullValues,
1179
1312
  });
1180
- return aqlExt.subquery(forStatementsFrag, node.filterNode ? (0, aql_1.aql) `FILTER ${processNode(node.filterNode, context)}` : (0, aql_1.aql) ``,
1313
+ const { independentAssignmentFrags, itemAssignmentFrags, itemBaseContext, innerFrag } = extractTraversalAssignmentsAsAql({ node, context, itemVar });
1314
+ return aqlExt.subquery(...independentAssignmentFrags, forStatementsFrag, node.filterNode ? (0, aql_1.aql) `FILTER ${processNode(node.filterNode, itemBaseContext)}` : (0, aql_1.aql) ``,
1181
1315
  // yes, we can SORT and LIMIT like this even if there are multiple FOR statements
1182
1316
  // because there is one result set for the cross product of all FOR statements
1183
1317
  // 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}`);
1318
+ generateSortAQL(node.orderBy, itemBaseContext), generateLimitClause(node) ?? (0, aql_1.aql) ``, ...itemAssignmentFrags, (0, aql_1.aql) `RETURN ${innerFrag}`);
1185
1319
  }
1186
1320
  /**
1187
1321
  * Produces:
1188
1322
  *
1189
1323
  * FOR node1 IN OUTBOUND source_hop1
1190
1324
  * 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
1325
+ * FOR root IN OUTBOUND hop2_target
1326
+ * LET extractedVar1 = variable1ValueExpr(root))
1327
+ * LET extractedVar2 = variable2ValueExpr(root.fieldSegment1.fieldSegment2)
1328
+ * FILTER filterExpr(root.fieldSegment1.fieldSegment2)
1329
+ * SORT root.fieldSegment1.fieldSegment2.sortField ASC
1194
1330
  * LIMIT skip, maxCount
1195
- * RETURN innerNode(item.fieldSegment1.fieldSegment2, { root: node2 })
1331
+ * RETURN innerNode(root.fieldSegment1.fieldSegment2, { root: node2 })
1196
1332
  *
1197
1333
  * or, if preserveNullValues is true and hop2_target is a to-1 relation:
1198
1334
  *
1199
1335
  * FOR node1 IN OUTBOUND source_hop1
1200
1336
  * 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
1337
+ * LET root = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
1338
+ * LET extractedVar1 = variable1ValueExpr(root)
1339
+ * LET extractedVar2 = variable2ValueExpr(root.fieldSegment1.fieldSegment2)
1340
+ * FILTER filterExpr(root.fieldSegment1.fieldSegment2)
1341
+ * SORT root.fieldSegment1.fieldSegment2.sortField ASC
1204
1342
  * LIMIT skip, maxCount
1205
- * RETURN innerNode(item.fieldSegment1.fieldSegment2, { root: node2 })
1343
+ * RETURN innerNode(root.fieldSegment1.fieldSegment2, { root: node2 })
1206
1344
  */
1207
1345
  function processTraversalWithListRelationSegmentsAndNonListFieldSegments(node, context) {
1208
1346
  if (!node.relationSegments.some((f) => f.isListSegment)) {
@@ -1224,11 +1362,19 @@ function processTraversalWithListRelationSegmentsAndNonListFieldSegments(node, c
1224
1362
  segments: node.fieldSegments,
1225
1363
  sourceFrag: rootVar,
1226
1364
  });
1227
- const innerContext = context.bindVariable(node.itemVariable, fieldTraversalFrag);
1365
+ const { independentAssignmentFrags, itemBaseContext, itemAssignmentFrags, rootAssignmentFrags, innerFrag, } = extractTraversalAssignmentsAsAql({
1366
+ node,
1367
+ context,
1368
+ rootVar,
1369
+ itemVar: fieldTraversalFrag,
1370
+ // there is no loop within the context of a root item where the root var assignment could
1371
+ // be pushed down into, so we don't need to guard against that
1372
+ wrapRootVarsInNoEval: false,
1373
+ });
1228
1374
  // note: we don't filter out NULL values even if preserveNullValues is false because that's currently
1229
1375
  // only a flag for performance - actually filtering out NULLs is done by a surrounding AggregationQueryNode
1230
1376
  // 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}`);
1377
+ return aqlExt.subquery(...independentAssignmentFrags, (0, aql_1.aql) `${forStatementsFrag}`, node.filterNode ? (0, aql_1.aql) `FILTER ${processNode(node.filterNode, itemBaseContext)}` : (0, aql_1.aql) ``, generateSortAQL(node.orderBy, itemBaseContext), generateLimitClause(node) ?? (0, aql_1.aql) ``, ...rootAssignmentFrags, ...itemAssignmentFrags, (0, aql_1.aql) `RETURN ${innerFrag}`);
1232
1378
  }
1233
1379
  /**
1234
1380
  * Produces:
@@ -1236,8 +1382,10 @@ function processTraversalWithListRelationSegmentsAndNonListFieldSegments(node, c
1236
1382
  * FIRST(
1237
1383
  * FOR node1 IN OUTBOUND source_hop1
1238
1384
  * FOR node2 in OUTBOUND hop1_hop2
1239
- * FOR item IN OUTBOUND hop2_target
1240
- * RETURN innerNode(item.fieldSegment1.fieldSegment2, { root: node2 })
1385
+ * FOR root IN OUTBOUND hop2_target
1386
+ * LET extractedVar1 = variable1ValueExpr(root)
1387
+ * LET extractedVar2 = variable2ValueExpr(root.fieldSegment1.fieldSegment2)
1388
+ * RETURN innerNode(root.fieldSegment1.fieldSegment2, { root: node2 })
1241
1389
  * )
1242
1390
  *
1243
1391
  * or, if preserveNullValues is true and hop2_target is a to-1 relation:
@@ -1245,8 +1393,10 @@ function processTraversalWithListRelationSegmentsAndNonListFieldSegments(node, c
1245
1393
  * FIRST(
1246
1394
  * FOR node1 IN OUTBOUND source_hop1
1247
1395
  * 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 })
1396
+ * LET root = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
1397
+ * LET extractedVar1 = variable1ValueExpr(root)
1398
+ * LET extractedVar2 = variable2ValueExpr(root.fieldSegment1.fieldSegment2)
1399
+ * RETURN innerNode(root.fieldSegment1.fieldSegment2, { root: node2 })
1250
1400
  * )
1251
1401
  */
1252
1402
  function processTraversalWithNonListRelationSegmentsAndNonListFieldSegments(node, context) {
@@ -1264,7 +1414,7 @@ function processTraversalWithNonListRelationSegmentsAndNonListFieldSegments(node
1264
1414
  }
1265
1415
  // this is very similar to processTraversalWithOnlyRelationSegmentsNoList(),
1266
1416
  // but instead of using the rootVar in mapping, we use the field traversal result
1267
- const rootVar = aql_1.aql.variable(`root`);
1417
+ const rootVar = aql_1.aql.variable('root');
1268
1418
  const forStatementsFrag = getRelationTraversalForStatements({
1269
1419
  node,
1270
1420
  innermostItemVar: rootVar,
@@ -1275,8 +1425,16 @@ function processTraversalWithNonListRelationSegmentsAndNonListFieldSegments(node
1275
1425
  segments: node.fieldSegments,
1276
1426
  sourceFrag: rootVar,
1277
1427
  });
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}`);
1428
+ const { independentAssignmentFrags, itemAssignmentFrags, rootAssignmentFrags, innerFrag } = extractTraversalAssignmentsAsAql({
1429
+ node,
1430
+ context,
1431
+ rootVar,
1432
+ itemVar: fieldTraversalFrag,
1433
+ // there is no loop within the context of a root item where the root var assignment could
1434
+ // be pushed down into, so we don't need to guard against that
1435
+ wrapRootVarsInNoEval: false,
1436
+ });
1437
+ return aqlExt.firstOfSubquery(...independentAssignmentFrags, (0, aql_1.aql) `${forStatementsFrag}`, ...rootAssignmentFrags, ...itemAssignmentFrags, (0, aql_1.aql) `RETURN ${innerFrag}`);
1280
1438
  }
1281
1439
  /**
1282
1440
  * Produces:
@@ -1284,7 +1442,9 @@ function processTraversalWithNonListRelationSegmentsAndNonListFieldSegments(node
1284
1442
  * FOR node1 IN OUTBOUND source_hop1
1285
1443
  * FOR node2 in OUTBOUND hop1_hop2
1286
1444
  * FOR root IN OUTBOUND hop2_target
1445
+ * LET extractedVar1 = NOEVAL(variable1ValueExpr(root))
1287
1446
  * FOR item IN root.fieldSegment1[*].fieldSegment2[** FILTER filterExpr(CURRENT)][*]
1447
+ * LET extractedVar2 = variable1ValueExpr(item)
1288
1448
  * SORT item.sortValues[0]
1289
1449
  * LIMIT skip, maxCount
1290
1450
  * RETURN innerNode(item, { root })
@@ -1294,7 +1454,9 @@ function processTraversalWithNonListRelationSegmentsAndNonListFieldSegments(node
1294
1454
  * FOR node1 IN OUTBOUND source_hop1
1295
1455
  * FOR node2 in OUTBOUND hop1_hop2
1296
1456
  * LET root = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
1457
+ * LET extractedVar1 = NOEVAL(variable1ValueExpr(root))
1297
1458
  * FOR item IN root.fieldSegment1[*].fieldSegment2[** FILTER filterExpr(CURRENT)][*]
1459
+ * LET extractedVar2 = variable1ValueExpr(item)
1298
1460
  * SORT item.sortValues[0]
1299
1461
  * LIMIT skip, maxCount
1300
1462
  * RETURN innerNode(item, { root })
@@ -1331,13 +1493,11 @@ function processTraversalWithRelationAndListFieldSegmentsUsingSubquery(node, con
1331
1493
  sourceFrag: rootVar,
1332
1494
  filterFrag: innerFilterFrag,
1333
1495
  });
1334
- const itemVar = aql_1.aql.variable(`item`);
1335
- const innerContext = context
1336
- .bindVariable(node.itemVariable, itemVar)
1337
- .bindVariable(node.rootEntityVariable, rootVar);
1496
+ const itemVar = aql_1.aql.variable(node.itemVariable.label ?? 'item');
1497
+ const { independentAssignmentFrags, rootAssignmentFrags, itemAssignmentFrags, itemBaseContext, innerFrag, } = extractTraversalAssignmentsAsAql({ node, context, rootVar, itemVar });
1338
1498
  // The fieldTraversalFrag will produce a list, so a simple RETURN mapFrag would result in nested lists
1339
1499
  // -> 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}`);
1500
+ return aqlExt.subquery(...independentAssignmentFrags, (0, aql_1.aql) `${forStatementsFrag}`, ...rootAssignmentFrags, (0, aql_1.aql) `FOR ${itemVar} IN ${fieldTraversalFrag}`, generateSortAQL(node.orderBy, itemBaseContext), generateLimitClause(node) ?? (0, aql_1.aql) ``, ...itemAssignmentFrags, (0, aql_1.aql) `RETURN ${innerFrag}`);
1341
1501
  }
1342
1502
  /**
1343
1503
  * Produces this if the result of the relation traversal is a single item:
@@ -1345,6 +1505,7 @@ function processTraversalWithRelationAndListFieldSegmentsUsingSubquery(node, con
1345
1505
  * FOR node1 IN OUTBOUND source_hop1
1346
1506
  * FOR node2 in OUTBOUND hop1_hop2
1347
1507
  * FOR root IN OUTBOUND hop2_target
1508
+ * LET extractedVar = NOEVAL(variableValueExpr(root))
1348
1509
  * FOR item IN root.fieldSegment1[*].fieldSegment2[**][*
1349
1510
  * FILTER filterExpr(CURRENT)
1350
1511
  * LIMIT skip, maxCount
@@ -1371,6 +1532,7 @@ function processTraversalWithRelationAndListFieldSegmentsUsingSubquery(node, con
1371
1532
  * FOR node1 IN OUTBOUND source_hop1
1372
1533
  * FOR node2 in OUTBOUND hop1_hop2
1373
1534
  * LET root = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
1535
+ * LET extractedVar = NOEVAL(variableValueExpr(root))
1374
1536
  * FOR item IN root.fieldSegment1[*].fieldSegment2[**][*
1375
1537
  * FILTER filterExpr(CURRENT)
1376
1538
  * LIMIT skip, maxCount
@@ -1408,15 +1570,6 @@ function processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWith
1408
1570
  preserveNullValues: node.preserveNullValues,
1409
1571
  context,
1410
1572
  });
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
1573
  // We can do the LIMIT within the field traversal's mapping function using an array expansion,
1421
1574
  // but only if the relation traversal yields at most one result (i.e. if it only follows 1:1 relations)
1422
1575
  const lastRelationSegment = node.relationSegments[node.relationSegments.length - 1];
@@ -1436,8 +1589,10 @@ function processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWith
1436
1589
  maxCount: node.maxCount,
1437
1590
  };
1438
1591
  }
1439
- let innerFilterFrag;
1592
+ // There is no place to put item-dependent LET statements in this variant, so we don't pass an itemVar
1593
+ const { independentAssignmentFrags, rootAssignmentFrags, rootContext, processedInnerNode } = extractTraversalAssignmentsAsAql({ node, context, rootVar });
1440
1594
  const filterNode = node.filterNode;
1595
+ let innerFilterFrag;
1441
1596
  if (filterNode) {
1442
1597
  innerFilterFrag = (itemFrag) => {
1443
1598
  // don't map rootEntityVariable
@@ -1446,6 +1601,9 @@ function processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWith
1446
1601
  return processNode(filterNode, innerContext);
1447
1602
  };
1448
1603
  }
1604
+ const innerMapFrag = processedInnerNode
1605
+ ? (itemFrag) => processNode(processedInnerNode, rootContext.bindVariable(node.itemVariable, itemFrag))
1606
+ : undefined;
1449
1607
  const fieldTraversalFrag = getFieldTraversalFragment({
1450
1608
  segments: node.fieldSegments,
1451
1609
  sourceFrag: rootVar,
@@ -1465,8 +1623,8 @@ function processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWith
1465
1623
  // FOR item IN root.children
1466
1624
  // RETURN item
1467
1625
  // (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}`);
1626
+ const itemVar = aql_1.aql.variable('item');
1627
+ return aqlExt.subquery(...independentAssignmentFrags, (0, aql_1.aql) `${forStatementsFrag}`, ...rootAssignmentFrags, (0, aql_1.aql) `FOR ${itemVar} IN ${fieldTraversalFrag}`, generateLimitClause(limitArgs) ?? (0, aql_1.aql) ``, (0, aql_1.aql) `RETURN ${itemVar}`);
1470
1628
  }
1471
1629
  /**
1472
1630
  * Produces:
@@ -1474,6 +1632,7 @@ function processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWith
1474
1632
  * FOR node1 IN OUTBOUND source_hop1
1475
1633
  * FOR node2 in OUTBOUND hop1_hop2
1476
1634
  * FOR root IN OUTBOUND hop2_target
1635
+ * LET extractedVar = NOEVAL(variableValueExpr(root))
1477
1636
  * FOR item IN root.fieldSegment1[*].fieldSegment2[**][*
1478
1637
  * FILTER filterExpr(CURRENT)
1479
1638
  * RETURN {
@@ -1493,6 +1652,7 @@ function processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWith
1493
1652
  * FOR node1 IN OUTBOUND source_hop1
1494
1653
  * FOR node2 in OUTBOUND hop1_hop2
1495
1654
  * LET root = FIRST(FOR node3 IN OUTBOUND hop2_target RETURN node3)
1655
+ * LET extractedVar = NOEVAL(variableValueExpr(root))
1496
1656
  * FOR item IN root.fieldSegment1[*].fieldSegment2[**][*
1497
1657
  * FILTER filterExpr(CURRENT)
1498
1658
  * RETURN {
@@ -1558,14 +1718,17 @@ function processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWith
1558
1718
  if there are many / large fields in the items that are not needed at all
1559
1719
  (because sorting probably copies the whole item into some temporary structure)
1560
1720
  */
1721
+ // There is no place to put item-dependent LET statements in this variant, so we don't pass an itemVar
1722
+ const { processedInnerNode, independentAssignmentFrags, rootAssignmentFrags, rootContext } = extractTraversalAssignmentsAsAql({ node, context, rootVar });
1561
1723
  // we sort after mapping roots to items, so we need to preserve the sort values that are based on the item
1562
1724
  // -> we produce items like this: { value: ..., sortValues: [...] }
1563
1725
  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) `}`);
1726
+ const innerContext = rootContext.bindVariable(node.itemVariable, itemFrag);
1727
+ const valueFrag = processedInnerNode
1728
+ ? processNode(processedInnerNode, innerContext)
1729
+ : itemFrag;
1730
+ const sortValueFrags = node.orderBy.clauses.map((c) => processNode(c.valueNode, innerContext));
1731
+ 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(sortValueFrags, (0, aql_1.aql) `,\n`)), (0, aql_1.aql) `]`)), (0, aql_1.aql) `}`);
1569
1732
  };
1570
1733
  const filterNode = node.filterNode;
1571
1734
  const innerFilterFrag = filterNode
@@ -1587,7 +1750,7 @@ function processTraversalWithRelationAndListFieldSegmentsUsingArrayExpansionWith
1587
1750
  const clauseFrags = node.orderBy.clauses.map((clause, index) => (0, aql_1.aql) `${itemVar}.sortValues[${aql_1.aql.integer(index)}]${dirAQL(clause.direction)}`);
1588
1751
  // The fieldTraversalFrag will produce a list, so a simple RETURN mapFrag would result in nested lists
1589
1752
  // -> 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`);
1753
+ return aqlExt.subquery(...independentAssignmentFrags, (0, aql_1.aql) `${forStatementsFrag}`, ...rootAssignmentFrags, (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
1754
  }
1592
1755
  /**
1593
1756
  * Produces:
@@ -1640,6 +1803,7 @@ function processTraversalWithOnlyFieldSegmentsUsingArrayExpansion(node, context)
1640
1803
  * FILTER filterExpr(item)
1641
1804
  * SORT item.sortField1 ASC, item.sortField1 DESC
1642
1805
  * LIMIT skip, maxCount
1806
+ * LET extractedVar = variableValueExpr(item)
1643
1807
  * RETURN innerNode(item)
1644
1808
  */
1645
1809
  function processTraversalWithOnlyFieldSegmentsUsingSubquery(node, context) {
@@ -1668,15 +1832,14 @@ function processTraversalWithOnlyFieldSegmentsUsingSubquery(node, context) {
1668
1832
  segments: node.fieldSegments,
1669
1833
  sourceFrag,
1670
1834
  });
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;
1835
+ const itemVar = aql_1.aql.variable(node.itemVariable.label ?? 'item');
1836
+ const { independentAssignmentFrags, itemAssignmentFrags, itemBaseContext, innerFrag } = extractTraversalAssignmentsAsAql({ node, context, itemVar });
1674
1837
  // filter in the subquery instead of in getFieldTraversalFragment() because the filter
1675
1838
  // expression might use subqueries
1676
1839
  const filterFrag = node.filterNode
1677
- ? (0, aql_1.aql) `FILTER ${processNode(node.filterNode, innerContext)}`
1840
+ ? (0, aql_1.aql) `FILTER ${processNode(node.filterNode, itemBaseContext)}`
1678
1841
  : (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}`);
1842
+ return aqlExt.subquery(...independentAssignmentFrags, (0, aql_1.aql) `FOR ${itemVar}`, (0, aql_1.aql) `IN ${fieldTraversalFrag}`, filterFrag, generateSortAQL(node.orderBy, itemBaseContext), generateLimitClause(node) ?? (0, aql_1.aql) ``, ...itemAssignmentFrags, (0, aql_1.aql) `RETURN ${innerFrag}`);
1680
1843
  }
1681
1844
  function getRelationTraversalForStatements({ node, innermostItemVar, context, preserveNullValues = false, }) {
1682
1845
  if (!node.relationSegments.length) {