From 31aa8b9c34edb59fe66c580960540b89bdcc4433 Mon Sep 17 00:00:00 2001 From: Ellie Eadington Date: Thu, 15 Oct 2026 10:48:45 +0100 Subject: [PATCH] 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 --- .../expression/DateUnitExpression.java | 6 +- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 51 +++++++++++--- .../expression/DateUnitExpressionTest.java | 66 +++++++++++++++++++ 3 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java b/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java index 61fa514ffc..f3c4adfd4b 100644 --- a/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java @@ -18,12 +18,16 @@ public class DateUnitExpression extends ASTNodeAccessImpl implements Expression private final DateUnit type; + private final String text; + public DateUnitExpression(DateUnit type) { this.type = Objects.requireNonNull(type); + this.text = type.toString(); } public DateUnitExpression(String DateUnitStr) { this.type = Objects.requireNonNull(DateUnit.from(DateUnitStr)); + this.text = DateUnitStr; } public DateUnit getType() { @@ -38,7 +42,7 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { - return type.toString(); + return text; } public enum DateUnit { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2210dbe9d8..9af727b3e8 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1039,6 +1039,47 @@ public class CCJSqlParser extends AbstractJSqlParser { function.setExtraKeyword(getNextToken().image); } + /** + * An unquoted YEAR, MONTH, DAY, HOUR, MINUTE or SECOND passed on its own as a function argument, as in + * DATEADD(day, 1, d), is a date unit. Anywhere else it is an ordinary identifier, so it is parsed as a + * Column and only converted here, once it is known to be a whole argument. + */ + private void normalizeDateUnitArguments(Function function) { + ExpressionList parameters = function.getParameters(); + if (parameters == null || parameters instanceof ParenthesedExpressionList) { + return; + } + ExpressionList normalized = null; + for (int i = 0; i < parameters.size(); i++) { + if (!isDateUnitColumn(parameters.get(i))) { + continue; + } + Column column = (Column) parameters.get(i); + DateUnitExpression unit = new DateUnitExpression(column.getColumnName()); + linkAST(unit, column.getASTNode()); + if (normalized == null) { + normalized = new ExpressionList(parameters); + normalized.setASTNode(parameters.getASTNode()); + } + normalized.set(i, unit); + } + if (normalized != null) { + function.setParameters(normalized); + } + } + + private static boolean isDateUnitColumn(Expression expression) { + if (!(expression instanceof Column)) { + return false; + } + Node node = ((Column) expression).getASTNode(); + if (node == null) { + return false; + } + Token first = node.jjtGetFirstToken(); + return first != null && first == node.jjtGetLastToken() && first.kind == K_DATE_LITERAL; + } + private void normalizeOdbcTimestampInterval(Function function) { if (!function.isEscaped() || function.getMultipartName().size() != 1 || !("TIMESTAMPADD".equalsIgnoreCase(function.getName()) @@ -12074,8 +12115,6 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD({ !interrupted && isFunctionAhead() }) retval = Function() [ LOOKAHEAD(2) retval = AnalyticExpression( (Function) retval ) ] - | LOOKAHEAD(2) retval = DateUnitExpression() - | LOOKAHEAD(2, {!interrupted}) retval = IntervalExpression() { dateExpressionAllowed = false; } | token= { retval = new DoubleValue(token.image); } @@ -12394,13 +12433,6 @@ DateTimeLiteralExpression DateTimeLiteralExpression() : { } } -DateUnitExpression DateUnitExpression() : { - Token t; -} -{ - t= { return new DateUnitExpression( t.image ); } -} - RangeExpression RangeExpression(Expression startExpression): { Expression endExpression; @@ -14052,6 +14084,7 @@ Function Function() #Function: | function = InternalFunction(false) ) { + normalizeDateUnitArguments(function); linkAST(function,jjtThis); return function; } diff --git a/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java index 164c9e112a..920070b8f1 100644 --- a/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java @@ -10,7 +10,10 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; +import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.test.TestUtils; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -27,4 +30,67 @@ void testParsing() throws JSQLParserException { Function f = select.getSelectItem(0).getExpression(Function.class); Assertions.assertThat(f.getParameters().get(1)).isInstanceOf(DateUnitExpression.class); } + + @Test + void testUnitArgumentKeepsItsSpelling() throws JSQLParserException { + String sqlStr = "SELECT DATEADD(day, 1, d) FROM t"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Function f = select.getSelectItem(0).getExpression(Function.class); + DateUnitExpression unit = (DateUnitExpression) f.getParameters().get(0); + Assertions.assertThat(unit.getType()).isEqualTo(DateUnitExpression.DateUnit.DAY); + Assertions.assertThat(unit).hasToString("day"); + } + + @Test + void testUnitWordOutsideFunctionArgumentsIsColumn() throws JSQLParserException { + String sqlStr = "SELECT year FROM t"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Column column = select.getSelectItem(0).getExpression(Column.class); + Assertions.assertThat(column.getColumnName()).isEqualTo("year"); + } + + @Test + void testUnitWordAsQualifierIsColumn() throws JSQLParserException { + String sqlStr = "SELECT month.amount FROM sales month"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Column column = select.getSelectItem(0).getExpression(Column.class); + Assertions.assertThat(column.getFullyQualifiedName()).isEqualTo("month.amount"); + } + + @Test + void testUnitWordWithinArgumentExpressionIsColumn() throws JSQLParserException { + String sqlStr = "SELECT COALESCE(year + 1, 0) FROM t"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Function f = select.getSelectItem(0).getExpression(Function.class); + Addition addition = (Addition) f.getParameters().get(0); + Assertions.assertThat(addition.getLeftExpression()).isInstanceOf(Column.class); + } + + @Test + void testQuotedUnitWordArgumentIsColumn() throws JSQLParserException { + String sqlStr = "SELECT DATEADD(\"day\", 1, d) FROM t"; + + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Function f = select.getSelectItem(0).getExpression(Function.class); + Assertions.assertThat(f.getParameters().get(0)).isInstanceOf(Column.class); + } + + @Test + void testUnitWordInUpdateSetTupleIsColumn() throws JSQLParserException { + String sqlStr = "UPDATE t SET (a, year) = (1, 2)"; + + Update update = (Update) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr); + + Assertions.assertThat(update.getUpdateSets().get(0).getColumn(1).getColumnName()) + .isEqualTo("year"); + } }