Skip to content

Commit e56d097

Browse files
authored
[Feature] Add table function relation syntax to SQL grammar (#5318)
* [Feature] Add table function relation to SQL grammar for vectorSearch() Add table function relation support to the SQL parser: - New `tableFunctionRelation` alternative in `relation` grammar rule - Named argument syntax: `key=value` (e.g., table='index', field='vec') - Alias is required by grammar (FROM func(...) AS alias) - AstBuilder emits existing TableFunction + SubqueryAlias AST nodes - 3 parser unit tests: basic parse, with WHERE/ORDER BY/LIMIT, alias required This is a pure grammar change — no execution support yet. Queries will parse successfully but fail at the Analyzer with "unsupported function". Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * Address review feedback on table function relation grammar 1. Canonicalize argument names at parser boundary: unquoteIdentifier + toLowerCase(Locale.ROOT) in visitTableFunctionRelation so FIELD='x' and `field`='x' both produce argName="field" 2. Make AS keyword optional (AS? alias) for consistency with tableAsRelation and subqueryAsRelation grammar rules 3. Strengthen test coverage: - Full structural AST assertion for WHERE + ORDER BY + LIMIT (verifies Sort, Limit, Filter nodes, not just toString) - Argument reorder test proves names resolve by name not position - Case canonicalization test (TABLE= → table=) - Alias-without-AS test (FROM func(...) v) Signed-off-by: Eric Wei <mengwei.eric@gmail.com> * Apply spotless formatting Signed-off-by: Eric Wei <mengwei.eric@gmail.com> --------- Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
1 parent c2c97db commit e56d097

3 files changed

Lines changed: 151 additions & 0 deletions

File tree

sql/src/main/antlr/OpenSearchSQLParser.g4

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,15 @@ fromClause
111111
relation
112112
: tableName (AS? alias)? # tableAsRelation
113113
| LR_BRACKET subquery = querySpecification RR_BRACKET AS? alias # subqueryAsRelation
114+
| qualifiedName LR_BRACKET tableFunctionArgs RR_BRACKET AS? alias # tableFunctionRelation
115+
;
116+
117+
tableFunctionArgs
118+
: tableFunctionArg (COMMA tableFunctionArg)*
119+
;
120+
121+
tableFunctionArg
122+
: ident EQUAL_SYMBOL functionArg
114123
;
115124

116125
whereClause

sql/src/main/java/org/opensearch/sql/sql/parser/AstBuilder.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,22 @@
1313
import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.SelectElementContext;
1414
import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.SubqueryAsRelationContext;
1515
import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.TableAsRelationContext;
16+
import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.TableFunctionRelationContext;
1617
import static org.opensearch.sql.sql.antlr.parser.OpenSearchSQLParser.WhereClauseContext;
1718
import static org.opensearch.sql.sql.parser.ParserUtils.getTextInQuery;
1819
import static org.opensearch.sql.utils.SystemIndexUtils.TABLE_INFO;
1920
import static org.opensearch.sql.utils.SystemIndexUtils.mappingTable;
2021

2122
import com.google.common.collect.ImmutableList;
2223
import java.util.Collections;
24+
import java.util.Locale;
2325
import java.util.Optional;
2426
import lombok.RequiredArgsConstructor;
2527
import org.antlr.v4.runtime.tree.ParseTree;
2628
import org.opensearch.sql.ast.expression.Alias;
2729
import org.opensearch.sql.ast.expression.AllFields;
2830
import org.opensearch.sql.ast.expression.Function;
31+
import org.opensearch.sql.ast.expression.UnresolvedArgument;
2932
import org.opensearch.sql.ast.expression.UnresolvedExpression;
3033
import org.opensearch.sql.ast.tree.DescribeRelation;
3134
import org.opensearch.sql.ast.tree.Filter;
@@ -34,6 +37,7 @@
3437
import org.opensearch.sql.ast.tree.Relation;
3538
import org.opensearch.sql.ast.tree.RelationSubquery;
3639
import org.opensearch.sql.ast.tree.SubqueryAlias;
40+
import org.opensearch.sql.ast.tree.TableFunction;
3741
import org.opensearch.sql.ast.tree.UnresolvedPlan;
3842
import org.opensearch.sql.ast.tree.Values;
3943
import org.opensearch.sql.common.antlr.SyntaxCheckException;
@@ -189,6 +193,24 @@ public UnresolvedPlan visitSubqueryAsRelation(SubqueryAsRelationContext ctx) {
189193
return new RelationSubquery(visit(ctx.subquery), subqueryAlias);
190194
}
191195

196+
@Override
197+
public UnresolvedPlan visitTableFunctionRelation(TableFunctionRelationContext ctx) {
198+
ImmutableList.Builder<UnresolvedExpression> args = ImmutableList.builder();
199+
ctx.tableFunctionArgs()
200+
.tableFunctionArg()
201+
.forEach(
202+
arg -> {
203+
String argName =
204+
StringUtils.unquoteIdentifier(arg.ident().getText()).toLowerCase(Locale.ROOT);
205+
UnresolvedExpression argValue = visitAstExpression(arg.functionArg());
206+
args.add(new UnresolvedArgument(argName, argValue));
207+
});
208+
TableFunction tableFunction =
209+
new TableFunction(visitAstExpression(ctx.qualifiedName()), args.build());
210+
String alias = StringUtils.unquoteIdentifier(ctx.alias().getText());
211+
return new SubqueryAlias(alias, tableFunction);
212+
}
213+
192214
@Override
193215
public UnresolvedPlan visitWhereClause(WhereClauseContext ctx) {
194216
return new Filter(visitAstExpression(ctx.expression()));

sql/src/test/java/org/opensearch/sql/sql/parser/AstBuilderTest.java

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
import org.opensearch.sql.ast.expression.DataType;
4141
import org.opensearch.sql.ast.expression.Literal;
4242
import org.opensearch.sql.ast.expression.NestedAllTupleFields;
43+
import org.opensearch.sql.ast.expression.UnresolvedArgument;
44+
import org.opensearch.sql.ast.tree.SubqueryAlias;
45+
import org.opensearch.sql.ast.tree.TableFunction;
4346
import org.opensearch.sql.common.antlr.SyntaxCheckException;
4447

4548
class AstBuilderTest extends AstBuilderTestBase {
@@ -131,6 +134,123 @@ public void can_build_from_index_with_alias_quoted() {
131134
buildAST("SELECT `t`.name FROM test `t` WHERE `t`.age = 30"));
132135
}
133136

137+
@Test
138+
public void can_build_from_table_function() {
139+
assertEquals(
140+
project(
141+
new SubqueryAlias(
142+
"v",
143+
new TableFunction(
144+
qualifiedName("vectorSearch"),
145+
ImmutableList.of(
146+
new UnresolvedArgument("table", stringLiteral("products")),
147+
new UnresolvedArgument("field", stringLiteral("embedding")),
148+
new UnresolvedArgument("vector", stringLiteral("[0.1,0.2]")),
149+
new UnresolvedArgument("option", stringLiteral("k=10"))))),
150+
AllFields.of()),
151+
buildAST(
152+
"SELECT * FROM vectorSearch("
153+
+ "table='products', field='embedding', "
154+
+ "vector='[0.1,0.2]', option='k=10') AS v"));
155+
}
156+
157+
@Test
158+
public void can_build_from_table_function_with_where_order_limit() {
159+
assertEquals(
160+
project(
161+
limit(
162+
sort(
163+
filter(
164+
new SubqueryAlias(
165+
"s",
166+
new TableFunction(
167+
qualifiedName("vectorSearch"),
168+
ImmutableList.of(
169+
new UnresolvedArgument("table", stringLiteral("products")),
170+
new UnresolvedArgument("field", stringLiteral("embedding")),
171+
new UnresolvedArgument("vector", stringLiteral("[0.1,0.2]")),
172+
new UnresolvedArgument("option", stringLiteral("k=10"))))),
173+
function("=", qualifiedName("s", "category"), stringLiteral("shoes"))),
174+
field(qualifiedName("s", "_score"), argument("asc", booleanLiteral(false)))),
175+
5,
176+
0),
177+
alias("s.title", qualifiedName("s", "title")),
178+
alias("s._score", qualifiedName("s", "_score"))),
179+
buildAST(
180+
"SELECT s.title, s._score FROM vectorSearch("
181+
+ "table='products', field='embedding', "
182+
+ "vector='[0.1,0.2]', option='k=10') AS s "
183+
+ "WHERE s.category = 'shoes' "
184+
+ "ORDER BY s._score DESC "
185+
+ "LIMIT 5"));
186+
}
187+
188+
@Test
189+
public void table_function_args_are_resolved_by_name_not_position() {
190+
assertEquals(
191+
project(
192+
new SubqueryAlias(
193+
"v",
194+
new TableFunction(
195+
qualifiedName("vectorSearch"),
196+
ImmutableList.of(
197+
new UnresolvedArgument("option", stringLiteral("k=10")),
198+
new UnresolvedArgument("field", stringLiteral("embedding")),
199+
new UnresolvedArgument("table", stringLiteral("products")),
200+
new UnresolvedArgument("vector", stringLiteral("[0.1,0.2]"))))),
201+
AllFields.of()),
202+
buildAST(
203+
"SELECT * FROM vectorSearch("
204+
+ "option='k=10', field='embedding', "
205+
+ "table='products', vector='[0.1,0.2]') AS v"));
206+
}
207+
208+
@Test
209+
public void table_function_arg_names_are_canonicalized() {
210+
assertEquals(
211+
project(
212+
new SubqueryAlias(
213+
"v",
214+
new TableFunction(
215+
qualifiedName("vectorSearch"),
216+
ImmutableList.of(
217+
new UnresolvedArgument("table", stringLiteral("products")),
218+
new UnresolvedArgument("field", stringLiteral("embedding")),
219+
new UnresolvedArgument("vector", stringLiteral("[0.1,0.2]")),
220+
new UnresolvedArgument("option", stringLiteral("k=10"))))),
221+
AllFields.of()),
222+
buildAST(
223+
"SELECT * FROM vectorSearch("
224+
+ "TABLE='products', FIELD='embedding', "
225+
+ "VECTOR='[0.1,0.2]', OPTION='k=10') AS v"));
226+
}
227+
228+
@Test
229+
public void table_function_allows_alias_without_as_keyword() {
230+
assertEquals(
231+
project(
232+
new SubqueryAlias(
233+
"v",
234+
new TableFunction(
235+
qualifiedName("vectorSearch"),
236+
ImmutableList.of(
237+
new UnresolvedArgument("table", stringLiteral("products")),
238+
new UnresolvedArgument("vector", stringLiteral("[0.1]"))))),
239+
AllFields.of()),
240+
buildAST("SELECT * FROM vectorSearch(table='products', vector='[0.1]') v"));
241+
}
242+
243+
@Test
244+
public void table_function_relation_requires_alias() {
245+
assertThrows(
246+
SyntaxCheckException.class,
247+
() ->
248+
buildAST(
249+
"SELECT * FROM vectorSearch("
250+
+ "table='products', field='embedding', "
251+
+ "vector='[0.1,0.2]', option='k=10')"));
252+
}
253+
134254
@Test
135255
public void can_build_where_clause() {
136256
assertEquals(

0 commit comments

Comments
 (0)