Skip to content

Commit cd71aad

Browse files
feat: Allow more Function keyword arguments (fixes 2 more Special Oracle tests)
Signed-off-by: Andreas Reichel <andreas@manticore-projects.com> Signed-off-by: manticore-projects <andreas@manticore-projects.com>
1 parent ff28f82 commit cd71aad

File tree

5 files changed

+94
-8
lines changed

5 files changed

+94
-8
lines changed

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8929,7 +8929,7 @@ Function InternalFunction(boolean escaped):
89298929
Limit limit;
89308930
Token extraKeywordToken;
89318931
String keywordArgName;
8932-
Expression keywordArgExpr;
8932+
ExpressionList keywordArgExprList;
89338933
List<Function.KeywordArgument> keywordArgs = null;
89348934
}
89358935
{
@@ -8988,16 +8988,21 @@ Function InternalFunction(boolean escaped):
89888988

89898989
// Generic keyword-argument tail: catches KEYWORD expr pairs that are not
89908990
// consumed by any of the explicit clauses above.
8991-
// Examples: SEPARATOR ',', USING utf8, DELIMITER CHR(10), FORMAT 'json'
8991+
// Examples: SEPARATOR ',', USING utf8, DELIMITER CHR(10),
8992+
// USING col1, col2, col3 (Oracle Data Mining)
89928993
(
89938994
LOOKAHEAD(2, { isKeywordArgumentAhead() })
89948995
keywordArgName = KeywordArgumentName()
8995-
keywordArgExpr = SimpleExpression()
8996+
keywordArgExprList = SimpleExpressionList()
89968997
{
89978998
if (keywordArgs == null) {
89988999
keywordArgs = new ArrayList<Function.KeywordArgument>();
89999000
}
9000-
keywordArgs.add(new Function.KeywordArgument(keywordArgName, keywordArgExpr));
9001+
// Unwrap single-element lists so the API returns the expression directly
9002+
Expression keywordArgValue = keywordArgExprList.size() == 1
9003+
? (Expression) keywordArgExprList.get(0)
9004+
: keywordArgExprList;
9005+
keywordArgs.add(new Function.KeywordArgument(keywordArgName, keywordArgValue));
90019006
}
90029007
)*
90039008

src/test/java/net/sf/jsqlparser/expression/FunctionKeywordArgumentTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package net.sf.jsqlparser.expression;
1111

1212
import net.sf.jsqlparser.JSQLParserException;
13+
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
1314
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1415
import net.sf.jsqlparser.statement.Statement;
1516
import net.sf.jsqlparser.statement.select.PlainSelect;
@@ -247,6 +248,34 @@ static Stream<Arguments> roundtripSqlProvider() {
247248
"ALL + ORDER BY + SEPARATOR",
248249
"SELECT my_agg(ALL col ORDER BY col SEPARATOR ',') FROM t"),
249250

251+
// -- Multi-value keyword arguments (USING col1, col2, ...) ---
252+
// Oracle Data Mining functions use USING followed by a
253+
// comma-separated column list.
254+
255+
Arguments.of(
256+
"Oracle PREDICTION with COST MODEL and USING column list",
257+
"SELECT PREDICTION(dt_sh_clas_sample COST MODEL USING cust_marital_status, education, household_size) FROM t"),
258+
259+
Arguments.of(
260+
"Oracle PREDICTION in WHERE clause",
261+
"SELECT cust_gender, COUNT(*) AS cnt FROM mining_data_apply_v WHERE PREDICTION(dt_sh_clas_sample COST MODEL USING cust_marital_status, education, household_size) = 1 GROUP BY cust_gender ORDER BY cust_gender"),
262+
263+
Arguments.of(
264+
"Oracle PREDICTION_PROBABILITY with USING",
265+
"SELECT PREDICTION_PROBABILITY(my_model USING col1, col2, col3) FROM t"),
266+
267+
Arguments.of(
268+
"Oracle CLUSTER_ID with USING",
269+
"SELECT CLUSTER_ID(my_model USING col1, col2) FROM t"),
270+
271+
Arguments.of(
272+
"USING with single column",
273+
"SELECT my_func(model USING col1) FROM t"),
274+
275+
Arguments.of(
276+
"USING with many columns",
277+
"SELECT my_func(model USING a, b, c, d, e) FROM t"),
278+
250279
// -- Keyword arg in different SQL contexts -------------------
251280

252281
Arguments.of(
@@ -291,6 +320,31 @@ void testRoundtrip(String label, String sql) throws JSQLParserException {
291320
+ " reparsed: " + stmt2);
292321
}
293322

323+
// ====================================================================
324+
// GitHub Issue #688 / #1257 - CONVERT(expr USING charset)
325+
// These were ParseExceptions before the generic keyword-arg tail.
326+
// ====================================================================
327+
328+
@Test
329+
void testIssue688_ConvertUsingGbk() throws JSQLParserException {
330+
// Exact SQL from issue #688 — was a ParseException before
331+
String sql = "SELECT * FROM a ORDER BY CONVERT(a.name USING gbk) DESC";
332+
Statement stmt = CCJSqlParserUtil.parse(sql);
333+
assertNotNull(stmt);
334+
// Roundtrip
335+
String deparsed = stmt.toString();
336+
assertEquals(deparsed, CCJSqlParserUtil.parse(deparsed).toString());
337+
}
338+
339+
@Test
340+
void testIssue1257_ConvertUsingGBK() throws JSQLParserException {
341+
// Exact SQL from issue #1257
342+
String sql =
343+
"SELECT id, name FROM tbl_template WHERE name LIKE ? ORDER BY CONVERT(name USING GBK) ASC";
344+
Statement stmt = CCJSqlParserUtil.parse(sql);
345+
assertNotNull(stmt);
346+
}
347+
294348
// ====================================================================
295349
// GROUP_CONCAT migration - now parsed as Function, not MySQLGroupConcat
296350
// ====================================================================
@@ -388,6 +442,31 @@ void testMultipleKeywordArguments() throws JSQLParserException {
388442
assertEquals("'utf8'", kwArgs.get(1).getExpression().toString());
389443
}
390444

445+
@Test
446+
void testMultiValueKeywordArgument_OraclePrediction() throws JSQLParserException {
447+
String sql = "SELECT PREDICTION(my_model COST MODEL USING col1, col2, col3) FROM t";
448+
Statement stmt = CCJSqlParserUtil.parse(sql);
449+
Function func = extractFirstFunction(stmt);
450+
451+
assertNotNull(func);
452+
assertEquals("PREDICTION", func.getName());
453+
454+
List<Function.KeywordArgument> kwArgs = func.getKeywordArguments();
455+
assertNotNull(kwArgs);
456+
assertEquals(2, kwArgs.size());
457+
458+
// COST MODEL — single value, unwrapped to Column
459+
assertEquals("COST", kwArgs.get(0).getKeyword().toUpperCase());
460+
assertEquals("MODEL", kwArgs.get(0).getExpression().toString());
461+
462+
// USING col1, col2, col3 — multi-value, kept as ExpressionList
463+
assertEquals("USING", kwArgs.get(1).getKeyword().toUpperCase());
464+
Expression usingExpr = kwArgs.get(1).getExpression();
465+
assertInstanceOf(ExpressionList.class,
466+
usingExpr, "Multi-value keyword arg should be an ExpressionList");
467+
assertEquals("col1, col2, col3", usingExpr.toString());
468+
}
469+
391470
@Test
392471
void testGetKeywordArgumentValue() throws JSQLParserException {
393472
String sql = "SELECT my_agg(col SEPARATOR ',' ENCODING 'utf8') FROM t";

src/test/java/net/sf/jsqlparser/statement/select/SpecialOracleTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public class SpecialOracleTest {
9090
"for_update01.sql", "for_update02.sql", "for_update03.sql", "function04.sql",
9191
"function05.sql", "for_update04.sql", "for_update05.sql", "for_update06.sql",
9292
"function01.sql", "function02.sql", "function03.sql",
93-
"function06.sql",
93+
"function06.sql", "function07.sql",
9494
"groupby01.sql",
9595
"groupby02.sql", "groupby03.sql", "groupby04.sql", "groupby05.sql", "groupby06.sql",
9696
"groupby08.sql", "groupby09.sql", "groupby10.sql", "groupby11.sql", "groupby12.sql",
@@ -119,7 +119,7 @@ public class SpecialOracleTest {
119119
"simple07.sql", "simple08.sql", "simple09.sql", "simple10.sql", "simple11.sql",
120120
"simple12.sql", "simple13.sql", "union01.sql", "union02.sql", "union03.sql",
121121
"union04.sql", "union05.sql", "union06.sql", "union07.sql", "union08.sql",
122-
"union09.sql", "union10.sql", "xmltable02.sql");
122+
"union09.sql", "union10.sql", "xmltable01.sql", "xmltable02.sql");
123123

124124
@Test
125125
public void testAllSqlsParseDeparse() throws IOException {

src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/function07.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ select cust_gender, count(*) as cnt, round(avg(age)) as avg_age
1919
--@FAILURE: Encountered: "(" / "(", at line 12, column 20, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:08
2020
--@FAILURE: Encountered: <OPENING_BRACKET> / "(", at line 12, column 20, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:17
2121
--@FAILURE: Encountered: <S_IDENTIFIER> / "cost", at line 12, column 39, in lexical state DEFAULT. recorded first on 12 Mar 2026, 20:27:52
22-
--@FAILURE: Encountered: <K_COMMA> / ",", at line 13, column 32, in lexical state DEFAULT. recorded first on 13 Mar 2026, 20:40:43
22+
--@FAILURE: Encountered: <K_COMMA> / ",", at line 13, column 32, in lexical state DEFAULT. recorded first on 13 Mar 2026, 20:40:43
23+
--@SUCCESSFULLY_PARSED_AND_DEPARSED first on 14 Mar 2026, 01:34:50

src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/xmltable01.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ warehouse2
1818

1919
--@FAILURE: Encountered unexpected token: "(" "(" recorded first on Aug 3, 2021, 7:20:08 AM
2020
--@FAILURE: Encountered: "(" / "(", at line 12, column 10, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:09
21-
--@FAILURE: Encountered: <OPENING_BRACKET> / "(", at line 12, column 10, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:18
21+
--@FAILURE: Encountered: <OPENING_BRACKET> / "(", at line 12, column 10, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:18
22+
--@SUCCESSFULLY_PARSED_AND_DEPARSED first on 14 Mar 2026, 01:34:50

0 commit comments

Comments
 (0)