Skip to content

Commit a1f2d7f

Browse files
committed
feat: V2 named-argument syntax with NamedArgRewriter
Add NamedArgRewriter SqlShuttle that normalizes V2/PPL relevance syntax into MAP-based form before Calcite validation. Transforms positional and key=value arguments into MAP[paramName, value] pairs matching PPL's internal representation for uniform pushdown rules. Refactor UnifiedFunctionSpec to instance-based design with fluent builder and Category record for grouping. Use SqlUserDefinedFunction for consistency with PPL path. Add error tests and QueryErrorAssert to test base. Signed-off-by: Chen Dai <daichen@amazon.com>
1 parent 3ccc97a commit a1f2d7f

7 files changed

Lines changed: 321 additions & 165 deletions

File tree

api/src/main/java/org/opensearch/sql/api/UnifiedQueryContext.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.calcite.schema.SchemaPlus;
2626
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
2727
import org.apache.calcite.sql.parser.SqlParser;
28+
import org.apache.calcite.sql.util.SqlOperatorTables;
2829
import org.apache.calcite.tools.FrameworkConfig;
2930
import org.apache.calcite.tools.Frameworks;
3031
import org.apache.calcite.tools.Programs;
@@ -241,12 +242,13 @@ public List<?> getSettings() {
241242
private FrameworkConfig buildFrameworkConfig() {
242243
SchemaPlus rootSchema = CalciteSchema.createRootSchema(true, cacheMetadata).plus();
243244
catalogs.forEach(rootSchema::add);
244-
UnifiedFunctionSpec.registerAll(rootSchema);
245245

246246
SchemaPlus defaultSchema = findSchemaByPath(rootSchema, defaultNamespace);
247247
return Frameworks.newConfigBuilder()
248248
.parserConfig(buildParserConfig())
249-
.operatorTable(SqlStdOperatorTable.instance())
249+
.operatorTable(
250+
SqlOperatorTables.chain(
251+
SqlStdOperatorTable.instance(), UnifiedFunctionSpec.RELEVANCE.operatorTable()))
250252
.defaultSchema(defaultSchema)
251253
.traitDefs((List<RelTraitDef>) null)
252254
.programs(Programs.calc(DefaultRelMetadataProvider.INSTANCE))

api/src/main/java/org/opensearch/sql/api/UnifiedQueryPlanner.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import static org.opensearch.sql.monitor.profile.MetricName.ANALYZE;
99

10+
import java.util.function.UnaryOperator;
1011
import lombok.RequiredArgsConstructor;
1112
import org.apache.calcite.rel.RelCollation;
1213
import org.apache.calcite.rel.RelCollations;
@@ -17,6 +18,7 @@
1718
import org.apache.calcite.sql.SqlNode;
1819
import org.apache.calcite.tools.Frameworks;
1920
import org.apache.calcite.tools.Planner;
21+
import org.opensearch.sql.api.parser.NamedArgRewriter;
2022
import org.opensearch.sql.api.parser.UnifiedQueryParser;
2123
import org.opensearch.sql.ast.tree.UnresolvedPlan;
2224
import org.opensearch.sql.calcite.CalciteRelNodeVisitor;
@@ -36,6 +38,9 @@ public class UnifiedQueryPlanner {
3638
/** Unified query context for profiling support. */
3739
private final UnifiedQueryContext context;
3840

41+
/** Post-planning validators applied to the RelNode after planning. */
42+
private final UnaryOperator<RelNode> postProcessor;
43+
3944
/**
4045
* Constructs a UnifiedQueryPlanner with a unified query context.
4146
*
@@ -47,6 +52,7 @@ public UnifiedQueryPlanner(UnifiedQueryContext context) {
4752
context.getPlanContext().queryType == QueryType.SQL
4853
? new CalciteNativeStrategy(context)
4954
: new CustomVisitorStrategy(context);
55+
this.postProcessor = buildPostProcessor(context);
5056
}
5157

5258
/**
@@ -58,7 +64,7 @@ public UnifiedQueryPlanner(UnifiedQueryContext context) {
5864
*/
5965
public RelNode plan(String query) {
6066
try {
61-
return context.measure(ANALYZE, () -> strategy.plan(query));
67+
return context.measure(ANALYZE, () -> postProcessor.apply(strategy.plan(query)));
6268
} catch (SyntaxCheckException e) {
6369
// Re-throw syntax error without wrapping
6470
throw e;
@@ -67,6 +73,10 @@ public RelNode plan(String query) {
6773
}
6874
}
6975

76+
private static UnaryOperator<RelNode> buildPostProcessor(UnifiedQueryContext context) {
77+
return UnaryOperator.identity();
78+
}
79+
7080
/** Strategy interface for language-specific planning logic. */
7181
private interface PlanningStrategy {
7282
RelNode plan(String query) throws Exception;
@@ -81,7 +91,8 @@ private static class CalciteNativeStrategy implements PlanningStrategy {
8191
public RelNode plan(String query) throws Exception {
8292
try (Planner planner = Frameworks.getPlanner(context.getPlanContext().config)) {
8393
SqlNode parsed = planner.parse(query);
84-
SqlNode validated = planner.validate(parsed);
94+
SqlNode rewritten = parsed.accept(NamedArgRewriter.INSTANCE);
95+
SqlNode validated = planner.validate(rewritten);
8596
RelRoot relRoot = planner.rel(validated);
8697
return relRoot.project();
8798
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.api.parser;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import org.apache.calcite.sql.SqlBasicCall;
11+
import org.apache.calcite.sql.SqlCall;
12+
import org.apache.calcite.sql.SqlKind;
13+
import org.apache.calcite.sql.SqlLiteral;
14+
import org.apache.calcite.sql.SqlNode;
15+
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
16+
import org.apache.calcite.sql.parser.SqlParserPos;
17+
import org.apache.calcite.sql.util.SqlShuttle;
18+
import org.checkerframework.checker.nullness.qual.Nullable;
19+
import org.opensearch.sql.api.spec.UnifiedFunctionSpec;
20+
21+
/**
22+
* Pre-validation rewriter that normalizes V2/PPL relevance function syntax into MAP-based form
23+
* matching PPL's internal representation. This ensures both SQL and PPL paths produce identical
24+
* query plans for pushdown rules.
25+
*
26+
* <p>Transforms: {@code match(name, 'John', operator='AND', boost=2.0)} into: {@code
27+
* match(MAP['field', name], MAP['query', 'John'], MAP['operator', 'AND'], MAP['boost', 2.0])}
28+
*
29+
* <p>Param names are resolved via {@link UnifiedFunctionSpec#getParamNames} from the operator's
30+
* {@link org.apache.calcite.sql.type.SqlOperandMetadata}. This runs before Calcite validation, when
31+
* operators are still unresolved.
32+
*/
33+
public final class NamedArgRewriter extends SqlShuttle {
34+
35+
public static final NamedArgRewriter INSTANCE = new NamedArgRewriter();
36+
37+
private NamedArgRewriter() {}
38+
39+
@Override
40+
public @Nullable SqlNode visit(SqlCall call) {
41+
SqlCall visited = (SqlCall) super.visit(call);
42+
if (visited == null) {
43+
return null;
44+
}
45+
UnifiedFunctionSpec spec = UnifiedFunctionSpec.of(visited.getOperator().getName());
46+
if (spec == null) {
47+
return visited;
48+
}
49+
return rewriteToMaps(visited, spec.getParamNames());
50+
}
51+
52+
/**
53+
* Wraps positional args in MAP[paramName, value] and flattens {@code key=value} comparisons into
54+
* MAP[key, value]. Produces the same MAP-based form as PPL's internal representation.
55+
*/
56+
private static SqlNode rewriteToMaps(SqlCall call, List<String> paramNames) {
57+
List<SqlNode> operands = call.getOperandList();
58+
List<SqlNode> result = new ArrayList<>();
59+
60+
for (int i = 0; i < operands.size(); i++) {
61+
SqlNode operand = operands.get(i);
62+
if (operand != null && operand.getKind() == SqlKind.EQUALS) {
63+
// Optional param: operator='AND' → MAP['operator', 'AND']
64+
SqlCall eq = (SqlCall) operand;
65+
result.add(toMap(eq.operand(0).toString(), eq.operand(1)));
66+
} else if (i < paramNames.size()) {
67+
// Required positional param: name → MAP['field', name]
68+
result.add(toMap(paramNames.get(i), operand));
69+
} else {
70+
result.add(operand);
71+
}
72+
}
73+
return new SqlBasicCall(
74+
call.getOperator(), result.toArray(SqlNode[]::new), call.getParserPosition());
75+
}
76+
77+
private static SqlNode toMap(String key, SqlNode value) {
78+
return new SqlBasicCall(
79+
SqlStdOperatorTable.MAP_VALUE_CONSTRUCTOR,
80+
new SqlNode[] {SqlLiteral.createCharString(key, SqlParserPos.ZERO), value},
81+
SqlParserPos.ZERO);
82+
}
83+
}

0 commit comments

Comments
 (0)