cube-js/cube · error · UserError

Incremental refresh key can only be used for partitioned pre

Error message

Incremental refresh key can only be used for partitioned pre-aggregations but set for non-partitioned '${cube}.${preAggregationName}'

What it means

`refreshKey: { incremental: true }` builds the refresh key from partition boundaries, so it requires the pre-aggregation to declare partitionGranularity. If incremental is set on a non-partitioned (plain rollup) pre-aggregation, this UserError is thrown during pre-aggregation SQL build.

Source

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

          if (preAggregation.refreshKey.sql) {
            return [
              preAggregationQueryForSql.paramAllocator.buildSqlAndParams(
                preAggregationQueryForSql.evaluateSql(cube, preAggregation.refreshKey.sql)
              ).concat({
                external: false,
                renewalThreshold: preAggregation.refreshKey.every
                  ? this.refreshKeyRenewalThresholdForInterval(preAggregation.refreshKey, false)
                  : this.defaultRefreshKeyRenewalThreshold(),
              })
            ];
          }

          // eslint-disable-next-line prefer-const
          let [refreshKey, refreshKeyExternal, refreshKeyQuery] = this.everyRefreshKeySql(preAggregation.refreshKey);
          const renewalThreshold = this.refreshKeyRenewalThresholdForInterval(preAggregation.refreshKey);
          if (preAggregation.refreshKey.incremental) {
            if (!preAggregation.partitionGranularity) {
              throw new UserError(`Incremental refresh key can only be used for partitioned pre-aggregations but set for non-partitioned '${cube}.${preAggregationName}'`);
            }
            // TODO Case when partitioned originalSql is resolved for query without time dimension.
            // Consider fallback to not using such originalSql for consistency?
            if (
              preAggregationQueryForSql.timeDimensions.length &&
              preAggregationQueryForSql.timeDimensions[0].dateRange
            ) {
              refreshKey = this.incrementalRefreshKey(
                preAggregationQueryForSql,
                refreshKey,
                { window: preAggregation.refreshKey.updateWindow, refreshKeyQuery }
              );
            }
          }

          if (preAggregation.refreshKey.every || preAggregation.refreshKey.incremental) {
            // An incremental key is wrapped into `CASE WHEN NOW() < <dateTo + updateWindow>`
            // against an allocated partition range param, so it is not reproducible from

View on GitHub (pinned to 7d981676b3)

Solutions

  1. Add partitionGranularity (and a timeDimensions partition reference) to the pre-aggregation, e.g. partitionGranularity: 'month'
  2. Remove `incremental: true` from the refreshKey if the pre-aggregation should stay unpartitioned
  3. Use a regular refreshKey (e.g. every: '1 hour') for non-partitioned rollups

Example fix

// before
main: {
  measureReferences: [orders.count],
  refreshKey: { incremental: true, updateWindow: '1 day' }
}
// after (option A: partition it)
main: {
  measureReferences: [orders.count],
  timeDimensionReference: orders.createdAt,
  partitionGranularity: 'month',
  refreshKey: { incremental: true, updateWindow: '1 day' }
}
Defensive patterns

Strategy: validation

Validate before calling

function validateIncremental(pa) {
  if (pa.refreshKey?.incremental && !pa.partitionGranularity)
    throw new Error('incremental refreshKey requires partitionGranularity');
}

Type guard

const isIncrementalPartitioned = (pa) => Boolean(pa?.refreshKey?.incremental) && Boolean(pa?.partitionGranularity);

Try / catch

try { await cubeApi.query(q); } catch (e) { if (/Incremental refresh key can only be used for partitioned/.test(e.message)) console.error('Add partitionGranularity or drop incremental:', e.message); throw e; }

Prevention

When it happens

Trigger: Defining a rollup without timeDimensionReference/partitionGranularity but with refreshKey: { incremental: true, updateWindow: '1 day' }.

Common situations: Copying an incremental refresh example into an existing unpartitioned pre-aggregation; removing the time partitioning while keeping the refreshKey config; automated config generation that always sets incremental: true.

Related errors


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