Skip to content

Commit 31aa8b9

Browse files
edeadingtonclaude
andcommitted
Only read date unit words as date units when they are function arguments
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: Claude Opus 5.5 <noreply@anthropic.com>
1 parent bdf8ac3 commit 31aa8b9

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
@@ -1039,6 +1039,47 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
10391039
function.setExtraKeyword(getNextToken().image);
10401040
}
10411041

1042+
/**
1043+
* An unquoted YEAR, MONTH, DAY, HOUR, MINUTE or SECOND passed on its own as a function argument, as in
1044+
* DATEADD(day, 1, d), is a date unit. Anywhere else it is an ordinary identifier, so it is parsed as a
1045+
* Column and only converted here, once it is known to be a whole argument.
1046+
*/
1047+
private void normalizeDateUnitArguments(Function function) {
1048+
ExpressionList<?> parameters = function.getParameters();
1049+
if (parameters == null || parameters instanceof ParenthesedExpressionList) {
1050+
return;
1051+
}
1052+
ExpressionList<Expression> normalized = null;
1053+
for (int i = 0; i < parameters.size(); i++) {
1054+
if (!isDateUnitColumn(parameters.get(i))) {
1055+
continue;
1056+
}
1057+
Column column = (Column) parameters.get(i);
1058+
DateUnitExpression unit = new DateUnitExpression(column.getColumnName());
1059+
linkAST(unit, column.getASTNode());
1060+
if (normalized == null) {
1061+
normalized = new ExpressionList<Expression>(parameters);
1062+
normalized.setASTNode(parameters.getASTNode());
1063+
}
1064+
normalized.set(i, unit);
1065+
}
1066+
if (normalized != null) {
1067+
function.setParameters(normalized);
1068+
}
1069+
}
1070+
1071+
private static boolean isDateUnitColumn(Expression expression) {
1072+
if (!(expression instanceof Column)) {
1073+
return false;
1074+
}
1075+
Node node = ((Column) expression).getASTNode();
1076+
if (node == null) {
1077+
return false;
1078+
}
1079+
Token first = node.jjtGetFirstToken();
1080+
return first != null && first == node.jjtGetLastToken() && first.kind == K_DATE_LITERAL;
1081+
}
1082+
10421083
private void normalizeOdbcTimestampInterval(Function function) {
10431084
if (!function.isEscaped() || function.getMultipartName().size() != 1
10441085
|| !("TIMESTAMPADD".equalsIgnoreCase(function.getName())
@@ -12074,8 +12115,6 @@ Expression PrimaryExpression() #PrimaryExpression:
1207412115
| LOOKAHEAD({ !interrupted && isFunctionAhead() })
1207512116
retval = Function() [ LOOKAHEAD(2) retval = AnalyticExpression( (Function) retval ) ]
1207612117

12077-
| LOOKAHEAD(2) retval = DateUnitExpression()
12078-
1207912118
| LOOKAHEAD(2, {!interrupted}) retval = IntervalExpression() { dateExpressionAllowed = false; }
1208012119

1208112120
| token=<S_DOUBLE> { retval = new DoubleValue(token.image); }
@@ -12394,13 +12433,6 @@ DateTimeLiteralExpression DateTimeLiteralExpression() : {
1239412433
}
1239512434
}
1239612435

12397-
DateUnitExpression DateUnitExpression() : {
12398-
Token t;
12399-
}
12400-
{
12401-
t=<K_DATE_LITERAL> { return new DateUnitExpression( t.image ); }
12402-
}
12403-
1240412436
RangeExpression RangeExpression(Expression startExpression):
1240512437
{
1240612438
Expression endExpression;
@@ -14052,6 +14084,7 @@ Function Function() #Function:
1405214084
| function = InternalFunction(false)
1405314085
)
1405414086
{
14087+
normalizeDateUnitArguments(function);
1405514088
linkAST(function,jjtThis);
1405614089
return function;
1405714090
}

‎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)