You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by zs...@apache.org on 2022/03/04 07:20:21 UTC

[ignite-3] branch main updated: IGNITE-16633 Adoption of a bunch of calcite related tickets from Ignite-2 - Fixes #698.

This is an automated email from the ASF dual-hosted git repository.

zstan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 157a8f0  IGNITE-16633 Adoption of a bunch of calcite related tickets from Ignite-2 - Fixes #698.
157a8f0 is described below

commit 157a8f09bd98314f548f1672467f7dd682b32df2
Author: zstan <st...@gmail.com>
AuthorDate: Fri Mar 4 10:10:29 2022 +0300

    IGNITE-16633 Adoption of a bunch of calcite related tickets from Ignite-2 - Fixes #698.
    
    IGNITE-15109 TIMESTAMPDIFF for MICROSECOND unit doesn't work.
    IGNITE-14975 Introduce suppot INTERVAL type.
    IGNITE-15596 java.lang.Integer cannot be cast to java.lang.Long with JOIN and EXISTS.
    IGNITE-16129 RelJsonReader can't deserialize enums.
    
    Signed-off-by: zstan <st...@gmail.com>
---
 .../internal/sql/engine/ItDataTypesTest.java       |  64 +++-
 .../internal/sql/engine/ItFunctionsTest.java       |   9 +
 .../ignite/internal/sql/engine/ItIntervalTest.java | 398 +++++++++++++++++++++
 .../ignite/internal/sql/engine/ItMetadataTest.java |   9 +-
 modules/sql-engine/src/main/codegen/config.fmpp    |   1 +
 .../src/main/codegen/includes/parserImpls.ftl      |  32 +-
 .../sql/engine/exec/exp/IgniteBuiltInMethod.java   |   8 +-
 .../internal/sql/engine/exec/exp/RexImpTable.java  |  15 +-
 .../sql/engine/exec/exp/RexToLixTranslator.java    |  39 ++
 .../internal/sql/engine/externalize/RelJson.java   |  10 +-
 .../sql/engine/prepare/IgniteConvertletTable.java  | 128 +++++++
 .../sql/engine/prepare/IgniteTypeCoercion.java     |  58 +++
 .../engine/sql/IgniteSqlIntervalTypeNameSpec.java  |  62 ++++
 .../sql/engine/type/IgniteTypeFactory.java         |  59 ++-
 .../ignite/internal/sql/engine/util/Commons.java   |   6 +-
 .../ignite/internal/sql/engine/util/TypeUtils.java |  36 +-
 .../sql/engine/planner/TableSpoolPlannerTest.java  |   6 +-
 17 files changed, 897 insertions(+), 43 deletions(-)

diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
index 57c512c..d9f8bfe 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
@@ -77,7 +77,7 @@ public class ItDataTypesTest extends AbstractBasicIntegrationTest {
     @Disabled("https://issues.apache.org/jira/browse/IGNITE-16292")
     @Test
     public void testCheckDefaultsAndNullables() {
-        sql("CREATE TABLE tbl(c1 int primary key, c2 int NOT NULL, c3 int NOT NULL DEFAULT 100)");
+        sql("CREATE TABLE tbl(c1 int PRIMARY KEY, c2 int NOT NULL, c3 int NOT NULL DEFAULT 100)");
 
         sql("INSERT INTO tbl(c1, c2) VALUES (1, 2)");
 
@@ -94,4 +94,66 @@ public class ItDataTypesTest extends AbstractBasicIntegrationTest {
         //todo: correct exception https://issues.apache.org/jira/browse/IGNITE-16095
         assertThrows(IgniteException.class, () -> sql("INSERT INTO tbl(c1, c2) VALUES (2, NULL)"));
     }
+
+    /**
+     * Tests numeric types mapping on Java types.
+     */
+    @Test
+    public void testNumericRanges() {
+        try {
+            sql("CREATE TABLE tbl(id int PRIMARY KEY, tiny TINYINT, small SMALLINT, i INTEGER, big BIGINT)");
+
+            sql("INSERT INTO tbl VALUES (1, " + Byte.MAX_VALUE + ", " + Short.MAX_VALUE + ", "
+                    + Integer.MAX_VALUE + ", " + Long.MAX_VALUE + ')');
+
+            assertQuery("SELECT tiny FROM tbl").returns(Byte.MAX_VALUE).check();
+            assertQuery("SELECT small FROM tbl").returns(Short.MAX_VALUE).check();
+            assertQuery("SELECT i FROM tbl").returns(Integer.MAX_VALUE).check();
+            assertQuery("SELECT big FROM tbl").returns(Long.MAX_VALUE).check();
+
+            sql("DELETE from tbl");
+
+            sql("INSERT INTO tbl VALUES (1, " + Byte.MIN_VALUE + ", " + Short.MIN_VALUE + ", "
+                    + Integer.MIN_VALUE + ", " + Long.MIN_VALUE + ')');
+
+            assertQuery("SELECT tiny FROM tbl").returns(Byte.MIN_VALUE).check();
+            assertQuery("SELECT small FROM tbl").returns(Short.MIN_VALUE).check();
+            assertQuery("SELECT i FROM tbl").returns(Integer.MIN_VALUE).check();
+            assertQuery("SELECT big FROM tbl").returns(Long.MIN_VALUE).check();
+        } finally {
+            sql("DROP TABLE if exists tbl");
+        }
+    }
+
+    /**
+     * Tests numeric type convertation on equals.
+     */
+    @Test
+    public void testNumericConvertingOnEquals() {
+        try {
+            sql("CREATE TABLE tbl(id int PRIMARY KEY, tiny TINYINT, small SMALLINT, i INTEGER, big BIGINT)");
+
+            sql("INSERT INTO tbl VALUES (-1, 1, 2, 3, 4), (0, 5, 5, 5, 5)");
+
+            assertQuery("SELECT t1.tiny FROM tbl t1 JOIN tbl t2 ON (t1.tiny=t2.small)").returns((byte) 5).check();
+            assertQuery("SELECT t1.small FROM tbl t1 JOIN tbl t2 ON (t1.small=t2.tiny)").returns((short) 5).check();
+
+            assertQuery("SELECT t1.tiny FROM tbl t1 JOIN tbl t2 ON (t1.tiny=t2.i)").returns((byte) 5).check();
+            assertQuery("SELECT t1.i FROM tbl t1 JOIN tbl t2 ON (t1.i=t2.tiny)").returns(5).check();
+
+            assertQuery("SELECT t1.tiny FROM tbl t1 JOIN tbl t2 ON (t1.tiny=t2.big)").returns((byte) 5).check();
+            assertQuery("SELECT t1.big FROM tbl t1 JOIN tbl t2 ON (t1.big=t2.tiny)").returns(5L).check();
+
+            assertQuery("SELECT t1.small FROM tbl t1 JOIN tbl t2 ON (t1.small=t2.i)").returns((short) 5).check();
+            assertQuery("SELECT t1.i FROM tbl t1 JOIN tbl t2 ON (t1.i=t2.small)").returns(5).check();
+
+            assertQuery("SELECT t1.small FROM tbl t1 JOIN tbl t2 ON (t1.small=t2.big)").returns((short) 5).check();
+            assertQuery("SELECT t1.big FROM tbl t1 JOIN tbl t2 ON (t1.big=t2.small)").returns(5L).check();
+
+            assertQuery("SELECT t1.i FROM tbl t1 JOIN tbl t2 ON (t1.i=t2.big)").returns(5).check();
+            assertQuery("SELECT t1.big FROM tbl t1 JOIN tbl t2 ON (t1.big=t2.i)").returns(5L).check();
+        } finally {
+            sql("DROP TABLE if exists tbl");
+        }
+    }
 }
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItFunctionsTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItFunctionsTest.java
index fa00aa3..67540b6 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItFunctionsTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItFunctionsTest.java
@@ -47,6 +47,15 @@ public class ItFunctionsTest extends AbstractBasicIntegrationTest {
     private static final Object[] NULL_RESULT = new Object[] { null };
 
     @Test
+    public void testTimestampDiffWithFractionsOfSecond() {
+        assertQuery("SELECT TIMESTAMPDIFF(MICROSECOND, TIMESTAMP '2022-02-01 10:30:28.000', "
+                + "TIMESTAMP '2022-02-01 10:30:28.128')").returns(128000).check();
+
+        assertQuery("SELECT TIMESTAMPDIFF(NANOSECOND, TIMESTAMP '2022-02-01 10:30:28.000', "
+                + "TIMESTAMP '2022-02-01 10:30:28.128')").returns(128000000L).check();
+    }
+
+    @Test
     public void testLength() {
         assertQuery("SELECT LENGTH('TEST')").returns(4).check();
         assertQuery("SELECT LENGTH(NULL)").returns(NULL_RESULT).check();
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItIntervalTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItIntervalTest.java
new file mode 100644
index 0000000..47211c9
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItIntervalTest.java
@@ -0,0 +1,398 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.time.Duration;
+import java.time.Period;
+import org.apache.ignite.lang.IgniteInternalException;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+/** Interval coverage tests. */
+public class ItIntervalTest extends AbstractBasicIntegrationTest {
+    @Override
+    protected int nodes() {
+        return 1;
+    }
+
+    /**
+     * Test returned result for interval data types.
+     */
+    @Test
+    public void testIntervalResult() {
+        assertEquals(Duration.ofDays(4), eval("INTERVAL 4 DAYS"));
+        assertEquals(Duration.ofSeconds(1), eval("INTERVAL 1 SECONDS"));
+        assertEquals(Duration.ofSeconds(-1), eval("INTERVAL -1 SECONDS"));
+        assertEquals(Duration.ofSeconds(123), eval("INTERVAL 123 SECONDS"));
+        assertEquals(Duration.ofSeconds(123), eval("INTERVAL '123' SECONDS(3)"));
+        assertEquals(Duration.ofMinutes(2), eval("INTERVAL 2 MINUTES"));
+        assertEquals(Duration.ofHours(3), eval("INTERVAL 3 HOURS"));
+        assertEquals(Duration.ofDays(4), eval("INTERVAL 4 DAYS"));
+        assertEquals(Period.ofMonths(5), eval("INTERVAL 5 MONTHS"));
+        assertEquals(Period.ofMonths(-5), eval("INTERVAL -5 MONTHS"));
+        assertEquals(Period.ofYears(6), eval("INTERVAL 6 YEARS"));
+        assertEquals(Period.of(1, 2, 0), eval("INTERVAL '1-2' YEAR TO MONTH"));
+        assertEquals(Duration.ofHours(25), eval("INTERVAL '1 1' DAY TO HOUR"));
+        assertEquals(Duration.ofMinutes(62), eval("INTERVAL '1:2' HOUR TO MINUTE"));
+        assertEquals(Duration.ofSeconds(63), eval("INTERVAL '1:3' MINUTE TO SECOND"));
+        assertEquals(Duration.ofSeconds(3723), eval("INTERVAL '1:2:3' HOUR TO SECOND"));
+        assertEquals(Duration.ofMillis(3723456), eval("INTERVAL '0 1:2:3.456' DAY TO SECOND"));
+
+        assertThrowsEx("SELECT INTERVAL '123' SECONDS", IgniteInternalException.class, "exceeds precision");
+    }
+
+    /**
+     * Test cast interval types to integer and integer to interval.
+     */
+    @Test
+    public void testIntervalIntCast() {
+        assertNull(eval("CAST(NULL::INTERVAL SECONDS AS INT)"));
+        assertNull(eval("CAST(NULL::INTERVAL MONTHS AS INT)"));
+        assertEquals(1, eval("CAST(INTERVAL 1 SECONDS AS INT)"));
+        assertEquals(2, eval("CAST(INTERVAL 2 MINUTES AS INT)"));
+        assertEquals(3, eval("CAST(INTERVAL 3 HOURS AS INT)"));
+        assertEquals(4, eval("CAST(INTERVAL 4 DAYS AS INT)"));
+        assertEquals(-4, eval("CAST(INTERVAL -4 DAYS AS INT)"));
+        assertEquals(5, eval("CAST(INTERVAL 5 MONTHS AS INT)"));
+        assertEquals(6, eval("CAST(INTERVAL 6 YEARS AS INT)"));
+        assertEquals(-6, eval("CAST(INTERVAL -6 YEARS AS INT)"));
+
+        assertNull(eval("CAST(NULL::INT AS INTERVAL SECONDS)"));
+        assertNull(eval("CAST(NULL::INT AS INTERVAL MONTHS)"));
+        assertEquals(Duration.ofSeconds(1), eval("CAST(1 AS INTERVAL SECONDS)"));
+        assertEquals(Duration.ofMinutes(2), eval("CAST(2 AS INTERVAL MINUTES)"));
+        assertEquals(Duration.ofHours(3), eval("CAST(3 AS INTERVAL HOURS)"));
+        assertEquals(Duration.ofDays(4), eval("CAST(4 AS INTERVAL DAYS)"));
+        assertEquals(Period.ofMonths(5), eval("CAST(5 AS INTERVAL MONTHS)"));
+        assertEquals(Period.ofYears(6), eval("CAST(6 AS INTERVAL YEARS)"));
+
+        // Compound interval types cannot be cast.
+        assertThrowsEx("SELECT CAST(INTERVAL '1-2' YEAR TO MONTH AS INT)", IgniteInternalException.class, "cannot convert");
+        assertThrowsEx("SELECT CAST(INTERVAL '1 2' DAY TO HOUR AS INT)", IgniteInternalException.class, "cannot convert");
+
+        assertThrowsEx("SELECT CAST(1 AS INTERVAL YEAR TO MONTH)", IgniteInternalException.class, "cannot convert");
+        assertThrowsEx("SELECT CAST(1 AS INTERVAL DAY TO HOUR)", IgniteInternalException.class, "cannot convert");
+    }
+
+    /**
+     * Test cast interval types to string and string to interval.
+     */
+    @Test
+    public void testIntervalStringCast() {
+        assertNull(eval("CAST(NULL::INTERVAL SECONDS AS VARCHAR)"));
+        assertNull(eval("CAST(NULL::INTERVAL MONTHS AS VARCHAR)"));
+        assertEquals("+1.234", eval("CAST(INTERVAL '1.234' SECONDS (1,3) AS VARCHAR)"));
+        assertEquals("+1.000000", eval("CAST(INTERVAL 1 SECONDS AS VARCHAR)"));
+        assertEquals("+2", eval("CAST(INTERVAL 2 MINUTES AS VARCHAR)"));
+        assertEquals("+3", eval("CAST(INTERVAL 3 HOURS AS VARCHAR)"));
+        assertEquals("+4", eval("CAST(INTERVAL 4 DAYS AS VARCHAR)"));
+        assertEquals("+5", eval("CAST(INTERVAL 5 MONTHS AS VARCHAR)"));
+        assertEquals("+6", eval("CAST(INTERVAL 6 YEARS AS VARCHAR)"));
+        assertEquals("+1-02", eval("CAST(INTERVAL '1-2' YEAR TO MONTH AS VARCHAR)"));
+        assertEquals("+1 02", eval("CAST(INTERVAL '1 2' DAY TO HOUR AS VARCHAR)"));
+        assertEquals("-1 02:03:04.000000", eval("CAST(INTERVAL '-1 2:3:4' DAY TO SECOND AS VARCHAR)"));
+
+        assertNull(eval("CAST(NULL::VARCHAR AS INTERVAL SECONDS)"));
+        assertNull(eval("CAST(NULL::VARCHAR AS INTERVAL MONTHS)"));
+        assertEquals(Duration.ofSeconds(1), eval("CAST('1' AS INTERVAL SECONDS)"));
+        assertEquals(Duration.ofMinutes(2), eval("CAST('2' AS INTERVAL MINUTES)"));
+        assertEquals(Duration.ofHours(3), eval("CAST('3' AS INTERVAL HOURS)"));
+        assertEquals(Duration.ofDays(4), eval("CAST('4' AS INTERVAL DAYS)"));
+        assertEquals(Duration.ofHours(26), eval("CAST('1 2' AS INTERVAL DAY TO HOUR)"));
+        assertEquals(Duration.ofMinutes(62), eval("CAST('1:2' AS INTERVAL HOUR TO MINUTE)"));
+        assertEquals(Duration.ofMillis(3723456), eval("CAST('0 1:2:3.456' AS INTERVAL DAY TO SECOND)"));
+        assertEquals(Duration.ofMillis(-3723456), eval("CAST('-0 1:2:3.456' AS INTERVAL DAY TO SECOND)"));
+        assertEquals(Period.ofMonths(5), eval("CAST('5' AS INTERVAL MONTHS)"));
+        assertEquals(Period.ofYears(6), eval("CAST('6' AS INTERVAL YEARS)"));
+        assertEquals(Period.of(1, 2, 0), eval("CAST('1-2' AS INTERVAL YEAR TO MONTH)"));
+    }
+
+    /**
+     * Test cast between interval types.
+     */
+    @Test
+    public void testIntervalToIntervalCast() {
+        assertNull(eval("CAST(NULL::INTERVAL MINUTE AS INTERVAL SECONDS)"));
+        assertNull(eval("CAST(NULL::INTERVAL YEAR AS INTERVAL MONTHS)"));
+        assertEquals(Duration.ofMinutes(1), eval("CAST(INTERVAL 60 SECONDS AS INTERVAL MINUTE)"));
+        assertEquals(Duration.ofHours(1), eval("CAST(INTERVAL 60 MINUTES AS INTERVAL HOUR)"));
+        assertEquals(Duration.ofDays(1), eval("CAST(INTERVAL 24 HOURS AS INTERVAL DAY)"));
+        assertEquals(Period.ofYears(1), eval("CAST(INTERVAL 1 YEAR AS INTERVAL MONTHS)"));
+        assertEquals(Period.ofYears(1), eval("CAST(INTERVAL 12 MONTHS AS INTERVAL YEARS)"));
+
+        // Cannot convert between month-year and day-time interval types.
+        assertThrowsEx("SELECT CAST(INTERVAL 1 MONTHS AS INTERVAL DAYS)", IgniteInternalException.class, "cannot convert");
+        assertThrowsEx("SELECT CAST(INTERVAL 1 DAYS AS INTERVAL MONTHS)", IgniteInternalException.class, "cannot convert");
+    }
+
+    /**
+     * Test DML statements with interval data type.
+     */
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-16637")
+    @Test
+    public void testDml() {
+        sql("CREATE TABLE test(id int PRIMARY KEY, ym INTERVAL YEAR, dt INTERVAL DAYS)");
+        sql("INSERT INTO test VALUES (1, INTERVAL 1 MONTH, INTERVAL 2 DAYS)");
+        sql("INSERT INTO test VALUES (2, INTERVAL 3 YEARS, INTERVAL 4 HOURS)");
+        sql("INSERT INTO test VALUES (3, INTERVAL '4-5' YEARS TO MONTHS, INTERVAL '6:7' HOURS TO MINUTES)");
+        sql("INSERT INTO test VALUES (4, NULL, NULL)");
+
+        assertThrowsEx("INSERT INTO test VALUES (5, INTERVAL 1 DAYS, INTERVAL 1 HOURS)", IgniteInternalException.class,
+                "cannot assign");
+
+        assertThrowsEx("INSERT INTO test VALUES (6, INTERVAL 1 YEARS, INTERVAL 1 MONTHS)", IgniteInternalException.class,
+                "cannot assign");
+
+        assertQuery("SELECT ym, dt FROM test")
+                .returns(Period.ofMonths(1), Duration.ofDays(2))
+                .returns(Period.ofYears(3), Duration.ofHours(4))
+                .returns(Period.of(4, 5, 0), Duration.ofMinutes(367))
+                .returns(null, null)
+                .check();
+
+        assertThrowsEx("SELECT * FROM test WHERE ym = INTERVAL 6 DAYS", IgniteInternalException.class, "Cannot apply");
+        assertThrowsEx("SELECT * FROM test WHERE dt = INTERVAL 6 YEARS", IgniteInternalException.class, "Cannot apply");
+
+        sql("UPDATE test SET dt = INTERVAL 3 DAYS WHERE ym = INTERVAL 1 MONTH");
+        sql("UPDATE test SET ym = INTERVAL 5 YEARS WHERE dt = INTERVAL 4 HOURS");
+        sql("UPDATE test SET ym = INTERVAL '6-7' YEARS TO MONTHS, dt = INTERVAL '8 9' DAYS TO HOURS "
+                + "WHERE ym = INTERVAL '4-5' YEARS TO MONTHS AND dt = INTERVAL '6:7' HOURS TO MINUTES");
+
+        assertThrowsEx("UPDATE test SET dt = INTERVAL 5 YEARS WHERE ym = INTERVAL 1 MONTH", IgniteInternalException.class,
+                "Cannot assign");
+
+        assertThrowsEx("UPDATE test SET ym = INTERVAL 8 YEARS WHERE dt = INTERVAL 1 MONTH", IgniteInternalException.class,
+                "Cannot apply");
+
+        assertQuery("SELECT * FROM test")
+                .returns(Period.ofMonths(1), Duration.ofDays(3))
+                .returns(Period.ofYears(5), Duration.ofHours(4))
+                .returns(Period.of(6, 7, 0), Duration.ofHours(201))
+                .returns(null, null)
+                .check();
+
+        assertThrowsEx("DELETE FROM test WHERE ym = INTERVAL 6 DAYS", IgniteInternalException.class, "cannot apply");
+        assertThrowsEx("DELETE FROM test WHERE dt = INTERVAL 6 YEARS", IgniteInternalException.class, "cannot apply");
+
+        sql("DELETE FROM test WHERE ym = INTERVAL 1 MONTH");
+        sql("DELETE FROM test WHERE dt = INTERVAL 4 HOURS");
+        sql("DELETE FROM test WHERE ym = INTERVAL '6-7' YEARS TO MONTHS AND dt = INTERVAL '8 9' DAYS TO HOURS");
+        sql("DELETE FROM test WHERE ym IS NULL AND dt IS NULL");
+
+        assertEquals(0, sql("SELECT * FROM test").size());
+
+        sql("ALTER TABLE test ADD (ym2 INTERVAL MONTH, dt2 INTERVAL HOURS)");
+
+        sql("INSERT INTO test(id, ym, ym2, dt, dt2) VALUES (7, INTERVAL 1 YEAR, INTERVAL 2 YEARS, "
+                + "INTERVAL 1 SECOND, INTERVAL 2 MINUTES)");
+
+        assertQuery("SELECT ym, ym2, dt, dt2 FROM test")
+                .returns(Period.ofYears(1), Period.ofYears(2), Duration.ofSeconds(1), Duration.ofMinutes(2))
+                .check();
+    }
+
+    /**
+     * Test interval arithmetic.
+     */
+    @Test
+    public void testIntervalArithmetic() {
+        // Date +/- interval.
+        assertEquals(Date.valueOf("2021-01-02"), eval("DATE '2021-01-01' + INTERVAL 1 DAY"));
+        assertEquals(Date.valueOf("2020-12-31"), eval("DATE '2021-01-01' - INTERVAL 1 DAY"));
+        assertEquals(Date.valueOf("2020-12-31"), eval("DATE '2021-01-01' + INTERVAL -1 DAY"));
+        assertEquals(Date.valueOf("2021-02-01"), eval("DATE '2021-01-01' + INTERVAL 1 MONTH"));
+        assertEquals(Date.valueOf("2022-01-01"), eval("DATE '2021-01-01' + INTERVAL 1 YEAR"));
+        assertEquals(Date.valueOf("2022-02-01"), eval("DATE '2021-01-01' + INTERVAL '1-1' YEAR TO MONTH"));
+
+        // Timestamp +/- interval.
+        assertEquals(Timestamp.valueOf("2021-01-01 00:00:01"),
+                eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 SECOND"));
+        assertEquals(Timestamp.valueOf("2021-01-01 00:00:01.123"),
+                eval("TIMESTAMP '2021-01-01 00:00:00.123' + INTERVAL 1 SECOND"));
+        assertEquals(Timestamp.valueOf("2021-01-01 00:00:01.123"),
+                eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL '1.123' SECOND"));
+        assertEquals(Timestamp.valueOf("2021-01-01 00:00:01.246"),
+                eval("TIMESTAMP '2021-01-01 00:00:00.123' + INTERVAL '1.123' SECOND"));
+        assertEquals(Timestamp.valueOf("2020-12-31 23:59:59"),
+                eval("TIMESTAMP '2021-01-01 00:00:00' - INTERVAL 1 SECOND"));
+        assertEquals(Timestamp.valueOf("2020-12-31 23:59:59"),
+                eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL -1 SECOND"));
+        assertEquals(Timestamp.valueOf("2021-01-01 00:01:00"),
+                eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 MINUTE"));
+        assertEquals(Timestamp.valueOf("2021-01-01 01:00:00"),
+                eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 HOUR"));
+        assertEquals(Timestamp.valueOf("2021-01-02 00:00:00"),
+                eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 DAY"));
+        assertEquals(Timestamp.valueOf("2021-02-01 00:00:00"),
+                eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 MONTH"));
+        assertEquals(Timestamp.valueOf("2022-01-01 00:00:00"),
+                eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL 1 YEAR"));
+        assertEquals(Timestamp.valueOf("2021-01-02 01:01:01.123"),
+                eval("TIMESTAMP '2021-01-01 00:00:00' + INTERVAL '1 1:1:1.123' DAY TO SECOND"));
+        assertEquals(Timestamp.valueOf("2022-02-01 01:01:01.123"),
+                eval("TIMESTAMP '2021-01-01 01:01:01.123' + INTERVAL '1-1' YEAR TO MONTH"));
+
+        // Time +/- interval.
+        assertEquals(Time.valueOf("00:00:01"), eval("TIME '00:00:00' + INTERVAL 1 SECOND"));
+        assertEquals(Time.valueOf("00:01:00"), eval("TIME '00:00:00' + INTERVAL 1 MINUTE"));
+        assertEquals(Time.valueOf("01:00:00"), eval("TIME '00:00:00' + INTERVAL 1 HOUR"));
+
+        // Date - date as interval.
+        assertEquals(Duration.ofDays(1), eval("(DATE '2021-01-02' - DATE '2021-01-01') DAYS"));
+        assertEquals(Duration.ofDays(-1), eval("(DATE '2021-01-01' - DATE '2021-01-02') DAYS"));
+        assertEquals(Duration.ofDays(1), eval("(DATE '2021-01-02' - DATE '2021-01-01') HOURS"));
+        assertEquals(Period.ofYears(1), eval("(DATE '2022-01-01' - DATE '2021-01-01') YEARS"));
+        assertEquals(Period.ofMonths(1), eval("(DATE '2021-02-01' - DATE '2021-01-01') MONTHS"));
+        assertEquals(Period.ofMonths(-1), eval("(DATE '2021-01-01' - DATE '2021-02-01') MONTHS"));
+        assertEquals(Period.ofMonths(0), eval("(DATE '2021-01-20' - DATE '2021-01-01') MONTHS"));
+
+        // Timestamp - timestamp as interval.
+        assertEquals(Duration.ofDays(1),
+                eval("(TIMESTAMP '2021-01-02 00:00:00' - TIMESTAMP '2021-01-01 00:00:00') DAYS"));
+        assertEquals(Duration.ofDays(-1),
+                eval("(TIMESTAMP '2021-01-01 00:00:00' - TIMESTAMP '2021-01-02 00:00:00') DAYS"));
+        assertEquals(Duration.ofHours(1),
+                eval("(TIMESTAMP '2021-01-01 01:00:00' - TIMESTAMP '2021-01-01 00:00:00') HOURS"));
+        assertEquals(Duration.ofMinutes(1),
+                eval("(TIMESTAMP '2021-01-01 00:01:00' - TIMESTAMP '2021-01-01 00:00:00') MINUTES"));
+        assertEquals(Duration.ofSeconds(1),
+                eval("(TIMESTAMP '2021-01-01 00:00:01' - TIMESTAMP '2021-01-01 00:00:00') SECONDS"));
+        assertEquals(Duration.ofMillis(123),
+                eval("(TIMESTAMP '2021-01-01 00:00:00.123' - TIMESTAMP '2021-01-01 00:00:00') SECONDS"));
+        assertEquals(Period.ofYears(1),
+                eval("(TIMESTAMP '2022-01-01 00:00:00' - TIMESTAMP '2021-01-01 00:00:00') YEARS"));
+        assertEquals(Period.ofMonths(1),
+                eval("(TIMESTAMP '2021-02-01 00:00:00' - TIMESTAMP '2021-01-01 00:00:00') MONTHS"));
+        assertEquals(Period.ofMonths(-1),
+                eval("(TIMESTAMP '2021-01-01 00:00:00' - TIMESTAMP '2021-02-01 00:00:00') MONTHS"));
+        assertEquals(Period.ofMonths(0),
+                eval("(TIMESTAMP '2021-01-20 00:00:00' - TIMESTAMP '2021-01-01 00:00:00') MONTHS"));
+
+        // Time - time as interval.
+        assertEquals(Duration.ofHours(1), eval("(TIME '02:00:00' - TIME '01:00:00') HOURS"));
+        assertEquals(Duration.ofMinutes(1), eval("(TIME '00:02:00' - TIME '00:01:00') HOURS"));
+        assertEquals(Duration.ofMinutes(1), eval("(TIME '00:02:00' - TIME '00:01:00') MINUTES"));
+        assertEquals(Duration.ofSeconds(1), eval("(TIME '00:00:02' - TIME '00:00:01') SECONDS"));
+        assertEquals(Duration.ofMillis(123), eval("(TIME '00:00:01.123' - TIME '00:00:01') SECONDS"));
+
+        // Interval +/- interval.
+        assertEquals(Duration.ofSeconds(2), eval("INTERVAL 1 SECONDS + INTERVAL 1 SECONDS"));
+        assertEquals(Duration.ofSeconds(1), eval("INTERVAL 2 SECONDS - INTERVAL 1 SECONDS"));
+        assertEquals(Duration.ofSeconds(61), eval("INTERVAL 1 MINUTE + INTERVAL 1 SECONDS"));
+        assertEquals(Duration.ofSeconds(59), eval("INTERVAL 1 MINUTE - INTERVAL 1 SECONDS"));
+        assertEquals(Duration.ofSeconds(59), eval("INTERVAL 1 MINUTE + INTERVAL -1 SECONDS"));
+        assertEquals(Duration.ofSeconds(3723), eval("INTERVAL 1 HOUR + INTERVAL '2:3' MINUTE TO SECONDS"));
+        assertEquals(Duration.ofSeconds(3477), eval("INTERVAL 1 HOUR - INTERVAL '2:3' MINUTE TO SECONDS"));
+        assertEquals(Duration.ofHours(25), eval("INTERVAL 1 DAY + INTERVAL 1 HOUR"));
+        assertEquals(Period.ofMonths(2), eval("INTERVAL 1 MONTH + INTERVAL 1 MONTH"));
+        assertEquals(Period.ofYears(2), eval("INTERVAL 1 YEAR + INTERVAL 1 YEAR"));
+        assertEquals(Period.of(1, 1, 0), eval("INTERVAL 1 YEAR + INTERVAL 1 MONTH"));
+        assertEquals(Period.ofMonths(11), eval("INTERVAL 1 YEAR - INTERVAL 1 MONTH"));
+        assertEquals(Period.ofMonths(11), eval("INTERVAL 1 YEAR + INTERVAL -1 MONTH"));
+        assertThrowsEx("SELECT INTERVAL 1 DAY + INTERVAL 1 MONTH", IgniteInternalException.class, "Cannot apply");
+
+        // Interval * scalar.
+        assertEquals(Duration.ofSeconds(2), eval("INTERVAL 1 SECONDS * 2"));
+        assertEquals(Duration.ofSeconds(-2), eval("INTERVAL -1 SECONDS * 2"));
+        assertEquals(Duration.ofMinutes(4), eval("INTERVAL 2 MINUTES * 2"));
+        assertEquals(Duration.ofHours(6), eval("INTERVAL 3 HOURS * 2"));
+        assertEquals(Duration.ofDays(8), eval("INTERVAL 4 DAYS * 2"));
+        assertEquals(Period.ofMonths(10), eval("INTERVAL 5 MONTHS * 2"));
+        assertEquals(Period.ofMonths(-10), eval("INTERVAL -5 MONTHS * 2"));
+        assertEquals(Period.ofYears(12), eval("INTERVAL 6 YEARS * 2"));
+        assertEquals(Period.of(2, 4, 0), eval("INTERVAL '1-2' YEAR TO MONTH * 2"));
+        assertEquals(Duration.ofHours(50), eval("INTERVAL '1 1' DAY TO HOUR * 2"));
+        assertEquals(Duration.ofMinutes(124), eval("INTERVAL '1:2' HOUR TO MINUTE * 2"));
+        assertEquals(Duration.ofSeconds(126), eval("INTERVAL '1:3' MINUTE TO SECOND * 2"));
+        assertEquals(Duration.ofSeconds(7446), eval("INTERVAL '1:2:3' HOUR TO SECOND * 2"));
+        assertEquals(Duration.ofMillis(7446912), eval("INTERVAL '0 1:2:3.456' DAY TO SECOND * 2"));
+
+        // Interval / scalar
+        assertEquals(Duration.ofSeconds(1), eval("INTERVAL 2 SECONDS / 2"));
+        assertEquals(Duration.ofSeconds(-1), eval("INTERVAL -2 SECONDS / 2"));
+        assertEquals(Duration.ofSeconds(30), eval("INTERVAL 1 MINUTES / 2"));
+        assertEquals(Duration.ofMinutes(90), eval("INTERVAL 3 HOURS / 2"));
+        assertEquals(Duration.ofDays(2), eval("INTERVAL 4 DAYS / 2"));
+        assertEquals(Period.ofMonths(2), eval("INTERVAL 5 MONTHS / 2"));
+        assertEquals(Period.ofMonths(-2), eval("INTERVAL -5 MONTHS / 2"));
+        assertEquals(Period.of(3, 6, 0), eval("INTERVAL 7 YEARS / 2"));
+        assertEquals(Period.ofMonths(7), eval("INTERVAL '1-2' YEAR TO MONTH / 2"));
+        assertEquals(Duration.ofHours(13), eval("INTERVAL '1 2' DAY TO HOUR / 2"));
+        assertEquals(Duration.ofMinutes(31), eval("INTERVAL '1:2' HOUR TO MINUTE / 2"));
+        assertEquals(Duration.ofSeconds(31), eval("INTERVAL '1:2' MINUTE TO SECOND / 2"));
+        assertEquals(Duration.ofSeconds(1862), eval("INTERVAL '1:2:4' HOUR TO SECOND / 2"));
+        assertEquals(Duration.ofMillis(1862228), eval("INTERVAL '0 1:2:4.456' DAY TO SECOND / 2"));
+    }
+
+    /**
+     * Test EXTRACT function with interval data types.
+     */
+    @Test
+    public void testExtract() {
+        assertEquals(2L, eval("EXTRACT(MONTH FROM INTERVAL 14 MONTHS)"));
+        assertEquals(0L, eval("EXTRACT(MONTH FROM INTERVAL 1 YEAR)"));
+        assertEquals(2L, eval("EXTRACT(MONTH FROM INTERVAL '1-2' YEAR TO MONTH)"));
+        assertEquals(1L, eval("EXTRACT(YEAR FROM INTERVAL '1-2' YEAR TO MONTH)"));
+        assertEquals(-1L, eval("EXTRACT(MONTH FROM INTERVAL -1 MONTHS)"));
+        assertEquals(-1L, eval("EXTRACT(YEAR FROM INTERVAL -14 MONTHS)"));
+        assertEquals(-2L, eval("EXTRACT(MONTH FROM INTERVAL -14 MONTHS)"));
+        assertEquals(-20L, eval("EXTRACT(MINUTE FROM INTERVAL '-10:20' HOURS TO MINUTES)"));
+        assertEquals(1L, eval("EXTRACT(DAY FROM INTERVAL '1 2:3:4.567' DAY TO SECOND)"));
+        assertEquals(2L, eval("EXTRACT(HOUR FROM INTERVAL '1 2:3:4.567' DAY TO SECOND)"));
+        assertEquals(3L, eval("EXTRACT(MINUTE FROM INTERVAL '1 2:3:4.567' DAY TO SECOND)"));
+        assertEquals(4L, eval("EXTRACT(SECOND FROM INTERVAL '1 2:3:4.567' DAY TO SECOND)"));
+        assertEquals(4567L, eval("EXTRACT(MILLISECOND FROM INTERVAL '1 2:3:4.567' DAY TO SECOND)"));
+        assertEquals(-1L, eval("EXTRACT(DAY FROM INTERVAL '-1 2:3:4.567' DAY TO SECOND)"));
+        assertEquals(-2L, eval("EXTRACT(HOUR FROM INTERVAL '-1 2:3:4.567' DAY TO SECOND)"));
+        assertEquals(-3L, eval("EXTRACT(MINUTE FROM INTERVAL '-1 2:3:4.567' DAY TO SECOND)"));
+        assertEquals(-4L, eval("EXTRACT(SECOND FROM INTERVAL '-1 2:3:4.567' DAY TO SECOND)"));
+        assertEquals(-4567L, eval("EXTRACT(MILLISECOND FROM INTERVAL '-1 2:3:4.567' DAY TO SECOND)"));
+
+        assertThrowsEx("SELECT EXTRACT(DAY FROM INTERVAL 1 MONTH)", IgniteInternalException.class, "Cannot apply");
+        assertThrowsEx("SELECT EXTRACT(MONTH FROM INTERVAL 1 DAY)", IgniteInternalException.class, "Cannot apply");
+    }
+
+    /**
+     * Test caching of expressions by digest.
+     */
+    @Test
+    public void testScalarCache() {
+        // These expressions differs only in return data type, so digest should include also data type correctly
+        // compile scalar for second expression (should not get compiled scalar from the cache).
+        assertEquals(Duration.ofDays(1), eval("(DATE '2021-01-02' - DATE '2021-01-01') DAYS"));
+        assertEquals(Period.ofMonths(0), eval("(DATE '2021-01-02' - DATE '2021-01-01') MONTHS"));
+    }
+
+    public Object eval(String exp) {
+        return sql("SELECT " + exp).get(0).get(0);
+    }
+
+    private void assertThrowsEx(String sql, Class<? extends Exception> cls, String errMsg) {
+        Exception ex = assertThrows(cls, () -> sql(sql));
+
+        assertTrue(ex.getMessage().toLowerCase().contains(errMsg.toLowerCase()));
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java
index 3060bca..ea0dd72 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java
@@ -79,9 +79,12 @@ public class ItMetadataTest extends AbstractBasicIntegrationTest {
 
     @Test
     public void infixTypeCast() {
-        assertQuery("select id, id::tinyint as tid, id::smallint as sid, id::varchar as vid from person")
-                .columnNames("ID", "TID", "SID", "VID")
-                .columnTypes(Integer.class, Byte.class, Short.class, String.class)
+        assertQuery("select id, id::tinyint as tid, id::smallint as sid, id::varchar as vid, id::interval hour, "
+                + "id::interval year from person")
+                .columnNames("ID", "TID", "SID", "VID", "ID :: INTERVAL INTERVAL_HOUR", "ID :: INTERVAL INTERVAL_YEAR")
+                // TODO: IGNITE-16635 replace byte arrays for correct types.
+                //.columnTypes(Integer.class, Byte.class, Short.class, String.class, Duration.class, Period.class)
+                .columnTypes(Integer.class, Byte.class, Short.class, String.class, byte[].class, byte[].class)
                 .check();
     }
 
diff --git a/modules/sql-engine/src/main/codegen/config.fmpp b/modules/sql-engine/src/main/codegen/config.fmpp
index ada5e57..57d3f80 100644
--- a/modules/sql-engine/src/main/codegen/config.fmpp
+++ b/modules/sql-engine/src/main/codegen/config.fmpp
@@ -38,6 +38,7 @@ data: {
       "org.apache.ignite.internal.sql.engine.sql.IgniteSqlCreateTableOption",
       "org.apache.ignite.internal.sql.engine.sql.IgniteSqlCreateTableOptionEnum",
       "org.apache.ignite.internal.sql.engine.sql.IgniteSqlDropIndex",
+      "org.apache.ignite.internal.sql.engine.sql.IgniteSqlIntervalTypeNameSpec",
       "org.apache.calcite.sql.ddl.SqlDdlNodes",
     ]
 
diff --git a/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl b/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl
index 9a140e8..c95183b 100644
--- a/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl
+++ b/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl
@@ -66,6 +66,32 @@ void CreateTableOption(List<SqlNode> list) :
     }
 }
 
+SqlDataTypeSpec DataTypeEx() :
+{
+    final SqlDataTypeSpec dt;
+}
+{
+    (
+        dt = DataType()
+    |
+        dt = IntervalType()
+    )
+    {
+        return dt;
+    }
+}
+
+SqlDataTypeSpec IntervalType() :
+{
+    final Span s;
+    final SqlIntervalQualifier intervalQualifier;
+}
+{
+    <INTERVAL> { s = span(); } intervalQualifier = IntervalQualifier() {
+        return new SqlDataTypeSpec(new IgniteSqlIntervalTypeNameSpec(intervalQualifier, s.end(this)), s.pos());
+    }
+}
+
 void TableElement(List<SqlNode> list) :
 {
     final SqlDataTypeSpec type;
@@ -77,7 +103,7 @@ void TableElement(List<SqlNode> list) :
     SqlIdentifier id = null;
 }
 {
-    id = SimpleIdentifier() type = DataType() nullable = NullableOptDefaultNull()
+    id = SimpleIdentifier() type = DataTypeEx() nullable = NullableOptDefaultNull()
     (
         <DEFAULT_> { s.add(this); } dflt = Literal() {
             strategy = ColumnStrategy.DEFAULT;
@@ -257,7 +283,7 @@ void InfixCast(List<Object> list, ExprContext exprContext, Span s) :
     <INFIX_CAST> {
         checkNonQueryExpression(exprContext);
     }
-    dt = DataType() {
+    dt = DataTypeEx() {
         list.add(
             new SqlParserUtil.ToTreeListItem(SqlLibraryOperators.INFIX_CAST,
                 s.pos()));
@@ -293,7 +319,7 @@ SqlNode ColumnWithType() :
 }
 {
     id = SimpleIdentifier()
-    type = DataType()
+    type = DataTypeEx()
     [
         <NOT> <NULL> {
             nullable = false;
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteBuiltInMethod.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteBuiltInMethod.java
index db8c1c1..953b9d7 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteBuiltInMethod.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/IgniteBuiltInMethod.java
@@ -19,6 +19,8 @@ package org.apache.ignite.internal.sql.engine.exec.exp;
 
 import java.lang.reflect.Method;
 import org.apache.calcite.linq4j.tree.Types;
+import org.apache.calcite.sql.SqlIntervalQualifier;
+import org.apache.calcite.sql.parser.SqlParserUtil;
 
 /**
  * Built-in methods.
@@ -26,7 +28,11 @@ import org.apache.calcite.linq4j.tree.Types;
 public enum IgniteBuiltInMethod {
     SYSTEM_RANGE2(IgniteSqlFunctions.class, "systemRange", Object.class, Object.class),
 
-    SYSTEM_RANGE3(IgniteSqlFunctions.class, "systemRange", Object.class, Object.class, Object.class);
+    SYSTEM_RANGE3(IgniteSqlFunctions.class, "systemRange", Object.class, Object.class, Object.class),
+
+    PARSE_INTERVAL_YEAR_MONTH(SqlParserUtil.class, "intervalToMonths", String.class, SqlIntervalQualifier.class),
+
+    PARSE_INTERVAL_DAY_TIME(SqlParserUtil.class, "intervalToMillis", String.class, SqlIntervalQualifier.class);
 
     public final Method method;
 
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java
index dbd52f4..3c5ee07 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexImpTable.java
@@ -1320,6 +1320,8 @@ public class RexImpTable {
             Expression operand = argValueList.get(1);
             final SqlTypeName sqlTypeName =
                     call.operands.get(1).getType().getSqlTypeName();
+            final boolean isIntervalType = SqlTypeUtil.isInterval(call.operands.get(1).getType());
+
             switch (unit) {
                 case MILLENNIUM:
                 case CENTURY:
@@ -1372,7 +1374,7 @@ public class RexImpTable {
                         return Expressions.constant(0L);
                     }
 
-                    operand = mod(operand, TimeUnit.MINUTE.multiplier.longValue());
+                    operand = mod(operand, TimeUnit.MINUTE.multiplier.longValue(), !isIntervalType);
                     return Expressions.multiply(
                             operand, Expressions.constant((long) (1 / unit.multiplier.doubleValue())));
                 case EPOCH:
@@ -1426,7 +1428,10 @@ public class RexImpTable {
                     // No-Op.
             }
 
-            operand = mod(operand, getFactor(unit));
+            // According to SQL standard result for interval data types should have the same sign as the source,
+            // but QUARTER is not covered by standard and negative values for QUARTER make no sense.
+            operand = mod(operand, getFactor(unit), unit == TimeUnit.QUARTER || !isIntervalType);
+
             if (unit == TimeUnit.QUARTER) {
                 operand = Expressions.subtract(operand, Expressions.constant(1L));
             }
@@ -1441,12 +1446,12 @@ public class RexImpTable {
         }
     }
 
-    private static Expression mod(Expression operand, long factor) {
+    private static Expression mod(Expression operand, long factor, boolean floorMod) {
         if (factor == 1L) {
             return operand;
         } else {
-            return Expressions.call(BuiltInMethod.FLOOR_MOD.method,
-                    operand, Expressions.constant(factor));
+            return floorMod ? Expressions.call(BuiltInMethod.FLOOR_MOD.method, operand, Expressions.constant(factor)) :
+                    Expressions.modulo(operand, Expressions.constant(factor));
         }
     }
 
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
index 4017f24..bd073c8 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/exp/RexToLixTranslator.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.sql.engine.exec.exp;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CASE;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.SEARCH;
 
+import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.Type;
 import java.math.BigDecimal;
@@ -66,6 +67,7 @@ import org.apache.calcite.runtime.Geometries;
 import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.sql.validate.SqlConformance;
@@ -514,6 +516,43 @@ public class RexToLixTranslator implements RexVisitor<RexToLixTranslator.Result>
                         // No-Op.
                 }
                 break;
+            case INTERVAL_YEAR:
+            case INTERVAL_YEAR_MONTH:
+            case INTERVAL_MONTH:
+            case INTERVAL_DAY:
+            case INTERVAL_DAY_HOUR:
+            case INTERVAL_DAY_MINUTE:
+            case INTERVAL_DAY_SECOND:
+            case INTERVAL_HOUR:
+            case INTERVAL_HOUR_MINUTE:
+            case INTERVAL_HOUR_SECOND:
+            case INTERVAL_MINUTE:
+            case INTERVAL_MINUTE_SECOND:
+            case INTERVAL_SECOND:
+                switch (sourceType.getSqlTypeName().getFamily()) {
+                    case CHARACTER:
+                        SqlIntervalQualifier intervalQualifier = targetType.getIntervalQualifier();
+
+                        Method method = intervalQualifier.isYearMonth()
+                                ? IgniteBuiltInMethod.PARSE_INTERVAL_YEAR_MONTH.method
+                                : IgniteBuiltInMethod.PARSE_INTERVAL_DAY_TIME.method;
+
+                        convert = Expressions.call(
+                                method,
+                                operand,
+                                Expressions.new_(SqlIntervalQualifier.class,
+                                        Expressions.constant(intervalQualifier.getStartUnit()),
+                                        Expressions.constant(intervalQualifier.getStartPrecisionPreservingDefault()),
+                                        Expressions.constant(intervalQualifier.getEndUnit()),
+                                        Expressions.constant(intervalQualifier.getFractionalSecondPrecisionPreservingDefault()),
+                                        Expressions.field(null, SqlParserPos.class, "ZERO")
+                                )
+                        );
+                        break;
+                    default:
+                        // No-Op.
+                }
+                break;
             default:
                 // No-Op.
         }
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJson.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJson.java
index 6f12e78..64646b9 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJson.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/externalize/RelJson.java
@@ -626,16 +626,16 @@ class RelJson {
                 SqlTypeName sqlTypeName = toEnum(map.get("type"));
                 Integer precision = (Integer) map.get("precision");
                 Integer scale = (Integer) map.get("scale");
+                RelDataType type;
+
                 if (SqlTypeName.INTERVAL_TYPES.contains(sqlTypeName)) {
                     TimeUnit startUnit = sqlTypeName.getStartUnit();
                     TimeUnit endUnit = sqlTypeName.getEndUnit();
-                    return typeFactory.createSqlIntervalType(
+                    type = typeFactory.createSqlIntervalType(
                             new SqlIntervalQualifier(startUnit, endUnit, SqlParserPos.ZERO));
                 } else if (sqlTypeName == SqlTypeName.ARRAY) {
-                    return typeFactory.createArrayType(toType(typeFactory, map.get("elementType")), -1);
-                }
-                RelDataType type;
-                if (precision == null) {
+                    type = typeFactory.createArrayType(toType(typeFactory, map.get("elementType")), -1);
+                } else if (precision == null) {
                     type = typeFactory.createSqlType(sqlTypeName);
                 } else if (scale == null) {
                     type = typeFactory.createSqlType(sqlTypeName, precision);
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteConvertletTable.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteConvertletTable.java
new file mode 100644
index 0000000..26b30da
--- /dev/null
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteConvertletTable.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.prepare;
+
+import java.math.BigDecimal;
+import java.util.List;
+import org.apache.calcite.avatica.util.DateTimeUtils;
+import org.apache.calcite.avatica.util.TimeUnit;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlIntervalQualifier;
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.type.SqlTypeUtil;
+import org.apache.calcite.sql2rel.ReflectiveConvertletTable;
+import org.apache.calcite.sql2rel.SqlRexContext;
+import org.apache.calcite.sql2rel.SqlRexConvertlet;
+import org.apache.calcite.sql2rel.SqlRexConvertletTable;
+import org.apache.calcite.sql2rel.StandardConvertletTable;
+
+/**
+ * Implementation of {@link SqlRexConvertletTable}.
+ */
+public class IgniteConvertletTable extends ReflectiveConvertletTable {
+    public static final IgniteConvertletTable INSTANCE = new IgniteConvertletTable();
+
+    private IgniteConvertletTable() {
+        // Replace Calcite's convertlet with our own.
+        registerOp(SqlStdOperatorTable.TIMESTAMP_DIFF, new TimestampDiffConvertlet());
+    }
+
+    /** {@inheritDoc} */
+    @Override public SqlRexConvertlet get(SqlCall call) {
+        SqlRexConvertlet res = super.get(call);
+
+        return res == null ? StandardConvertletTable.INSTANCE.get(call) : res;
+    }
+
+    /** Convertlet that handles the {@code TIMESTAMPDIFF} function. */
+    private static class TimestampDiffConvertlet implements SqlRexConvertlet {
+        /** {@inheritDoc} */
+        @Override public RexNode convertCall(SqlRexContext cx, SqlCall call) {
+            // TIMESTAMPDIFF(unit, t1, t2)
+            //    => (t2 - t1) UNIT
+            final RexBuilder rexBuilder = cx.getRexBuilder();
+            final SqlLiteral unitLiteral = call.operand(0);
+            TimeUnit unit = unitLiteral.getValueAs(TimeUnit.class);
+            BigDecimal multiplier = BigDecimal.ONE;
+            BigDecimal divider = BigDecimal.ONE;
+            SqlTypeName sqlTypeName = unit == TimeUnit.NANOSECOND
+                    ? SqlTypeName.BIGINT
+                    : SqlTypeName.INTEGER;
+            switch (unit) {
+                case MICROSECOND:
+                case MILLISECOND:
+                case NANOSECOND:
+                    divider = unit.multiplier;
+                    unit = TimeUnit.MILLISECOND;
+                    break;
+                case WEEK:
+                    multiplier = BigDecimal.valueOf(DateTimeUtils.MILLIS_PER_SECOND);
+                    divider = unit.multiplier;
+                    unit = TimeUnit.SECOND;
+                    break;
+                case QUARTER:
+                    divider = unit.multiplier;
+                    unit = TimeUnit.MONTH;
+                    break;
+                default:
+                    break;
+            }
+            final SqlIntervalQualifier qualifier =
+                    new SqlIntervalQualifier(unit, null, SqlParserPos.ZERO);
+            final RexNode op2 = cx.convertExpression(call.operand(2));
+            final RexNode op1 = cx.convertExpression(call.operand(1));
+            final RelDataType intervalType =
+                    cx.getTypeFactory().createTypeWithNullability(
+                            cx.getTypeFactory().createSqlIntervalType(qualifier),
+                            op1.getType().isNullable() || op2.getType().isNullable());
+            final RexCall rexCall = (RexCall) rexBuilder.makeCall(
+                    intervalType, SqlStdOperatorTable.MINUS_DATE,
+                    List.of(op2, op1));
+            final RelDataType intType =
+                    cx.getTypeFactory().createTypeWithNullability(
+                            cx.getTypeFactory().createSqlType(sqlTypeName),
+                            SqlTypeUtil.containsNullable(rexCall.getType()));
+
+            RexNode e;
+
+            // Since Calcite converts internal time representation to seconds during cast we need our own cast
+            // method to keep fraction of seconds.
+            if (unit == TimeUnit.MILLISECOND) {
+                e = makeCastMilliseconds(rexBuilder, intType, rexCall);
+            } else {
+                e = rexBuilder.makeCast(intType, rexCall);
+            }
+
+            return rexBuilder.multiplyDivide(e, multiplier, divider);
+        }
+
+        /**
+         * Creates a call to cast milliseconds interval.
+         */
+        static RexNode makeCastMilliseconds(RexBuilder builder, RelDataType type, RexNode exp) {
+            return builder.ensureType(type, builder.decodeIntervalOrDecimal(exp), false);
+        }
+    }
+}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java
new file mode 100644
index 0000000..55a69a5
--- /dev/null
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.prepare;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.type.SqlTypeUtil;
+import org.apache.calcite.sql.validate.SqlValidator;
+import org.apache.calcite.sql.validate.SqlValidatorScope;
+import org.apache.calcite.sql.validate.implicit.TypeCoercionImpl;
+
+/** Implicit type cast implementation. */
+public class IgniteTypeCoercion extends TypeCoercionImpl {
+    public IgniteTypeCoercion(RelDataTypeFactory typeFactory, SqlValidator validator) {
+        super(typeFactory, validator);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean needToCast(SqlValidatorScope scope, SqlNode node, RelDataType toType) {
+        if (SqlTypeUtil.isInterval(toType)) {
+            RelDataType fromType = validator.deriveType(scope, node);
+
+            if (SqlTypeUtil.isInterval(fromType)) {
+                // Two different families of intervals: INTERVAL_DAY_TIME and INTERVAL_YEAR_MONTH.
+                return fromType.getSqlTypeName().getFamily() != toType.getSqlTypeName().getFamily();
+            }
+        } else if (SqlTypeUtil.isIntType(toType)) {
+            RelDataType fromType = validator.deriveType(scope, node);
+
+            if (fromType == null) {
+                return false;
+            }
+
+            if (SqlTypeUtil.isIntType(fromType) && fromType.getSqlTypeName() != toType.getSqlTypeName()) {
+                return true;
+            }
+        }
+
+        return super.needToCast(scope, node, toType);
+    }
+}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlIntervalTypeNameSpec.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlIntervalTypeNameSpec.java
new file mode 100644
index 0000000..e063ca4
--- /dev/null
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/sql/IgniteSqlIntervalTypeNameSpec.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.sql;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlIntervalQualifier;
+import org.apache.calcite.sql.SqlTypeNameSpec;
+import org.apache.calcite.sql.SqlWriter;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.validate.SqlValidator;
+import org.apache.calcite.util.Litmus;
+
+/**
+ * A SQL type name specification of interval types.
+ */
+public class IgniteSqlIntervalTypeNameSpec extends SqlTypeNameSpec {
+    private final SqlIntervalQualifier sqlIntervalQualifier;
+
+    public IgniteSqlIntervalTypeNameSpec(SqlIntervalQualifier intervalQualifier, SqlParserPos pos) {
+        super(new SqlIdentifier(intervalQualifier.typeName().name(), pos), pos);
+        sqlIntervalQualifier = intervalQualifier;
+    }
+
+    /** {@inheritDoc} */
+    @Override public RelDataType deriveType(SqlValidator validator) {
+        return validator.getTypeFactory().createSqlIntervalType(sqlIntervalQualifier);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
+        writer.keyword("INTERVAL");
+
+        getTypeName().unparse(writer, leftPrec, rightPrec);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equalsDeep(SqlTypeNameSpec spec, Litmus litmus) {
+        if (!(spec instanceof IgniteSqlIntervalTypeNameSpec)) {
+            return false;
+        }
+
+        IgniteSqlIntervalTypeNameSpec that = (IgniteSqlIntervalTypeNameSpec) spec;
+
+        return getTypeName().equalsDeep(that.getTypeName(), litmus);
+    }
+}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java
index a18746e..a6df9bc 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/type/IgniteTypeFactory.java
@@ -26,18 +26,22 @@ import java.math.BigInteger;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.sql.Timestamp;
+import java.time.Duration;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.Period;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import org.apache.calcite.avatica.util.ByteString;
+import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.runtime.Geometries;
+import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlUtil;
+import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.BasicSqlType;
 import org.apache.calcite.sql.type.IntervalSqlType;
 import org.apache.ignite.internal.schema.configuration.SchemaConfigurationConverter;
@@ -47,13 +51,23 @@ import org.apache.ignite.schema.definition.ColumnType;
  * Ignite type factory.
  */
 public class IgniteTypeFactory extends JavaTypeFactoryImpl {
+    /** Interval qualifier to create year-month interval types. */
+    private static final SqlIntervalQualifier INTERVAL_QUALIFIER_YEAR_MONTH = new SqlIntervalQualifier(TimeUnit.YEAR,
+            TimeUnit.MONTH, SqlParserPos.ZERO);
+
+    /** Interval qualifier to create day-time interval types. */
+    private static final SqlIntervalQualifier INTERVAL_QUALIFIER_DAY_TIME = new SqlIntervalQualifier(TimeUnit.DAY,
+            TimeUnit.SECOND, SqlParserPos.ZERO);
+
+    /** Default charset. */
+    private final Charset charset;
 
     /**
      * Constructor.
      * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
      */
     public IgniteTypeFactory() {
-        super(IgniteTypeSystem.INSTANCE);
+        this(IgniteTypeSystem.INSTANCE);
     }
 
     /**
@@ -63,6 +77,14 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
      */
     public IgniteTypeFactory(RelDataTypeSystem typeSystem) {
         super(typeSystem);
+
+        if (SqlUtil.translateCharacterSetName(Charset.defaultCharset().name()) != null) {
+            // Use JVM default charset rather then Calcite default charset (ISO-8859-1).
+            charset = Charset.defaultCharset();
+        } else {
+            // If JVM default charset is not supported by Calcite - use UTF-8.
+            charset = StandardCharsets.UTF_8;
+        }
     }
 
     /** {@inheritDoc} */
@@ -192,11 +214,13 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
                 case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
                     return LocalDateTime.class;
                 case INTEGER:
+                    return type.isNullable() ? Integer.class : int.class;
                 case INTERVAL_YEAR:
                 case INTERVAL_YEAR_MONTH:
                 case INTERVAL_MONTH:
-                    return type.isNullable() ? Integer.class : int.class;
+                    return Period.class;
                 case BIGINT:
+                    return type.isNullable() ? Long.class : long.class;
                 case INTERVAL_DAY:
                 case INTERVAL_DAY_HOUR:
                 case INTERVAL_DAY_MINUTE:
@@ -207,7 +231,7 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
                 case INTERVAL_MINUTE:
                 case INTERVAL_MINUTE_SECOND:
                 case INTERVAL_SECOND:
-                    return type.isNullable() ? Long.class : long.class;
+                    return Duration.class;
                 case SMALLINT:
                     return type.isNullable() ? Short.class : short.class;
                 case TINYINT:
@@ -266,14 +290,31 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
     /** {@inheritDoc} */
     @Override
     public Charset getDefaultCharset() {
-        // Use JVM default charset rather then Calcite default charset (ISO-8859-1).
-        Charset jvmDefault = Charset.defaultCharset();
+        return charset;
+    }
 
-        if (SqlUtil.translateCharacterSetName(jvmDefault.name().toUpperCase(Locale.ROOT)) == null) {
-            jvmDefault = StandardCharsets.UTF_8;
+    /** {@inheritDoc} */
+    @Override public RelDataType toSql(RelDataType type) {
+        if (type instanceof JavaType) {
+            Class<?> clazz = ((JavaType) type).getJavaClass();
+
+            if (clazz == Duration.class) {
+                return createTypeWithNullability(createSqlIntervalType(INTERVAL_QUALIFIER_DAY_TIME), true);
+            } else if (clazz == Period.class) {
+                return createTypeWithNullability(createSqlIntervalType(INTERVAL_QUALIFIER_YEAR_MONTH), true);
+            }
+        }
+
+        return super.toSql(type);
+    }
+
+    /** {@inheritDoc} */
+    @Override public RelDataType createType(Type type) {
+        if (type == Duration.class || type == Period.class) {
+            return createJavaType((Class<?>) type);
         }
 
-        return jvmDefault;
+        return super.createType(type);
     }
 
     private boolean allEquals(List<RelDataType> types) {
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/Commons.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/Commons.java
index 4b60ea9..7715808 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/Commons.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/Commons.java
@@ -95,6 +95,8 @@ import org.apache.ignite.internal.sql.engine.exec.exp.RexExecutorImpl;
 import org.apache.ignite.internal.sql.engine.metadata.cost.IgniteCostFactory;
 import org.apache.ignite.internal.sql.engine.prepare.AbstractMultiStepPlan;
 import org.apache.ignite.internal.sql.engine.prepare.ExplainPlan;
+import org.apache.ignite.internal.sql.engine.prepare.IgniteConvertletTable;
+import org.apache.ignite.internal.sql.engine.prepare.IgniteTypeCoercion;
 import org.apache.ignite.internal.sql.engine.prepare.MultiStepPlan;
 import org.apache.ignite.internal.sql.engine.prepare.PlanningContext;
 import org.apache.ignite.internal.sql.engine.prepare.QueryPlan;
@@ -138,6 +140,7 @@ public final class Commons {
                                     .build()
                     )
             )
+            .convertletTable(IgniteConvertletTable.INSTANCE)
             .parserConfig(
                     SqlParser.config()
                             .withParserFactory(IgniteSqlParserImpl.FACTORY)
@@ -146,7 +149,8 @@ public final class Commons {
             .sqlValidatorConfig(SqlValidator.Config.DEFAULT
                     .withIdentifierExpansion(true)
                     .withDefaultNullCollation(NullCollation.LOW)
-                    .withSqlConformance(IgniteSqlConformance.INSTANCE))
+                    .withSqlConformance(IgniteSqlConformance.INSTANCE)
+                    .withTypeCoercionFactory(IgniteTypeCoercion::new))
             // Dialects support.
             .operatorTable(SqlOperatorTables.chain(
                     SqlLibraryOperatorTableFactory.INSTANCE
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
index fa82561..10b2217 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
@@ -24,12 +24,14 @@ import static org.apache.ignite.internal.util.CollectionUtils.nullOrEmpty;
 
 import java.lang.reflect.Type;
 import java.sql.Timestamp;
+import java.time.Duration;
+import java.time.Period;
 import java.util.Arrays;
-import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
@@ -61,17 +63,13 @@ import org.jetbrains.annotations.Nullable;
  * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
  */
 public class TypeUtils {
-    private static final EnumSet<SqlTypeName> CONVERTABLE_SQL_TYPES = EnumSet.of(
-            SqlTypeName.DATE,
-            SqlTypeName.TIME,
-            SqlTypeName.TIMESTAMP
-    );
-
     private static final Set<Type> CONVERTABLE_TYPES = Set.of(
             java.util.Date.class,
             java.sql.Date.class,
             java.sql.Time.class,
-            java.sql.Timestamp.class
+            java.sql.Timestamp.class,
+            Duration.class,
+            Period.class
     );
 
     /**
@@ -266,10 +264,12 @@ public class TypeUtils {
     }
 
     private static Function<Object, Object> fieldConverter(ExecutionContext<?> ectx, RelDataType fieldType) {
-        if (CONVERTABLE_SQL_TYPES.contains(fieldType.getSqlTypeName())) {
-            Type storageType = ectx.getTypeFactory().getJavaClass(fieldType);
+        Type storageType = ectx.getTypeFactory().getJavaClass(fieldType);
+
+        if (isConvertableType(storageType)) {
             return v -> fromInternal(ectx, v, storageType);
         }
+
         return Function.identity();
     }
 
@@ -286,12 +286,13 @@ public class TypeUtils {
      * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
      */
     public static boolean isConvertableType(RelDataType type) {
-        return CONVERTABLE_SQL_TYPES.contains(type.getSqlTypeName());
+        return type instanceof RelDataTypeFactoryImpl.JavaType
+                && isConvertableType(((RelDataTypeFactoryImpl.JavaType) type).getJavaClass());
     }
 
     private static boolean hasConvertableFields(RelDataType resultType) {
         return RelOptUtil.getFieldTypeList(resultType).stream()
-                .anyMatch(t -> CONVERTABLE_SQL_TYPES.contains(t.getSqlTypeName()));
+                .anyMatch(TypeUtils::isConvertableType);
     }
 
     /**
@@ -319,6 +320,11 @@ public class TypeUtils {
             return SqlFunctions.toLong((java.util.Date) val, DataContext.Variable.TIME_ZONE.get(ectx));
         } else if (storageType == java.util.Date.class) {
             return SqlFunctions.toLong((java.util.Date) val, DataContext.Variable.TIME_ZONE.get(ectx));
+        } else if (storageType == Duration.class) {
+            return TimeUnit.SECONDS.toMillis(((Duration) val).getSeconds())
+                    + TimeUnit.NANOSECONDS.toMillis(((Duration) val).getNano());
+        } else if (storageType == Period.class) {
+            return (int) ((Period) val).toTotalMonths();
         } else {
             return val;
         }
@@ -340,6 +346,10 @@ public class TypeUtils {
             return new Timestamp((Long) val - DataContext.Variable.TIME_ZONE.<TimeZone>get(ectx).getOffset((Long) val));
         } else if (storageType == java.util.Date.class && val instanceof Long) {
             return new java.util.Date((Long) val - DataContext.Variable.TIME_ZONE.<TimeZone>get(ectx).getOffset((Long) val));
+        } else if (storageType == Duration.class && val instanceof Long) {
+            return Duration.ofMillis((Long) val);
+        } else if (storageType == Period.class && val instanceof Integer) {
+            return Period.of((Integer) val / 12, (Integer) val % 12, 0);
         } else {
             return val;
         }
@@ -385,7 +395,7 @@ public class TypeUtils {
             case OTHER:
                 return NativeTypes.blobOf(type.getPrecision());
             default:
-                assert false : "Unexpected type of result: " + type;
+                assert false : "Unexpected type of result: " + type.getSqlTypeName();
                 return null;
         }
     }
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TableSpoolPlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TableSpoolPlannerTest.java
index 75ab749..f1144ad 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TableSpoolPlannerTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/TableSpoolPlannerTest.java
@@ -20,8 +20,8 @@ package org.apache.ignite.internal.sql.engine.planner;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import org.apache.calcite.plan.RelOptUtil;
-import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
 import org.apache.ignite.internal.sql.engine.rel.IgniteTableSpool;
 import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
 import org.apache.ignite.internal.sql.engine.trait.IgniteDistribution;
@@ -79,7 +79,7 @@ public class TableSpoolPlannerTest extends AbstractPlannerTest {
                 + "from t0 "
                 + "join t1 on t0.jid > t1.jid";
 
-        RelNode phys = physicalPlan(sql, publicSchema,
+        IgniteRel phys = physicalPlan(sql, publicSchema,
                 "MergeJoinConverter", "NestedLoopJoinConverter", "FilterSpoolMergeRule");
 
         assertNotNull(phys);
@@ -87,5 +87,7 @@ public class TableSpoolPlannerTest extends AbstractPlannerTest {
         IgniteTableSpool tblSpool = findFirstNode(phys, byClass(IgniteTableSpool.class));
 
         assertNotNull(tblSpool, "Invalid plan:\n" + RelOptUtil.toString(phys));
+
+        checkSplitAndSerialization(phys, publicSchema);
     }
 }