Skip to content

feat: remap query virtual columns to clustered base table columns when equivalent#19659

Open
clintropolis wants to merge 3 commits into
apache:masterfrom
clintropolis:clustered-vc-equivalence
Open

feat: remap query virtual columns to clustered base table columns when equivalent#19659
clintropolis wants to merge 3 commits into
apache:masterfrom
clintropolis:clustered-vc-equivalence

Conversation

@clintropolis

Copy link
Copy Markdown
Member

Description

This PR is a minor optimization to clustered segments to allow query virtual columns to be substituted with physical columns if the clustered segment was created with an 'equivalent' column (clustered segments preserve any virtual columns used during their creation in their metadata). For example, if the function lower(some_column) is used to create the column, after this patch running the same expression at query time will be swapped out to read the value from the column directly instead of recomputing the lower operation.

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity Findings
P0 0
P1 0
P2 2
P3 0
Total 2

This is an automated review by Codex GPT-5.5

// the materialized physical column.
Filter rewritten = spec.getFilter() == null ? null : rewriteFor(group);
if (rewritten != null) {
rewritten = rewritten.rewriteRequiredColumns(virtualColumnRemap);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Seed residual filter rewrites with identity mappings

When a remap exists, this rewrites the entire residual filter using only virtualColumnRemap. Filters such as EqualityFilter and RangeFilter require the rewrite map to contain their own required column and throw if it is absent. A query with v0 remapped plus an unrelated residual filter like region = 'us-east-1' will fold/remap v0, leave region as a residual filter, then pass only {v0 -> tenant_lower} here and fail during cursor construction. Build an identity map for rewritten.getRequiredColumns() before overlaying the remap.

}
final VirtualColumn equivalent = groupVcs.findEquivalent(queryNode);
if (equivalent != null && !outputName.equals(equivalent.getOutputName())) {
candidates.put(outputName, equivalent.getOutputName());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Only remap to readable clustered columns

findEquivalent searches all group virtual-column metadata, but not every group virtual-column output is a stored/readable column. For example, clustered base-table query-granularity carriers live in virtualColumns and are explicitly not stored in columns. If a query VC with a different name matches such a metadata-only VC, this records a remap, rebuildCursorBuildSpec drops the query VC, and the selector reads the missing target column, producing null or incorrect results. Gate candidates to clustering columns or names present in the clustered summary's stored columns.

@FrankChen021 FrankChen021 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity Findings
P0 0
P1 0
P2 2
P3 0
Total 2
Severity Findings
P0 0
P1 0
P2 2
P3 0
Total 2

Reviewed 11 of 11 changed files.

Found 2 issues: residual filter rewriting can fail for unchanged columns, and remapping can target metadata-only virtual columns that group cursors cannot read.


This is an automated review by Codex GPT-5.5

// the materialized physical column.
Filter rewritten = spec.getFilter() == null ? null : rewriteFor(group);
if (rewritten != null) {
rewritten = rewritten.rewriteRequiredColumns(virtualColumnRemap);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Seed filter rewrites with identity entries

When virtualColumnRemap is non-empty, rebuildCursorBuildSpec calls rewriteRequiredColumns with only the remapped VC entries. Existing filter implementations throw if their own required column is missing from the map, so a query that remaps one VC but still has an unchanged residual predicate, such as a materialized VC filter plus region = 'us-east-1', fails during cursor construction instead of evaluating the residual column. Build an identity map for the rewritten filter's required columns and overlay the remap, as the aggregate projection path does.

}
final VirtualColumn equivalent = groupVcs.findEquivalent(queryNode);
if (equivalent != null && !outputName.equals(equivalent.getOutputName())) {
candidates.put(outputName, equivalent.getOutputName());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Only remap to columns the group cursor can read

buildClusterVirtualColumnRemap treats any groupVcs.findEquivalent result as a readable materialized column, but clustered summaries also carry metadata-only virtual columns. For example, the query-granularity carrier __virtualGranularity is explicitly not a stored column; an equivalent query VC with another name is now dropped and remapped to __virtualGranularity, which the clustering/per-group selector cannot serve. That returns null/empty results instead of recomputing or reading __time. Restrict remap targets to clustering columns or stored group columns, or special-case the granularity carrier.

// either the per-group constant clustering column or a per-group physical column, both served directly by the
// ClusteringColumnSelectorFactory (the cursor factory additionally wraps it with a RemapColumnSelectorFactory so
// the original query VC names resolve to the materialized columns).
final List<VirtualColumn> prunedVcs = new ArrayList<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming seems backwards. aren't these actually whare are not being pruned out of the query?

Comment on lines +427 to +452
private static List<String> collectDimension(Cursor cursor, String column)
{
final DimensionSelector sel =
cursor.getColumnSelectorFactory().makeDimensionSelector(DefaultDimensionSpec.of(column));
final List<String> out = new ArrayList<>();
while (!cursor.isDone()) {
out.add(sel.getRow().size() == 0 ? null : sel.lookupName(sel.getRow().get(0)));
cursor.advance();
}
return out;
}

private static List<String> collectObjectVector(VectorCursor cursor, String column)
{
final VectorObjectSelector sel = cursor.getColumnSelectorFactory().makeObjectSelector(column);
final List<String> out = new ArrayList<>();
while (!cursor.isDone()) {
final Object[] vector = sel.getObjectVector();
final int size = cursor.getCurrentVectorSize();
for (int i = 0; i < size; i++) {
out.add((String) vector[i]);
}
cursor.advance();
}
return out;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move to top or bottom of file to avoid interleaving with tests

Comment on lines +99 to +104
* When there is a remap, the matched query virtual columns (the remap keys) are dropped from the spec's virtual
* columns, and the per-group filter's required columns are rewritten via the remap so that non-clustering-VC filter
* leaves point at the materialized physical column (clustering-VC leaves were already folded to TRUE / FALSE by the
* per-group rewrite walk, so they won't reference the dropped virtual columns). The grouping / select / aggregator
* references are served instead by the {@link org.apache.druid.segment.RemapColumnSelectorFactory} the cursor
* factory wraps around the {@link ClusteringColumnSelectorFactory}.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clustering-VC leaves were already folded to TRUE / FALSE by the per-group rewrite walk, so they won't reference the dropped virtual columns

This is a not entirely true. walkClusterGroupFilter only folds Equality, In, and Null. Everything else falls through unchanged. This will result in wrong results when something like a Range filter on a query vc with equivalent clustering column survives the walk.

Repro wrong empty results:

  • tenant_lower := lower(tenant) clustered vc
  • v0 := lower(tenant) ⇒ remap v0 → tenant_lower
  • same test tenants as always
  • RangeFilter("v0", ["a","z"])
  • Both groups survive and you expect all rows but get empty result back

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants