diff --git a/src/main/java/net/sf/jsqlparser/expression/IntervalExpression.java b/src/main/java/net/sf/jsqlparser/expression/IntervalExpression.java
index 9c028c769..7e496b4ce 100644
--- a/src/main/java/net/sf/jsqlparser/expression/IntervalExpression.java
+++ b/src/main/java/net/sf/jsqlparser/expression/IntervalExpression.java
@@ -18,6 +18,7 @@ public class IntervalExpression extends ASTNodeAccessImpl implements Expression
private final boolean intervalKeyword;
private String parameter = null;
private String intervalType = null;
+ private IntervalQualifier intervalQualifier = null;
private Expression expression = null;
public IntervalExpression() {
@@ -47,12 +48,42 @@ public void setParameter(String parameter) {
this.parameter = parameter;
}
+ /**
+ * Returns the non-standard single-identifier interval type (e.g. MySQL {@code INTERVAL 1 foo}),
+ * or {@code null} when a structured {@link IntervalQualifier} is present.
+ *
+ * @deprecated the structured {@link #getIntervalQualifier()} is the canonical representation.
+ * This legacy field is kept only for the non-standard single-identifier form.
+ * @return the legacy interval type, or {@code null} if a qualifier is set
+ */
+ @Deprecated
public String getIntervalType() {
return intervalType;
}
+ /**
+ * Sets the non-standard single-identifier interval type (e.g. MySQL {@code INTERVAL 1 foo}) and
+ * clears any structured qualifier.
+ *
+ * @deprecated use {@link #setIntervalQualifier(IntervalQualifier)} for the standard form.
+ * @param intervalType the legacy interval type
+ */
+ @Deprecated
public void setIntervalType(String intervalType) {
this.intervalType = intervalType;
+ this.intervalQualifier = null;
+ }
+
+ public IntervalQualifier getIntervalQualifier() {
+ return intervalQualifier;
+ }
+
+ /**
+ * Sets the structured interval qualifier and clears the legacy interval type.
+ */
+ public void setIntervalQualifier(IntervalQualifier intervalQualifier) {
+ this.intervalQualifier = intervalQualifier;
+ this.intervalType = null;
}
public Expression getExpression() {
@@ -67,7 +98,9 @@ public void setExpression(Expression expression) {
public String toString() {
return (intervalKeyword ? "INTERVAL " : "")
+ Objects.toString(expression, parameter)
- + (intervalType != null ? " " + intervalType : "");
+ + (intervalQualifier != null
+ ? " " + intervalQualifier.toString()
+ : (intervalType != null ? " " + intervalType : ""));
}
@Override
@@ -80,11 +113,22 @@ public IntervalExpression withParameter(String parameter) {
return this;
}
+ /**
+ * @deprecated use {@link #withIntervalQualifier(IntervalQualifier)} for the standard form.
+ * @param intervalType the legacy interval type
+ * @return this instance
+ */
+ @Deprecated
public IntervalExpression withIntervalType(String intervalType) {
this.setIntervalType(intervalType);
return this;
}
+ public IntervalExpression withIntervalQualifier(IntervalQualifier intervalQualifier) {
+ this.setIntervalQualifier(intervalQualifier);
+ return this;
+ }
+
public IntervalExpression withExpression(Expression expression) {
this.setExpression(expression);
return this;
diff --git a/src/main/java/net/sf/jsqlparser/expression/IntervalQualifier.java b/src/main/java/net/sf/jsqlparser/expression/IntervalQualifier.java
new file mode 100644
index 000000000..a9676c69c
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/expression/IntervalQualifier.java
@@ -0,0 +1,123 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2019 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.expression;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Models an SQL interval qualifier as defined by the SQL standard (see
+ * PostgreSQL: Interval
+ * Input), e.g. {@code YEAR}, {@code DAY TO SECOND}, {@code DAY(9) TO SECOND} or
+ * {@code SECOND(2, 4)}.
+ *
+ * It is shared by {@link IntervalExpression} (the {@code INTERVAL ...} expression literal, used in
+ * DML) and by {@code net.sf.jsqlparser.statement.create.table.ColDataType} (the {@code INTERVAL}
+ * column / cast target type, used in DDL), so the qualifier is represented once and round-trips
+ * consistently across all contexts.
+ */
+public final class IntervalQualifier implements Serializable {
+
+ private final String leadingField;
+ private final Integer leadingFieldPrecision;
+ private final String trailingField;
+ private final Integer fractionalSecondsPrecision;
+
+ public IntervalQualifier(
+ String leadingField, Integer leadingFieldPrecision, String trailingField,
+ Integer fractionalSecondsPrecision) {
+ this.leadingField = leadingField;
+ this.leadingFieldPrecision = leadingFieldPrecision;
+ this.trailingField = trailingField;
+ this.fractionalSecondsPrecision = fractionalSecondsPrecision;
+ }
+
+ public String getLeadingField() {
+ return leadingField;
+ }
+
+ public Integer getLeadingFieldPrecision() {
+ return leadingFieldPrecision;
+ }
+
+ public String getTrailingField() {
+ return trailingField;
+ }
+
+ public Integer getFractionalSecondsPrecision() {
+ return fractionalSecondsPrecision;
+ }
+
+ public IntervalQualifier withLeadingField(String leadingField) {
+ return new IntervalQualifier(
+ leadingField, leadingFieldPrecision, trailingField, fractionalSecondsPrecision);
+ }
+
+ public IntervalQualifier withLeadingFieldPrecision(Integer leadingFieldPrecision) {
+ return new IntervalQualifier(
+ leadingField, leadingFieldPrecision, trailingField, fractionalSecondsPrecision);
+ }
+
+ public IntervalQualifier withTrailingField(String trailingField) {
+ return new IntervalQualifier(
+ leadingField, leadingFieldPrecision, trailingField, fractionalSecondsPrecision);
+ }
+
+ public IntervalQualifier withFractionalSecondsPrecision(Integer fractionalSecondsPrecision) {
+ return new IntervalQualifier(
+ leadingField, leadingFieldPrecision, trailingField, fractionalSecondsPrecision);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof IntervalQualifier)) {
+ return false;
+ }
+ IntervalQualifier that = (IntervalQualifier) o;
+ return Objects.equals(leadingField, that.leadingField)
+ && Objects.equals(leadingFieldPrecision, that.leadingFieldPrecision)
+ && Objects.equals(trailingField, that.trailingField)
+ && Objects.equals(fractionalSecondsPrecision, that.fractionalSecondsPrecision);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ leadingField, leadingFieldPrecision, trailingField, fractionalSecondsPrecision);
+ }
+
+ /**
+ * Renders the qualifier back to SQL, e.g. {@code DAY}, {@code DAY TO SECOND},
+ * {@code DAY(9) TO SECOND} or {@code SECOND(2, 4)}.
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(Objects.toString(leadingField, ""));
+ if (leadingFieldPrecision != null) {
+ sb.append("(").append(leadingFieldPrecision);
+ // For a single-field qualifier such as SECOND(2, 4), the fractional seconds
+ // precision is rendered together with the leading precision.
+ if (trailingField == null && fractionalSecondsPrecision != null) {
+ sb.append(", ").append(fractionalSecondsPrecision);
+ }
+ sb.append(")");
+ }
+ if (trailingField != null) {
+ sb.append(" TO ").append(trailingField);
+ if (fractionalSecondsPrecision != null) {
+ sb.append("(").append(fractionalSecondsPrecision).append(")");
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java
index b7b7a9ae9..a3ddcdb9f 100644
--- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java
+++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java
@@ -9,6 +9,7 @@
*/
package net.sf.jsqlparser.statement.create.table;
+import net.sf.jsqlparser.expression.IntervalQualifier;
import net.sf.jsqlparser.statement.select.PlainSelect;
import java.io.Serializable;
@@ -26,6 +27,7 @@ public class ColDataType implements Serializable {
private String dataType;
private List argumentsStringList;
private String characterSet;
+ private IntervalQualifier intervalQualifier;
private List arrayData = new ArrayList();
public ColDataType() {
@@ -76,6 +78,14 @@ public void setCharacterSet(String characterSet) {
this.characterSet = characterSet;
}
+ public IntervalQualifier getIntervalQualifier() {
+ return intervalQualifier;
+ }
+
+ public void setIntervalQualifier(IntervalQualifier intervalQualifier) {
+ this.intervalQualifier = intervalQualifier;
+ }
+
public List getArrayData() {
return arrayData;
}
@@ -95,6 +105,7 @@ public String toString() {
arraySpec.append("]");
}
return dataType
+ + (intervalQualifier != null ? " " + intervalQualifier.toString() : "")
+ (argumentsStringList != null
? " " + PlainSelect.getStringList(argumentsStringList, true, true)
: "")
@@ -117,6 +128,11 @@ public ColDataType withCharacterSet(String characterSet) {
return this;
}
+ public ColDataType withIntervalQualifier(IntervalQualifier intervalQualifier) {
+ this.setIntervalQualifier(intervalQualifier);
+ return this;
+ }
+
public ColDataType withArrayData(List arrayData) {
this.setArrayData(arrayData);
return this;
@@ -161,6 +177,7 @@ public final boolean equals(Object o) {
return dataType.equalsIgnoreCase(that.dataType)
&& Objects.equals(argumentsStringList, that.argumentsStringList)
&& Objects.equals(characterSet, that.characterSet)
+ && Objects.equals(intervalQualifier, that.intervalQualifier)
&& Objects.equals(arrayData, that.arrayData);
}
@@ -169,6 +186,7 @@ public int hashCode() {
int result = dataType.hashCode();
result = 31 * result + Objects.hashCode(argumentsStringList);
result = 31 * result + Objects.hashCode(characterSet);
+ result = 31 * result + Objects.hashCode(intervalQualifier);
result = 31 * result + Objects.hashCode(arrayData);
return result;
}
diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java
index 184088fe7..e50ccad6a 100644
--- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java
+++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java
@@ -1311,6 +1311,10 @@ public StringBuilder visit(ExtractExpression extractExpression, S context) {
}
@Override
+ @SuppressWarnings("deprecation")
+ // The deparser must round-trip both the canonical IntervalQualifier and the deprecated
+ // legacy interval type (non-standard form such as MySQL INTERVAL 1 foo), so it reads
+ // getIntervalType() intentionally.
public StringBuilder visit(IntervalExpression intervalExpression, S context) {
if (intervalExpression.isUsingIntervalKeyword()) {
builder.append("INTERVAL ");
@@ -1320,7 +1324,9 @@ public StringBuilder visit(IntervalExpression intervalExpression, S context)
} else {
builder.append(intervalExpression.getParameter());
}
- if (intervalExpression.getIntervalType() != null) {
+ if (intervalExpression.getIntervalQualifier() != null) {
+ builder.append(" ").append(intervalExpression.getIntervalQualifier().toString());
+ } else if (intervalExpression.getIntervalType() != null) {
builder.append(" ").append(intervalExpression.getIntervalType());
}
return builder;
diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
index 47903e9a5..f0767bfd2 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -9065,6 +9065,7 @@ IntervalExpression IntervalExpression() : {
Token token = null;
Expression expr = null;
boolean signed = false;
+ IntervalQualifier qualifier = null;
}
{
@@ -9088,7 +9089,13 @@ IntervalExpression IntervalExpression() : {
interval.setParameter((signed?"-":"") + token.image);
}
}
- [ LOOKAHEAD(2) (token = | token = ) { interval.setIntervalType(token.image); } ]
+ [
+ // Standard qualifier (field [TO field] [(precision)]) such as DAY or DAY TO SECOND.
+ LOOKAHEAD(2, { getToken(1).kind == K_DATE_LITERAL }) qualifier = IntervalQualifier() { interval.setIntervalQualifier(qualifier); }
+ // Non-standard single identifier field (e.g. MySQL `INTERVAL 1 foo`), kept for
+ // backwards compatibility.
+ | LOOKAHEAD(2) (token = ) { interval.setIntervalType(token.image); }
+ ]
{
return interval;
}
@@ -9096,7 +9103,7 @@ IntervalExpression IntervalExpression() : {
IntervalExpression IntervalExpressionWithoutInterval(Expression expr) : {
IntervalExpression interval;
- Token token;
+ IntervalQualifier qualifier = null;
boolean signed = false;
}
{
@@ -9104,13 +9111,58 @@ IntervalExpression IntervalExpressionWithoutInterval(Expression expr) : {
interval = new IntervalExpression(false);
interval.setExpression(expr);
}
- token =
+ qualifier = IntervalQualifier()
{
- interval.setIntervalType(token.image);
+ interval.setIntervalQualifier(qualifier);
return interval;
}
}
+/**
+ * Parses an SQL interval qualifier, e.g. {@code DAY}, {@code DAY TO SECOND},
+ * {@code DAY(9) TO SECOND} or {@code SECOND(2, 4)}, as defined by the SQL standard.
+ * Shared by {@link IntervalExpression} (DML interval literals) and by {@code ColDataType}
+ * (DDL {@code INTERVAL} column / cast target types).
+ */
+IntervalQualifier IntervalQualifier() : {
+ Token field = null;
+ Token toField = null;
+ Token leadingPrecision = null;
+ Token fractionalPrecision = null;
+ Token trailingFractionalPrecision = null;
+}
+{
+ field =
+ [
+ LOOKAHEAD(2) "(" leadingPrecision =
+ [ "," fractionalPrecision = ]
+ ")"
+ ]
+ [
+ LOOKAHEAD(2) toField =
+ [ LOOKAHEAD(2) "(" trailingFractionalPrecision = ")" ]
+ ]
+ {
+ // SECOND(2, 4) TO SECOND(3) specifies fractional seconds precision for both the leading
+ // and the trailing field. Reject such contradictory input instead of silently dropping one
+ // of the values. The rejection is placed last so the production does not complete normally
+ // and JavaCC does not append an unreachable "missing return" guard.
+ if (fractionalPrecision == null || trailingFractionalPrecision == null) {
+ return new IntervalQualifier(
+ field != null ? field.image : null,
+ leadingPrecision != null ? Integer.valueOf(leadingPrecision.image) : null,
+ toField != null ? toField.image : null,
+ trailingFractionalPrecision != null
+ ? Integer.valueOf(trailingFractionalPrecision.image)
+ : (fractionalPrecision != null
+ ? Integer.valueOf(fractionalPrecision.image) : null));
+ }
+ throw new ParseException(
+ "Invalid interval qualifier: fractional seconds precision is specified for both"
+ + " the leading field and the trailing field");
+ }
+}
+
KeepExpression KeepExpression() : {
KeepExpression keep = new KeepExpression();
Token token;
@@ -10818,6 +10870,7 @@ ColDataType ColDataType():
List name;
ColDataType arrayType;
ColDataType nestedType = null;
+ IntervalQualifier intervalQualifier = null;
int precision = -1;
int scale = -1;
@@ -10859,6 +10912,15 @@ ColDataType ColDataType():
| tk=
) { schema = tk.image; }
+ // Consume an optional INTERVAL qualifier such as `hour to minute` or
+ // `day(9) to second`. Only applicable when the matched type is an INTERVAL and
+ // is immediately followed by a field word, so other types (and the existing
+ // `interval (2)` precision form handled below) are unaffected.
+ [
+ LOOKAHEAD({ "interval".equalsIgnoreCase(schema)
+ && getToken(1).kind == K_DATE_LITERAL })
+ intervalQualifier = IntervalQualifier() { colDataType.setIntervalQualifier(intervalQualifier); }
+ ]
[ LOOKAHEAD(2) "." arrayType = ColDataType() { schema += "." + arrayType.toString(); } ]
{ colDataType.setDataType(schema); }
)
diff --git a/src/test/java/net/sf/jsqlparser/expression/IntervalExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/IntervalExpressionTest.java
index 30645789f..f966ff2de 100644
--- a/src/test/java/net/sf/jsqlparser/expression/IntervalExpressionTest.java
+++ b/src/test/java/net/sf/jsqlparser/expression/IntervalExpressionTest.java
@@ -10,9 +10,16 @@
package net.sf.jsqlparser.expression;
import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.test.TestUtils;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
class IntervalExpressionTest {
@@ -24,4 +31,127 @@ void testExtractExpressionIssue2172() throws JSQLParserException {
sqlStr = "SELECT UNIX_TIMESTAMP(date_sub(date_sub(date_format(now(),'%y-%m-%d'),interval extract(day from now())-1 day),interval 1 month))*1000";
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
}
+
+ // ---- Interval qualifier support (issue #1728 / SQL standard) ----
+
+ @Test
+ void testIntervalSingleFieldRoundTrip() throws JSQLParserException {
+ TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT INTERVAL '1' HOUR", true);
+ }
+
+ @Test
+ void testIntervalFieldToFieldRoundTrip() throws JSQLParserException {
+ TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT INTERVAL '1' HOUR TO MINUTE", true);
+ TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT INTERVAL '4 5:12' DAY TO MINUTE", true);
+ }
+
+ @Test
+ void testIntervalFieldWithPrecisionRoundTrip() throws JSQLParserException {
+ TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT INTERVAL '400' DAY(3)", true);
+ }
+
+ @Test
+ void testIntervalFieldToFieldWithPrecisionRoundTrip() throws JSQLParserException {
+ TestUtils.assertSqlCanBeParsedAndDeparsed(
+ "SELECT INTERVAL '4 5:12:10.222' DAY TO SECOND(3)", true);
+ TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT INTERVAL '400 5' DAY(3) TO HOUR", true);
+ }
+
+ @Test
+ void testIntervalSecondWithLeadingAndFractionalPrecisionRoundTrip() throws JSQLParserException {
+ TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT INTERVAL '30.12345' SECOND(2, 4)", true);
+ }
+
+ @Test
+ void testStandardSingleFieldUsesQualifier() throws JSQLParserException {
+ String sql = "SELECT INTERVAL '1' DAY";
+ Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true);
+ IntervalExpression interval = (IntervalExpression) select.getPlainSelect()
+ .getSelectItems().get(0).getExpression();
+
+ // The standard field is modeled by the canonical qualifier, not the legacy type.
+ IntervalQualifier qualifier = interval.getIntervalQualifier();
+ assertNotNull(qualifier);
+ assertEquals("DAY", qualifier.getLeadingField());
+ assertNull(interval.getIntervalType());
+ assertEquals("INTERVAL '1' DAY", interval.toString());
+ TestUtils.assertExpressionCanBeDeparsedAs(interval, "INTERVAL '1' DAY");
+ }
+
+ @Test
+ void testIntervalQualifierStructurallyAttached() throws JSQLParserException {
+ String sql = "SELECT INTERVAL '1' HOUR TO MINUTE";
+ Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true);
+ IntervalExpression interval = (IntervalExpression) select.getPlainSelect()
+ .getSelectItems().get(0).getExpression();
+ IntervalQualifier qualifier = interval.getIntervalQualifier();
+ assertNotNull(qualifier, "qualifier must be a structured property of the interval");
+ assertEquals("HOUR", qualifier.getLeadingField());
+ assertEquals("MINUTE", qualifier.getTrailingField());
+ assertEquals("HOUR TO MINUTE", qualifier.toString());
+ // The canonical qualifier replaces the legacy type, which must stay null.
+ assertNull(interval.getIntervalType());
+ TestUtils.assertExpressionCanBeDeparsedAs(interval, "INTERVAL '1' HOUR TO MINUTE");
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void testLegacySetterOverridesStructuredQualifier() throws JSQLParserException {
+ Select select = (Select) CCJSqlParserUtil.parse("SELECT INTERVAL '1' DAY");
+ IntervalExpression interval = (IntervalExpression) select.getPlainSelect()
+ .getSelectItems().get(0).getExpression();
+
+ interval.setIntervalType("HOUR");
+
+ assertNull(interval.getIntervalQualifier());
+ assertEquals("HOUR", interval.getIntervalType());
+ assertEquals("INTERVAL '1' HOUR", interval.toString());
+ TestUtils.assertExpressionCanBeDeparsedAs(interval, "INTERVAL '1' HOUR");
+ TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT " + interval, true);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void testStructuredSetterOverridesLegacyType() throws JSQLParserException {
+ IntervalExpression interval = new IntervalExpression()
+ .withExpression(new StringValue("1"))
+ .withIntervalType("DAY");
+
+ interval.withIntervalQualifier(new IntervalQualifier("HOUR", null, "MINUTE", null));
+
+ assertEquals("HOUR TO MINUTE", interval.getIntervalQualifier().toString());
+ assertNull(interval.getIntervalType());
+ assertEquals("INTERVAL '1' HOUR TO MINUTE", interval.toString());
+ TestUtils.assertExpressionCanBeDeparsedAs(interval, "INTERVAL '1' HOUR TO MINUTE");
+ TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT " + interval, true);
+ }
+
+ @Test
+ void testPostfixIntervalQualifierRoundTrip() throws JSQLParserException {
+ // Oracle-style postfix qualifier: (expr) DAY(9) TO SECOND
+ TestUtils.assertSqlCanBeParsedAndDeparsed(
+ "SELECT (systimestamp - order_date) DAY(9) TO SECOND FROM orders", true);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void testNonStandardIntervalTypeUsesLegacyGetter() throws JSQLParserException {
+ // MySQL-style non-standard single-identifier field: INTERVAL 1 foo. The deprecated legacy
+ // type is kept exactly for this case; no structured qualifier is attached.
+ Select select = (Select) TestUtils.assertSqlCanBeParsedAndDeparsed(
+ "SELECT INTERVAL 1 foo", true);
+ IntervalExpression interval = (IntervalExpression) select.getPlainSelect()
+ .getSelectItems().get(0).getExpression();
+
+ assertNull(interval.getIntervalQualifier());
+ assertEquals("foo", interval.getIntervalType());
+ }
+
+ @Test
+ void testContradictoryFractionalPrecisionRejected() {
+ // SECOND(2, 4) TO SECOND(3) specifies fractional seconds precision twice. Reject instead of
+ // silently dropping the leading 4 (issue raised in review of PR #2456).
+ assertThrows(JSQLParserException.class,
+ () -> CCJSqlParserUtil.parse("SELECT INTERVAL '1' SECOND(2, 4) TO SECOND(3)"));
+ }
}
diff --git a/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java b/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java
index 5fdae46c2..5d282fb5f 100644
--- a/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/create/table/ColDataTypeTest.java
@@ -10,10 +10,13 @@
package net.sf.jsqlparser.statement.create.table;
import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.IntervalQualifier;
import net.sf.jsqlparser.test.TestUtils;
import org.junit.jupiter.api.Test;
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
class ColDataTypeTest {
@Test
@@ -57,4 +60,41 @@ void testStruct() throws JSQLParserException {
" );\n";
assertSqlCanBeParsedAndDeparsed(sqlStr, true);
}
+
+ // ---- INTERVAL column type with qualifier (issue #1728 / SQL standard) ----
+
+ @Test
+ void testIntervalColumnFieldToFieldRoundTrip() throws JSQLParserException {
+ assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a interval hour to minute)", true);
+ assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a interval year to month)", true);
+ assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a interval day to second)", true);
+ }
+
+ @Test
+ void testIntervalColumnBarePrecisionUnaffected() throws JSQLParserException {
+ // the leading-field-less precision form must keep working
+ assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a interval(2))", true);
+ }
+
+ @Test
+ void testIntervalQualifierStructurallyAttachedToColumnType() throws JSQLParserException {
+ CreateTable create = (CreateTable) assertSqlCanBeParsedAndDeparsed(
+ "CREATE TABLE t (len interval hour to minute)", true);
+ ColumnDefinition len = create.getColumnDefinitions().stream()
+ .filter(c -> c.getColumnName().equalsIgnoreCase("len"))
+ .findFirst()
+ .orElseThrow();
+ // The qualifier must be a structured property of the column type, not a bare string.
+ IntervalQualifier qualifier = len.getColDataType().getIntervalQualifier();
+ assertNotNull(qualifier);
+ assertEquals("hour", qualifier.getLeadingField());
+ assertEquals("minute", qualifier.getTrailingField());
+ }
+
+ @Test
+ void testCastAsIntervalWithQualifierRoundTrip() throws JSQLParserException {
+ assertSqlCanBeParsedAndDeparsed(
+ "SELECT CAST(col AS INTERVAL DAY TO SECOND)", true);
+ assertSqlCanBeParsedAndDeparsed("SELECT CAST(col AS INTERVAL HOUR)", true);
+ }
}
diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SpecialOracleTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SpecialOracleTest.java
index 4c95872f8..2650c5cd1 100644
--- a/src/test/java/net/sf/jsqlparser/statement/select/SpecialOracleTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/select/SpecialOracleTest.java
@@ -98,7 +98,8 @@ public class SpecialOracleTest {
"groupby13.sql", "groupby14.sql", "groupby15.sql", "groupby16.sql", "groupby17.sql",
"groupby19.sql", "groupby20.sql", "groupby21.sql", "groupby22.sql", "groupby23.sql",
"insert02.sql", "insert04.sql", "insert05.sql", "insert06.sql", "insert07.sql",
- "insert11.sql", "insert12.sql", "interval02.sql", "interval04.sql",
+ "insert11.sql", "insert12.sql", "interval01.sql", "interval02.sql",
+ "interval03.sql", "interval04.sql",
"interval05.sql", "join01.sql",
"join02.sql", "join03.sql", "join04.sql", "join06.sql", "join07.sql", "join08.sql",
"join09.sql", "join10.sql", "join11.sql", "join12.sql", "join13.sql", "join14.sql",
diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/interval01.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/interval01.sql
index 4ad2121c9..978d421f7 100644
--- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/interval01.sql
+++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/interval01.sql
@@ -10,7 +10,4 @@
select (systimestamp - order_date) day(9) to second from orders
where order_id = 2458
---@FAILURE: Encountered unexpected token: "(" "(" recorded first on Aug 3, 2021, 7:20:08 AM
---@FAILURE: Encountered: "(" / "(", at line 10, column 39, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:08
---@FAILURE: Encountered: / "(", at line 10, column 39, in lexical state DEFAULT. recorded first on 9 Jul 2025, 17:09:17
---@FAILURE: --- recorded first on 11 Apr 2026, 22:18:19
\ No newline at end of file
+--@SUCCESSFULLY_PARSED_AND_DEPARSED first on 2026年8月10日 上午11:23:23
\ No newline at end of file
diff --git a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/interval03.sql b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/interval03.sql
index 25e2b711a..cfae5bf60 100644
--- a/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/interval03.sql
+++ b/src/test/resources/net/sf/jsqlparser/statement/select/oracle-tests/interval03.sql
@@ -25,6 +25,4 @@ select
,interval :a day
from dual
---@FAILURE: Encountered unexpected token: "second" recorded first on Aug 3, 2021, 7:20:08 AM
---@FAILURE: Encountered: / "second", at line 11, column 34, in lexical state DEFAULT. recorded first on 15 May 2025, 16:24:08
---@FAILURE: --- recorded first on 11 Apr 2026, 22:18:19
\ No newline at end of file
+--@SUCCESSFULLY_PARSED_AND_DEPARSED first on 2026年8月10日 上午11:23:23
\ No newline at end of file