Skip to content

Commit 09f793b

Browse files
committed
fixed some suggestions from AI pr review
Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com>
1 parent 019028f commit 09f793b

10 files changed

Lines changed: 135 additions & 41 deletions

File tree

sandbox/plugins/dsl-query-executor/src/main/java/org/opensearch/dsl/aggregation/AggregationTreeWalker.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Map;
2323
import java.util.stream.Collectors;
24+
import java.util.stream.IntStream;
2425

2526
/**
2627
* Recursively walks the DSL aggregation tree and produces one {@link AggregationMetadata}
@@ -76,12 +77,14 @@ private void walkRecursive(
7677
for (AggregationBuilder aggBuilder : aggs) {
7778
AggregationType<?> type = registry.get(aggBuilder.getClass());
7879

79-
if (type instanceof BucketTranslator) {
80+
if (type == null) {
81+
throw new ConversionException("No translator registered for aggregation type: " + aggBuilder.getClass().getSimpleName());
82+
} else if (type instanceof BucketTranslator) {
8083
handleBucket((BucketTranslator<AggregationBuilder>) type, aggBuilder, currentGroupings, granularities, rowType);
8184
} else if (type instanceof MetricTranslator) {
8285
handleMetric((MetricTranslator<AggregationBuilder>) type, aggBuilder, currentGroupings, granularities, rowType);
8386
} else {
84-
throw new ConversionException("Unsupported aggregation type: " + aggBuilder.getClass().getSimpleName());
87+
throw new ConversionException("Unsupported aggregation translator kind: " + type.getClass().getSimpleName());
8588
}
8689
}
8790
}
@@ -144,6 +147,8 @@ private static String granularityKey(List<GroupingInfo> groupings) {
144147
if (groupings.isEmpty()) {
145148
return "";
146149
}
147-
return groupings.stream().flatMap(g -> g.getFieldNames().stream()).collect(Collectors.joining(","));
150+
return IntStream.range(0, groupings.size())
151+
.mapToObj(i -> i + ":" + String.join(",", groupings.get(i).getFieldNames()))
152+
.collect(Collectors.joining("|"));
148153
}
149154
}

sandbox/plugins/dsl-query-executor/src/main/java/org/opensearch/dsl/aggregation/metric/AbstractMetricTranslator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public AggregateCall toAggregateCall(T agg, RelDataType rowType) throws Conversi
4848
throw new ConversionException("Aggregation field '" + fieldName + "' not found in schema");
4949
}
5050

51+
// Calcite enforces the return type to be same as input type; eg: AVG int→double coercion happens in response layer.
5152
return AggregateCall.create(
5253
getAggFunction(),
5354
false,

sandbox/plugins/dsl-query-executor/src/main/java/org/opensearch/dsl/converter/ProjectConverter.java

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,18 @@ private RelNode createProjection(RelNode input, String[] includes, String[] excl
6161
RelDataType rowType = input.getRowType();
6262
List<RexNode> projects = new ArrayList<>();
6363
List<String> fieldNames = new ArrayList<>();
64+
Set<String> seen = new HashSet<>();
6465

6566
if (includes != null && includes.length > 0) {
66-
// Include mode: only listed fields
67+
// Include mode: only listed fields, then apply excludes as post-filter
68+
Set<String> excludeSet = (excludes != null && excludes.length > 0) ? buildExcludeSet(excludes, rowType) : Set.of();
6769
for (String pattern : includes) {
6870
if (pattern.contains("*")) {
69-
resolveWildcard(pattern, rowType, rexBuilder, projects, fieldNames);
71+
resolveWildcard(pattern, rowType, rexBuilder, projects, fieldNames, seen, excludeSet);
7072
} else {
71-
resolveField(pattern, rowType, rexBuilder, projects, fieldNames);
73+
if (!excludeSet.contains(pattern)) {
74+
resolveField(pattern, rowType, rexBuilder, projects, fieldNames, seen);
75+
}
7276
}
7377
}
7478
} else {
@@ -85,11 +89,23 @@ private RelNode createProjection(RelNode input, String[] includes, String[] excl
8589
return LogicalProject.create(input, List.of(), projects, fieldNames);
8690
}
8791

92+
private static Pattern toWildcardRegex(String wildcardPattern) {
93+
String[] parts = wildcardPattern.split("\\*", -1);
94+
StringBuilder regex = new StringBuilder();
95+
for (int i = 0; i < parts.length; i++) {
96+
if (i > 0) {
97+
regex.append(".*");
98+
}
99+
regex.append(Pattern.quote(parts[i]));
100+
}
101+
return Pattern.compile(regex.toString());
102+
}
103+
88104
private Set<String> buildExcludeSet(String[] excludes, RelDataType rowType) {
89105
Set<String> excludeSet = new HashSet<>();
90106
for (String pattern : excludes) {
91107
if (pattern.contains("*")) {
92-
Pattern compiled = Pattern.compile(pattern.replace(".", "\\.").replace("*", ".*"));
108+
Pattern compiled = toWildcardRegex(pattern);
93109
for (RelDataTypeField field : rowType.getFieldList()) {
94110
if (compiled.matcher(field.getName()).matches()) {
95111
excludeSet.add(field.getName());
@@ -107,25 +123,35 @@ private void resolveWildcard(
107123
RelDataType rowType,
108124
RexBuilder rexBuilder,
109125
List<RexNode> projects,
110-
List<String> fieldNames
126+
List<String> fieldNames,
127+
Set<String> seen,
128+
Set<String> excludeSet
111129
) {
112-
Pattern compiled = Pattern.compile(pattern.replace(".", "\\.").replace("*", ".*"));
130+
Pattern compiled = toWildcardRegex(pattern);
113131
for (RelDataTypeField field : rowType.getFieldList()) {
114-
if (compiled.matcher(field.getName()).matches()) {
132+
if (compiled.matcher(field.getName()).matches() && !excludeSet.contains(field.getName()) && seen.add(field.getName())) {
115133
projects.add(rexBuilder.makeInputRef(field.getType(), field.getIndex()));
116134
fieldNames.add(field.getName());
117135
}
118136
}
119137
// No error if nothing matches — consistent with OpenSearch core, which returns empty _source
120138
}
121139

122-
private void resolveField(String fieldName, RelDataType rowType, RexBuilder rexBuilder, List<RexNode> projects, List<String> fieldNames)
123-
throws ConversionException {
140+
private void resolveField(
141+
String fieldName,
142+
RelDataType rowType,
143+
RexBuilder rexBuilder,
144+
List<RexNode> projects,
145+
List<String> fieldNames,
146+
Set<String> seen
147+
) throws ConversionException {
124148
RelDataTypeField field = rowType.getField(fieldName, false, false);
125149
if (field == null) {
126150
throw new ConversionException("Field '" + fieldName + "' not found in schema");
127151
}
128-
projects.add(rexBuilder.makeInputRef(field.getType(), field.getIndex()));
129-
fieldNames.add(field.getName());
152+
if (seen.add(field.getName())) {
153+
projects.add(rexBuilder.makeInputRef(field.getType(), field.getIndex()));
154+
fieldNames.add(field.getName());
155+
}
130156
}
131157
}

sandbox/plugins/dsl-query-executor/src/main/java/org/opensearch/dsl/converter/SearchSourceConverter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ public QueryPlans convert(SearchSourceBuilder searchSource, String indexName) th
105105
QueryPlans.Builder builder = new QueryPlans.Builder();
106106

107107
// Hits path: Scan → Filter → Project → Sort
108-
if (size > 0 || !hasAggs) {
108+
// size=0 skips hits — total doc count comes from analytics plugin metadata
109+
if (size > 0) {
109110
RelNode hits = projectConverter.convert(base, ctx);
110111
hits = sortConverter.convert(hits, ctx);
111112
builder.add(new QueryPlans.QueryPlan(QueryPlans.Type.HITS, hits));

sandbox/plugins/dsl-query-executor/src/main/java/org/opensearch/dsl/executor/QueryPlans.java

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,6 @@ public record QueryPlan(Type type, RelNode relNode) {
4646
Objects.requireNonNull(type, "type must not be null");
4747
Objects.requireNonNull(relNode, "relNode must not be null");
4848
}
49-
50-
/** Returns what part of the response this plan produces. */
51-
@Override
52-
public Type type() {
53-
return type;
54-
}
55-
56-
/** Returns the Calcite logical plan to execute. */
57-
@Override
58-
public RelNode relNode() {
59-
return relNode;
60-
}
6149
}
6250

6351
private final List<QueryPlan> plans;
@@ -106,11 +94,8 @@ public Builder add(QueryPlan plan) {
10694
return this;
10795
}
10896

109-
/** Builds the plans. At least one must have been added. */
97+
/** Builds the plans. May be empty (e.g. size=0, no aggs — only metadata response). */
11098
public QueryPlans build() {
111-
if (plans.isEmpty()) {
112-
throw new IllegalStateException("QueryPlans must have at least one plan");
113-
}
11499
return new QueryPlans(plans);
115100
}
116101
}

sandbox/plugins/dsl-query-executor/src/main/java/org/opensearch/dsl/result/SearchResponseBuilder.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@ private SearchResponseBuilder() {}
2929
* @param convertTimeNanos time spent in DSL-to-RelNode conversion, in nanoseconds
3030
* @return a SearchResponse
3131
*/
32-
// TODO: planExecutor.execute() should return execution timing so we can add it here
33-
// and construct tookInMillis = convertTime + executionTime + responseBuildTime.
32+
// TODO: Analytics plugin should return execution metadata alongside Iterable<Object[]> rows:
33+
// - executionTimeNanos: query execution time (tookInMillis = convertTime + executionTime + responseBuildTime)
34+
// - totalDocCount: total matching documents for hits.total (similar to Lucene's TotalHitCountCollector)
35+
// - terminatedEarly: whether execution was terminated early
36+
// - timedOut: whether execution timed out
37+
// - shardInfo: total/successful/skipped/failed shard counts
3438
public static SearchResponse build(List<ExecutionResult> results, long convertTimeNanos) {
35-
// TODO: populate hits and aggregations from results
39+
// TODO: populate HITS and AGGREGATION plan types from results
3640
long tookInMillis = convertTimeNanos / 1_000_000;
3741
SearchHits hits = SearchHits.empty(true);
3842
SearchResponseSections sections = new SearchResponseSections(hits, null, null, false, null, null, 0);

sandbox/plugins/dsl-query-executor/src/test/java/org/opensearch/dsl/aggregation/metric/MetricTranslatorTests.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ public class MetricTranslatorTests extends OpenSearchTestCase {
2525

2626
public void testAvgTranslator() throws ConversionException {
2727
AvgMetricTranslator translator = new AvgMetricTranslator();
28-
AggregateCall call = translator.toAggregateCall(new AvgAggregationBuilder("avg_price").field("price"), ctx.getRowType());
28+
AggregateCall call = translator.toAggregateCall(
29+
new AvgAggregationBuilder("avg_price")
30+
.field("price"), ctx.getRowType());
2931

3032
assertEquals(SqlKind.AVG, call.getAggregation().getKind());
3133
assertEquals("avg_price", call.getName());
@@ -35,23 +37,29 @@ public void testAvgTranslator() throws ConversionException {
3537

3638
public void testSumTranslator() throws ConversionException {
3739
SumMetricTranslator translator = new SumMetricTranslator();
38-
AggregateCall call = translator.toAggregateCall(new SumAggregationBuilder("total").field("price"), ctx.getRowType());
40+
AggregateCall call = translator.toAggregateCall(
41+
new SumAggregationBuilder("total")
42+
.field("price"), ctx.getRowType());
3943

4044
assertEquals(SqlKind.SUM, call.getAggregation().getKind());
4145
assertEquals("total", call.getName());
4246
}
4347

4448
public void testMinTranslator() throws ConversionException {
4549
MinMetricTranslator translator = new MinMetricTranslator();
46-
AggregateCall call = translator.toAggregateCall(new MinAggregationBuilder("min_price").field("price"), ctx.getRowType());
50+
AggregateCall call = translator.toAggregateCall(
51+
new MinAggregationBuilder("min_price")
52+
.field("price"), ctx.getRowType());
4753

4854
assertEquals(SqlKind.MIN, call.getAggregation().getKind());
4955
assertEquals("min_price", call.getName());
5056
}
5157

5258
public void testMaxTranslator() throws ConversionException {
5359
MaxMetricTranslator translator = new MaxMetricTranslator();
54-
AggregateCall call = translator.toAggregateCall(new MaxAggregationBuilder("max_price").field("price"), ctx.getRowType());
60+
AggregateCall call = translator.toAggregateCall(
61+
new MaxAggregationBuilder("max_price")
62+
.field("price"), ctx.getRowType());
5563

5664
assertEquals(SqlKind.MAX, call.getAggregation().getKind());
5765
assertEquals("max_price", call.getName());
@@ -62,7 +70,8 @@ public void testThrowsForUnknownField() {
6270

6371
expectThrows(
6472
ConversionException.class,
65-
() -> translator.toAggregateCall(new AvgAggregationBuilder("bad").field("nonexistent"), ctx.getRowType())
73+
() -> translator.toAggregateCall(new AvgAggregationBuilder("bad")
74+
.field("nonexistent"), ctx.getRowType())
6675
);
6776
}
6877

sandbox/plugins/dsl-query-executor/src/test/java/org/opensearch/dsl/converter/ProjectConverterTests.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,55 @@ public void testWildcardNoMatchReturnsEmptyProjection() throws ConversionExcepti
109109
assertTrue(result instanceof LogicalProject);
110110
assertEquals(0, result.getRowType().getFieldCount());
111111
}
112+
113+
public void testIncludesWithExcludesAppliesPostFilter() throws ConversionException {
114+
SearchSourceBuilder source = new SearchSourceBuilder().fetchSource(
115+
new FetchSourceContext(true, new String[] { "name", "price", "brand" }, new String[] { "price" })
116+
);
117+
ConversionContext ctx = TestUtils.createContext(source);
118+
RelNode result = converter.convert(scan, ctx);
119+
120+
assertTrue(result instanceof LogicalProject);
121+
assertEquals(2, result.getRowType().getFieldCount());
122+
assertEquals("name", result.getRowType().getFieldNames().get(0));
123+
assertEquals("brand", result.getRowType().getFieldNames().get(1));
124+
}
125+
126+
public void testWildcardIncludesWithExcludes() throws ConversionException {
127+
// Include all fields matching "* ", exclude "rating"
128+
SearchSourceBuilder source = new SearchSourceBuilder().fetchSource(
129+
new FetchSourceContext(true, new String[] { "*" }, new String[] { "rating" })
130+
);
131+
ConversionContext ctx = TestUtils.createContext(source);
132+
RelNode result = converter.convert(scan, ctx);
133+
134+
assertTrue(result instanceof LogicalProject);
135+
assertEquals(3, result.getRowType().getFieldCount());
136+
assertFalse(result.getRowType().getFieldNames().contains("rating"));
137+
}
138+
139+
public void testOverlappingWildcardsDoNotProduceDuplicates() throws ConversionException {
140+
// Both "n*" and "na*" match "name" — should only appear once
141+
SearchSourceBuilder source = new SearchSourceBuilder().fetchSource(
142+
new FetchSourceContext(true, new String[] { "n*", "na*" }, null)
143+
);
144+
ConversionContext ctx = TestUtils.createContext(source);
145+
RelNode result = converter.convert(scan, ctx);
146+
147+
assertTrue(result instanceof LogicalProject);
148+
assertEquals(1, result.getRowType().getFieldCount());
149+
assertEquals("name", result.getRowType().getFieldNames().get(0));
150+
}
151+
152+
public void testDuplicateExactFieldsProjectedOnce() throws ConversionException {
153+
SearchSourceBuilder source = new SearchSourceBuilder().fetchSource(
154+
new FetchSourceContext(true, new String[] { "name", "name" }, null)
155+
);
156+
ConversionContext ctx = TestUtils.createContext(source);
157+
RelNode result = converter.convert(scan, ctx);
158+
159+
assertTrue(result instanceof LogicalProject);
160+
assertEquals(1, result.getRowType().getFieldCount());
161+
assertEquals("name", result.getRowType().getFieldNames().get(0));
162+
}
112163
}

sandbox/plugins/dsl-query-executor/src/test/java/org/opensearch/dsl/converter/SearchSourceConverterTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,14 @@ public void testNoAggsProducesOnlyHitsPlan() throws ConversionException {
9292
assertTrue(plans.has(QueryPlans.Type.HITS));
9393
assertFalse(plans.has(QueryPlans.Type.AGGREGATION));
9494
}
95+
96+
public void testSizeZeroNoAggsProducesNoPlans() throws ConversionException {
97+
// size=0 with no aggs produces no plans — total doc count comes from analytics plugin metadata
98+
SearchSourceBuilder source = new SearchSourceBuilder().size(0);
99+
QueryPlans plans = converter.convert(source, "test-index");
100+
101+
assertEquals(0, plans.getAll().size());
102+
assertFalse(plans.has(QueryPlans.Type.HITS));
103+
assertFalse(plans.has(QueryPlans.Type.AGGREGATION));
104+
}
95105
}

sandbox/plugins/dsl-query-executor/src/test/java/org/opensearch/dsl/executor/QueryPlansTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ public void testGetReturnsMultiplePlansOfSameType() {
5050
assertEquals(2, plans.get(QueryPlans.Type.AGGREGATION).size());
5151
}
5252

53-
public void testBuilderThrowsOnEmpty() {
54-
expectThrows(IllegalStateException.class, () -> new QueryPlans.Builder().build());
53+
public void testBuilderAllowsEmpty() {
54+
// Empty plans are valid (e.g. size=0, no aggs — metadata-only response)
55+
QueryPlans plans = new QueryPlans.Builder().build();
56+
assertEquals(0, plans.getAll().size());
5557
}
5658

5759
public void testGetReturnsEmptyForMissingType() {

0 commit comments

Comments
 (0)