Skip to content

Commit 04d23c7

Browse files
authored
IGNITE-24825 Sql. Added boundary values validation for temporal types (#5515)
1 parent b3d7bf1 commit 04d23c7

File tree

27 files changed

+1095
-133
lines changed

27 files changed

+1095
-133
lines changed

docs/_docs/sql-reference/data-types.adoc

+22-3
Original file line numberDiff line numberDiff line change
@@ -122,22 +122,41 @@ Maximum length: `65536`
122122
== Date and Time Types
123123

124124
=== TIME
125-
Possible values: The time data type. The format is `hh:mm[:ss]`.
125+
Possible values: [`00-00-00`, `23-59-59.999`].
126+
127+
The format is `HH:mm:ss[.SSS]`.
126128

127129
Default precision: `0`
128130

129131
Maximum precision: `3`
130132

131133
=== DATE
132-
Possible values: The date data type.
134+
Possible values: [`0001-01-01`, `9999-12-31`].
133135

134136
The format is `yyyy-MM-dd`.
135137

136138
=== TIMESTAMP
137139

138140
WARNING: The timestamp data type only supports precision up to milliseconds (3 symbols). Any values past the 3rd symbol will be ignored.
139141

140-
Possible values: The timestamp data type. The format is `yyyy-MM-dd hh:mm:ss[.mmm]`.
142+
Possible values: [`0001-01-01 18:00:00`, `9999-12-31 05:59:59.999`].
143+
144+
The format is `yyyy-MM-dd HH:mm:ss[.SSS]`.
145+
146+
Default precision: `6`
147+
148+
Maximum precision: `9`
149+
150+
=== TIMESTAMP WITH LOCAL TIME ZONE
151+
152+
WARNING: The timestamp with local time zone data type only supports precision up to milliseconds (3 symbols). Any values past the 3rd symbol will be ignored.
153+
154+
Unlike `TIMESTAMP`, the value is always stored in UTC time zone.
155+
The value passed to/from the user is converted from/to UTC using the user's session time zone.
156+
157+
Possible values (in UTC time zone): [`0001-01-01 18:00:00`, `9999-12-31 05:59:59.999`].
158+
159+
The format is `yyyy-MM-dd HH:mm:ss[.SSS]`.
141160

142161
Default precision: `6`
143162

modules/api/src/testFixtures/java/org/apache/ignite/table/AbstractImmutableTupleTest.java

+16-11
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import java.time.LocalTime;
3737
import java.time.Year;
3838
import java.time.ZoneId;
39-
import java.time.temporal.ChronoUnit;
39+
import java.time.ZoneOffset;
4040
import java.time.temporal.Temporal;
4141
import java.util.Random;
4242
import java.util.UUID;
@@ -400,22 +400,18 @@ private static int tailFactor(int precision) {
400400
case DECIMAL:
401401
return BigDecimal.valueOf(rnd.nextInt(), 5);
402402
case DATE: {
403-
Year year = Year.of(rnd.nextInt(20_000) - 10000);
403+
Year year = Year.of(1 + rnd.nextInt(9998));
404404
return LocalDate.ofYearDay(year.getValue(), rnd.nextInt(year.length()) + 1);
405405
}
406406
case TIME:
407407
return LocalTime.of(rnd.nextInt(24), rnd.nextInt(60), rnd.nextInt(60),
408408
rnd.nextInt(1_000_000) * 1000);
409-
case DATETIME: {
410-
Year year = Year.of(rnd.nextInt(20_000) - 10000);
411-
LocalDate localDate = LocalDate.ofYearDay(year.getValue(), rnd.nextInt(year.length()) + 1);
412-
LocalTime localTime = LocalTime.of(rnd.nextInt(24), rnd.nextInt(60), rnd.nextInt(60),
413-
rnd.nextInt(1_000) * 1000_000);
414-
return LocalDateTime.of(localDate, localTime);
415-
}
409+
case DATETIME:
410+
return LocalDateTime.ofInstant(generateInstant(rnd), ZoneOffset.UTC);
411+
416412
case TIMESTAMP:
417-
return Instant.ofEpochMilli(rnd.nextLong()).truncatedTo(ChronoUnit.SECONDS)
418-
.plusNanos(rnd.nextInt(1_000_000_000));
413+
return generateInstant(rnd);
414+
419415
case UUID:
420416
return new UUID(rnd.nextLong(), rnd.nextLong());
421417

@@ -440,4 +436,13 @@ private static int tailFactor(int precision) {
440436
throw new IllegalArgumentException("Unsupported type: " + type);
441437
}
442438
}
439+
440+
private static Instant generateInstant(Random rnd) {
441+
long minTs = LocalDateTime.of(LocalDate.of(1, 1, 1), LocalTime.MIN)
442+
.minusSeconds(ZoneOffset.MIN.getTotalSeconds()).toInstant(ZoneOffset.UTC).toEpochMilli();
443+
long maxTs = LocalDateTime.of(LocalDate.of(9999, 12, 31), LocalTime.MAX)
444+
.minusSeconds(ZoneOffset.MAX.getTotalSeconds()).toInstant(ZoneOffset.UTC).toEpochMilli();
445+
446+
return Instant.ofEpochMilli(minTs + (long) (rnd.nextDouble() * (maxTs - minTs)));
447+
}
443448
}

modules/marshaller-common/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ dependencies {
3737
testImplementation libs.jmh.core
3838

3939
testFixturesApi project(':ignite-api')
40+
testFixturesApi(testFixtures(project(':ignite-schema')))
4041
testFixturesImplementation testFixtures(project(':ignite-core'))
4142
}

modules/marshaller-common/src/testFixtures/java/org/apache/ignite/internal/marshaller/testobjects/TestObjectWithAllTypes.java

+30-31
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@
1717

1818
package org.apache.ignite.internal.marshaller.testobjects;
1919

20-
import static org.apache.ignite.internal.util.TemporalTypeUtils.normalizeNanos;
20+
import static org.apache.ignite.internal.schema.SchemaTestUtils.generateRandomValue;
2121

2222
import java.math.BigDecimal;
2323
import java.time.Instant;
2424
import java.time.LocalDate;
2525
import java.time.LocalDateTime;
2626
import java.time.LocalTime;
27-
import java.time.temporal.ChronoUnit;
2827
import java.util.Arrays;
2928
import java.util.Objects;
3029
import java.util.Random;
3130
import java.util.UUID;
3231
import org.apache.ignite.internal.testframework.IgniteTestUtils;
32+
import org.apache.ignite.internal.type.NativeTypes;
3333

3434
/**
3535
* Test object.
@@ -40,35 +40,34 @@ public class TestObjectWithAllTypes {
4040
* Creates an object with random data.
4141
*/
4242
public static TestObjectWithAllTypes randomObject(Random rnd) {
43-
final TestObjectWithAllTypes obj = new TestObjectWithAllTypes();
44-
45-
obj.primitiveBooleanCol = rnd.nextBoolean();
46-
obj.primitiveByteCol = (byte) rnd.nextInt(255);
47-
obj.primitiveShortCol = (short) rnd.nextInt(65535);
48-
obj.primitiveIntCol = rnd.nextInt();
49-
obj.primitiveLongCol = rnd.nextLong();
50-
obj.primitiveFloatCol = rnd.nextFloat();
51-
obj.primitiveDoubleCol = rnd.nextDouble();
52-
53-
obj.booleanCol = rnd.nextBoolean();
54-
obj.byteCol = (byte) rnd.nextInt(255);
55-
obj.shortCol = (short) rnd.nextInt(65535);
56-
obj.intCol = rnd.nextInt();
57-
obj.longCol = rnd.nextLong();
58-
obj.floatCol = rnd.nextFloat();
59-
obj.doubleCol = rnd.nextDouble();
60-
61-
obj.uuidCol = new UUID(rnd.nextLong(), rnd.nextLong());
62-
63-
obj.dateCol = LocalDate.ofYearDay(1990 + rnd.nextInt(50), 1 + rnd.nextInt(360));
64-
obj.timeCol = LocalTime.of(rnd.nextInt(24), rnd.nextInt(60));
65-
obj.dateTimeCol = LocalDateTime.of(obj.dateCol, obj.timeCol);
66-
obj.timestampCol = Instant.ofEpochMilli(rnd.nextLong()).truncatedTo(ChronoUnit.SECONDS)
67-
.plusNanos(normalizeNanos(rnd.nextInt(1_000_000_000), 6));
68-
69-
obj.stringCol = IgniteTestUtils.randomString(rnd, rnd.nextInt(255));
70-
obj.bytesCol = IgniteTestUtils.randomBytes(rnd, rnd.nextInt(255));
71-
obj.decimalCol = BigDecimal.valueOf(rnd.nextLong(), 3);
43+
TestObjectWithAllTypes obj = new TestObjectWithAllTypes();
44+
45+
obj.primitiveBooleanCol = (boolean) generateRandomValue(rnd, NativeTypes.BOOLEAN);
46+
obj.primitiveByteCol = (byte) generateRandomValue(rnd, NativeTypes.INT8);
47+
obj.primitiveShortCol = (short) generateRandomValue(rnd, NativeTypes.INT16);
48+
obj.primitiveIntCol = (int) generateRandomValue(rnd, NativeTypes.INT32);
49+
obj.primitiveLongCol = (long) generateRandomValue(rnd, NativeTypes.INT64);
50+
obj.primitiveFloatCol = (float) generateRandomValue(rnd, NativeTypes.FLOAT);
51+
obj.primitiveDoubleCol = (double) generateRandomValue(rnd, NativeTypes.DOUBLE);
52+
53+
obj.booleanCol = (boolean) generateRandomValue(rnd, NativeTypes.BOOLEAN);
54+
obj.byteCol = (byte) generateRandomValue(rnd, NativeTypes.INT8);
55+
obj.shortCol = (short) generateRandomValue(rnd, NativeTypes.INT16);
56+
obj.intCol = (int) generateRandomValue(rnd, NativeTypes.INT32);
57+
obj.longCol = (long) generateRandomValue(rnd, NativeTypes.INT64);
58+
obj.floatCol = (float) generateRandomValue(rnd, NativeTypes.FLOAT);
59+
obj.doubleCol = (double) generateRandomValue(rnd, NativeTypes.DOUBLE);
60+
61+
obj.uuidCol = (UUID) generateRandomValue(rnd, NativeTypes.UUID);
62+
63+
obj.dateCol = (LocalDate) generateRandomValue(rnd, NativeTypes.DATE);
64+
obj.timeCol = (LocalTime) generateRandomValue(rnd, NativeTypes.time(0));
65+
obj.dateTimeCol = (LocalDateTime) generateRandomValue(rnd, NativeTypes.datetime(0));
66+
obj.timestampCol = (Instant) generateRandomValue(rnd, NativeTypes.timestamp(6));
67+
68+
obj.stringCol = (String) generateRandomValue(rnd, NativeTypes.stringOf(rnd.nextInt(255)));
69+
obj.bytesCol = (byte[]) generateRandomValue(rnd, NativeTypes.blobOf(255));
70+
obj.decimalCol = (BigDecimal) generateRandomValue(rnd, NativeTypes.decimalOf(10, 3));
7271

7372
obj.nullLongCol = null;
7473
obj.nullBytesCol = null;

modules/platforms/dotnet/Apache.Ignite.Tests/Proto/ColocationHashTests.cs

+11-5
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ public class ColocationHashTests : IgniteTestsBase
8686
((char)BinaryTupleCommon.VarlenEmptyByte).ToString(),
8787
Guid.Empty,
8888
Guid.NewGuid(),
89-
new LocalDate(9876, 7, 30),
90-
new LocalDate(2, 1, 1),
89+
90+
// Maximum allowed DATE value.
91+
new LocalDate(9999, 12, 31),
92+
93+
// Minimum allowed DATE value.
9194
new LocalDate(1, 1, 1),
9295
default(LocalDate),
9396
new LocalTime(9, 8, 7, 6),
@@ -96,10 +99,13 @@ public class ColocationHashTests : IgniteTestsBase
9699
LocalTime.Noon,
97100
LocalDateTime.FromDateTime(DateTime.UtcNow).TimeOfDay,
98101
default(LocalTime),
99-
new LocalDateTime(year: 1, month: 1, day: 1, hour: 1, minute: 1, second: 1, millisecond: 1),
100-
new LocalDateTime(year: 2022, month: 10, day: 22, hour: 10, minute: 30, second: 55, millisecond: 123),
102+
103+
// Minimum allowed DATETIME value.
104+
new LocalDateTime(year: 1, month: 1, day: 1, hour: 18, minute: 0, second: 0, millisecond: 0),
105+
106+
// Maximum allowed DATETIME value.
107+
new LocalDateTime(year: 9999, month: 12, day: 31, hour: 05, minute: 59, second: 59, millisecond: 999),
101108
LocalDateTime.FromDateTime(DateTime.UtcNow),
102-
default(LocalDateTime),
103109
Instant.FromUnixTimeSeconds(0),
104110
default(Instant)
105111
};

modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@
1919

2020
import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
2121

22+
import java.time.Instant;
23+
import java.time.LocalDate;
24+
import java.time.LocalDateTime;
2225
import org.apache.ignite.internal.tostring.IgniteToStringExclude;
2326
import org.apache.ignite.internal.tostring.S;
2427
import org.apache.ignite.internal.type.NativeType;
28+
import org.apache.ignite.internal.type.NativeTypeSpec;
2529
import org.apache.ignite.internal.type.NativeTypes;
2630
import org.apache.ignite.internal.type.VarlenNativeType;
2731
import org.jetbrains.annotations.Nullable;
@@ -210,7 +214,7 @@ public void validate(@Nullable Object val) {
210214
if (objType != null && type.mismatch(objType)) {
211215
boolean specMatches = objType.spec() == type.spec();
212216

213-
if (specMatches && type instanceof VarlenNativeType) {
217+
if (specMatches && type instanceof VarlenNativeType) {
214218
String error = format("Value too long [column='{}', type={}]", name, type.displayName());
215219
throw new InvalidTypeException(error);
216220
} else {
@@ -221,6 +225,25 @@ public void validate(@Nullable Object val) {
221225
throw new InvalidTypeException(error);
222226
}
223227
}
228+
229+
if (val == null) {
230+
return;
231+
}
232+
233+
if (type.spec() == NativeTypeSpec.DATE) {
234+
checkBounds((LocalDate) val, SchemaUtils.DATE_MIN, SchemaUtils.DATE_MAX);
235+
} else if (type.spec() == NativeTypeSpec.DATETIME) {
236+
checkBounds((LocalDateTime) val, SchemaUtils.DATETIME_MIN, SchemaUtils.DATETIME_MAX);
237+
} else if (type.spec() == NativeTypeSpec.TIMESTAMP) {
238+
checkBounds((Instant) val, SchemaUtils.TIMESTAMP_MIN, SchemaUtils.TIMESTAMP_MAX);
239+
}
240+
}
241+
242+
private <T extends Comparable<T>> void checkBounds(T value, T min, T max) {
243+
if (value.compareTo(min) < 0 || value.compareTo(max) > 0) {
244+
throw new ValueOutOfBoundsException(format("Value is out of allowed range"
245+
+ " (column='{}', value='{}', min='{}', max='{}').", name, value, min, max));
246+
}
224247
}
225248

226249
/**

modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaUtils.java

+25
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717

1818
package org.apache.ignite.internal.schema;
1919

20+
import java.time.Instant;
21+
import java.time.LocalDate;
22+
import java.time.LocalDateTime;
23+
import java.time.LocalTime;
24+
import java.time.ZoneOffset;
2025
import java.util.List;
2126
import org.apache.ignite.internal.catalog.descriptors.CatalogTableDescriptor;
2227
import org.apache.ignite.internal.schema.catalog.CatalogToSchemaDescriptorConverter;
@@ -27,6 +32,26 @@
2732
* Stateless schema utils that produces helper methods for schema preparation.
2833
*/
2934
public class SchemaUtils {
35+
/** Minimum allowed date value ({@code 0001-01-01}). */
36+
public static final LocalDate DATE_MIN = LocalDate.of(1, 1, 1);
37+
38+
/** Maximum allowed date value ({@code 9999-12-31}). */
39+
public static final LocalDate DATE_MAX = LocalDate.of(9999, 12, 31);
40+
41+
/** Minimum allowed datetime value ({@code 0001-01-01 18:00:00}). */
42+
public static final LocalDateTime DATETIME_MIN =
43+
LocalDateTime.of(DATE_MIN, LocalTime.MIN).minusSeconds(ZoneOffset.MIN.getTotalSeconds());
44+
45+
/** Maximum allowed datetime value ({@code 9999-12-31 05:59:59.999999999}). */
46+
public static final LocalDateTime DATETIME_MAX =
47+
LocalDateTime.of(DATE_MAX, LocalTime.MAX).minusSeconds(ZoneOffset.MAX.getTotalSeconds());
48+
49+
/** Minimum allowed timestamp value ({@code 0001-01-01 18:00:00 UTC}). */
50+
public static final Instant TIMESTAMP_MIN = DATETIME_MIN.toInstant(ZoneOffset.UTC);
51+
52+
/** Maximum allowed timestamp value ({@code 9999-12-31 05:59:59.999999999 UTC}. */
53+
public static final Instant TIMESTAMP_MAX = DATETIME_MAX.toInstant(ZoneOffset.UTC);
54+
3055
/**
3156
* Creates schema descriptor for the table with specified descriptor.
3257
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.ignite.internal.schema;
19+
20+
/**
21+
* An exception thrown when an attempt to write value out of allowed range is performed.
22+
*/
23+
class ValueOutOfBoundsException extends SchemaMismatchException {
24+
/** Serial version uid. */
25+
private static final long serialVersionUID = 0L;
26+
27+
/**
28+
* Constructor.
29+
*
30+
* @param msg Error message.
31+
*/
32+
ValueOutOfBoundsException(String msg) {
33+
super(msg);
34+
}
35+
}

0 commit comments

Comments
 (0)