apache/druid · error · QueryUnsupportedException

Cannot apply virtual columns

Error message

Cannot apply virtual columns [%s] with naive apply.

What it means

The naive materialization path of LazilyDecoratedRowsAndColumns cannot apply virtual columns; it only handles skipping rows and selecting existing columns. When virtualColumns is set when the naive path is reached, Druid throws UOE. The fast cursor-based path must be used instead.

Solutions

  1. Ensure the base RowsAndColumns supports CursorFactory so the non-naive path is used
  2. Apply virtual columns before wrapping in LazilyDecoratedRowsAndColumns
  3. Remove virtual column decoration from this stage and compute derived columns elsewhere
Defensive patterns

Strategy: type-guard

Validate before calling

if (decoration.getVirtualColumns() != null && !(base instanceof CursorFactory)) {
  throw new IllegalStateException("virtual columns require a cursor-capable base");
}

Type guard

boolean supportsCursorPaths(RowsAndColumns rac) {
  return rac.as(CursorFactory.class) != null;
}

Try / catch

try {
  rac = lazilyDecorated.materialize();
} catch (UnsupportedOperationException e) {
  if (e.getMessage().contains("virtual columns") && e.getMessage().contains("naive")) {
    rac = applyVirtualColumnsManually(base, vcs);
  } else throw e;
}

Prevention

When it happens

Trigger: materialize() falls back to naiveMaterialize() (because the base is not a CursorFactory) while virtual columns are configured on the decoration.

Common situations: Wrapping a data object that doesn't support cursors while also decorating with virtual column expressions; internal pipeline wiring that combines naive materialization with VCs.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/8b348465b4e4a1fb. Report an issue: GitHub.

Appendix: source

Thrown at processing/src/main/java/org/apache/druid/query/rowsandcols/LazilyDecoratedRowsAndColumns.java:350

        rowsToSkip = new BitSet(numRows);
      }

      final ValueMatcher matcher = filter.makeMatcher(selectorFactory);

      for (; rowId.get() < numRows; rowId.incrementAndGet()) {
        final int theId = rowId.get();
        if (rowsToSkip.get(theId)) {
          continue;
        }

        if (!matcher.matches(false)) {
          rowsToSkip.set(theId);
        }
      }
    }

    if (virtualColumns != null) {
      throw new UOE("Cannot apply virtual columns [%s] with naive apply.", virtualColumns);
    }

    ArrayList<String> columnsToGenerate = new ArrayList<>();
    if (viewableColumns != null) {
      columnsToGenerate.addAll(viewableColumns);
    } else {
      columnsToGenerate.addAll(rac.getColumnNames());
      // When/if we support virtual columns from here, we should auto-add them to the list here as well as they expand
      // the implicit project when no projection is defined
    }

    // There is all sorts of sub-optimal things in this code, but we just ignore them for now as it is difficult to
    // optimally build frames for incremental data processing.  In order to build the frame object here, we must first
    // materialize everything in memory so that we know how long things are, such that we can allocate a big byte[]
    // so that the Frame.wrap() call can be given just a big byte[] to do its reading from.
    //
    // It would be a bit better if we could just build the per-column byte[] and somehow re-generate a Frame from
    // that.  But it's also possible that this impedence mis-match is a function of using column-oriented frames and

View on GitHub (pinned to 9b90983fd2)