cube-js/cube · error · UserError

Granularity "${this.granularity}" of dimension ${timeDimensi

Error message

Granularity "${this.granularity}" of dimension ${timeDimension.dimension} ${cause}

What it means

After resolving a custom granularity, the constructor checks it has an `interval`. If instead it only has `sql` (predefined-granularity-only feature) or is missing interval entirely, it throws this UserError explaining the cause. Custom granularities must be interval-based.

Source

Thrown at packages/cubejs-schema-compiler/src/adapter/Granularity.ts:55

    if (this.predefinedGranularity) {
      this.granularityInterval = `1 ${this.granularity}`;
    } else {
      const customGranularity = this.query.cacheValue(
        ['customGranularity', timeDimension.dimension, this.granularity],
        () => query.cubeEvaluator
          .resolveGranularity([...query.cubeEvaluator.parsePath('dimensions', timeDimension.dimension), 'granularities', this.granularity])
      );

      if (!customGranularity) {
        throw new UserError(`Granularity "${timeDimension.granularity}" does not exist in dimension ${timeDimension.dimension}`);
      }

      if (!customGranularity.interval) {
        const cause = customGranularity.sql
          ? 'is defined with \'sql\', which is only supported for predefined granularities'
          : 'has no interval';
        throw new UserError(`Granularity "${this.granularity}" of dimension ${timeDimension.dimension} ${cause}`);
      }

      this.granularityInterval = customGranularity.interval;

      if (customGranularity.origin) {
        this.origin = moment.tz(customGranularity.origin, query.timezone);
      } else if (customGranularity.offset) {
        // Needed because if interval is week-based, offset is expected to be relative to the start of a week
        this.fixOriginForWeeksIfNeeded();
        this.granularityOffset = customGranularity.offset;
        this.origin = addInterval(this.origin, parseSqlInterval(customGranularity.offset));
      } else {
        this.fixOriginForWeeksIfNeeded();
      }
    }
  }

  private fixOriginForWeeksIfNeeded() {

View on GitHub (pinned to 7d981676b3)

Solutions

  1. Replace `sql:` with a valid `interval` (e.g. `interval: '15 minutes'`) in the custom granularity definition.
  2. Add a missing `interval` property to the custom granularity.
  3. Use a predefined granularity if you need sql-based behavior.

Example fix

// before
granularities: { quarterHour: { sql: `${CUBE}.timestamp` } }
// after
granularities: { quarterHour: { interval: '15 minutes' } }
Defensive patterns

Strategy: validation

Validate before calling

for (const [name, g] of Object.entries(dim.granularities || {})) {
  if (!g.interval) throw new Error(`Custom granularity "${name}" must define an interval`);
}

Try / catch

try { await cube.load(q) } catch (e) { if (/has no interval|only supported for predefined granularities/.test(e.message)) { /* fix schema: replace sql with interval */ } else throw e; }

Prevention

When it happens

Trigger: Defining a custom granularity entry in a dimension using `sql:` instead of `interval:`, or declaring a custom granularity object without either field, then querying with that granularity.

Common situations: Developers copying the shape of predefined granularities (which are sql-based) into a custom granularities block; forgetting the interval key; schema migration from older experimental syntax.

Related errors


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