Skip to content

Commit f272f1b

Browse files
committed
fix: model interval qualifier as a structured property (#1728)
Represent the SQL interval qualifier (field [TO field] [(precision)], e.g. DAY, HOUR TO MINUTE, DAY(9) TO SECOND, SECOND(2, 4)) as a structured IntervalExpression.IntervalQualifier, and consume it consistently in the three places it appears: - DML interval literals: SELECT INTERVAL '1' HOUR TO MINUTE (IntervalExpression / IntervalExpressionWithoutInterval) - DDL column types: CREATE TABLE t (c interval hour to minute) (ColDataType) - cast target types: CAST(x AS INTERVAL DAY TO SECOND) (reuses ColDataType) Previously the qualifier was stored as a single String and only the bare one-token form worked; field-TO-field, leading-field precision and fractional-seconds precision all failed to parse in every context. The qualifier is now a first-class property of both IntervalExpression and ColDataType rather than text appended to the data type string, so it round-trips losslessly and can be inspected structurally. This makes the long-standing Oracle interval01.sql (postfix day(9) to second) and interval03.sql (full qualifier matrix including second(2,4)) parse and de-parse, moving them to expected successes. Fixes #1728 Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent 32a7949 commit f272f1b

9 files changed

Lines changed: 310 additions & 14 deletions

File tree

src/main/java/net/sf/jsqlparser/expression/IntervalExpression.java

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class IntervalExpression extends ASTNodeAccessImpl implements Expression
1818
private final boolean intervalKeyword;
1919
private String parameter = null;
2020
private String intervalType = null;
21+
private IntervalQualifier intervalQualifier = null;
2122
private Expression expression = null;
2223

2324
public IntervalExpression() {
@@ -55,6 +56,14 @@ public void setIntervalType(String intervalType) {
5556
this.intervalType = intervalType;
5657
}
5758

59+
public IntervalQualifier getIntervalQualifier() {
60+
return intervalQualifier;
61+
}
62+
63+
public void setIntervalQualifier(IntervalQualifier intervalQualifier) {
64+
this.intervalQualifier = intervalQualifier;
65+
}
66+
5867
public Expression getExpression() {
5968
return expression;
6069
}
@@ -67,7 +76,9 @@ public void setExpression(Expression expression) {
6776
public String toString() {
6877
return (intervalKeyword ? "INTERVAL " : "")
6978
+ Objects.toString(expression, parameter)
70-
+ (intervalType != null ? " " + intervalType : "");
79+
+ (intervalQualifier != null
80+
? " " + intervalQualifier.toString()
81+
: (intervalType != null ? " " + intervalType : ""));
7182
}
7283

7384
@Override
@@ -85,6 +96,11 @@ public IntervalExpression withIntervalType(String intervalType) {
8596
return this;
8697
}
8798

99+
public IntervalExpression withIntervalQualifier(IntervalQualifier intervalQualifier) {
100+
this.setIntervalQualifier(intervalQualifier);
101+
return this;
102+
}
103+
88104
public IntervalExpression withExpression(Expression expression) {
89105
this.setExpression(expression);
90106
return this;
@@ -93,4 +109,121 @@ public IntervalExpression withExpression(Expression expression) {
93109
public <E extends Expression> E getExpression(Class<E> type) {
94110
return type.cast(getExpression());
95111
}
112+
113+
/**
114+
* Models an SQL interval qualifier as defined by the SQL standard (see
115+
* <a href="https://www.postgresql.org/docs/current/datatype-datetime.html">PostgreSQL: Interval
116+
* Input</a>), e.g. {@code YEAR}, {@code DAY TO SECOND}, {@code DAY(9) TO SECOND} or
117+
* {@code SECOND(2, 4)}.
118+
* <p>
119+
* It is shared by {@link IntervalExpression} (the {@code INTERVAL ...} expression literal, used
120+
* in DML) and by {@code net.sf.jsqlparser.statement.create.table.ColDataType} (the
121+
* {@code INTERVAL} column / cast target type, used in DDL), so the qualifier is represented
122+
* once and round-trips consistently across all contexts.
123+
*/
124+
public static class IntervalQualifier {
125+
126+
private final String leadingField;
127+
private final Integer leadingFieldPrecision;
128+
private final String trailingField;
129+
private final Integer fractionalSecondsPrecision;
130+
131+
public IntervalQualifier(
132+
String leadingField, Integer leadingFieldPrecision, String trailingField,
133+
Integer fractionalSecondsPrecision) {
134+
this.leadingField = leadingField;
135+
this.leadingFieldPrecision = leadingFieldPrecision;
136+
this.trailingField = trailingField;
137+
this.fractionalSecondsPrecision = fractionalSecondsPrecision;
138+
}
139+
140+
public String getLeadingField() {
141+
return leadingField;
142+
}
143+
144+
public Integer getLeadingFieldPrecision() {
145+
return leadingFieldPrecision;
146+
}
147+
148+
public String getTrailingField() {
149+
return trailingField;
150+
}
151+
152+
public Integer getFractionalSecondsPrecision() {
153+
return fractionalSecondsPrecision;
154+
}
155+
156+
public IntervalQualifier withLeadingField(String leadingField) {
157+
return new IntervalQualifier(
158+
leadingField, leadingFieldPrecision, trailingField,
159+
fractionalSecondsPrecision);
160+
}
161+
162+
public IntervalQualifier withLeadingFieldPrecision(Integer leadingFieldPrecision) {
163+
return new IntervalQualifier(
164+
leadingField, leadingFieldPrecision, trailingField,
165+
fractionalSecondsPrecision);
166+
}
167+
168+
public IntervalQualifier withTrailingField(String trailingField) {
169+
return new IntervalQualifier(
170+
leadingField, leadingFieldPrecision, trailingField,
171+
fractionalSecondsPrecision);
172+
}
173+
174+
public IntervalQualifier withFractionalSecondsPrecision(
175+
Integer fractionalSecondsPrecision) {
176+
return new IntervalQualifier(
177+
leadingField, leadingFieldPrecision, trailingField,
178+
fractionalSecondsPrecision);
179+
}
180+
181+
@Override
182+
public boolean equals(Object o) {
183+
if (this == o) {
184+
return true;
185+
}
186+
if (!(o instanceof IntervalQualifier)) {
187+
return false;
188+
}
189+
IntervalQualifier that = (IntervalQualifier) o;
190+
return Objects.equals(leadingField, that.leadingField)
191+
&& Objects.equals(leadingFieldPrecision, that.leadingFieldPrecision)
192+
&& Objects.equals(trailingField, that.trailingField)
193+
&& Objects.equals(
194+
fractionalSecondsPrecision, that.fractionalSecondsPrecision);
195+
}
196+
197+
@Override
198+
public int hashCode() {
199+
return Objects.hash(
200+
leadingField, leadingFieldPrecision, trailingField,
201+
fractionalSecondsPrecision);
202+
}
203+
204+
/**
205+
* Renders the qualifier back to SQL, e.g. {@code DAY}, {@code DAY TO SECOND},
206+
* {@code DAY(9) TO SECOND} or {@code SECOND(2, 4)}.
207+
*/
208+
@Override
209+
public String toString() {
210+
StringBuilder sb = new StringBuilder(Objects.toString(leadingField, ""));
211+
if (leadingFieldPrecision != null) {
212+
sb.append("(").append(leadingFieldPrecision);
213+
// For a single-field qualifier such as SECOND(2, 4), the fractional seconds
214+
// precision is rendered together with the leading precision.
215+
if (trailingField == null && fractionalSecondsPrecision != null) {
216+
sb.append(", ").append(fractionalSecondsPrecision);
217+
}
218+
sb.append(")");
219+
}
220+
if (trailingField != null) {
221+
sb.append(" TO ").append(trailingField);
222+
if (fractionalSecondsPrecision != null) {
223+
sb.append("(").append(fractionalSecondsPrecision).append(")");
224+
}
225+
}
226+
return sb.toString();
227+
}
228+
}
96229
}

src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package net.sf.jsqlparser.statement.create.table;
1111

12+
import net.sf.jsqlparser.expression.IntervalExpression.IntervalQualifier;
1213
import net.sf.jsqlparser.statement.select.PlainSelect;
1314

1415
import java.io.Serializable;
@@ -26,6 +27,7 @@ public class ColDataType implements Serializable {
2627
private String dataType;
2728
private List<String> argumentsStringList;
2829
private String characterSet;
30+
private IntervalQualifier intervalQualifier;
2931
private List<Integer> arrayData = new ArrayList<Integer>();
3032

3133
public ColDataType() {
@@ -76,6 +78,14 @@ public void setCharacterSet(String characterSet) {
7678
this.characterSet = characterSet;
7779
}
7880

81+
public IntervalQualifier getIntervalQualifier() {
82+
return intervalQualifier;
83+
}
84+
85+
public void setIntervalQualifier(IntervalQualifier intervalQualifier) {
86+
this.intervalQualifier = intervalQualifier;
87+
}
88+
7989
public List<Integer> getArrayData() {
8090
return arrayData;
8191
}
@@ -95,6 +105,7 @@ public String toString() {
95105
arraySpec.append("]");
96106
}
97107
return dataType
108+
+ (intervalQualifier != null ? " " + intervalQualifier.toString() : "")
98109
+ (argumentsStringList != null
99110
? " " + PlainSelect.getStringList(argumentsStringList, true, true)
100111
: "")
@@ -117,6 +128,11 @@ public ColDataType withCharacterSet(String characterSet) {
117128
return this;
118129
}
119130

131+
public ColDataType withIntervalQualifier(IntervalQualifier intervalQualifier) {
132+
this.setIntervalQualifier(intervalQualifier);
133+
return this;
134+
}
135+
120136
public ColDataType withArrayData(List<Integer> arrayData) {
121137
this.setArrayData(arrayData);
122138
return this;
@@ -161,6 +177,7 @@ public final boolean equals(Object o) {
161177
return dataType.equalsIgnoreCase(that.dataType)
162178
&& Objects.equals(argumentsStringList, that.argumentsStringList)
163179
&& Objects.equals(characterSet, that.characterSet)
180+
&& Objects.equals(intervalQualifier, that.intervalQualifier)
164181
&& Objects.equals(arrayData, that.arrayData);
165182
}
166183

@@ -169,6 +186,7 @@ public int hashCode() {
169186
int result = dataType.hashCode();
170187
result = 31 * result + Objects.hashCode(argumentsStringList);
171188
result = 31 * result + Objects.hashCode(characterSet);
189+
result = 31 * result + Objects.hashCode(intervalQualifier);
172190
result = 31 * result + Objects.hashCode(arrayData);
173191
return result;
174192
}

src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1320,7 +1320,9 @@ public <S> StringBuilder visit(IntervalExpression intervalExpression, S context)
13201320
} else {
13211321
builder.append(intervalExpression.getParameter());
13221322
}
1323-
if (intervalExpression.getIntervalType() != null) {
1323+
if (intervalExpression.getIntervalQualifier() != null) {
1324+
builder.append(" ").append(intervalExpression.getIntervalQualifier().toString());
1325+
} else if (intervalExpression.getIntervalType() != null) {
13241326
builder.append(" ").append(intervalExpression.getIntervalType());
13251327
}
13261328
return builder;

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

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9065,6 +9065,7 @@ IntervalExpression IntervalExpression() : {
90659065
Token token = null;
90669066
Expression expr = null;
90679067
boolean signed = false;
9068+
IntervalExpression.IntervalQualifier qualifier = null;
90689069
}
90699070
{
90709071

@@ -9088,29 +9089,67 @@ IntervalExpression IntervalExpression() : {
90889089
interval.setParameter((signed?"-":"") + token.image);
90899090
}
90909091
}
9091-
[ LOOKAHEAD(2) (token = <S_IDENTIFIER> | token = <K_DATE_LITERAL>) { interval.setIntervalType(token.image); } ]
9092+
[
9093+
// Standard qualifier (field [TO field] [(precision)]) such as DAY or DAY TO SECOND.
9094+
LOOKAHEAD(2, { getToken(1).kind == K_DATE_LITERAL }) qualifier = IntervalQualifier() { interval.setIntervalQualifier(qualifier); }
9095+
// Non-standard single identifier field (e.g. MySQL `INTERVAL 1 foo`), kept for
9096+
// backwards compatibility.
9097+
| LOOKAHEAD(2) (token = <S_IDENTIFIER>) { interval.setIntervalType(token.image); }
9098+
]
90929099
{
90939100
return interval;
90949101
}
90959102
}
90969103

90979104
IntervalExpression IntervalExpressionWithoutInterval(Expression expr) : {
90989105
IntervalExpression interval;
9099-
Token token;
9106+
IntervalExpression.IntervalQualifier qualifier = null;
91009107
boolean signed = false;
91019108
}
91029109
{
91039110
{
91049111
interval = new IntervalExpression(false);
91059112
interval.setExpression(expr);
91069113
}
9107-
token = <K_DATE_LITERAL>
9114+
qualifier = IntervalQualifier()
91089115
{
9109-
interval.setIntervalType(token.image);
9116+
interval.setIntervalQualifier(qualifier);
91109117
return interval;
91119118
}
91129119
}
91139120

9121+
/**
9122+
* Parses an SQL interval qualifier, e.g. {@code DAY}, {@code DAY TO SECOND},
9123+
* {@code DAY(9) TO SECOND} or {@code SECOND(2, 4)}, as defined by the SQL standard.
9124+
* Shared by {@link IntervalExpression} (DML interval literals) and by {@code ColDataType}
9125+
* (DDL {@code INTERVAL} column / cast target types).
9126+
*/
9127+
IntervalExpression.IntervalQualifier IntervalQualifier() : {
9128+
Token field = null;
9129+
Token toField = null;
9130+
Token leadingPrecision = null;
9131+
Token fractionalPrecision = null;
9132+
}
9133+
{
9134+
field = <K_DATE_LITERAL>
9135+
[
9136+
"(" leadingPrecision = <S_LONG>
9137+
[ "," fractionalPrecision = <S_LONG> ]
9138+
")"
9139+
]
9140+
[
9141+
<K_TO> toField = <K_DATE_LITERAL>
9142+
[ "(" fractionalPrecision = <S_LONG> ")" ]
9143+
]
9144+
{
9145+
return new IntervalExpression.IntervalQualifier(
9146+
field != null ? field.image : null,
9147+
leadingPrecision != null ? Integer.valueOf(leadingPrecision.image) : null,
9148+
toField != null ? toField.image : null,
9149+
fractionalPrecision != null ? Integer.valueOf(fractionalPrecision.image) : null);
9150+
}
9151+
}
9152+
91149153
KeepExpression KeepExpression() : {
91159154
KeepExpression keep = new KeepExpression();
91169155
Token token;
@@ -10818,6 +10857,7 @@ ColDataType ColDataType():
1081810857
List<String> name;
1081910858
ColDataType arrayType;
1082010859
ColDataType nestedType = null;
10860+
IntervalExpression.IntervalQualifier intervalQualifier = null;
1082110861

1082210862
int precision = -1;
1082310863
int scale = -1;
@@ -10859,6 +10899,15 @@ ColDataType ColDataType():
1085910899
| tk=<K_NAME>
1086010900
) { schema = tk.image; }
1086110901

10902+
// Consume an optional INTERVAL qualifier such as `hour to minute` or
10903+
// `day(9) to second`. Only applicable when the matched type is an INTERVAL and
10904+
// is immediately followed by a field word, so other types (and the existing
10905+
// `interval (2)` precision form handled below) are unaffected.
10906+
[
10907+
LOOKAHEAD({ "interval".equalsIgnoreCase(schema)
10908+
&& getToken(1).kind == K_DATE_LITERAL })
10909+
intervalQualifier = IntervalQualifier() { colDataType.setIntervalQualifier(intervalQualifier); }
10910+
]
1086210911
[ LOOKAHEAD(2) "." arrayType = ColDataType() { schema += "." + arrayType.toString(); } ]
1086310912
{ colDataType.setDataType(schema); }
1086410913
)

0 commit comments

Comments
 (0)