cube-js/cube · error · UserError

Can not construct joins for the query, potential loop detect

Error message

Can not construct joins for the query, potential loop detected

What it means

BaseQuery builds the join graph by iteratively collecting join hints until the tree stabilizes. As a safety net, if the iteration exceeds 10000 rounds (meaning join trees keep changing and never converge — a cyclic or contradictory join definition), it throws this UserError instead of hanging.

Source

Thrown at packages/cubejs-schema-compiler/src/adapter/BaseQuery.js:458

      prevJoin = newJoin;
      newJoin = this.joinGraph.buildJoin(allJoinHints);
      const allJoinHintsFlatten = new Set(allJoinHints.flat());
      const joinMembersJoinHints = this.collectJoinHintsFromMembers(this.joinMembersFromJoin(newJoin));

      const iterationCollectedHints = joinMembersJoinHints.filter(j => !allJoinHintsFlatten.has(j));
      newJoinHintsCollectedCnt = iterationCollectedHints.length;
      cnt++;
      if (newJoin && newJoin.joins.length > 0) {
        // Even if there is no join tree changes, we still
        // push correctly ordered join hints, collected from the resolving of members of join tree
        // upfront the all existing query members. This ensures the correct cube join order
        // with transitive joins even if they are already presented among query members.
        newCollectedHints = this.enrichedJoinHintsFromJoinTree(newJoin, joinMembersJoinHints);
      }
    } while (newJoin?.joins.length > 0 && !this.isJoinTreesEqual(prevJoin, newJoin) && cnt < 10000 && newJoinHintsCollectedCnt > 0);

    if (cnt >= 10000) {
      throw new UserError('Can not construct joins for the query, potential loop detected');
    }

    return this.joinGraph.buildJoin(constructJH());
  }

  cacheValue(key, fn, { contextPropNames, inputProps, cache } = {}) {
    const currentContext = this.safeEvaluateSymbolContext();
    if (contextPropNames) {
      const contextKey = {};
      for (const element of contextPropNames) {
        contextKey[element] = currentContext[element];
      }
      key = key.concat([JSON.stringify(contextKey)]);
    }
    const { value, resultProps } = (cache || this.compilerCache).cache(
      key,
      () => {
        if (inputProps) {

View on GitHub (pinned to 7d981676b3)

Solutions

  1. Review joins in the data model for cycles and remove the loop
  2. Break transitive chains by adding the needed direct join explicitly
  3. Reduce the members in the query to isolate which cubes cause the loop
  4. Test the join path with a minimal query adding one cube at a time

Example fix

// before (cycle in schema)
joins: Orders: { sql: "${Carts.id} = ${Orders.cartId}", ... }
joins: Carts: { sql: "${Orders.id} = ${Carts.orderId}", ... }
// after (single direction)
joins: Carts: { sql: "${Orders.id} = ${Carts.orderId}", ... }
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const data = await cubeApi.load(query);
} catch (e) {
  if (/potential loop detected/.test(e.message)) {
    console.error('Join cycle in schema; reduce members or fix joins', e.message);
    throw new Error('Query cannot be planned: join loop in schema');
  }
  throw e;
}

Prevention

When it happens

Trigger: Cubes whose join definitions form a cycle or mutually referencing joins that keep generating new hints each round, so joinTreeForHints never reaches a fixed point within 10000 iterations.

Common situations: Large schemas with transitive joins between many cubes, mistakenly defining A->B, B->C, C->A style loops, or joins referencing members that pull in more joins indefinitely after schema edits.

Related errors


AI-assisted analysis of cube-js/cube@7d981676b3 (2026-09-02). Data as JSON: /api/errors/5cd6d89d3e5c3e0b. Report an issue: GitHub.