Skip to content

Commit 35978d7

Browse files
committed
feat(sql): Support ARRAY[] multi-field syntax for relevance functions
Add support for standard SQL ARRAY syntax in multi-field relevance functions (multi_match, simple_query_string, query_string). The NamedArgRewriter expands ARRAY['f1','f2'] into a MAP with VARCHAR-typed field names and default weight 1.0, producing plans compatible with the Analytics engine pushdown rules. Syntax: multi_match(ARRAY['name', 'department'], 'John') The key technique is wrapping each field literal in CAST(... AS VARCHAR) at the SqlNode level so Calcite's validator produces bare RexLiterals without type-widening CASTs in the final plan. Signed-off-by: Chen Dai <daichen@amazon.com>
1 parent 0ff1eec commit 35978d7

2 files changed

Lines changed: 69 additions & 1 deletion

File tree

api/src/main/java/org/opensearch/sql/api/spec/search/NamedArgRewriter.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@
55

66
package org.opensearch.sql.api.spec.search;
77

8+
import java.math.BigDecimal;
9+
import java.util.ArrayList;
810
import java.util.List;
911
import lombok.AccessLevel;
1012
import lombok.NoArgsConstructor;
13+
import org.apache.calcite.sql.SqlBasicTypeNameSpec;
1114
import org.apache.calcite.sql.SqlCall;
15+
import org.apache.calcite.sql.SqlDataTypeSpec;
1216
import org.apache.calcite.sql.SqlIdentifier;
1317
import org.apache.calcite.sql.SqlKind;
1418
import org.apache.calcite.sql.SqlLiteral;
1519
import org.apache.calcite.sql.SqlNode;
1620
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
1721
import org.apache.calcite.sql.parser.SqlParserPos;
22+
import org.apache.calcite.sql.type.SqlTypeName;
1823
import org.apache.calcite.sql.util.SqlShuttle;
1924
import org.checkerframework.checker.nullness.qual.Nullable;
2025
import org.opensearch.sql.api.spec.UnifiedFunctionSpec;
@@ -31,6 +36,10 @@ public final class NamedArgRewriter extends SqlShuttle {
3136

3237
public static final NamedArgRewriter INSTANCE = new NamedArgRewriter();
3338

39+
private static final SqlDataTypeSpec VARCHAR_TYPE =
40+
new SqlDataTypeSpec(
41+
new SqlBasicTypeNameSpec(SqlTypeName.VARCHAR, SqlParserPos.ZERO), SqlParserPos.ZERO);
42+
3443
@Override
3544
public @Nullable SqlNode visit(SqlCall call) {
3645
SqlCall visited = (SqlCall) super.visit(call);
@@ -44,6 +53,7 @@ public final class NamedArgRewriter extends SqlShuttle {
4453
* Rewrites each argument into a MAP entry. For match(name, 'John', operator='AND'):
4554
* <li>Positional arg: name → MAP('field', name)
4655
* <li>Named arg: operator='AND' → MAP('operator', 'AND')
56+
* <li>ARRAY arg: ARRAY['f1','f2'] → MAP('fields', MAP(CAST('f1' AS VARCHAR), 1, ...))
4757
*/
4858
private static SqlCall rewriteToMaps(SqlCall call, List<String> paramNames) {
4959
List<SqlNode> operands = call.getOperandList();
@@ -62,12 +72,34 @@ private static SqlCall rewriteToMaps(SqlCall call, List<String> paramNames) {
6272
throw new IllegalArgumentException(
6373
String.format("Invalid arguments for function '%s'", call.getOperator().getName()));
6474
}
65-
maps[i] = toMap(paramNames.get(i), op);
75+
String paramName = paramNames.get(i);
76+
if ("fields".equals(paramName)
77+
&& op instanceof SqlCall arrayCall
78+
&& arrayCall.getOperator() == SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR) {
79+
maps[i] = toMap(paramName, expandFieldsArray(arrayCall));
80+
} else {
81+
maps[i] = toMap(paramName, op);
82+
}
6683
}
6784
}
6885
return call.getOperator().createCall(call.getParserPosition(), maps);
6986
}
7087

88+
/** Expands ARRAY['f1', 'f2'] into MAP(CAST('f1' AS VARCHAR), 1, CAST('f2' AS VARCHAR), 1). */
89+
private static SqlNode expandFieldsArray(SqlCall arrayCall) {
90+
List<SqlNode> mapArgs = new ArrayList<>();
91+
for (SqlNode element : arrayCall.getOperandList()) {
92+
mapArgs.add(castToVarchar(element));
93+
mapArgs.add(SqlLiteral.createExactNumeric(BigDecimal.ONE.toPlainString(), SqlParserPos.ZERO));
94+
}
95+
return SqlStdOperatorTable.MAP_VALUE_CONSTRUCTOR.createCall(
96+
SqlParserPos.ZERO, mapArgs.toArray(SqlNode[]::new));
97+
}
98+
99+
private static SqlNode castToVarchar(SqlNode node) {
100+
return SqlStdOperatorTable.CAST.createCall(SqlParserPos.ZERO, node, VARCHAR_TYPE);
101+
}
102+
71103
private static SqlNode toMap(String key, SqlNode value) {
72104
return SqlStdOperatorTable.MAP_VALUE_CONSTRUCTOR.createCall(
73105
SqlParserPos.ZERO, SqlLiteral.createCharString(key, SqlParserPos.ZERO), value);

api/src/test/java/org/opensearch/sql/api/UnifiedRelevanceSearchSqlTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,42 @@ SELECT upper(name) FROM catalog.employees\
145145
// FIXME: Calcite's SQL parser does not support V2 bracket field list syntax ['field1', 'field2'].
146146
// Multi-field relevance functions only accept a single column reference in the Calcite SQL path.
147147

148+
@Test
149+
public void testMultiMatchArraySyntax() {
150+
givenQuery(
151+
"""
152+
SELECT * FROM catalog.employees
153+
WHERE multi_match(ARRAY['name', 'department'], 'John')\
154+
""")
155+
.assertPlanContains(
156+
"multi_match(MAP('fields', MAP('name':VARCHAR, 1, 'department':VARCHAR, 1)),"
157+
+ " MAP('query', 'John'))");
158+
}
159+
160+
@Test
161+
public void testSimpleQueryStringArraySyntax() {
162+
givenQuery(
163+
"""
164+
SELECT * FROM catalog.employees
165+
WHERE simple_query_string(ARRAY['name', 'department'], 'John')\
166+
""")
167+
.assertPlanContains(
168+
"simple_query_string(MAP('fields', MAP('name':VARCHAR, 1,"
169+
+ " 'department':VARCHAR, 1)), MAP('query', 'John'))");
170+
}
171+
172+
@Test
173+
public void testQueryStringArraySyntax() {
174+
givenQuery(
175+
"""
176+
SELECT * FROM catalog.employees
177+
WHERE query_string(ARRAY['name', 'department'], 'John')\
178+
""")
179+
.assertPlanContains(
180+
"query_string(MAP('fields', MAP('name':VARCHAR, 1,"
181+
+ " 'department':VARCHAR, 1)), MAP('query', 'John'))");
182+
}
183+
148184
@Test
149185
public void testMultiMatchBracketSyntaxNotSupported() {
150186
givenInvalidQuery(

0 commit comments

Comments
 (0)