-
Notifications
You must be signed in to change notification settings - Fork 3.8k
feat: restrict virtual column usage in clustered segments, allow remapping them at query time #19659
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat: restrict virtual column usage in clustered segments, allow remapping them at query time #19659
Changes from all commits
f7de9b6
b76b28a
98c1abc
da07e9a
3d678dc
19267a7
ef517c1
f395a86
3a37a79
045f1c5
5cba105
1d4ea4b
3ec2cac
b9ec612
16ab6a0
298adac
dce7820
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,24 +33,29 @@ | |
| import org.apache.druid.query.aggregation.AggregatorFactory; | ||
| import org.apache.druid.segment.VirtualColumn; | ||
| import org.apache.druid.segment.VirtualColumns; | ||
| import org.apache.druid.segment.column.ColumnCapabilities; | ||
| import org.apache.druid.segment.column.ColumnHolder; | ||
| import org.apache.druid.segment.column.ColumnType; | ||
| import org.apache.druid.segment.column.RowSignature; | ||
| import org.apache.druid.segment.projections.Projections; | ||
| import org.apache.druid.utils.CollectionUtils; | ||
|
|
||
| import javax.annotation.Nullable; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * {@link BaseTableProjectionSpec} for the clustered-value-groups base table mode: rows are partitioned into per-tuple | ||
| * "cluster groups" keyed by one or more typed clustering columns, optionally derived from {@link #virtualColumns}. | ||
| * Essentially, each group is stored as its own internal sub-segment.. | ||
| * Essentially, each group is stored as its own internal sub-segment. Logically, a clustered base table spec is a | ||
| * projection of an (imaginary/unstored) base table into the stored clustered table. | ||
| * <p> | ||
| * The operator declares a single ordered {@link #columns} list — the full set of columns in segment order, plus a | ||
| * The operator declares a single ordered {@link #columns} list, the full set of columns in segment order, plus a | ||
| * {@link #clusteringColumns} list of NAMES designating the leading prefix of {@link #columns} that rows are | ||
| * clustered by. The time position is an explicit positional entry in {@link #columns} named {@code __time}; clustering | ||
| * by the time column is not yet supported, so {@code __time} must be a non-clustering column. A clustered base table | ||
|
|
@@ -94,6 +99,7 @@ public ClusteredValueGroupsBaseTableProjectionSpec( | |
| { | ||
| validate(columns, clusteringColumns); | ||
| this.virtualColumns = virtualColumns == null ? VirtualColumns.EMPTY : virtualColumns; | ||
| validateVirtualColumns(this.virtualColumns, columns); | ||
| this.columns = Collections.unmodifiableList(new ArrayList<>(columns)); | ||
| this.clusteringColumns = Collections.unmodifiableList(new ArrayList<>(clusteringColumns)); | ||
| this.clusteringColumnSchemas = this.columns.subList(0, this.clusteringColumns.size()); | ||
|
|
@@ -311,6 +317,98 @@ private static void validate(List<DimensionSchema> columns, List<String> cluster | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Rules to keep virtual columns definitions reasonable: | ||
| * <ul> | ||
| * <li><b>dot notation</b>: a virtual column that {@link VirtualColumn#usesDotNotation()} is referenced as | ||
| * {@code name.subfield} rather than by a fixed output name, so it has no stable identity to materialize or cluster | ||
| * on; it is rejected outright.</li> | ||
| * <li><b>inputs</b>: every input of a virtual column must be a stored column (declared in {@code columns}) or | ||
| * another virtual column in the spec.</li> | ||
| * <li><b>outputs</b>: every virtual column must either be materialized (its output declared in {@code columns}) or | ||
| * be an intermediary that feeds another virtual column. A virtual column that is neither materializes nothing and | ||
| * is used by nothing and dead metadata and so it is rejected.</li> | ||
| * <li><b>output type</b>: a materialized virtual column must produce the {@link ColumnType} declared for its column | ||
| * in {@code columns}. This is intentionally strict to avoid a virtual column silently filling a column with a | ||
| * coerced/mismatched type.</li> | ||
| * </ul> | ||
| * The query-granularity carrier ({@link Granularities#GRANULARITY_VIRTUAL_COLUMN_NAME}) is special handled to | ||
| * capture how __time is computed, so it is exempt from the output rule. | ||
| */ | ||
| private static void validateVirtualColumns(VirtualColumns virtualColumns, List<DimensionSchema> columns) | ||
| { | ||
| final VirtualColumn[] all = virtualColumns.getVirtualColumns(); | ||
| if (all.length == 0) { | ||
| return; | ||
| } | ||
| // Declared column types, doubling as the ColumnInspector used to infer each materialized virtual column's output | ||
| // type (an expression's output type can depend on its input column types). | ||
| final RowSignature.Builder signatureBuilder = RowSignature.builder(); | ||
| for (DimensionSchema column : columns) { | ||
| signatureBuilder.add(column.getName(), column.getColumnType()); | ||
| } | ||
| final RowSignature rowSignature = signatureBuilder.build(); | ||
| // The output rule below lets a virtual column go unstored when it is exempt: an intermediary that feeds another | ||
| // virtual column (collected during the input pass), or the metadata-only query-granularity carrier (seeded here). | ||
| final Set<String> outputExempt = new HashSet<>(); | ||
| outputExempt.add(Granularities.GRANULARITY_VIRTUAL_COLUMN_NAME); | ||
| // input rule: every input must be a stored column or another virtual column; an input that is a virtual column | ||
| // makes that virtual column an intermediary, exempt from having to be materialized itself. | ||
| for (VirtualColumn virtualColumn : all) { | ||
| if (virtualColumn.usesDotNotation()) { | ||
| throw InvalidInput.exception( | ||
| "virtual column [%s] uses dot notation, which is not supported for clusteredValueGroups base table specs", | ||
| virtualColumn.getOutputName() | ||
| ); | ||
| } | ||
| for (String input : virtualColumn.requiredColumns()) { | ||
| final boolean isStored = rowSignature.contains(input); | ||
| final boolean isVirtual = virtualColumns.exists(input); | ||
| if (!isStored && !isVirtual) { | ||
| throw InvalidInput.exception( | ||
| "virtual column [%s] reads column [%s], which is neither a stored column nor another virtual column;" | ||
| + " clustered base table virtual columns must be computable from stored columns (retain [%s] in" | ||
| + " 'columns', or use a transformSpec for in-place transforms)", | ||
| virtualColumn.getOutputName(), | ||
| input, | ||
| input | ||
| ); | ||
| } | ||
| if (isVirtual) { | ||
| outputExempt.add(input); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Normalize dot-notation virtual-column dependencies For a required field such as |
||
| } | ||
| } | ||
| } | ||
| // output rule: every virtual column must be materialized (stored) or exempt (intermediary / granularity carrier); | ||
| // a materialized virtual column must additionally produce the declared type of the column it fills. | ||
| for (VirtualColumn virtualColumn : all) { | ||
| final String outputName = virtualColumn.getOutputName(); | ||
| final boolean stored = rowSignature.contains(outputName); | ||
| if (!stored && !outputExempt.contains(outputName)) { | ||
| throw InvalidInput.exception( | ||
| "virtual column [%s] is not stored (not declared in 'columns') and does not feed another virtual column;" | ||
| + " clustered base table virtual columns must materialize a stored column or be an input to one that does", | ||
| outputName | ||
| ); | ||
| } | ||
| if (stored) { | ||
| final ColumnCapabilities outputCapabilities = | ||
| virtualColumns.getColumnCapabilitiesWithFallback(rowSignature, outputName); | ||
| final ColumnType outputType = ColumnType.fromCapabilities(outputCapabilities); | ||
| final ColumnType declaredType = rowSignature.getColumnType(outputName).orElse(null); | ||
| if (outputType != null && !outputType.equals(declaredType)) { | ||
| throw InvalidInput.exception( | ||
| "virtual column [%s] produces type [%s] but the column it materializes is declared as [%s] in 'columns';" | ||
| + " a clustered base table virtual column must match the declared type of the column it fills", | ||
| outputName, | ||
| outputType, | ||
| declaredType | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static DruidException clusteringPrefixException( | ||
| List<DimensionSchema> columns, | ||
| List<String> clusteringColumns | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that we are going to be (potentially) remapping query time VCs to use materialized columns instead of re-compute. Do we need to validate that the VCs who have physical output columns have matching type?
like if physical column is
type longand the VC isx * 1.5 type doublean equivalent remapping would use the long instead of expected doubleor do we just live with this as dealers choice by the person who owns the datasource and whether or not they like chaos