Skip to content

Commit d1de84c

Browse files
mountaincrabedeadingtonclaude
authored
Only read date unit words as date units when they are function arguments (#2836)
An unquoted YEAR, MONTH, DAY, HOUR, MINUTE or SECOND was parsed as a DateUnitExpression anywhere an expression could start, so a column with one of those names lost its spelling and its qualifier, and broke UPDATE ... SET (a, year) = ... with a ClassCastException. The word is now parsed as a Column and converted to a DateUnitExpression only when it is a whole argument of a function, which keeps its original spelling. Co-authored-by: Ellie Eadington <ellie.eadington@matillion.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f44ef8f commit d1de84c

3 files changed

Lines changed: 113 additions & 10 deletions

File tree

‎src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ public class DateUnitExpression extends ASTNodeAccessImpl implements Expression
1818

1919
private final DateUnit type;
2020

21+
private final String text;
22+
2123
public DateUnitExpression(DateUnit type) {
2224
this.type = Objects.requireNonNull(type);
25+
this.text = type.toString();
2326
}
2427

2528
public DateUnitExpression(String DateUnitStr) {
2629
this.type = Objects.requireNonNull(DateUnit.from(DateUnitStr));
30+
this.text = DateUnitStr;
2731
}
2832

2933
public DateUnit getType() {
@@ -38,7 +42,7 @@ public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
3842

3943
@Override
4044
public String toString() {
41-
return type.toString();
45+
return text;
4246
}
4347

4448
public enum DateUnit {

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

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,47 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
13131313
function.setExtraKeyword(getNextToken().image);
13141314
}
13151315

1316+
/**
1317+
* An unquoted YEAR, MONTH, DAY, HOUR, MINUTE or SECOND passed on its own as a function argument, as in
1318+
* DATEADD(day, 1, d), is a date unit. Anywhere else it is an ordinary identifier, so it is parsed as a
1319+
* Column and only converted here, once it is known to be a whole argument.
1320+
*/
1321+
private void normalizeDateUnitArguments(Function function) {
1322+
ExpressionList<?> parameters = function.getParameters();
1323+
if (parameters == null || parameters instanceof ParenthesedExpressionList) {
1324+
return;
1325+
}
1326+
ExpressionList<Expression> normalized = null;
1327+
for (int i = 0; i < parameters.size(); i++) {
1328+
if (!isDateUnitColumn(parameters.get(i))) {
1329+
continue;
1330+
}
1331+
Column column = (Column) parameters.get(i);
1332+
DateUnitExpression unit = new DateUnitExpression(column.getColumnName());
1333+
linkAST(unit, column.getASTNode());
1334+
if (normalized == null) {
1335+
normalized = new ExpressionList<Expression>(parameters);
1336+
normalized.setASTNode(parameters.getASTNode());
1337+
}
1338+
normalized.set(i, unit);
1339+
}
1340+
if (normalized != null) {
1341+
function.setParameters(normalized);
1342+
}
1343+
}
1344+
1345+
private static boolean isDateUnitColumn(Expression expression) {
1346+
if (!(expression instanceof Column)) {
1347+
return false;
1348+
}
1349+
Node node = ((Column) expression).getASTNode();
1350+
if (node == null) {
1351+
return false;
1352+
}
1353+
Token first = node.jjtGetFirstToken();
1354+
return first != null && first == node.jjtGetLastToken() && first.kind == K_DATE_LITERAL;
1355+
}
1356+
13161357
private void normalizeOdbcTimestampInterval(Function function) {
13171358
if (!function.isEscaped() || function.getMultipartName().size() != 1
13181359
|| !("TIMESTAMPADD".equalsIgnoreCase(function.getName())
@@ -12868,8 +12909,6 @@ Expression PrimaryExpression() #PrimaryExpression:
1286812909
| LOOKAHEAD({ !interrupted && isFunctionAhead() })
1286912910
retval = Function() [ LOOKAHEAD(2) retval = AnalyticExpression( (Function) retval ) ]
1287012911

12871-
| LOOKAHEAD(2) retval = DateUnitExpression()
12872-
1287312912
| LOOKAHEAD(2, {!interrupted}) retval = IntervalExpression() { dateExpressionAllowed = false; }
1287412913

1287512914
| token=<S_DOUBLE> { retval = new DoubleValue(token.image); }
@@ -13191,13 +13230,6 @@ DateTimeLiteralExpression DateTimeLiteralExpression() : {
1319113230
}
1319213231
}
1319313232

13194-
DateUnitExpression DateUnitExpression() : {
13195-
Token t;
13196-
}
13197-
{
13198-
t=<K_DATE_LITERAL> { return new DateUnitExpression( t.image ); }
13199-
}
13200-
1320113233
RangeExpression RangeExpression(Expression startExpression):
1320213234
{
1320313235
Expression endExpression;
@@ -14851,6 +14883,7 @@ Function Function() #Function:
1485114883
| function = InternalFunction(false)
1485214884
)
1485314885
{
14886+
normalizeDateUnitArguments(function);
1485414887
linkAST(function,jjtThis);
1485514888
return function;
1485614889
}

‎src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java‎

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

1212
import net.sf.jsqlparser.JSQLParserException;
13+
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
14+
import net.sf.jsqlparser.schema.Column;
1315
import net.sf.jsqlparser.statement.select.PlainSelect;
16+
import net.sf.jsqlparser.statement.update.Update;
1417
import net.sf.jsqlparser.test.TestUtils;
1518
import org.assertj.core.api.Assertions;
1619
import org.junit.jupiter.api.Test;
@@ -27,4 +30,67 @@ void testParsing() throws JSQLParserException {
2730
Function f = select.getSelectItem(0).getExpression(Function.class);
2831
Assertions.assertThat(f.getParameters().get(1)).isInstanceOf(DateUnitExpression.class);
2932
}
33+
34+
@Test
35+
void testUnitArgumentKeepsItsSpelling() throws JSQLParserException {
36+
String sqlStr = "SELECT DATEADD(day, 1, d) FROM t";
37+
38+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr);
39+
40+
Function f = select.getSelectItem(0).getExpression(Function.class);
41+
DateUnitExpression unit = (DateUnitExpression) f.getParameters().get(0);
42+
Assertions.assertThat(unit.getType()).isEqualTo(DateUnitExpression.DateUnit.DAY);
43+
Assertions.assertThat(unit).hasToString("day");
44+
}
45+
46+
@Test
47+
void testUnitWordOutsideFunctionArgumentsIsColumn() throws JSQLParserException {
48+
String sqlStr = "SELECT year FROM t";
49+
50+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr);
51+
52+
Column column = select.getSelectItem(0).getExpression(Column.class);
53+
Assertions.assertThat(column.getColumnName()).isEqualTo("year");
54+
}
55+
56+
@Test
57+
void testUnitWordAsQualifierIsColumn() throws JSQLParserException {
58+
String sqlStr = "SELECT month.amount FROM sales month";
59+
60+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr);
61+
62+
Column column = select.getSelectItem(0).getExpression(Column.class);
63+
Assertions.assertThat(column.getFullyQualifiedName()).isEqualTo("month.amount");
64+
}
65+
66+
@Test
67+
void testUnitWordWithinArgumentExpressionIsColumn() throws JSQLParserException {
68+
String sqlStr = "SELECT COALESCE(year + 1, 0) FROM t";
69+
70+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr);
71+
72+
Function f = select.getSelectItem(0).getExpression(Function.class);
73+
Addition addition = (Addition) f.getParameters().get(0);
74+
Assertions.assertThat(addition.getLeftExpression()).isInstanceOf(Column.class);
75+
}
76+
77+
@Test
78+
void testQuotedUnitWordArgumentIsColumn() throws JSQLParserException {
79+
String sqlStr = "SELECT DATEADD(\"day\", 1, d) FROM t";
80+
81+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr);
82+
83+
Function f = select.getSelectItem(0).getExpression(Function.class);
84+
Assertions.assertThat(f.getParameters().get(0)).isInstanceOf(Column.class);
85+
}
86+
87+
@Test
88+
void testUnitWordInUpdateSetTupleIsColumn() throws JSQLParserException {
89+
String sqlStr = "UPDATE t SET (a, year) = (1, 2)";
90+
91+
Update update = (Update) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr);
92+
93+
Assertions.assertThat(update.getUpdateSets().get(0).getColumn(1).getColumnName())
94+
.isEqualTo("year");
95+
}
3096
}

0 commit comments

Comments
 (0)