Skip to content

Commit b915571

Browse files
committed
Add arbitrary precision interval grammar
Add grammar support for standard SQL interval qualifier syntax in preparation for supporting arbitrary precision intervals. Currently, all day-time intervals are evaluated as DAY TO SECOND intervals, while year-month intervals are evaluated as YEAR TO MONTH intervals.
1 parent 7316c57 commit b915571

File tree

25 files changed

+721
-270
lines changed

25 files changed

+721
-270
lines changed

core/trino-grammar/src/main/antlr4/io/trino/grammar/sql/SqlBase.g4

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -759,11 +759,7 @@ booleanValue
759759
;
760760

761761
interval
762-
: INTERVAL sign=(PLUS | MINUS)? string from=intervalField (TO to=intervalField)?
763-
;
764-
765-
intervalField
766-
: YEAR | MONTH | DAY | HOUR | MINUTE | SECOND
762+
: INTERVAL sign=(PLUS | MINUS)? string intervalQualifier
767763
;
768764

769765
normalForm
@@ -772,7 +768,7 @@ normalForm
772768

773769
type
774770
: ROW '(' rowField (',' rowField)* ')' #rowType
775-
| INTERVAL from=intervalField (TO to=intervalField)? #intervalType
771+
| INTERVAL intervalQualifier #intervalType
776772
| base=TIMESTAMP ('(' precision = typeParameter ')')? (WITHOUT TIME ZONE)? #dateTimeType
777773
| base=TIMESTAMP ('(' precision = typeParameter ')')? WITH TIME ZONE #dateTimeType
778774
| base=TIME ('(' precision = typeParameter ')')? (WITHOUT TIME ZONE)? #dateTimeType
@@ -784,6 +780,18 @@ type
784780
| identifier ('(' typeParameter (',' typeParameter)* ')')? #genericType
785781
;
786782

783+
intervalQualifier
784+
: YEAR ('(' precision=INTEGER_VALUE ')')? TO MONTH #compositeYearToMonthInterval
785+
| field=(YEAR | MONTH) ('(' precision=INTEGER_VALUE ')')? #simpleYearMonthInterval
786+
| start=(DAY | HOUR | MINUTE) ('(' leadingPrecision=INTEGER_VALUE ')')?
787+
TO (
788+
end=HOUR |
789+
end=MINUTE |
790+
end=SECOND ('(' fractionalPrecision=INTEGER_VALUE ')')?) #compositeDayTimeInterval
791+
| field=(MONTH | DAY | HOUR | MINUTE) ('(' precision=INTEGER_VALUE ')')? #simpleDayTimeInterval
792+
| SECOND ('(' leadingPrecision=INTEGER_VALUE (',' fractionalPrecision=INTEGER_VALUE)? ')')? #secondsDayTimeInterval
793+
;
794+
787795
rowField
788796
: type
789797
| identifier type;

core/trino-main/src/main/java/io/trino/sql/analyzer/ExpressionAnalyzer.java

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
import io.trino.sql.tree.Cast;
8080
import io.trino.sql.tree.CoalesceExpression;
8181
import io.trino.sql.tree.ComparisonExpression;
82+
import io.trino.sql.tree.CompositeIntervalQualifier;
8283
import io.trino.sql.tree.CurrentCatalog;
8384
import io.trino.sql.tree.CurrentDate;
8485
import io.trino.sql.tree.CurrentPath;
@@ -103,7 +104,9 @@
103104
import io.trino.sql.tree.IfExpression;
104105
import io.trino.sql.tree.InListExpression;
105106
import io.trino.sql.tree.InPredicate;
107+
import io.trino.sql.tree.IntervalField;
106108
import io.trino.sql.tree.IntervalLiteral;
109+
import io.trino.sql.tree.IntervalQualifier;
107110
import io.trino.sql.tree.IsNotNullPredicate;
108111
import io.trino.sql.tree.IsNullPredicate;
109112
import io.trino.sql.tree.JsonArray;
@@ -142,6 +145,7 @@
142145
import io.trino.sql.tree.RowPattern;
143146
import io.trino.sql.tree.SearchedCaseExpression;
144147
import io.trino.sql.tree.SimpleCaseExpression;
148+
import io.trino.sql.tree.SimpleIntervalQualifier;
145149
import io.trino.sql.tree.SkipTo;
146150
import io.trino.sql.tree.SortItem;
147151
import io.trino.sql.tree.SortItem.Ordering;
@@ -172,6 +176,7 @@
172176
import java.util.Map;
173177
import java.util.Objects;
174178
import java.util.Optional;
179+
import java.util.OptionalInt;
175180
import java.util.Set;
176181
import java.util.function.BiFunction;
177182
import java.util.function.Function;
@@ -208,6 +213,7 @@
208213
import static io.trino.spi.StandardErrorCode.INVALID_ARGUMENTS;
209214
import static io.trino.spi.StandardErrorCode.INVALID_DEFAULT_COLUMN_VALUE;
210215
import static io.trino.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT;
216+
import static io.trino.spi.StandardErrorCode.INVALID_INTERVAL_QUALIFIER;
211217
import static io.trino.spi.StandardErrorCode.INVALID_LITERAL;
212218
import static io.trino.spi.StandardErrorCode.INVALID_NAVIGATION_NESTING;
213219
import static io.trino.spi.StandardErrorCode.INVALID_ORDER_BY;
@@ -1266,13 +1272,18 @@ private Type processTimestampLiteral(GenericLiteral node)
12661272
@Override
12671273
protected Type visitIntervalLiteral(IntervalLiteral node, Context context)
12681274
{
1269-
Type type;
1270-
if (node.isYearToMonth()) {
1271-
type = INTERVAL_YEAR_MONTH;
1272-
}
1273-
else {
1274-
type = INTERVAL_DAY_TIME;
1275-
}
1275+
validateIntervalQualifier(node.qualifier());
1276+
1277+
IntervalField field = switch (node.qualifier()) {
1278+
case SimpleIntervalQualifier simple -> simple.getField();
1279+
case CompositeIntervalQualifier composite -> composite.getTo(); // we only need to check one. The other one is validated in validateIntervalQualifier
1280+
};
1281+
1282+
Type type = switch (field) {
1283+
case IntervalField.Year(), IntervalField.Month() -> INTERVAL_YEAR_MONTH;
1284+
case IntervalField.Day(), IntervalField.Hour(), IntervalField.Minute(), IntervalField.Second(OptionalInt _) -> INTERVAL_DAY_TIME;
1285+
};
1286+
12761287
try {
12771288
literalInterpreter.evaluate(node, type);
12781289
}
@@ -3428,6 +3439,37 @@ private void addOrReplaceExpressionsCoercion(Collection<NodeRef<Expression>> exp
34283439
}
34293440
}
34303441

3442+
private static void validateIntervalQualifier(IntervalQualifier qualifier)
3443+
{
3444+
if (qualifier instanceof CompositeIntervalQualifier composite) {
3445+
boolean valid = (composite.getFrom() instanceof IntervalField.Year() && composite.getTo() instanceof IntervalField.Month()) ||
3446+
(composite.getFrom() instanceof IntervalField.Day() && (
3447+
composite.getTo() instanceof IntervalField.Hour() ||
3448+
composite.getTo() instanceof IntervalField.Minute() ||
3449+
composite.getTo() instanceof IntervalField.Second(OptionalInt _))) ||
3450+
(composite.getFrom() instanceof IntervalField.Hour() && (
3451+
composite.getTo() instanceof IntervalField.Minute() ||
3452+
composite.getTo() instanceof IntervalField.Second(OptionalInt _))) ||
3453+
(composite.getFrom() instanceof IntervalField.Minute() && composite.getTo() instanceof IntervalField.Second(OptionalInt _));
3454+
3455+
if (!valid) {
3456+
throw semanticException(INVALID_INTERVAL_QUALIFIER, qualifier, "Invalid INTERVAL qualifier");
3457+
}
3458+
}
3459+
3460+
if (qualifier instanceof SimpleIntervalQualifier simple && (
3461+
simple.getPrecision().isPresent() ||
3462+
(simple.getField() instanceof IntervalField.Second(OptionalInt fractionalPrecision) && fractionalPrecision.isPresent()))) {
3463+
throw semanticException(NOT_SUPPORTED, qualifier, "Only INTERVAL literals with default precision are supported");
3464+
}
3465+
3466+
if (qualifier instanceof CompositeIntervalQualifier composite && (
3467+
composite.getPrecision().isPresent() ||
3468+
(composite.getTo() instanceof IntervalField.Second(OptionalInt fractionalPrecision) && fractionalPrecision.isPresent()))) {
3469+
throw semanticException(NOT_SUPPORTED, qualifier, "Only INTERVAL literals with default precision are supported");
3470+
}
3471+
}
3472+
34313473
private static class Context
34323474
{
34333475
private final Scope scope;

core/trino-main/src/main/java/io/trino/sql/analyzer/LiteralInterpreter.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,21 @@
3333
import io.trino.sql.tree.AstVisitor;
3434
import io.trino.sql.tree.BinaryLiteral;
3535
import io.trino.sql.tree.BooleanLiteral;
36+
import io.trino.sql.tree.CompositeIntervalQualifier;
3637
import io.trino.sql.tree.DecimalLiteral;
3738
import io.trino.sql.tree.DoubleLiteral;
3839
import io.trino.sql.tree.GenericLiteral;
40+
import io.trino.sql.tree.IntervalField;
3941
import io.trino.sql.tree.IntervalLiteral;
4042
import io.trino.sql.tree.Literal;
4143
import io.trino.sql.tree.LongLiteral;
4244
import io.trino.sql.tree.NullLiteral;
45+
import io.trino.sql.tree.SimpleIntervalQualifier;
4346
import io.trino.sql.tree.StringLiteral;
47+
import io.trino.type.IntervalDayTimeType;
48+
import io.trino.type.IntervalYearMonthType;
4449

50+
import java.util.Optional;
4551
import java.util.function.Function;
4652

4753
import static io.airlift.slice.Slices.utf8Slice;
@@ -157,10 +163,25 @@ protected Object visitGenericLiteral(GenericLiteral node, Void context)
157163
@Override
158164
protected Long visitIntervalLiteral(IntervalLiteral node, Void context)
159165
{
160-
if (node.isYearToMonth()) {
161-
return node.getSign().multiplier() * parseYearMonthInterval(node.getValue(), node.getStartField(), node.getEndField());
162-
}
163-
return node.getSign().multiplier() * parseDayTimeInterval(node.getValue(), node.getStartField(), node.getEndField());
166+
// TODO: the value should be interpreted according to the analyzed type. However, currently the analyzed type
167+
// is hard-coded to either INTERVAL DAY TO SECOND or INTERVAL YEAR TO MONTH, as arbitrary precision
168+
// invervals are not yet supported in the underlying type system.
169+
170+
IntervalField start = switch (node.qualifier()) {
171+
case SimpleIntervalQualifier simple -> simple.getField();
172+
case CompositeIntervalQualifier composite -> composite.getFrom();
173+
};
174+
175+
Optional<IntervalField> end = switch (node.qualifier()) {
176+
case SimpleIntervalQualifier _ -> Optional.empty();
177+
case CompositeIntervalQualifier composite -> Optional.of(composite.getTo());
178+
};
179+
180+
return switch (type) {
181+
case IntervalDayTimeType _ -> node.getSign().multiplier() * parseDayTimeInterval(node.getValue(), start, end);
182+
case IntervalYearMonthType _ -> node.getSign().multiplier() * parseYearMonthInterval(node.getValue(), start, end);
183+
default -> throw new UnsupportedOperationException("Unhandled interval type: " + type);
184+
};
164185
}
165186

166187
@Override

core/trino-main/src/main/java/io/trino/sql/analyzer/TypeSignatureTranslator.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@
2727
import io.trino.spi.type.VarcharType;
2828
import io.trino.sql.ReservedIdentifiers;
2929
import io.trino.sql.parser.SqlParser;
30+
import io.trino.sql.tree.CompositeIntervalQualifier;
3031
import io.trino.sql.tree.DataType;
3132
import io.trino.sql.tree.DataTypeParameter;
3233
import io.trino.sql.tree.DateTimeDataType;
3334
import io.trino.sql.tree.GenericDataType;
3435
import io.trino.sql.tree.Identifier;
3536
import io.trino.sql.tree.IntervalDataType;
37+
import io.trino.sql.tree.IntervalField;
38+
import io.trino.sql.tree.NodeLocation;
3639
import io.trino.sql.tree.NumericParameter;
3740
import io.trino.sql.tree.RowDataType;
3841
import io.trino.sql.tree.TypeParameter;
@@ -41,6 +44,7 @@
4144
import java.util.ArrayList;
4245
import java.util.List;
4346
import java.util.Optional;
47+
import java.util.OptionalInt;
4448
import java.util.Set;
4549
import java.util.TreeSet;
4650

@@ -177,15 +181,22 @@ private static TypeSignature toTypeSignature(RowDataType type, Set<String> typeV
177181

178182
private static TypeSignature toTypeSignature(IntervalDataType type)
179183
{
180-
if (type.getFrom() == IntervalDataType.Field.YEAR && type.getTo() == IntervalDataType.Field.MONTH) {
184+
if (type.qualifier() instanceof CompositeIntervalQualifier qualifier &&
185+
qualifier.getFrom() instanceof IntervalField.Year() &&
186+
qualifier.getTo() instanceof IntervalField.Month &&
187+
qualifier.getPrecision().isEmpty()) {
181188
return INTERVAL_YEAR_MONTH.getTypeSignature();
182189
}
183190

184-
if (type.getFrom() == IntervalDataType.Field.DAY && type.getTo() == IntervalDataType.Field.SECOND) {
191+
if (type.qualifier() instanceof CompositeIntervalQualifier qualifier &&
192+
qualifier.getFrom() instanceof IntervalField.Day() &&
193+
qualifier.getTo() instanceof IntervalField.Second(OptionalInt fractionalPrecision) &&
194+
qualifier.getPrecision().isEmpty() &&
195+
fractionalPrecision.isEmpty()) {
185196
return INTERVAL_DAY_TIME.getTypeSignature();
186197
}
187198

188-
throw new TrinoException(NOT_SUPPORTED, format("INTERVAL %s TO %s type not supported", type.getFrom(), type.getTo()));
199+
throw new TrinoException(NOT_SUPPORTED, format("INTERVAL %s type not supported", type.qualifier()));
189200
}
190201

191202
private static TypeSignature toTypeSignature(DateTimeDataType type, Set<String> typeVariables)
@@ -235,8 +246,20 @@ private static String canonicalize(Identifier identifier)
235246
static DataType toDataType(TypeSignature typeSignature)
236247
{
237248
return switch (typeSignature.getBase()) {
238-
case INTERVAL_YEAR_TO_MONTH -> new IntervalDataType(Optional.empty(), IntervalDataType.Field.YEAR, IntervalDataType.Field.MONTH);
239-
case INTERVAL_DAY_TO_SECOND -> new IntervalDataType(Optional.empty(), IntervalDataType.Field.DAY, IntervalDataType.Field.SECOND);
249+
case INTERVAL_YEAR_TO_MONTH -> new IntervalDataType(
250+
Optional.empty(),
251+
new CompositeIntervalQualifier(
252+
new NodeLocation(1, 1),
253+
OptionalInt.empty(),
254+
new IntervalField.Year(),
255+
new IntervalField.Month()));
256+
case INTERVAL_DAY_TO_SECOND -> new IntervalDataType(
257+
Optional.empty(),
258+
new CompositeIntervalQualifier(
259+
new NodeLocation(1, 1),
260+
OptionalInt.empty(),
261+
new IntervalField.Day(),
262+
new IntervalField.Second(OptionalInt.empty())));
240263
case TIMESTAMP_WITH_TIME_ZONE -> new DateTimeDataType(
241264
Optional.empty(),
242265
DateTimeDataType.Type.TIMESTAMP,

core/trino-main/src/main/java/io/trino/sql/planner/TranslationMap.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import io.trino.sql.tree.Cast;
6565
import io.trino.sql.tree.CoalesceExpression;
6666
import io.trino.sql.tree.ComparisonExpression;
67+
import io.trino.sql.tree.CompositeIntervalQualifier;
6768
import io.trino.sql.tree.CurrentCatalog;
6869
import io.trino.sql.tree.CurrentDate;
6970
import io.trino.sql.tree.CurrentPath;
@@ -83,6 +84,7 @@
8384
import io.trino.sql.tree.IfExpression;
8485
import io.trino.sql.tree.InListExpression;
8586
import io.trino.sql.tree.InPredicate;
87+
import io.trino.sql.tree.IntervalField;
8688
import io.trino.sql.tree.IntervalLiteral;
8789
import io.trino.sql.tree.IsNotNullPredicate;
8890
import io.trino.sql.tree.IsNullPredicate;
@@ -109,6 +111,7 @@
109111
import io.trino.sql.tree.Row;
110112
import io.trino.sql.tree.SearchedCaseExpression;
111113
import io.trino.sql.tree.SimpleCaseExpression;
114+
import io.trino.sql.tree.SimpleIntervalQualifier;
112115
import io.trino.sql.tree.StringLiteral;
113116
import io.trino.sql.tree.SubscriptExpression;
114117
import io.trino.sql.tree.Trim;
@@ -396,12 +399,26 @@ private io.trino.sql.ir.Expression translate(IntervalLiteral expression)
396399
{
397400
Type type = analysis.getType(expression);
398401

402+
// TODO: the value should be interpreted according to the analyzed type. However, currently the analyzed type
403+
// is hard-coded to either INTERVAL DAY TO SECOND or INTERVAL YEAR TO MONTH, as arbitrary precision
404+
// invervals are not yet supported in the underlying type system.
405+
406+
IntervalField start = switch (expression.qualifier()) {
407+
case SimpleIntervalQualifier simple -> simple.getField();
408+
case CompositeIntervalQualifier composite -> composite.getFrom();
409+
};
410+
411+
Optional<IntervalField> end = switch (expression.qualifier()) {
412+
case SimpleIntervalQualifier _ -> Optional.empty();
413+
case CompositeIntervalQualifier composite -> Optional.of(composite.getTo());
414+
};
415+
399416
return new Constant(
400417
type,
401418
switch (type) {
402-
case IntervalYearMonthType t -> expression.getSign().multiplier() * parseYearMonthInterval(expression.getValue(), expression.getStartField(), expression.getEndField());
403-
case IntervalDayTimeType t -> expression.getSign().multiplier() * parseDayTimeInterval(expression.getValue(), expression.getStartField(), expression.getEndField());
404-
default -> throw new IllegalArgumentException("Unexpected type for IntervalLiteral: %s" + type);
419+
case IntervalDayTimeType _ -> expression.getSign().multiplier() * parseDayTimeInterval(expression.getValue(), start, end);
420+
case IntervalYearMonthType _ -> expression.getSign().multiplier() * parseYearMonthInterval(expression.getValue(), start, end);
421+
default -> throw new UnsupportedOperationException("Unhandled interval type: " + type);
405422
});
406423
}
407424

0 commit comments

Comments
 (0)