Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 148 additions & 2 deletions src/main/java/net/sf/jsqlparser/expression/IntervalExpression.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@

import net.sf.jsqlparser.parser.ASTNodeAccessImpl;

import java.io.Serializable;
import java.util.Objects;

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() {
Expand Down Expand Up @@ -47,12 +49,32 @@ public void setParameter(String parameter) {
this.parameter = parameter;
}

/**
* Returns the interval type for backwards compatibility. If a structured qualifier is present,
* its SQL representation is returned.
*/
public String getIntervalType() {
return intervalType;
return intervalQualifier != null ? intervalQualifier.toString() : intervalType;
}

/**
* Sets the legacy interval type and clears any structured qualifier.
*/
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() {
Expand All @@ -67,7 +89,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
Expand All @@ -85,6 +109,11 @@ public IntervalExpression withIntervalType(String intervalType) {
return this;
}

public IntervalExpression withIntervalQualifier(IntervalQualifier intervalQualifier) {
this.setIntervalQualifier(intervalQualifier);
return this;
}

public IntervalExpression withExpression(Expression expression) {
this.setExpression(expression);
return this;
Expand All @@ -93,4 +122,121 @@ public IntervalExpression withExpression(Expression expression) {
public <E extends Expression> E getExpression(Class<E> type) {
return type.cast(getExpression());
}

/**
* Models an SQL interval qualifier as defined by the SQL standard (see
* <a href="https://www.postgresql.org/docs/current/datatype-datetime.html">PostgreSQL: Interval
* Input</a>), e.g. {@code YEAR}, {@code DAY TO SECOND}, {@code DAY(9) TO SECOND} or
* {@code SECOND(2, 4)}.
* <p>
* 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 static 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
package net.sf.jsqlparser.statement.create.table;

import net.sf.jsqlparser.expression.IntervalExpression.IntervalQualifier;
import net.sf.jsqlparser.statement.select.PlainSelect;

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

public ColDataType() {
Expand Down Expand Up @@ -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<Integer> getArrayData() {
return arrayData;
}
Expand All @@ -95,6 +105,7 @@ public String toString() {
arraySpec.append("]");
}
return dataType
+ (intervalQualifier != null ? " " + intervalQualifier.toString() : "")
+ (argumentsStringList != null
? " " + PlainSelect.getStringList(argumentsStringList, true, true)
: "")
Expand All @@ -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<Integer> arrayData) {
this.setArrayData(arrayData);
return this;
Expand Down Expand Up @@ -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);
}

Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1320,7 +1320,9 @@ public <S> 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;
Expand Down
57 changes: 53 additions & 4 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -9065,6 +9065,7 @@ IntervalExpression IntervalExpression() : {
Token token = null;
Expression expr = null;
boolean signed = false;
IntervalExpression.IntervalQualifier qualifier = null;
}
{

Expand All @@ -9088,29 +9089,67 @@ IntervalExpression IntervalExpression() : {
interval.setParameter((signed?"-":"") + token.image);
}
}
[ LOOKAHEAD(2) (token = <S_IDENTIFIER> | token = <K_DATE_LITERAL>) { 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 = <S_IDENTIFIER>) { interval.setIntervalType(token.image); }
]
{
return interval;
}
}

IntervalExpression IntervalExpressionWithoutInterval(Expression expr) : {
IntervalExpression interval;
Token token;
IntervalExpression.IntervalQualifier qualifier = null;
boolean signed = false;
}
{
{
interval = new IntervalExpression(false);
interval.setExpression(expr);
}
token = <K_DATE_LITERAL>
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).
*/
IntervalExpression.IntervalQualifier IntervalQualifier() : {
Token field = null;
Token toField = null;
Token leadingPrecision = null;
Token fractionalPrecision = null;
}
{
field = <K_DATE_LITERAL>
[
"(" leadingPrecision = <S_LONG>
[ "," fractionalPrecision = <S_LONG> ]
")"
]
[
<K_TO> toField = <K_DATE_LITERAL>
[ "(" fractionalPrecision = <S_LONG> ")" ]
]
{
return new IntervalExpression.IntervalQualifier(
field != null ? field.image : null,
leadingPrecision != null ? Integer.valueOf(leadingPrecision.image) : null,
toField != null ? toField.image : null,
fractionalPrecision != null ? Integer.valueOf(fractionalPrecision.image) : null);
}
}

KeepExpression KeepExpression() : {
KeepExpression keep = new KeepExpression();
Token token;
Expand Down Expand Up @@ -10818,6 +10857,7 @@ ColDataType ColDataType():
List<String> name;
ColDataType arrayType;
ColDataType nestedType = null;
IntervalExpression.IntervalQualifier intervalQualifier = null;

int precision = -1;
int scale = -1;
Expand Down Expand Up @@ -10859,6 +10899,15 @@ ColDataType ColDataType():
| tk=<K_NAME>
) { 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); }
)
Expand Down
Loading
Loading