You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by tl...@apache.org on 2021/04/15 12:48:48 UTC

[ignite] branch sql-calcite updated: IGNITE-13547 Calcite integration. CREATE TABLE support

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

tledkov pushed a commit to branch sql-calcite
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/sql-calcite by this push:
     new 5ba33d3  IGNITE-13547 Calcite integration. CREATE TABLE support
5ba33d3 is described below

commit 5ba33d3f98a4421f32df62880c17426a7f5b8b22
Author: korlov42 <ko...@gridgain.com>
AuthorDate: Thu Apr 15 15:48:29 2021 +0300

    IGNITE-13547 Calcite integration. CREATE TABLE support
---
 modules/calcite/pom.xml                            |  96 ++-
 modules/calcite/src/main/codegen/config.fmpp       | 642 +++++++++++++++++++++
 .../src/main/codegen/includes/parserImpls.ftl      | 181 ++++++
 .../java/org/apache/calcite/sql/IgniteSqlNode.java |  31 +
 .../query/calcite/CalciteQueryProcessor.java       |   5 +-
 .../query/calcite/exec/ExecutionServiceImpl.java   |  46 +-
 .../query/calcite/exec/ddl/DdlCommandHandler.java  | 215 +++++++
 .../processors/query/calcite/prepare/DdlPlan.java  |  46 ++
 .../query/calcite/prepare/IgnitePlanner.java       |   6 +
 .../calcite/prepare/ddl/ColumnDefinition.java      |  83 +++
 .../calcite/prepare/ddl/CreateTableCommand.java    | 304 ++++++++++
 .../query/calcite/prepare/ddl/DdlCommand.java      |  21 +
 .../prepare/ddl/DdlSqlToCommandConverter.java      | 344 +++++++++++
 .../query/calcite/schema/TableDescriptorImpl.java  |  47 +-
 .../query/calcite/sql/IgniteSqlCreateTable.java    | 117 ++++
 .../calcite/sql/IgniteSqlCreateTableOption.java    |  91 +++
 .../sql/IgniteSqlCreateTableOptionEnum.java        |  55 ++
 .../query/calcite/CalciteQueryProcessorTest.java   |  28 +-
 .../query/calcite/CreateTableIntegrationTest.java  | 276 +++++++++
 .../calcite/planner/JoinColocationPlannerTest.java |  18 +-
 .../query/calcite/sql/SqlDdlParserTest.java        | 325 +++++++++++
 .../ignite/testsuites/IgniteCalciteTestSuite.java  |   5 +
 .../cache/query/IgniteQueryErrorCode.java          |   3 +
 .../processors/query/GridQueryTypeDescriptor.java  |  12 +
 .../internal/processors/query/QueryEntityEx.java   |  20 +-
 .../processors/query/QueryTypeDescriptorImpl.java  |  13 +
 .../internal/processors/query/QueryUtils.java      |  68 ++-
 .../apache/ignite/testframework/GridTestUtils.java |  16 +
 .../processors/query/h2/CommandProcessor.java      |  63 +-
 29 files changed, 3073 insertions(+), 104 deletions(-)

diff --git a/modules/calcite/pom.xml b/modules/calcite/pom.xml
index 1491b62..81171e7 100644
--- a/modules/calcite/pom.xml
+++ b/modules/calcite/pom.xml
@@ -20,7 +20,8 @@
 <!--
     POM file.
 -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
     <!-- Module specific package versions -->
@@ -70,12 +71,6 @@
 
         <dependency>
             <groupId>org.apache.calcite</groupId>
-            <artifactId>calcite-babel</artifactId>
-            <version>${calcite.version}</version>
-        </dependency>
-
-        <dependency>
-            <groupId>org.apache.calcite</groupId>
             <artifactId>calcite-linq4j</artifactId>
             <version>${calcite.version}</version>
         </dependency>
@@ -203,6 +198,91 @@
                 <groupId>org.apache.felix</groupId>
                 <artifactId>maven-bundle-plugin</artifactId>
             </plugin>
-          </plugins>
+            <plugin>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-fmpp-resources</id>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/codegen</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/codegen</directory>
+                                    <filtering>false</filtering>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>unpack-parser-template</id>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>unpack</goal>
+                        </goals>
+                        <configuration>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>org.apache.calcite</groupId>
+                                    <artifactId>calcite-core</artifactId>
+                                    <type>jar</type>
+                                    <overWrite>true</overWrite>
+                                    <outputDirectory>${project.build.directory}/</outputDirectory>
+                                    <includes>codegen/templates/Parser.jj</includes>
+                                </artifactItem>
+                            </artifactItems>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>com.googlecode.fmpp-maven-plugin</groupId>
+                <artifactId>fmpp-maven-plugin</artifactId>
+                <version>1.0</version>
+                <configuration>
+                    <cfgFile>${project.build.directory}/codegen/config.fmpp</cfgFile>
+                    <templateDirectory>${project.build.directory}/codegen/templates</templateDirectory>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>generate-fmpp-sources</id>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>generate</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>javacc-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>javacc</id>
+                        <goals>
+                            <goal>javacc</goal>
+                        </goals>
+                        <configuration>
+                            <sourceDirectory>${project.build.directory}/generated-sources/fmpp</sourceDirectory>
+                            <outputDirectory>${project.build.directory}/generated-sources/javacc</outputDirectory>
+                            <includes>
+                                <include>**/Parser.jj</include>
+                            </includes>
+                            <lookAhead>2</lookAhead>
+                            <isStatic>false</isStatic>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
     </build>
 </project>
diff --git a/modules/calcite/src/main/codegen/config.fmpp b/modules/calcite/src/main/codegen/config.fmpp
new file mode 100644
index 0000000..a7db7a4
--- /dev/null
+++ b/modules/calcite/src/main/codegen/config.fmpp
@@ -0,0 +1,642 @@
+# 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.
+
+data: {
+  # Data declarations for this parser.
+  #
+  # Default declarations are in default_config.fmpp; if you do not include a
+  # declaration ('imports' or 'nonReservedKeywords', for example) in this file,
+  # FMPP will use the declaration from default_config.fmpp.
+  parser: {
+    # Generated parser implementation class package and name
+    package: "org.apache.ignite.internal.processors.query.calcite.sql",
+    class: "IgniteSqlParserImpl",
+
+    # List of additional classes and packages to import.
+    # Example: "org.apache.calcite.sql.*", "java.util.List".
+    imports: [
+      "org.apache.calcite.sql.SqlCreate",
+      "org.apache.calcite.sql.SqlLiteral",
+      "org.apache.calcite.schema.ColumnStrategy",
+      "org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTable",
+      "org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum",
+      "org.apache.calcite.sql.ddl.SqlDdlNodes",
+    ]
+
+    # List of new keywords. Example: "DATABASES", "TABLES". If the keyword is
+    # not a reserved keyword, add it to the 'nonReservedKeywords' section.
+    keywords: [
+      "SEMI"
+      "IF"
+      "TEMPLATE"
+      "BACKUPS"
+      "AFFINITY_KEY"
+      "ATOMICITY"
+      "WRITE_SYNCHRONIZATION_MODE"
+      "CACHE_GROUP"
+      "CACHE_NAME"
+      "DATA_REGION"
+#     "KEY_TYPE" // already presented in Calcite
+      "VALUE_TYPE"
+      "ENCRYPTED"
+    ]
+
+    # List of non-reserved keywords to add;
+    # items in this list become non-reserved
+    nonReservedKeywords: [
+      "SEMI"
+      "TEMPLATE"
+      "BACKUPS"
+      "AFFINITY_KEY"
+      "ATOMICITY"
+      "WRITE_SYNCHRONIZATION_MODE"
+      "CACHE_GROUP"
+      "CACHE_NAME"
+      "DATA_REGION"
+#     "KEY_TYPE" // already presented in Calcite
+      "VALUE_TYPE"
+      "ENCRYPTED"
+
+      # The following keywords are reserved in core Calcite,
+      # are reserved in some version of SQL,
+      # but are not reserved in Babel.
+      #
+      # Words that are commented out (e.g. "AND") are still reserved.
+      # These are the most important reserved words, and SQL cannot be
+      # unambiguously parsed if they are not reserved. For example, if
+      # "INNER" is not reserved then in the query
+      #
+      #   select * from emp inner join dept using (deptno)"
+      #
+      # "inner" could be a table alias for "emp".
+      #
+      "A"
+      "ABS"
+      "ABSOLUTE"
+      "ACTION"
+      "ADD"
+      "AFTER"
+#     "ALL"
+      "ALLOCATE"
+      "ALLOW"
+      "ALTER"
+      "AND"
+#     "ANY"
+      "ARE"
+      "ARRAY"
+#     "ARRAY_AGG" # not a keyword in Calcite
+      "ARRAY_MAX_CARDINALITY"
+      "AS"
+      "ASC"
+      "ASENSITIVE"
+      "ASSERTION"
+      "ASYMMETRIC"
+      "AT"
+      "ATOMIC"
+      "AUTHORIZATION"
+      "AVG"
+      "BEFORE"
+      "BEGIN"
+      "BEGIN_FRAME"
+      "BEGIN_PARTITION"
+      "BETWEEN"
+      "BIGINT"
+      "BINARY"
+      "BIT"
+#     "BIT_LENGTH" # not a keyword in Calcite
+      "BLOB"
+      "BOOLEAN"
+      "BOTH"
+      "BREADTH"
+      "BY"
+      "C"
+#     "CALL"
+      "CALLED"
+      "CARDINALITY"
+      "CASCADE"
+      "CASCADED"
+#     "CASE"
+      "CAST"
+      "CATALOG"
+      "CEIL"
+      "CEILING"
+      "CHAR"
+      "CHARACTER"
+      "CHARACTER_LENGTH"
+      "CHAR_LENGTH"
+      "CHECK"
+      "CLASSIFIER"
+      "CLOB"
+      "CLOSE"
+      "COALESCE"
+      "COLLATE"
+      "COLLATION"
+      "COLLECT"
+      "COLUMN"
+      "COMMIT"
+      "CONDITION"
+      "CONNECT"
+      "CONNECTION"
+#     "CONSTRAINT"
+      "CONSTRAINTS"
+      "CONSTRUCTOR"
+      "CONTAINS"
+      "CONTINUE"
+      "CONVERT"
+      "CORR"
+      "CORRESPONDING"
+      "COUNT"
+      "COVAR_POP"
+      "COVAR_SAMP"
+#     "CREATE"
+#     "CROSS"
+      "CUBE"
+      "CUME_DIST"
+#     "CURRENT"
+      "CURRENT_CATALOG"
+      "CURRENT_DATE"
+      "CURRENT_DEFAULT_TRANSFORM_GROUP"
+      "CURRENT_PATH"
+      "CURRENT_ROLE"
+      "CURRENT_ROW"
+      "CURRENT_SCHEMA"
+      "CURRENT_TIME"
+      "CURRENT_TIMESTAMP"
+      "CURRENT_TRANSFORM_GROUP_FOR_TYPE"
+      "CURRENT_USER"
+#     "CURSOR"
+      "CYCLE"
+      "DATA"
+#     "DATE"
+      "DAY"
+      "DEALLOCATE"
+      "DEC"
+      "DECIMAL"
+      "DECLARE"
+#     "DEFAULT"
+      "DEFERRABLE"
+      "DEFERRED"
+#     "DEFINE"
+#     "DELETE"
+      "DENSE_RANK"
+      "DEPTH"
+      "DEREF"
+      "DESC"
+#     "DESCRIBE" # must be reserved
+      "DESCRIPTOR"
+      "DETERMINISTIC"
+      "DIAGNOSTICS"
+      "DISALLOW"
+      "DISCONNECT"
+#     "DISTINCT"
+#     "DO"  # not a keyword in Calcite
+      "DOMAIN"
+      "DOUBLE"
+#     "DROP" # probably must be reserved
+      "DYNAMIC"
+      "EACH"
+      "ELEMENT"
+      "ELSE"
+#     "ELSEIF" # not a keyword in Calcite
+      "EMPTY"
+      "END"
+#     "END-EXEC" # not a keyword in Calcite, and contains '-'
+      "END_FRAME"
+      "END_PARTITION"
+      "EQUALS"
+      "ESCAPE"
+      "EVERY"
+#     "EXCEPT" # must be reserved
+      "EXCEPTION"
+      "EXEC"
+      "EXECUTE"
+      "EXISTS"
+#     "EXIT" # not a keyword in Calcite
+      "EXP"
+#     "EXPLAIN" # must be reserved
+      "EXTEND"
+      "EXTERNAL"
+      "EXTRACT"
+      "FALSE"
+#     "FETCH"
+      "FILTER"
+      "FIRST"
+      "FIRST_VALUE"
+      "FLOAT"
+      "FLOOR"
+      "FOR"
+      "FOREIGN"
+#     "FOREVER" # not a keyword in Calcite
+      "FOUND"
+      "FRAME_ROW"
+      "FREE"
+#     "FROM" # must be reserved
+#     "FULL" # must be reserved
+      "FUNCTION"
+      "FUSION"
+      "G"
+      "GENERAL"
+      "GET"
+      "GLOBAL"
+      "GO"
+      "GOTO"
+#     "GRANT"
+#     "GROUP"
+#     "GROUPING"
+      "GROUPS"
+#     "HANDLER" # not a keyword in Calcite
+#     "HAVING"
+      "HOLD"
+      "HOUR"
+      "IDENTITY"
+#     "IF" # not a keyword in Calcite
+      # "ILIKE"
+      "IMMEDIATE"
+      "IMMEDIATELY"
+      "IMPORT"
+#     "IN"
+      "INDICATOR"
+      "INITIAL"
+      "INITIALLY"
+#     "INNER"
+      "INOUT"
+      "INPUT"
+      "INSENSITIVE"
+#     "INSERT"
+      "INT"
+      "INTEGER"
+#     "INTERSECT"
+      "INTERSECTION"
+#     "INTERVAL"
+#     "INTO"
+      "IS"
+      "ISOLATION"
+#     "ITERATE" # not a keyword in Calcite
+#     "JOIN"
+      "JSON_ARRAY"
+      "JSON_ARRAYAGG"
+      "JSON_EXISTS"
+      "JSON_OBJECT"
+      "JSON_OBJECTAGG"
+      "JSON_QUERY"
+      "JSON_VALUE"
+      "K"
+#     "KEEP" # not a keyword in Calcite
+      "KEY"
+      "LAG"
+      "LANGUAGE"
+      "LARGE"
+      "LAST"
+      "LAST_VALUE"
+#     "LATERAL"
+      "LEAD"
+      "LEADING"
+#     "LEAVE" # not a keyword in Calcite
+#     "LEFT"
+      "LEVEL"
+      "LIKE"
+      "LIKE_REGEX"
+#     "LIMIT"
+      "LN"
+      "LOCAL"
+      "LOCALTIME"
+      "LOCALTIMESTAMP"
+      "LOCATOR"
+#     "LOOP" # not a keyword in Calcite
+      "LOWER"
+      "M"
+      "MAP"
+      "MATCH"
+      "MATCHES"
+      "MATCH_NUMBER"
+#     "MATCH_RECOGNIZE"
+      "MAX"
+#     "MAX_CARDINALITY" # not a keyword in Calcite
+      "MEASURES"
+      "MEMBER"
+#     "MERGE"
+      "METHOD"
+      "MIN"
+#     "MINUS"
+      "MINUTE"
+      "MOD"
+      "MODIFIES"
+      "MODULE"
+      "MONTH"
+      "MULTISET"
+      "NAME"
+      "NAMES"
+      "NATIONAL"
+#     "NATURAL"
+      "NCHAR"
+      "NCLOB"
+#     "NEW"
+#     "NEXT"
+      "NO"
+      "NONE"
+      "NORMALIZE"
+      "NOT"
+      "NTH_VALUE"
+      "NTILE"
+#     "NULL"
+      "NULLIF"
+      "NUMERIC"
+      "OBJECT"
+      "OCCURRENCES_REGEX"
+      "OCTET_LENGTH"
+      "OF"
+#     "OFFSET"
+      "OLD"
+      "OMIT"
+#     "ON"
+      "ONE"
+      "ONLY"
+      "OPEN"
+      "OPTION"
+      "OR"
+#     "ORDER"
+      "ORDINALITY"
+      "OUT"
+#     "OUTER"
+      "OUTPUT"
+#     "OVER"
+      "OVERLAPS"
+      "OVERLAY"
+      "PAD"
+      "PARAMETER"
+      "PARTIAL"
+#     "PARTITION"
+      "PATH"
+#     "PATTERN"
+      "PER"
+      "PERCENT"
+      "PERCENTILE_CONT"
+      "PERCENTILE_DISC"
+      "PERCENT_RANK"
+      "PERIOD"
+      "PERMUTE"
+      "PORTION"
+      "POSITION"
+      "POSITION_REGEX"
+      "POWER"
+      "PRECEDES"
+      "PRECISION"
+      "PREPARE"
+      "PRESERVE"
+      "PREV"
+#     "PRIMARY"
+      "PRIOR"
+      "PRIVILEGES"
+      "PROCEDURE"
+      "PUBLIC"
+#     "RANGE"
+      "RANK"
+      "READ"
+      "READS"
+      "REAL"
+      "RECURSIVE"
+      "REF"
+      "REFERENCES"
+      "REFERENCING"
+      "REGR_AVGX"
+      "REGR_AVGY"
+      "REGR_COUNT"
+      "REGR_INTERCEPT"
+      "REGR_R2"
+      "REGR_SLOPE"
+      "REGR_SXX"
+      "REGR_SXY"
+      "REGR_SYY"
+      "RELATIVE"
+      "RELEASE"
+#     "REPEAT" # not a keyword in Calcite
+      "RESET"
+#     "RESIGNAL" # not a keyword in Calcite
+      "RESTRICT"
+      "RESULT"
+      "RETURN"
+      "RETURNS"
+      "REVOKE"
+#     "RIGHT"
+      # "RLIKE"
+      "ROLE"
+      "ROLLBACK"
+#     "ROLLUP"
+      "ROUTINE"
+#     "ROW"
+#     "ROWS"
+      "ROW_NUMBER"
+      "RUNNING"
+      "SAVEPOINT"
+      "SCHEMA"
+      "SCOPE"
+      "SCROLL"
+      "SEARCH"
+      "SECOND"
+      "SECTION"
+      "SEEK"
+#     "SELECT"
+      "SENSITIVE"
+      "SESSION"
+      "SESSION_USER"
+#     "SET"
+#     "SETS"
+      "SHOW"
+#     "SIGNAL" # not a keyword in Calcite
+      "SIMILAR"
+      "SIZE"
+#     "SKIP" # messes with JavaCC's <SKIP> token
+      "SMALLINT"
+#     "SOME"
+      "SPACE"
+      "SPECIFIC"
+      "SPECIFICTYPE"
+      "SQL"
+#     "SQLCODE" # not a keyword in Calcite
+#     "SQLERROR" # not a keyword in Calcite
+      "SQLEXCEPTION"
+      "SQLSTATE"
+      "SQLWARNING"
+      "SQRT"
+      "START"
+      "STATE"
+      "STATIC"
+      "STDDEV_POP"
+      "STDDEV_SAMP"
+#     "STREAM"
+      "SUBMULTISET"
+      "SUBSET"
+      "SUBSTRING"
+      "SUBSTRING_REGEX"
+      "SUCCEEDS"
+      "SUM"
+      "SYMMETRIC"
+      "SYSTEM"
+      "SYSTEM_TIME"
+      "SYSTEM_USER"
+#     "TABLE"
+#     "TABLESAMPLE"
+      "TEMPORARY"
+#     "THEN"
+#     "TIME"
+#     "TIMESTAMP"
+      "TIMEZONE_HOUR"
+      "TIMEZONE_MINUTE"
+      "TINYINT"
+      "TO"
+      "TRAILING"
+      "TRANSACTION"
+      "TRANSLATE"
+      "TRANSLATE_REGEX"
+      "TRANSLATION"
+      "TREAT"
+      "TRIGGER"
+      "TRIM"
+      "TRIM_ARRAY"
+      "TRUE"
+      "TRUNCATE"
+      "UESCAPE"
+      "UNDER"
+#     "UNDO" # not a keyword in Calcite
+#     "UNION"
+      "UNIQUE"
+      "UNKNOWN"
+#     "UNNEST"
+#     "UNTIL" # not a keyword in Calcite
+#     "UPDATE"
+      "UPPER"
+      "UPSERT"
+      "USAGE"
+      "USER"
+#     "USING"
+      "VALUE"
+#     "VALUES"
+      "VALUE_OF"
+      "VARBINARY"
+      "VARCHAR"
+      "VARYING"
+      "VAR_POP"
+      "VAR_SAMP"
+      "VERSION"
+      "VERSIONING"
+#     "VERSIONS" # not a keyword in Calcite
+      "VIEW"
+#     "WHEN"
+      "WHENEVER"
+#     "WHERE"
+#     "WHILE" # not a keyword in Calcite
+      "WIDTH_BUCKET"
+#     "WINDOW"
+#     "WITH"
+      "WITHIN"
+      "WITHOUT"
+      "WORK"
+      "WRITE"
+      "YEAR"
+      "ZONE"
+    ]
+
+    # List of non-reserved keywords to add;
+    # items in this list become non-reserved.
+    nonReservedKeywordsToAdd: [
+    ]
+
+    # List of non-reserved keywords to remove;
+    # items in this list become reserved.
+    nonReservedKeywordsToRemove: [
+    ]
+
+    # List of additional join types. Each is a method with no arguments.
+    # Example: "LeftSemiJoin".
+    joinTypes: [
+ #     "LeftSemiJoin"
+    ]
+
+    # List of methods for parsing builtin function calls.
+    # Return type of method implementation should be "SqlNode".
+    # Example: "DateFunctionCall()".
+    builtinFunctionCallMethods: [
+  #     "DateFunctionCall()"
+   #    "DateaddFunctionCall()"
+    ]
+
+    # List of methods for parsing custom SQL statements.
+    # Return type of method implementation should be 'SqlNode'.
+    # Example: "SqlShowDatabases()", "SqlShowTables()".
+    statementParserMethods: [
+    ]
+
+    # List of methods for parsing extensions to "CREATE [OR REPLACE]" calls.
+    # Each must accept arguments "(SqlParserPos pos, boolean replace)".
+    # Example: "SqlCreateForeignSchema".
+    createStatementParserMethods: [
+      "SqlCreateTable"
+    ]
+
+    # List of methods for parsing extensions to "DROP" calls.
+    # Each must accept arguments "(SqlParserPos pos)".
+    # Example: "SqlDropSchema".
+    dropStatementParserMethods: [
+    ]
+
+    # List of methods for parsing extensions to "DROP" calls.
+    # Each must accept arguments "(SqlParserPos pos)".
+    # Example: "SqlDropSchema".
+    alterStatementParserMethods: [
+    ]
+
+    # List of methods for parsing custom literals.
+    # Return type of method implementation should be "SqlNode".
+    # Example: ParseJsonLiteral().
+    literalParserMethods: [
+    ]
+
+    # List of methods for parsing custom data types.
+    # Return type of method implementation should be "SqlTypeNameSpec".
+    # Example: SqlParseTimeStampZ().
+    dataTypeParserMethods: [
+    ]
+
+    # Binary operators tokens.
+    # Example: "< INFIX_CAST: \"::\" >".
+    binaryOperatorsTokens: [
+      "< INFIX_CAST: \"::\" >"
+    ]
+
+    # Binary operators initialization.
+    # Example: "InfixCast".
+    extraBinaryExpressions: [
+      "InfixCast"
+    ]
+
+    # List of files in @includes directory that have parser method
+    # implementations for parsing custom SQL statements, literals or types
+    # given as part of "statementParserMethods", "literalParserMethods" or
+    # "dataTypeParserMethods".
+    # Example: "parserImpls.ftl".
+    implementationFiles: [
+      "parserImpls.ftl"
+    ]
+
+    includePosixOperators: false
+    includeCompoundIdentifier: true
+    includeBraces: true
+    includeAdditionalDeclarations: false
+  }
+}
+
+freemarkerLinks: {
+  includes: includes/
+}
diff --git a/modules/calcite/src/main/codegen/includes/parserImpls.ftl b/modules/calcite/src/main/codegen/includes/parserImpls.ftl
new file mode 100644
index 0000000..6583b7d
--- /dev/null
+++ b/modules/calcite/src/main/codegen/includes/parserImpls.ftl
@@ -0,0 +1,181 @@
+<#--
+// 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.
+-->
+
+boolean IfNotExistsOpt() :
+{
+}
+{
+    <IF> <NOT> <EXISTS> { return true; }
+|
+    { return false; }
+}
+
+SqlNodeList CreateTableOptionList() :
+{
+    List<SqlNode> list = new ArrayList<SqlNode>();
+    final Span s = Span.of();
+}
+{
+    CreateTableOption(list)
+    (
+        <COMMA> { s.add(this); } CreateTableOption(list)
+    )*
+    {
+        return new SqlNodeList(list, s.end(this));
+    }
+}
+
+IgniteSqlCreateTableOptionEnum CreateTableOptionEnumOpt() :
+{
+}
+{
+    <TEMPLATE> { return IgniteSqlCreateTableOptionEnum.TEMPLATE; }
+|
+    <BACKUPS> { return IgniteSqlCreateTableOptionEnum.BACKUPS; }
+|
+    <AFFINITY_KEY> { return IgniteSqlCreateTableOptionEnum.AFFINITY_KEY; }
+|
+    <ATOMICITY> { return IgniteSqlCreateTableOptionEnum.ATOMICITY; }
+|
+    <WRITE_SYNCHRONIZATION_MODE> { return IgniteSqlCreateTableOptionEnum.WRITE_SYNCHRONIZATION_MODE; }
+|
+    <CACHE_GROUP> { return IgniteSqlCreateTableOptionEnum.CACHE_GROUP; }
+|
+    <CACHE_NAME> { return IgniteSqlCreateTableOptionEnum.CACHE_NAME; }
+|
+    <DATA_REGION> { return IgniteSqlCreateTableOptionEnum.DATA_REGION; }
+|
+    <KEY_TYPE> { return IgniteSqlCreateTableOptionEnum.KEY_TYPE; }
+|
+    <VALUE_TYPE> { return IgniteSqlCreateTableOptionEnum.VALUE_TYPE; }
+|
+    <ENCRYPTED> { return IgniteSqlCreateTableOptionEnum.ENCRYPTED; }
+}
+
+void CreateTableOption(List<SqlNode> list) :
+{
+    final Span s;
+    final IgniteSqlCreateTableOptionEnum key;
+    final SqlNode val;
+}
+{
+    key = CreateTableOptionEnumOpt() { s = span(); }
+    <EQ>
+    (
+        val = Literal()
+    |
+        val = SimpleIdentifier()
+    ) {
+        list.add(new IgniteSqlCreateTableOption(key, val, s.end(this)));
+    }
+}
+
+void TableElement(List<SqlNode> list) :
+{
+    final SqlDataTypeSpec type;
+    final boolean nullable;
+    final SqlNodeList columnList;
+    final Span s = Span.of();
+    final ColumnStrategy strategy;
+    final SqlNode dflt;
+    SqlIdentifier id = null;
+}
+{
+    id = SimpleIdentifier() type = DataType() nullable = NullableOptDefaultTrue()
+    (
+        <DEFAULT_> { s.add(this); } dflt = Literal() {
+            strategy = ColumnStrategy.DEFAULT;
+        }
+    |
+        {
+            dflt = null;
+            strategy = nullable ? ColumnStrategy.NULLABLE
+                : ColumnStrategy.NOT_NULLABLE;
+        }
+    )
+    [
+        <PRIMARY> { s.add(this); } <KEY> {
+            columnList = SqlNodeList.of(id);
+            list.add(SqlDdlNodes.primary(s.end(columnList), null, columnList));
+        }
+    ]
+    {
+        list.add(
+            SqlDdlNodes.column(s.add(id).end(this), id,
+                type.withNullable(nullable), dflt, strategy));
+    }
+|
+    [ <CONSTRAINT> { s.add(this); } id = SimpleIdentifier() ]
+    <PRIMARY> { s.add(this); } <KEY>
+    columnList = ParenthesizedSimpleIdentifierList() {
+        list.add(SqlDdlNodes.primary(s.end(columnList), id, columnList));
+    }
+}
+
+SqlNodeList TableElementList() :
+{
+    final Span s;
+    final List<SqlNode> list = new ArrayList<SqlNode>();
+}
+{
+    <LPAREN> { s = span(); }
+    TableElement(list)
+    (
+        <COMMA> TableElement(list)
+    )*
+    <RPAREN> {
+        return new SqlNodeList(list, s.end(this));
+    }
+}
+
+SqlCreate SqlCreateTable(Span s, boolean replace) :
+{
+    final boolean ifNotExists;
+    final SqlIdentifier id;
+    final SqlNodeList columnList;
+    final SqlNodeList optionList;
+}
+{
+    <TABLE>
+    ifNotExists = IfNotExistsOpt()
+    id = CompoundIdentifier()
+    columnList = TableElementList()
+    (
+        <WITH> { s.add(this); } optionList = CreateTableOptionList()
+    |
+        { optionList = null; }
+    )
+    {
+        return new IgniteSqlCreateTable(s.end(this), ifNotExists, id, columnList, optionList);
+    }
+}
+
+void InfixCast(List<Object> list, ExprContext exprContext, Span s) :
+{
+    final SqlDataTypeSpec dt;
+}
+{
+    <INFIX_CAST> {
+        checkNonQueryExpression(exprContext);
+    }
+    dt = DataType() {
+        list.add(
+            new SqlParserUtil.ToTreeListItem(SqlLibraryOperators.INFIX_CAST,
+                s.pos()));
+        list.add(dt);
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/calcite/sql/IgniteSqlNode.java b/modules/calcite/src/main/java/org/apache/calcite/sql/IgniteSqlNode.java
new file mode 100644
index 0000000..5ce6fb0
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/calcite/sql/IgniteSqlNode.java
@@ -0,0 +1,31 @@
+/*
+ * 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.calcite.sql;
+
+import org.apache.calcite.sql.parser.SqlParserPos;
+
+/** A {@link SqlNode} that exposes constructor for descendant classes. */
+public abstract class IgniteSqlNode extends SqlNode {
+    /**
+     * Creates a node.
+     *
+     * @param pos Parser position, must not be null.
+     */
+    protected IgniteSqlNode(SqlParserPos pos) {
+        super(pos);
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java
index 53184da..9a52947 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java
@@ -24,7 +24,6 @@ import org.apache.calcite.plan.Contexts;
 import org.apache.calcite.sql.fun.SqlLibrary;
 import org.apache.calcite.sql.fun.SqlLibraryOperatorTableFactory;
 import org.apache.calcite.sql.parser.SqlParser;
-import org.apache.calcite.sql.parser.babel.SqlBabelParserImpl;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql2rel.SqlToRelConverter;
@@ -57,6 +56,7 @@ import org.apache.ignite.internal.processors.query.calcite.prepare.QueryPlanCach
 import org.apache.ignite.internal.processors.query.calcite.prepare.QueryPlanCacheImpl;
 import org.apache.ignite.internal.processors.query.calcite.schema.SchemaHolder;
 import org.apache.ignite.internal.processors.query.calcite.schema.SchemaHolderImpl;
+import org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlParserImpl;
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeSystem;
 import org.apache.ignite.internal.processors.query.calcite.util.LifecycleAware;
 import org.apache.ignite.internal.processors.query.calcite.util.Service;
@@ -78,7 +78,7 @@ public class CalciteQueryProcessor extends GridProcessorAdapter implements Query
             .withDecorrelationEnabled(true))
         .parserConfig(
             SqlParser.config()
-                .withParserFactory(SqlBabelParserImpl.FACTORY)
+                .withParserFactory(IgniteSqlParserImpl.FACTORY)
                 .withLex(Lex.ORACLE)
                 .withConformance(SqlConformanceEnum.DEFAULT))
         .sqlValidatorConfig(SqlValidator.Config.DEFAULT
@@ -88,6 +88,7 @@ public class CalciteQueryProcessor extends GridProcessorAdapter implements Query
         .operatorTable(SqlLibraryOperatorTableFactory.INSTANCE
             .getOperatorTable(
                 SqlLibrary.STANDARD,
+                SqlLibrary.POSTGRESQL,
                 SqlLibrary.ORACLE,
                 SqlLibrary.MYSQL))
         // Context provides a way to store data within the planner session that can be accessed in planner rules.
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionServiceImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionServiceImpl.java
index fa97dbd..b9ca57d 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionServiceImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionServiceImpl.java
@@ -41,6 +41,7 @@ import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelRoot;
 import org.apache.calcite.rel.hint.Hintable;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlDdl;
 import org.apache.calcite.sql.SqlExplain;
 import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.sql.SqlNode;
@@ -68,6 +69,7 @@ import org.apache.ignite.internal.processors.query.IgniteSQLException;
 import org.apache.ignite.internal.processors.query.QueryCancellable;
 import org.apache.ignite.internal.processors.query.QueryContext;
 import org.apache.ignite.internal.processors.query.calcite.CalciteQueryProcessor;
+import org.apache.ignite.internal.processors.query.calcite.exec.ddl.DdlCommandHandler;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.Inbox;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.Node;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.Outbox;
@@ -83,6 +85,7 @@ import org.apache.ignite.internal.processors.query.calcite.metadata.FragmentMapp
 import org.apache.ignite.internal.processors.query.calcite.metadata.MappingService;
 import org.apache.ignite.internal.processors.query.calcite.metadata.RemoteException;
 import org.apache.ignite.internal.processors.query.calcite.prepare.CacheKey;
+import org.apache.ignite.internal.processors.query.calcite.prepare.DdlPlan;
 import org.apache.ignite.internal.processors.query.calcite.prepare.ExplainPlan;
 import org.apache.ignite.internal.processors.query.calcite.prepare.FieldsMetadata;
 import org.apache.ignite.internal.processors.query.calcite.prepare.FieldsMetadataImpl;
@@ -99,6 +102,7 @@ import org.apache.ignite.internal.processors.query.calcite.prepare.QueryPlanCach
 import org.apache.ignite.internal.processors.query.calcite.prepare.QueryTemplate;
 import org.apache.ignite.internal.processors.query.calcite.prepare.Splitter;
 import org.apache.ignite.internal.processors.query.calcite.prepare.ValidationResult;
+import org.apache.ignite.internal.processors.query.calcite.prepare.ddl.DdlSqlToCommandConverter;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
 import org.apache.ignite.internal.processors.query.calcite.schema.SchemaHolder;
@@ -112,6 +116,7 @@ import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.calcite.util.HintUtils;
 import org.apache.ignite.internal.processors.query.calcite.util.ListFieldsQueryCursor;
 import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils;
+import org.apache.ignite.internal.processors.query.h2.H2Utils;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.T2;
 import org.apache.ignite.internal.util.typedef.X;
@@ -120,6 +125,7 @@ import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import static java.util.Collections.singletonList;
+import static org.apache.calcite.rel.type.RelDataType.PRECISION_NOT_SPECIFIED;
 import static org.apache.ignite.internal.processors.query.calcite.CalciteQueryProcessor.FRAMEWORK_CONFIG;
 import static org.apache.ignite.internal.processors.query.calcite.externalize.RelJsonReader.fromJson;
 
@@ -176,6 +182,12 @@ public class ExecutionServiceImpl<Row> extends AbstractService implements Execut
     /** */
     private final RowHandler<Row> handler;
 
+    /** */
+    private final DdlCommandHandler ddlCmdHnd;
+
+    /** */
+    private final DdlSqlToCommandConverter ddlConverter;
+
     /**
      * @param ctx Kernal.
      */
@@ -185,6 +197,11 @@ public class ExecutionServiceImpl<Row> extends AbstractService implements Execut
 
         discoLsnr = (e, c) -> onNodeLeft(e.eventNode().id());
         running = new ConcurrentHashMap<>();
+        ddlConverter = new DdlSqlToCommandConverter();
+
+        ddlCmdHnd = new DdlCommandHandler(
+            ctx::query, ctx.cache(), ctx.security(), () -> schemaHolder().schema()
+        );
     }
 
     /**
@@ -552,6 +569,9 @@ public class ExecutionServiceImpl<Row> extends AbstractService implements Execut
             case EXPLAIN:
                 return prepareExplain(sqlNode, ctx);
 
+            case CREATE_TABLE:
+                return prepareDdl(sqlNode, ctx);
+
             default:
                 throw new IgniteSQLException("Unsupported operation [" +
                     "sqlNodeKind=" + sqlNode.getKind() + "; " +
@@ -597,6 +617,15 @@ public class ExecutionServiceImpl<Row> extends AbstractService implements Execut
     }
 
     /** */
+    private QueryPlan prepareDdl(SqlNode sqlNode, PlanningContext ctx) {
+        assert sqlNode instanceof SqlDdl : sqlNode == null ? "null" : sqlNode.getClass().getName();
+
+        SqlDdl ddlNode = (SqlDdl)sqlNode;
+
+        return new DdlPlan(ddlConverter.convert(ddlNode, ctx));
+    }
+
+    /** */
     private IgniteRel optimize(SqlNode sqlNode, IgnitePlanner planner) {
         try {
             // Convert to Relational operators graph
@@ -647,7 +676,7 @@ public class ExecutionServiceImpl<Row> extends AbstractService implements Execut
     private FieldsMetadata explainFieldsMetadata(PlanningContext ctx) {
         IgniteTypeFactory factory = ctx.typeFactory();
         RelDataType planStrDataType =
-            factory.createSqlType(SqlTypeName.VARCHAR, RelDataType.PRECISION_NOT_SPECIFIED);
+            factory.createSqlType(SqlTypeName.VARCHAR, PRECISION_NOT_SPECIFIED);
         T2<String, RelDataType> planField = new T2<>(ExplainPlan.PLAN_COL_NAME, planStrDataType);
         RelDataType planDataType = factory.createStructType(singletonList(planField));
 
@@ -663,6 +692,8 @@ public class ExecutionServiceImpl<Row> extends AbstractService implements Execut
                 return executeQuery(qryId, (MultiStepPlan) plan, pctx);
             case EXPLAIN:
                 return executeExplain((ExplainPlan)plan, pctx);
+            case DDL:
+                return executeDdl((DdlPlan)plan, pctx);
 
             default:
                 throw new AssertionError("Unexpected plan type: " + plan);
@@ -670,6 +701,19 @@ public class ExecutionServiceImpl<Row> extends AbstractService implements Execut
     }
 
     /** */
+    private FieldsQueryCursor<List<?>> executeDdl(DdlPlan plan, PlanningContext pctx) {
+        try {
+            ddlCmdHnd.handle(pctx, plan.command());
+        }
+        catch (IgniteCheckedException e) {
+            throw new IgniteSQLException("Failed to execute DDL statement [stmt=" + pctx.query() +
+                ", err=" + e.getMessage() + ']', e);
+        }
+
+        return H2Utils.zeroCursor();
+    }
+
+    /** */
     private FieldsQueryCursor<List<?>> executeQuery(UUID qryId, MultiStepPlan plan, PlanningContext pctx) {
         plan.init(pctx);
 
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ddl/DdlCommandHandler.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ddl/DdlCommandHandler.java
new file mode 100644
index 0000000..529bcae
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ddl/DdlCommandHandler.java
@@ -0,0 +1,215 @@
+/*
+ * 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.processors.query.calcite.exec.ddl;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.internal.processors.cache.GridCacheProcessor;
+import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
+import org.apache.ignite.internal.processors.query.GridQueryProcessor;
+import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import org.apache.ignite.internal.processors.query.QueryEntityEx;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.calcite.prepare.PlanningContext;
+import org.apache.ignite.internal.processors.query.calcite.prepare.ddl.ColumnDefinition;
+import org.apache.ignite.internal.processors.query.calcite.prepare.ddl.CreateTableCommand;
+import org.apache.ignite.internal.processors.query.calcite.prepare.ddl.DdlCommand;
+import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
+import org.apache.ignite.internal.processors.query.schema.SchemaOperationException;
+import org.apache.ignite.internal.processors.security.IgniteSecurity;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.lang.IgniteUuid;
+import org.apache.ignite.plugin.security.SecurityPermission;
+
+import static org.apache.ignite.internal.processors.query.QueryUtils.convert;
+import static org.apache.ignite.internal.processors.query.QueryUtils.isDdlOnSchemaSupported;
+
+/** */
+public class DdlCommandHandler {
+    /** */
+    private final Supplier<GridQueryProcessor> qryProcessorSupp;
+
+    /** */
+    private final GridCacheProcessor cacheProcessor;
+
+    /** */
+    private final IgniteSecurity security;
+
+    /** */
+    private final Supplier<SchemaPlus> schemaSupp;
+
+    /** */
+    public DdlCommandHandler(Supplier<GridQueryProcessor> qryProcessorSupp, GridCacheProcessor cacheProcessor,
+        IgniteSecurity security, Supplier<SchemaPlus> schemaSupp) {
+        this.qryProcessorSupp = qryProcessorSupp;
+        this.cacheProcessor = cacheProcessor;
+        this.security = security;
+        this.schemaSupp = schemaSupp;
+    }
+
+    /** */
+    public void handle(PlanningContext pctx, DdlCommand cmd) throws IgniteCheckedException {
+        if (cmd instanceof CreateTableCommand)
+            handle0(pctx, (CreateTableCommand)cmd);
+
+        else {
+            throw new IgniteSQLException("Unsupported DDL operation [" +
+                "cmdName=" + (cmd == null ? null : cmd.getClass().getSimpleName()) + "; " +
+                "querySql=\"" + pctx.query() + "\"]", IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+        }
+    }
+
+    /** */
+    private void handle0(PlanningContext pctx, CreateTableCommand cmd) throws IgniteCheckedException {
+        security.authorize(cmd.cacheName(), SecurityPermission.CACHE_CREATE);
+
+        isDdlOnSchemaSupported(cmd.schemaName());
+
+        if (schemaSupp.get().getSubSchema(cmd.schemaName()).getTable(cmd.tableName()) != null) {
+            if (cmd.ifNotExists())
+                return;
+
+            throw new SchemaOperationException(SchemaOperationException.CODE_TABLE_EXISTS, cmd.tableName());
+        }
+
+        CacheConfiguration<?, ?> ccfg = new CacheConfiguration<>(cmd.tableName());
+
+        QueryEntity e = toQueryEntity(cmd, pctx);
+
+        ccfg.setQueryEntities(Collections.singleton(e));
+        ccfg.setSqlSchema(cmd.schemaName());
+
+        SchemaOperationException err =
+            QueryUtils.checkQueryEntityConflicts(ccfg, cacheProcessor.cacheDescriptors().values());
+
+        if (err != null)
+            throw convert(err);
+
+        qryProcessorSupp.get().dynamicTableCreate(
+            cmd.schemaName(),
+            e,
+            cmd.templateName(),
+            cmd.cacheName(),
+            cmd.cacheGroup(),
+            cmd.dataRegionName(),
+            cmd.affinityKey(),
+            cmd.atomicityMode(),
+            cmd.writeSynchronizationMode(),
+            cmd.backups(),
+            cmd.ifNotExists(),
+            cmd.encrypted(),
+            null
+        );
+    }
+
+    /** */
+    private QueryEntity toQueryEntity(CreateTableCommand cmd, PlanningContext pctx) {
+        QueryEntity res = new QueryEntity();
+
+        res.setTableName(cmd.tableName());
+
+        Set<String> notNullFields = null;
+
+        HashMap<String, Object> dfltValues = new HashMap<>();
+
+        Map<String, Integer> precision = new HashMap<>();
+        Map<String, Integer> scale = new HashMap<>();
+
+        IgniteTypeFactory tf = pctx.typeFactory();
+
+        for (ColumnDefinition col : cmd.columns()) {
+            String name = col.name();
+
+            res.addQueryField(name, tf.getJavaClass(col.type()).getTypeName(), null);
+
+            if (!col.nullable()) {
+                if (notNullFields == null)
+                    notNullFields = new HashSet<>();
+
+                notNullFields.add(name);
+            }
+
+            if (col.defaultValue() != null)
+                dfltValues.put(name, col.defaultValue());
+
+            if (col.precision() != null)
+                precision.put(name, col.precision());
+
+            if (col.scale() != null)
+                scale.put(name, col.scale());
+        }
+
+        if (!F.isEmpty(dfltValues))
+            res.setDefaultFieldValues(dfltValues);
+
+        if (!F.isEmpty(precision))
+            res.setFieldsPrecision(precision);
+
+        if (!F.isEmpty(scale))
+            res.setFieldsScale(scale);
+
+        String valTypeName = QueryUtils.createTableValueTypeName(cmd.schemaName(), cmd.tableName());
+
+        String keyTypeName;
+        if ((!F.isEmpty(cmd.primaryKeyColumns()) && cmd.primaryKeyColumns().size() > 1) || !F.isEmpty(cmd.keyTypeName())) {
+            keyTypeName = cmd.keyTypeName();
+
+            if (F.isEmpty(keyTypeName))
+                keyTypeName = QueryUtils.createTableKeyTypeName(valTypeName);
+
+            if (!F.isEmpty(cmd.primaryKeyColumns()))
+                res.setKeyFields(new LinkedHashSet<>(cmd.primaryKeyColumns()));
+        }
+        else if (!F.isEmpty(cmd.primaryKeyColumns()) && cmd.primaryKeyColumns().size() == 1) {
+            String pkFieldName = cmd.primaryKeyColumns().get(0);
+
+            keyTypeName = res.getFields().get(pkFieldName);
+
+            res.setKeyFieldName(pkFieldName);
+        }
+        else {
+            // if pk is not explicitly set, we create it ourselves
+            keyTypeName = IgniteUuid.class.getName();
+
+            res = new QueryEntityEx(res).implicitPk(true);
+        }
+
+        res.setValueType(F.isEmpty(cmd.valueTypeName()) ? valTypeName : cmd.valueTypeName());
+        res.setKeyType(keyTypeName);
+
+        if (!F.isEmpty(notNullFields)) {
+            QueryEntityEx res0 = new QueryEntityEx(res);
+
+            res0.setNotNullFields(notNullFields);
+
+            res = res0;
+        }
+
+        return res;
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/DdlPlan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/DdlPlan.java
new file mode 100644
index 0000000..79d3880
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/DdlPlan.java
@@ -0,0 +1,46 @@
+/*
+ * 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.processors.query.calcite.prepare;
+
+import org.apache.ignite.internal.processors.query.calcite.prepare.ddl.DdlCommand;
+
+/** */
+public class DdlPlan implements QueryPlan {
+    /** */
+    private final DdlCommand cmd;
+
+    /** */
+    public DdlPlan(DdlCommand cmd) {
+        this.cmd = cmd;
+    }
+
+    /** */
+    public DdlCommand command() {
+        return cmd;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Type type() {
+        return Type.DDL;
+    }
+
+    /** {@inheritDoc} */
+    @Override public QueryPlan copy() {
+        return this;
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java
index bd584b9..ec7daaf 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java
@@ -23,6 +23,7 @@ import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
+
 import com.google.common.collect.ImmutableList;
 import org.apache.calcite.plan.Context;
 import org.apache.calcite.plan.RelOptCluster;
@@ -46,6 +47,7 @@ import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexExecutor;
+import org.apache.calcite.sql.SqlDataTypeSpec;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
@@ -186,6 +188,10 @@ public class IgnitePlanner implements Planner, RelOptTable.ViewExpander {
         return Pair.of(validatedNode, type);
     }
 
+    public RelDataType conver(SqlDataTypeSpec typeSpec) {
+        return typeSpec.deriveType(validator());
+    }
+
     /**
      * Validates a SQL statement.
      *
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/ColumnDefinition.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/ColumnDefinition.java
new file mode 100644
index 0000000..b865078
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/ColumnDefinition.java
@@ -0,0 +1,83 @@
+/*
+ * 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.processors.query.calcite.prepare.ddl;
+
+import java.util.Objects;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.jetbrains.annotations.Nullable;
+
+/** Definies a particular column within table. */
+public class ColumnDefinition {
+    /** */
+    private final String name;
+
+    /** */
+    private final RelDataType type;
+
+    /** */
+    private final Object dflt;
+
+    /** Creates a column definition. */
+    public ColumnDefinition(String name, RelDataType type, @Nullable Object dflt) {
+        this.name = Objects.requireNonNull(name, "name");
+        this.type = Objects.requireNonNull(type, "type");
+        this.dflt = dflt;
+    }
+
+    /**
+     * @return Column's name.
+     */
+    public String name() {
+        return name;
+    }
+
+    /**
+     * @return Column's type.
+     */
+    public RelDataType type() {
+        return type;
+    }
+
+    /**
+     * @return Column's default value.
+     */
+    public @Nullable Object defaultValue() {
+        return dflt;
+    }
+
+    /**
+     * @return {@code true} if this column accepts nulls.
+     */
+    public boolean nullable() {
+        return type.isNullable();
+    }
+
+    /**
+     * @return Column's precision.
+     */
+    public Integer precision() {
+        return type.getPrecision() != RelDataType.PRECISION_NOT_SPECIFIED ? type.getPrecision() : null;
+    }
+
+    /**
+     * @return Column's scale.
+     */
+    public Integer scale() {
+        return type.getScale() != RelDataType.SCALE_NOT_SPECIFIED ? type.getScale() : null;
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/CreateTableCommand.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/CreateTableCommand.java
new file mode 100644
index 0000000..f474f49
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/CreateTableCommand.java
@@ -0,0 +1,304 @@
+/*
+ * 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.processors.query.calcite.prepare.ddl;
+
+import java.util.List;
+
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheWriteSynchronizationMode;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * CREATE TABLE statement.
+ */
+public class CreateTableCommand implements DdlCommand {
+    /**
+     * Schema name upon which this statement has been issued - <b>not</b> the name of the schema where this new table
+     * will be created.
+     */
+    private String schemaName;
+
+    /** Table name. */
+    private String tblName;
+
+    /** Cache name upon which new cache configuration for this table must be based. */
+    private String templateName;
+
+    /** Name of new cache associated with this table. */
+    private String cacheName;
+
+    /** Name of cache key type. */
+    private String keyTypeName;
+
+    /** Name of cache value type. */
+    private String valTypeName;
+
+    /** Group to put new cache into. */
+    private String cacheGrp;
+
+    /** Atomicity mode for new cache. */
+    private CacheAtomicityMode atomicityMode;
+
+    /** Write sync mode. */
+    private CacheWriteSynchronizationMode writeSyncMode;
+
+    /** Backups number for new cache. */
+    private Integer backups;
+
+    /** Quietly ignore this command if table already exists. */
+    private boolean ifNotExists;
+
+    /** Columns. */
+    private List<ColumnDefinition> cols;
+
+    /** Primary key columns. */
+    private List<String> pkCols;
+
+    /** Name of the column that represents affinity key. */
+    private String affinityKey;
+
+    /** Data region. */
+    private String dataRegionName;
+
+    /** Encrypted flag. */
+    private boolean encrypted;
+
+    /**
+     * @return Cache name upon which new cache configuration for this table must be based.
+     */
+    public String templateName() {
+        return templateName;
+    }
+
+    /**
+     * @param templateName Cache name upon which new cache configuration for this table must be based.
+     */
+    public void templateName(String templateName) {
+        this.templateName = templateName;
+    }
+
+    /**
+     * @return Name of new cache associated with this table.
+     */
+    public String cacheName() {
+        return cacheName;
+    }
+
+    /**
+     * @param cacheName Name of new cache associated with this table.
+     */
+    public void cacheName(String cacheName) {
+        this.cacheName = cacheName;
+    }
+
+    /**
+     * @return Name of cache key type.
+     */
+    public String keyTypeName() {
+        return keyTypeName;
+    }
+
+    /**
+     * @param keyTypeName Name of cache key type.
+     */
+    public void keyTypeName(String keyTypeName) {
+        this.keyTypeName = keyTypeName;
+    }
+
+    /**
+     * @return Name of cache value type.
+     */
+    public String valueTypeName() {
+        return valTypeName;
+    }
+
+    /**
+     * @param valTypeName Name of cache value type.
+     */
+    public void valueTypeName(String valTypeName) {
+        this.valTypeName = valTypeName;
+    }
+
+    /**
+     * @return Group to put new cache into.
+     */
+    public String cacheGroup() {
+        return cacheGrp;
+    }
+
+    /**
+     * @param cacheGrp Group to put new cache into.
+     */
+    public void cacheGroup(String cacheGrp) {
+        this.cacheGrp = cacheGrp;
+    }
+
+    /**
+     * @return Atomicity mode for new cache.
+     */
+    public CacheAtomicityMode atomicityMode() {
+        return atomicityMode;
+    }
+
+    /**
+     * @param atomicityMode Atomicity mode for new cache.
+     */
+    public void atomicityMode(CacheAtomicityMode atomicityMode) {
+        this.atomicityMode = atomicityMode;
+    }
+
+    /**
+     * @return Write sync mode for new cache.
+     */
+    public CacheWriteSynchronizationMode writeSynchronizationMode() {
+        return writeSyncMode;
+    }
+
+    /**
+     * @param writeSyncMode Write sync mode for new cache.
+     */
+    public void writeSynchronizationMode(CacheWriteSynchronizationMode writeSyncMode) {
+        this.writeSyncMode = writeSyncMode;
+    }
+
+    /**
+     * @return Backups number for new cache.
+     */
+    @Nullable public Integer backups() {
+        return backups;
+    }
+
+    /**
+     * @param backups Backups number for new cache.
+     */
+    public void backups(Integer backups) {
+        this.backups = backups;
+    }
+
+    /**
+     * @return Columns.
+     */
+    public List<ColumnDefinition> columns() {
+        return cols;
+    }
+
+    /**
+     * @param cols Columns.
+     */
+    public void columns(List<ColumnDefinition> cols) {
+        this.cols = cols;
+    }
+
+    /**
+     * @return Primary key columns.
+     */
+    public List<String> primaryKeyColumns() {
+        return pkCols;
+    }
+
+    /**
+     * @param pkCols Primary key columns.
+     */
+    public void primaryKeyColumns(List<String> pkCols) {
+        this.pkCols = pkCols;
+    }
+
+    /**
+     * @return Name of the column that represents affinity key.
+     */
+    public String affinityKey() {
+        return affinityKey;
+    }
+
+    /**
+     * @param affinityKey Name of the column that represents affinity key.
+     */
+    public void affinityKey(String affinityKey) {
+        this.affinityKey = affinityKey;
+    }
+
+    /**
+     * @return Schema name upon which this statement has been issued.
+     */
+    public String schemaName() {
+        return schemaName;
+    }
+
+    /**
+     * @param schemaName Schema name upon which this statement has been issued.
+     */
+    public void schemaName(String schemaName) {
+        this.schemaName = schemaName;
+    }
+
+    /**
+     * @return Table name.
+     */
+    public String tableName() {
+        return tblName;
+    }
+
+    /**
+     * @param tblName Table name.
+     */
+    public void tableName(String tblName) {
+        this.tblName = tblName;
+    }
+
+    /**
+     * @return Quietly ignore this command if table already exists.
+     */
+    public boolean ifNotExists() {
+        return ifNotExists;
+    }
+
+    /**
+     * @param ifNotExists Quietly ignore this command if table already exists.
+     */
+    public void ifNotExists(boolean ifNotExists) {
+        this.ifNotExists = ifNotExists;
+    }
+
+    /**
+     * @return Data region name.
+     */
+    public String dataRegionName() {
+        return dataRegionName;
+    }
+
+    /**
+     * @param dataRegionName Data region name.
+     */
+    public void dataRegionName(String dataRegionName) {
+        this.dataRegionName = dataRegionName;
+    }
+
+    /**
+     * @return Encrypted flag.
+     */
+    public boolean encrypted() {
+        return encrypted;
+    }
+
+    /**
+     * @param encrypted Encrypted flag.
+     */
+    public void encrypted(boolean encrypted) {
+        this.encrypted = encrypted;
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/DdlCommand.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/DdlCommand.java
new file mode 100644
index 0000000..0d46ad4
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/DdlCommand.java
@@ -0,0 +1,21 @@
+/*
+ * 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.processors.query.calcite.prepare.ddl;
+
+/** Common interface to group all DDL operations. */
+public interface DdlCommand {
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/DdlSqlToCommandConverter.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/DdlSqlToCommandConverter.java
new file mode 100644
index 0000000..4d63e76
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/ddl/DdlSqlToCommandConverter.java
@@ -0,0 +1,344 @@
+/*
+ * 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.processors.query.calcite.prepare.ddl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlDdl;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlNumericLiteral;
+import org.apache.calcite.sql.ddl.SqlColumnDeclaration;
+import org.apache.calcite.sql.ddl.SqlKeyConstraint;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheWriteSynchronizationMode;
+import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
+import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.calcite.prepare.IgnitePlanner;
+import org.apache.ignite.internal.processors.query.calcite.prepare.PlanningContext;
+import org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTable;
+import org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOption;
+import org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum;
+import org.apache.ignite.internal.util.typedef.F;
+
+import static org.apache.calcite.sql.type.SqlTypeName.BOOLEAN;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.AFFINITY_KEY;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.ATOMICITY;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.BACKUPS;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.CACHE_GROUP;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.CACHE_NAME;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.DATA_REGION;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.ENCRYPTED;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.KEY_TYPE;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.TEMPLATE;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.VALUE_TYPE;
+import static org.apache.ignite.internal.processors.query.calcite.sql.IgniteSqlCreateTableOptionEnum.WRITE_SYNCHRONIZATION_MODE;
+
+/** */
+public class DdlSqlToCommandConverter {
+    /** Processor that validates a value is a Sql Identifier. */
+    private static final BiFunction<IgniteSqlCreateTableOption, PlanningContext, String> VALUE_IS_IDENTIFIER_VALIDATOR = (opt, ctx) -> {
+        if (!(opt.value() instanceof SqlIdentifier) || !((SqlIdentifier)opt.value()).isSimple())
+            throwOptionParsingException(opt, "a simple identifier", ctx.query());
+
+        return ((SqlIdentifier)opt.value()).getSimple();
+    };
+
+    /** Processor that unconditionally throws an AssertionException. */
+    private static final TableOptionProcessor<Void> UNSUPPORTED_OPTION_PROCESSOR = new TableOptionProcessor<>(
+        null,
+        (opt, ctx) -> {
+            throw new AssertionError("Unsupported option " + opt.key());
+        },
+        null);
+
+    /** Map of the supported table option processors. */
+    private final Map<IgniteSqlCreateTableOptionEnum, TableOptionProcessor<?>> tblOptionProcessors = Stream.of(
+        new TableOptionProcessor<>(TEMPLATE, VALUE_IS_IDENTIFIER_VALIDATOR, CreateTableCommand::templateName),
+        new TableOptionProcessor<>(AFFINITY_KEY, VALUE_IS_IDENTIFIER_VALIDATOR, CreateTableCommand::affinityKey),
+        new TableOptionProcessor<>(CACHE_GROUP, VALUE_IS_IDENTIFIER_VALIDATOR, CreateTableCommand::cacheGroup),
+        new TableOptionProcessor<>(CACHE_NAME, VALUE_IS_IDENTIFIER_VALIDATOR, CreateTableCommand::cacheName),
+        new TableOptionProcessor<>(DATA_REGION, VALUE_IS_IDENTIFIER_VALIDATOR, CreateTableCommand::dataRegionName),
+        new TableOptionProcessor<>(KEY_TYPE, VALUE_IS_IDENTIFIER_VALIDATOR, CreateTableCommand::keyTypeName),
+        new TableOptionProcessor<>(VALUE_TYPE, VALUE_IS_IDENTIFIER_VALIDATOR, CreateTableCommand::valueTypeName),
+        new TableOptionProcessor<>(ATOMICITY, validatorForEnumValue(CacheAtomicityMode.class), CreateTableCommand::atomicityMode),
+        new TableOptionProcessor<>(WRITE_SYNCHRONIZATION_MODE, validatorForEnumValue(CacheWriteSynchronizationMode.class), CreateTableCommand::writeSynchronizationMode),
+        new TableOptionProcessor<>(BACKUPS, (opt, ctx) -> {
+                if (!(opt.value() instanceof SqlNumericLiteral)
+                    || !((SqlNumericLiteral)opt.value()).isInteger()
+                    || ((SqlLiteral)opt.value()).intValue(true) < 0
+                )
+                    throwOptionParsingException(opt, "a non-negative integer", ctx.query());
+
+                return ((SqlLiteral)opt.value()).intValue(true);
+            }, CreateTableCommand::backups),
+        new TableOptionProcessor<>(ENCRYPTED, (opt, ctx) -> {
+            if (!(opt.value() instanceof SqlLiteral) && ((SqlLiteral)opt.value()).getTypeName() != BOOLEAN)
+                throwOptionParsingException(opt, "a boolean", ctx.query());
+
+            return ((SqlLiteral)opt.value()).booleanValue();
+        }, CreateTableCommand::encrypted)
+        ).collect(Collectors.toMap(TableOptionProcessor::key, Function.identity()));
+
+    /**
+     * Converts a given ddl AST to a ddl command.
+     *
+     * @param ddlNode Root node of the given AST.
+     * @param ctx Planning context.
+     */
+    public DdlCommand convert(SqlDdl ddlNode, PlanningContext ctx) {
+        if (ddlNode instanceof IgniteSqlCreateTable)
+            return convertCreateTable((IgniteSqlCreateTable)ddlNode, ctx);
+
+        throw new IgniteSQLException("Unsupported operation [" +
+            "sqlNodeKind=" + ddlNode.getKind() + "; " +
+            "querySql=\"" + ctx.query() + "\"]", IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+    }
+
+    /**
+     * Converts a given CreateTable AST to a CreateTable command.
+     *
+     * @param createTblNode Root node of the given AST.
+     * @param ctx Planning context.
+     */
+    private CreateTableCommand convertCreateTable(IgniteSqlCreateTable createTblNode, PlanningContext ctx) {
+        String schemaName, tableName;
+
+        if (createTblNode.name().isSimple()) {
+            schemaName = ctx.schemaName();
+            tableName = createTblNode.name().getSimple();
+        }
+        else {
+            SqlIdentifier schemaId = createTblNode.name().skipLast(1);
+
+            if (!schemaId.isSimple()) {
+                throw new IgniteSQLException("Unexpected value of schemaName [" +
+                    "expected a simple identifier, but was " + schemaId + "; " +
+                    "querySql=\"" + ctx.query() + "\"]", IgniteQueryErrorCode.PARSING);
+            }
+
+            schemaName = schemaId.getSimple();
+
+            SqlIdentifier tableId = createTblNode.name().getComponent(schemaId.names.size());
+
+            if (!tableId.isSimple()) {
+                throw new IgniteSQLException("Unexpected value of tableName [" +
+                    "expected a simple identifier, but was " + tableId + "; " +
+                    "querySql=\"" + ctx.query() + "\"]", IgniteQueryErrorCode.PARSING);
+            }
+
+            tableName = tableId.getSimple();
+        }
+
+        ensureSchemaExists(ctx, schemaName);
+
+        CreateTableCommand createTblCmd = new CreateTableCommand();
+
+        createTblCmd.schemaName(schemaName);
+        createTblCmd.tableName(tableName);
+        createTblCmd.ifNotExists(createTblNode.ifNotExists());
+        createTblCmd.templateName(QueryUtils.TEMPLATE_PARTITIONED);
+
+        if (createTblNode.createOptionList() != null) {
+            for (SqlNode optNode : createTblNode.createOptionList().getList()) {
+                IgniteSqlCreateTableOption opt = (IgniteSqlCreateTableOption)optNode;
+
+                tblOptionProcessors.getOrDefault(opt.key(), UNSUPPORTED_OPTION_PROCESSOR).process(opt, ctx, createTblCmd);
+            }
+        }
+
+        List<SqlColumnDeclaration> colDeclarations = createTblNode.columnList().getList().stream()
+            .filter(SqlColumnDeclaration.class::isInstance)
+            .map(SqlColumnDeclaration.class::cast)
+            .collect(Collectors.toList());
+
+        IgnitePlanner planner = ctx.planner();
+
+        List<ColumnDefinition> cols = new ArrayList<>();
+
+        for (SqlColumnDeclaration col : colDeclarations) {
+            if (!col.name.isSimple())
+                throw new IgniteSQLException("Unexpected value of columnName [" +
+                    "expected a simple identifier, but was " + col.name + "; " +
+                    "querySql=\"" + ctx.query() + "\"]", IgniteQueryErrorCode.PARSING);
+
+            String name = col.name.getSimple();
+            RelDataType type = planner.conver(col.dataType);
+
+            Object dflt = null;
+            if (col.expression != null)
+                dflt = ((SqlLiteral)col.expression).getValue();
+
+            cols.add(new ColumnDefinition(name, type, dflt));
+        }
+
+        createTblCmd.columns(cols);
+
+        List<SqlKeyConstraint> pkConstraints = createTblNode.columnList().getList().stream()
+            .filter(SqlKeyConstraint.class::isInstance)
+            .map(SqlKeyConstraint.class::cast)
+            .collect(Collectors.toList());
+
+        if (pkConstraints.size() > 1)
+            throw new IgniteSQLException("Unexpected amount of primary key constraints [" +
+                "expected at most one, but was " + pkConstraints.size() + "; " +
+                "querySql=\"" + ctx.query() + "\"]", IgniteQueryErrorCode.PARSING);
+
+        if (!F.isEmpty(pkConstraints)) {
+            Set<String> dedupSet = new HashSet<>();
+
+            List<String> pkCols = pkConstraints.stream()
+                .map(pk -> pk.getOperandList().get(1))
+                .map(SqlNodeList.class::cast)
+                .flatMap(l -> l.getList().stream())
+                .map(SqlIdentifier.class::cast)
+                .map(SqlIdentifier::getSimple)
+                .filter(dedupSet::add)
+                .collect(Collectors.toList());
+
+            createTblCmd.primaryKeyColumns(pkCols);
+        }
+
+        return createTblCmd;
+    }
+
+    /** */
+    private void ensureSchemaExists(PlanningContext ctx, String schemaName) {
+        if (ctx.catalogReader().getRootSchema().getSubSchema(schemaName, true) == null)
+            throw new IgniteSQLException("Schema with name " + schemaName + " not found",
+                IgniteQueryErrorCode.SCHEMA_NOT_FOUND);
+    }
+
+    /**
+     * Short cut for validating that option value is a simple identifier.
+     *
+     * @param opt An option to validate.
+     * @param ctx Planning context.
+     * @throws IgniteSQLException In case the validation was failed.
+     */
+    private String paramIsSqlIdentifierValidator(IgniteSqlCreateTableOption opt, PlanningContext ctx) {
+        if (!(opt.value() instanceof SqlIdentifier) || !((SqlIdentifier)opt.value()).isSimple())
+            throwOptionParsingException(opt, "a simple identifier", ctx.query());
+
+        return ((SqlIdentifier)opt.value()).getSimple();
+    }
+    
+    /**
+     * Creates a validator for an option which value should be value of given enumeration.
+     *
+     * @param clz Enumeration class to create validator for.
+     */
+    private static <T extends Enum<T>> BiFunction<IgniteSqlCreateTableOption, PlanningContext, T> validatorForEnumValue(
+        Class<T> clz
+    ) {
+        return (opt, ctx) -> {
+            T val = null;
+
+            if (opt.value() instanceof SqlIdentifier) {
+                val = Arrays.stream(clz.getEnumConstants())
+                    .filter(m -> m.name().equalsIgnoreCase(opt.value().toString()))
+                    .findFirst()
+                    .orElse(null);
+            }
+
+            if (val == null)
+                throwOptionParsingException(opt, "values are "
+                    + Arrays.toString(clz.getEnumConstants()), ctx.query());
+
+            return val;
+        };
+    }
+
+    /**
+     * Throws exception with message relates to validation of create table option.
+     *
+     * @param opt An option which validation was failed.
+     * @param exp A string representing expected values.
+     * @param qry A query the validation was failed for.
+     */
+    private static void throwOptionParsingException(IgniteSqlCreateTableOption opt, String exp, String qry) {
+        throw new IgniteSQLException("Unexpected value for param " + opt.key() + " [" +
+            "expected " + exp + ", but was " + opt.value() + "; " +
+            "querySql=\"" + qry + "\"]", IgniteQueryErrorCode.PARSING);
+    }
+
+    /** */
+    private static class TableOptionProcessor<T> {
+        /** */
+        private final IgniteSqlCreateTableOptionEnum key;
+
+        /** */
+        private final BiFunction<IgniteSqlCreateTableOption, PlanningContext, T> validator;
+
+        /** */
+        private final BiConsumer<CreateTableCommand, T> valSetter;
+
+        /**
+         * @param key Option key this processor is supopsed to handle.
+         * @param validator Validator that derives a value from a {@link SqlNode},
+         *                 validates it and then returns if validation passed,
+         *                 throws an exeption otherwise.
+         * @param valSetter Setter sets the value recived from the validator
+         *                 to the given {@link CreateTableCommand}.
+         */
+        private TableOptionProcessor(
+            IgniteSqlCreateTableOptionEnum key,
+            BiFunction<IgniteSqlCreateTableOption, PlanningContext, T> validator,
+            BiConsumer<CreateTableCommand, T> valSetter
+        ) {
+            this.key = key;
+            this.validator = validator;
+            this.valSetter = valSetter;
+        }
+
+        /**
+         * Processes the given option, validates it's value and then sets the appropriate
+         * field in a given command, throws an exception if the validation failed.
+         *
+         * @param opt Option to validate.
+         * @param ctx Planning context.
+         * @param cmd Command instance to set a validation result.
+         */
+        private void process(IgniteSqlCreateTableOption opt, PlanningContext ctx, CreateTableCommand cmd) {
+            assert key == null || key == opt.key() : "Unexpected create table option [expected=" + key + ", actual=" + opt.key() + "]";
+
+            valSetter.accept(cmd, validator.apply(opt, ctx));
+        }
+
+        /**
+         * @return Key this processor is supposed to handle.
+         */
+        private IgniteSqlCreateTableOptionEnum key() {
+            return key;
+        }
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/TableDescriptorImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/TableDescriptorImpl.java
index 2d3881c..44c8437 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/TableDescriptorImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/TableDescriptorImpl.java
@@ -68,6 +68,7 @@ import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteBiTuple;
+import org.apache.ignite.lang.IgniteUuid;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -123,8 +124,23 @@ public class TableDescriptorImpl extends NullInitializerExpressionFactory
         // A _key/_val field is virtual in case there is an alias or a property(es) mapped to the _key/_val field.
         BitSet virtualFields = new BitSet();
 
-        descriptors.add(
-            new KeyValDescriptor(QueryUtils.KEY_FIELD_NAME, typeDesc.keyClass(), true, QueryUtils.KEY_COL));
+        if (typeDesc.implicitPk()) {
+            // pk is not set, thus we need to provide default value for autogenerated key
+            descriptors.add(
+                new KeyValDescriptor(QueryUtils.KEY_FIELD_NAME, typeDesc.keyClass(), true, QueryUtils.KEY_COL) {
+                    @Override public Object defaultValue() {
+                        return IgniteUuid.randomUuid();
+                    }
+                }
+            );
+
+            virtualFields.set(0);
+        }
+        else {
+            descriptors.add(
+                new KeyValDescriptor(QueryUtils.KEY_FIELD_NAME, typeDesc.keyClass(), true, QueryUtils.KEY_COL));
+        }
+
         descriptors.add(
             new KeyValDescriptor(QueryUtils.VAL_FIELD_NAME, typeDesc.valueClass(), false, QueryUtils.VAL_COL));
 
@@ -323,21 +339,28 @@ public class TableDescriptorImpl extends NullInitializerExpressionFactory
 
         Object key = handler.get(keyField, row);
 
-        if (key == null) {
-            key = newVal(typeDesc.keyTypeName(), typeDesc.keyClass());
+        if (key != null)
+            return TypeUtils.fromInternal(ectx, key, descriptors[QueryUtils.KEY_COL].storageType());
 
-            // skip _key and _val
-            for (int i = 2; i < descriptors.length; i++) {
-                final ColumnDescriptor desc = descriptors[i];
+        // skip _key and _val
+        for (int i = 2; i < descriptors.length; i++) {
+            final ColumnDescriptor desc = descriptors[i];
 
-                Object fieldVal = handler.get(i, row);
+            if (!desc.field() || !desc.key())
+                continue;
+
+            Object fieldVal = handler.get(i, row);
 
-                if (desc.field() && desc.key() && fieldVal != null)
-                    desc.set(key, TypeUtils.fromInternal(ectx, fieldVal, desc.storageType()));
+            if (fieldVal != null) {
+                if (key == null)
+                    key = newVal(typeDesc.keyTypeName(), typeDesc.keyClass());
+
+                desc.set(key, TypeUtils.fromInternal(ectx, fieldVal, desc.storageType()));
             }
         }
-        else
-            key = TypeUtils.fromInternal(ectx, key, descriptors[QueryUtils.KEY_COL].storageType());
+
+        if (key == null)
+            key = descriptors[QueryUtils.KEY_COL].defaultValue();
 
         return key;
     }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlCreateTable.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlCreateTable.java
new file mode 100644
index 0000000..e6a28f4
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlCreateTable.java
@@ -0,0 +1,117 @@
+/*
+ * 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.processors.query.calcite.sql;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.calcite.sql.SqlCreate;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.SqlSpecialOperator;
+import org.apache.calcite.sql.SqlWriter;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.util.ImmutableNullableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Parse tree for {@code CREATE TABLE} statement with Ignite specific features.
+ */
+public class IgniteSqlCreateTable extends SqlCreate {
+    /** */
+    private final SqlIdentifier name;
+
+    /** */
+    private final @Nullable SqlNodeList columnList;
+
+    /** */
+    private final @Nullable SqlNodeList createOptionList;
+
+    /** */
+    private static final SqlOperator OPERATOR =
+        new SqlSpecialOperator("CREATE TABLE", SqlKind.CREATE_TABLE);
+
+    /** Creates a SqlCreateTable. */
+    protected IgniteSqlCreateTable(SqlParserPos pos, boolean ifNotExists,
+        SqlIdentifier name, @Nullable SqlNodeList columnList, @Nullable SqlNodeList createOptionList) {
+        super(OPERATOR, pos, false, ifNotExists);
+        this.name = Objects.requireNonNull(name, "name");
+        this.columnList = columnList;
+        this.createOptionList = createOptionList;
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("nullness")
+    @Override public List<SqlNode> getOperandList() {
+        return ImmutableNullableList.of(name, columnList, createOptionList);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
+        writer.keyword("CREATE");
+        writer.keyword("TABLE");
+        if (ifNotExists)
+            writer.keyword("IF NOT EXISTS");
+
+        name.unparse(writer, leftPrec, rightPrec);
+        if (columnList != null) {
+            SqlWriter.Frame frame = writer.startList("(", ")");
+            for (SqlNode c : columnList) {
+                writer.sep(",");
+                c.unparse(writer, 0, 0);
+            }
+            writer.endList(frame);
+        }
+
+        if (createOptionList != null) {
+            writer.keyword("WITH");
+
+            createOptionList.unparse(writer, 0, 0);
+        }
+    }
+
+    /**
+     * @return Name of the table.
+     */
+    public SqlIdentifier name() {
+        return name;
+    }
+
+    /**
+     * @return List of the specified columns and constraints.
+     */
+    public SqlNodeList columnList() {
+        return columnList;
+    }
+
+    /**
+     * @return List of the specified options to create table with.
+     */
+    public SqlNodeList createOptionList() {
+        return createOptionList;
+    }
+
+    /**
+     * @return Whether the IF NOT EXISTS is specified.
+     */
+    public boolean ifNotExists() {
+        return ifNotExists;
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlCreateTableOption.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlCreateTableOption.java
new file mode 100644
index 0000000..6191568
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlCreateTableOption.java
@@ -0,0 +1,91 @@
+/*
+ * 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.processors.query.calcite.sql;
+
+import org.apache.calcite.sql.IgniteSqlNode;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlWriter;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.util.SqlVisitor;
+import org.apache.calcite.sql.validate.SqlValidator;
+import org.apache.calcite.sql.validate.SqlValidatorScope;
+import org.apache.calcite.util.Litmus;
+
+/** An AST node representing option to create table with. */
+public class IgniteSqlCreateTableOption extends IgniteSqlNode {
+    /** Option key. */
+    private final IgniteSqlCreateTableOptionEnum key;
+
+    /** Option value. */
+    private final SqlNode value;
+
+    /** Creates IgniteSqlCreateTableOption. */
+    public IgniteSqlCreateTableOption(IgniteSqlCreateTableOptionEnum key, SqlNode value, SqlParserPos pos) {
+        super(pos);
+
+        this.key = key;
+        this.value = value;
+    }
+
+    /** {@inheritDoc} */
+    @Override public SqlNode clone(SqlParserPos pos) {
+        return new IgniteSqlCreateTableOption(key, value, pos);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
+        writer.keyword(key.name());
+        writer.keyword("=");
+        value.unparse(writer, leftPrec, rightPrec);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void validate(SqlValidator validator, SqlValidatorScope scope) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@inheritDoc} */
+    @Override public <R> R accept(SqlVisitor<R> visitor) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equalsDeep(SqlNode node, Litmus litmus) {
+        if (!(node instanceof IgniteSqlCreateTableOption))
+            return litmus.fail("{} != {}", this, node);
+
+        IgniteSqlCreateTableOption that = (IgniteSqlCreateTableOption)node;
+        if (key != that.key)
+            return litmus.fail("{} != {}", this, node);
+
+        return value.equalsDeep(that.value, litmus);
+    }
+
+    /**
+     * @return Option's key.
+     */
+    public IgniteSqlCreateTableOptionEnum key() {
+        return key;
+    }
+
+    /**
+     * @return Option's value.
+     */
+    public SqlNode value() {
+        return value;
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlCreateTableOptionEnum.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlCreateTableOptionEnum.java
new file mode 100644
index 0000000..e1d71ad
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/sql/IgniteSqlCreateTableOptionEnum.java
@@ -0,0 +1,55 @@
+/*
+ * 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.processors.query.calcite.sql;
+
+/**
+ * Enumerates the Ignite specific options for CREATE TABLE statement.
+ */
+public enum IgniteSqlCreateTableOptionEnum {
+    /** A name of the required cache template. */
+    TEMPLATE,
+
+    /** A number of partition backups. */
+    BACKUPS,
+
+    /** A name of the desired affinity key column. */
+    AFFINITY_KEY,
+
+    /** An atomicity mode for the underlying cache. */
+    ATOMICITY,
+
+    /** A write synchronization mode for the underlying cache. */
+    WRITE_SYNCHRONIZATION_MODE,
+
+    /** A name the group the underlying cache belongs to. */
+    CACHE_GROUP,
+
+    /** A name of the underlying cache created by the command. */
+    CACHE_NAME,
+
+    /** A name of the data region where table entries should be stored. */
+    DATA_REGION,
+
+    /** A name of the custom key type that is used from the key-value and other non-SQL APIs in Ignite. */
+    KEY_TYPE,
+
+    /** A name of the custom value type that is used from the key-value and other non-SQL APIs in Ignite. */
+    VALUE_TYPE,
+
+    /** This flag specified whether the encryption should be enabled for the underlying cache. */
+    ENCRYPTED,
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessorTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessorTest.java
index bee1930..e2a8617 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessorTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessorTest.java
@@ -42,6 +42,7 @@ import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.IgniteEx;
 import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.internal.processors.cache.query.QueryCursorEx;
 import org.apache.ignite.internal.processors.query.QueryEngine;
 import org.apache.ignite.internal.processors.query.calcite.exec.MailboxRegistryImpl;
 import org.apache.ignite.internal.processors.query.calcite.exec.rel.Inbox;
@@ -56,6 +57,7 @@ import org.apache.ignite.testframework.ListeningTestLogger;
 import org.apache.ignite.testframework.LogListener;
 import org.apache.ignite.testframework.junits.WithSystemProperty;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.hamcrest.CoreMatchers;
 import org.junit.Test;
 
 import static org.apache.ignite.internal.processors.query.calcite.QueryChecker.awaitReservationsRelease;
@@ -754,7 +756,8 @@ public class CalciteQueryProcessorTest extends GridCommonAbstractTest {
 
         QueryEngine engine = Commons.lookupComponent(grid(1).context(), QueryEngine.class);
 
-        List<FieldsQueryCursor<List<?>>> query = engine.query(null, "PUBLIC", "INSERT INTO DEVELOPER VALUES (?, ?, ?)", 0, "Igor", 1);
+        List<FieldsQueryCursor<List<?>>> query = engine.query(null, "PUBLIC",
+            "INSERT INTO DEVELOPER(_key, name, projectId) VALUES (?, ?, ?)", 0, "Igor", 1);
 
         assertEquals(1, query.size());
 
@@ -996,6 +999,29 @@ public class CalciteQueryProcessorTest extends GridCommonAbstractTest {
         }
     }
 
+    /**
+     * Verifies infix cast operator.
+     */
+    @Test
+    public void testInfixTypeCast() {
+        execute(client, "drop table if exists test_tbl");
+        execute(client, "create table test_tbl(id int primary key, val varchar)");
+
+        QueryEngine engineSrv = Commons.lookupComponent(grid(1).context(), QueryEngine.class);
+
+        FieldsQueryCursor<List<?>> cur = engineSrv.query(null, "PUBLIC",
+            "select id, id::tinyint as tid, id::smallint as sid, id::varchar as vid from test_tbl").get(0);
+
+        assertThat(cur, CoreMatchers.instanceOf(QueryCursorEx.class));
+
+        QueryCursorEx<?> qCur = (QueryCursorEx<?>)cur;
+
+        assertThat(qCur.fieldsMeta().get(0).fieldTypeName(), equalTo(Integer.class.getName()));
+        assertThat(qCur.fieldsMeta().get(1).fieldTypeName(), equalTo(Byte.class.getName()));
+        assertThat(qCur.fieldsMeta().get(2).fieldTypeName(), equalTo(Short.class.getName()));
+        assertThat(qCur.fieldsMeta().get(3).fieldTypeName(), equalTo(String.class.getName()));
+    }
+
     /** */
     private static List<String> deriveColumnNamesFromCursor(FieldsQueryCursor cursor) {
         List<String> names = new ArrayList<>(cursor.getColumnsCount());
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CreateTableIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CreateTableIntegrationTest.java
new file mode 100644
index 0000000..80d61e0
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/CreateTableIntegrationTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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.processors.query.calcite;
+
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.cache.CacheWriteSynchronizationMode;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.query.FieldsQueryCursor;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.configuration.SqlConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.cache.IgniteInternalCache;
+import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.hamcrest.CustomMatcher;
+import org.hamcrest.Matcher;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.apache.ignite.internal.util.IgniteUtils.map;
+import static org.apache.ignite.testframework.GridTestUtils.hasSize;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.junit.Assert.assertThat;
+
+/** */
+public class CreateTableIntegrationTest extends GridCommonAbstractTest {
+    /** */
+    private static final String CLIENT_NODE_NAME = "client";
+
+    /** */
+    private static final String DATA_REGION_NAME = "test_data_region";
+
+    /** */
+    private IgniteEx client;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        startGrids(1);
+
+        client = startClientGrid(CLIENT_NODE_NAME);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        return super.getConfiguration(igniteInstanceName)
+            .setSqlConfiguration(
+                new SqlConfiguration().setSqlSchemas("MY_SCHEMA")
+            )
+            .setDataStorageConfiguration(
+                new DataStorageConfiguration()
+                    .setDataRegionConfigurations(new DataRegionConfiguration().setName(DATA_REGION_NAME))
+            );
+    }
+
+    /** */
+    @Before
+    public void init() {
+        client = grid(CLIENT_NODE_NAME);
+    }
+
+    /** */
+    @After
+    public void cleanUp() {
+        client.destroyCaches(client.cacheNames());
+    }
+
+    /**
+     * Creates table with two columns, where the first column is PK,
+     * and verifies created cache.
+     */
+    @Test
+    public void createTableSimpleCase() {
+        Set<String> cachesBefore = new HashSet<>(client.cacheNames());
+
+        executeSql("create table my_table (id int primary key, val varchar)");
+
+        Set<String> newCaches = new HashSet<>(client.cacheNames());
+        newCaches.removeAll(cachesBefore);
+
+        assertThat(newCaches, hasSize(1));
+
+        CacheConfiguration<?, ?> ccfg = client.cachex(newCaches.iterator().next()).configuration();
+
+        assertThat(ccfg.getQueryEntities(), hasSize(1));
+
+        QueryEntity ent = ccfg.getQueryEntities().iterator().next();
+
+        assertThat(ent.getTableName(), equalTo("MY_TABLE"));
+        assertThat(ent.getKeyFieldName(), equalTo("ID"));
+        assertThat(
+            ent.getFields(),
+            equalTo(new LinkedHashMap<>(
+                map(
+                    "ID", Integer.class.getName(),
+                    "VAL", String.class.getName()
+                )
+            ))
+        );
+    }
+
+    /**
+     * Creates table with all possible options except template,
+     * and verifies created cache.
+     */
+    @Test
+    public void createTableWithOptions() {
+        Set<String> cachesBefore = new HashSet<>(client.cacheNames());
+
+        executeSql("create table my_table (id1 int, id2 int, val varchar, primary key(id1, id2)) with " +
+            " backups=2," +
+            " affinity_key=id2," +
+            " atomicity=transactional," +
+            " write_synchronization_mode=full_async," +
+            " cache_group=my_cache_group," +
+            " cache_name=my_cache_name," +
+            " data_region=\"" + DATA_REGION_NAME + "\"," +
+            " key_type=my_key_type," +
+            " value_type=my_value_type"
+// due to uncertain reason a test hangs if encryption is enabled
+//            " encrypted=true"
+        );
+
+        Set<String> newCaches = new HashSet<>(client.cacheNames());
+        newCaches.removeAll(cachesBefore);
+
+        assertThat(newCaches, hasSize(1));
+
+        CacheConfiguration<?, ?> ccfg = client.cachex(newCaches.iterator().next()).configuration();
+
+        assertThat(ccfg.getQueryEntities(), hasSize(1));
+        assertThat(ccfg.getBackups(), equalTo(2));
+        assertThat(ccfg.getAtomicityMode(), equalTo(CacheAtomicityMode.TRANSACTIONAL));
+        assertThat(ccfg.getWriteSynchronizationMode(), equalTo(CacheWriteSynchronizationMode.FULL_ASYNC));
+        assertThat(ccfg.getGroupName(), equalTo("MY_CACHE_GROUP"));
+        assertThat(ccfg.getName(), equalTo("MY_CACHE_NAME"));
+        assertThat(ccfg.getDataRegionName(), equalTo(DATA_REGION_NAME));
+        assertThat(ccfg.getKeyConfiguration()[0].getAffinityKeyFieldName(), equalTo("ID2"));
+
+        QueryEntity ent = ccfg.getQueryEntities().iterator().next();
+
+        assertThat(ent.getTableName(), equalTo("MY_TABLE"));
+        assertThat(ent.getKeyType(), equalTo("MY_KEY_TYPE"));
+        assertThat(ent.getValueType(), equalTo("MY_VALUE_TYPE"));
+        assertThat(ent.getKeyFields(), equalTo(new LinkedHashSet<>(F.asList("ID1", "ID2"))));
+    }
+
+    /**
+     * Creates several tables with specified templates,
+     * and verifies created caches.
+     */
+    @Test
+    @SuppressWarnings("rawtypes")
+    public void createTableWithTemplate() {
+        Set<String> cachesBefore = new HashSet<>(client.cacheNames());
+
+        executeSql("create table my_table_replicated (id int, val varchar) with template=replicated, cache_name=repl");
+        executeSql("create table my_table_partitioned (id int, val varchar) with template=partitioned, cache_name=part");
+
+        Set<String> newCaches = new HashSet<>(client.cacheNames());
+        newCaches.removeAll(cachesBefore);
+
+        assertThat(newCaches, hasSize(2));
+
+        List<CacheConfiguration> ccfgs = newCaches.stream()
+            .map(client::cachex)
+            .map(IgniteInternalCache::configuration)
+            .collect(Collectors.toList());
+
+        assertThat(ccfgs, hasItem(matches("replicated cache",
+            ccfg -> "REPL".equals(ccfg.getName()) && ccfg.getCacheMode() == CacheMode.REPLICATED)));
+        assertThat(ccfgs, hasItem(matches("partitioned cache",
+            ccfg -> "PART".equals(ccfg.getName()) && ccfg.getCacheMode() == CacheMode.PARTITIONED)));
+    }
+
+    /**
+     * Tries to create several tables with the same name.
+     */
+    @Test
+    @SuppressWarnings("ThrowableNotThrown")
+    public void createTableIfNotExists() {
+        executeSql("create table my_table (id int, val varchar)");
+
+        GridTestUtils.assertThrows(log,
+            () -> executeSql("create table my_table (id int, val varchar)"),
+            IgniteSQLException.class, "Table already exists: MY_TABLE");
+
+        executeSql("create table if not exists my_table (id int, val varchar)");
+    }
+
+    /**
+     * Creates a table without a primary key and then insert a few rows.
+     */
+    @Test
+    public void createTableWithoutPk() {
+        executeSql("create table my_table (f1 int, f2 varchar)");
+
+        executeSql("insert into my_table(f1, f2) values (1, '1'),(2, '2')");
+        executeSql("insert into my_table values (1, '1'),(2, '2')");
+
+        assertThat(executeSql("select * from my_table"), hasSize(4));
+    }
+
+    /**
+     * Creates a table in a different schema.
+     */
+    @Test
+    public void createTableCustomSchema() {
+        executeSql("create table my_schema.my_table (f1 int, f2 varchar)");
+
+        executeSql("insert into my_schema.my_table(f1, f2) values (1, '1'),(2, '2')");
+        executeSql("insert into my_schema.my_table values (1, '1'),(2, '2')");
+
+        assertThat(executeSql("select * from my_schema.my_table"), hasSize(4));
+    }
+
+    /** */
+    private List<List<?>> executeSql(String sql) {
+        List<FieldsQueryCursor<List<?>>> cur = queryProcessor().query(null, "PUBLIC", sql);
+
+        try (QueryCursor<List<?>> srvCursor = cur.get(0)) {
+            return srvCursor.getAll();
+        }
+    }
+
+    /** */
+    private CalciteQueryProcessor queryProcessor() {
+        return Commons.lookupComponent(client.context(), CalciteQueryProcessor.class);
+    }
+
+    /**
+     * Matcher to verify that an object of the expected type and matches the given predicat.
+     *
+     * @param desc Description for this matcher.
+     * @param pred Addition check that would be applied to the object.
+     * @return {@code true} in case the object if instance of the given class and matches the predicat.
+     */
+    private static Matcher<CacheConfiguration<?, ?>> matches(String desc, Predicate<CacheConfiguration<?, ?>> pred) {
+        return new CustomMatcher<CacheConfiguration<?, ?>>(desc) {
+            @Override public boolean matches(Object item) {
+                return item instanceof CacheConfiguration && pred.test((CacheConfiguration<?, ?>)item);
+            }
+        };
+    }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/JoinColocationPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/JoinColocationPlannerTest.java
index e378766..d833245 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/JoinColocationPlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/JoinColocationPlannerTest.java
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.internal.processors.query.calcite.planner;
 
-import java.util.Collection;
 import java.util.List;
 
 import org.apache.calcite.plan.RelOptUtil;
@@ -38,10 +37,9 @@ import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactor
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeSystem;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.CU;
-import org.hamcrest.CustomMatcher;
-import org.hamcrest.Matcher;
 import org.junit.Test;
 
+import static org.apache.ignite.testframework.GridTestUtils.hasSize;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.is;
@@ -271,18 +269,4 @@ public class JoinColocationPlannerTest extends AbstractPlannerTest {
 
         return schema;
     }
-
-    /**
-     * Matcher to verify size of the collection.
-     *
-     * @param size Required size.
-     * @return {@code true} in case collection is not null and has an exactly the same size.
-     */
-    private static <T extends Collection<?>> Matcher<T> hasSize(int size) {
-        return new CustomMatcher<T>("should be non empty with size=" + size) {
-            @Override public boolean matches(Object item) {
-                return item instanceof Collection && ((Collection<?>)item).size() == size;
-            }
-        };
-    }
 }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/sql/SqlDdlParserTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/sql/SqlDdlParserTest.java
new file mode 100644
index 0000000..428dc6c
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/sql/SqlDdlParserTest.java
@@ -0,0 +1,325 @@
+/*
+ * 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.processors.query.calcite.sql;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlNumericLiteral;
+import org.apache.calcite.sql.ddl.SqlColumnDeclaration;
+import org.apache.calcite.sql.ddl.SqlKeyConstraint;
+import org.apache.calcite.sql.parser.SqlParseException;
+import org.apache.calcite.sql.parser.SqlParser;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.hamcrest.CustomMatcher;
+import org.hamcrest.Matcher;
+import org.junit.Test;
+
+import static java.util.Collections.singleton;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Test suite to verify parsing of the DDL command.
+ */
+public class SqlDdlParserTest extends GridCommonAbstractTest {
+    /**
+     * Very simple case where only table name and a few columns are presented.
+     */
+    @Test
+    public void createTableSimpleCase() throws SqlParseException {
+        String query = "create table my_table(id int, val varchar)";
+
+        SqlNode node = parse(query);
+
+        assertThat(node, instanceOf(IgniteSqlCreateTable.class));
+
+        IgniteSqlCreateTable createTable = (IgniteSqlCreateTable)node;
+
+        assertThat(createTable.name().names, is(ImmutableList.of("MY_TABLE")));
+        assertThat(createTable.ifNotExists, is(false));
+        assertThat(createTable.columnList(), hasItem(columnWithName("ID")));
+        assertThat(createTable.columnList(), hasItem(columnWithName("VAL")));
+    }
+
+    /**
+     * Parsing of CREATE TABLE statement with quoted identifiers.
+     */
+    @Test
+    public void createTableQuotedIdentifiers() throws SqlParseException {
+        String query = "create table \"My_Table\"(\"Id\" int, \"Val\" varchar)";
+
+        SqlNode node = parse(query);
+
+        assertThat(node, instanceOf(IgniteSqlCreateTable.class));
+
+        IgniteSqlCreateTable createTable = (IgniteSqlCreateTable)node;
+
+        assertThat(createTable.name().names, is(ImmutableList.of("My_Table")));
+        assertThat(createTable.ifNotExists, is(false));
+        assertThat(createTable.columnList(), hasItem(columnWithName("Id")));
+        assertThat(createTable.columnList(), hasItem(columnWithName("Val")));
+    }
+
+    /**
+     * Parsing of CREATE TABLE statement with IF NOT EXISTS.
+     */
+    @Test
+    public void createTableIfNotExists() throws SqlParseException {
+        String query = "create table if not exists my_table(id int, val varchar)";
+
+        SqlNode node = parse(query);
+
+        assertThat(node, instanceOf(IgniteSqlCreateTable.class));
+
+        IgniteSqlCreateTable createTable = (IgniteSqlCreateTable)node;
+
+        assertThat(createTable.name().names, is(ImmutableList.of("MY_TABLE")));
+        assertThat(createTable.ifNotExists, is(true));
+        assertThat(createTable.columnList(), hasItem(columnWithName("ID")));
+        assertThat(createTable.columnList(), hasItem(columnWithName("VAL")));
+    }
+
+    /**
+     * Parsing of CREATE TABLE with specified PK constraint where constraint
+     * is a shortcut within a column definition.
+     */
+    @Test
+    public void createTableWithPkCase1() throws SqlParseException {
+        String query = "create table my_table(id int primary key, val varchar)";
+
+        SqlNode node = parse(query);
+
+        assertThat(node, instanceOf(IgniteSqlCreateTable.class));
+
+        IgniteSqlCreateTable createTable = (IgniteSqlCreateTable)node;
+
+        assertThat(createTable.name().names, is(ImmutableList.of("MY_TABLE")));
+        assertThat(createTable.ifNotExists, is(false));
+        assertThat(createTable.columnList(), hasItem(ofTypeMatching(
+            "PK constraint with name \"ID\"", SqlKeyConstraint.class,
+            constraint -> hasItem(ofTypeMatching("identifier \"ID\"", SqlIdentifier.class, id -> "ID".equals(id.names.get(0))))
+                .matches(constraint.getOperandList().get(1))
+                && constraint.getOperandList().get(0) == null
+                && constraint.isA(singleton(SqlKind.PRIMARY_KEY)))));
+    }
+
+    /**
+     * Parsing of CREATE TABLE with specified PK constraint where constraint
+     * is set explicitly and has no name.
+     */
+    @Test
+    public void createTableWithPkCase2() throws SqlParseException {
+        String query = "create table my_table(id int, val varchar, primary key(id))";
+
+        SqlNode node = parse(query);
+
+        assertThat(node, instanceOf(IgniteSqlCreateTable.class));
+
+        IgniteSqlCreateTable createTable = (IgniteSqlCreateTable)node;
+
+        assertThat(createTable.name().names, is(ImmutableList.of("MY_TABLE")));
+        assertThat(createTable.ifNotExists, is(false));
+        assertThat(createTable.columnList(), hasItem(ofTypeMatching(
+            "PK constraint without name containing column \"ID\"", SqlKeyConstraint.class,
+            constraint -> hasItem(ofTypeMatching("identifier \"ID\"", SqlIdentifier.class, id -> "ID".equals(id.names.get(0))))
+                .matches(constraint.getOperandList().get(1))
+                && constraint.getOperandList().get(0) == null
+                && constraint.isA(singleton(SqlKind.PRIMARY_KEY)))));
+    }
+
+    /**
+     * Parsing of CREATE TABLE with specified PK constraint where constraint
+     * is set explicitly and has a name.
+     */
+    @Test
+    public void createTableWithPkCase3() throws SqlParseException {
+        String query = "create table my_table(id int, val varchar, constraint pk_key primary key(id))";
+
+        SqlNode node = parse(query);
+
+        assertThat(node, instanceOf(IgniteSqlCreateTable.class));
+
+        IgniteSqlCreateTable createTable = (IgniteSqlCreateTable)node;
+
+        assertThat(createTable.name().names, is(ImmutableList.of("MY_TABLE")));
+        assertThat(createTable.ifNotExists, is(false));
+        assertThat(createTable.columnList(), hasItem(ofTypeMatching(
+            "PK constraint with name \"PK_KEY\" containing column \"ID\"", SqlKeyConstraint.class,
+            constraint -> hasItem(ofTypeMatching("identifier \"ID\"", SqlIdentifier.class, id -> "ID".equals(id.names.get(0))))
+                .matches(constraint.getOperandList().get(1))
+                && "PK_KEY".equals(((SqlIdentifier)constraint.getOperandList().get(0)).names.get(0))
+                && constraint.isA(singleton(SqlKind.PRIMARY_KEY)))));
+    }
+
+    /**
+     * Parsing of CREATE TABLE with specified PK constraint where constraint
+     * consists of several columns.
+     */
+    @Test
+    public void createTableWithPkCase4() throws SqlParseException {
+        String query = "create table my_table(id1 int, id2 int, val varchar, primary key(id1, id2))";
+
+        SqlNode node = parse(query);
+
+        assertThat(node, instanceOf(IgniteSqlCreateTable.class));
+
+        IgniteSqlCreateTable createTable = (IgniteSqlCreateTable)node;
+
+        assertThat(createTable.name().names, is(ImmutableList.of("MY_TABLE")));
+        assertThat(createTable.ifNotExists, is(false));
+        assertThat(createTable.columnList(), hasItem(ofTypeMatching(
+            "PK constraint with two columns", SqlKeyConstraint.class,
+            constraint -> hasItem(ofTypeMatching("identifier \"ID1\"", SqlIdentifier.class, id -> "ID1".equals(id.names.get(0))))
+                .matches(constraint.getOperandList().get(1))
+                && hasItem(ofTypeMatching("identifier \"ID2\"", SqlIdentifier.class, id -> "ID2".equals(id.names.get(0))))
+                .matches(constraint.getOperandList().get(1))
+                && constraint.getOperandList().get(0) == null
+                && constraint.isA(singleton(SqlKind.PRIMARY_KEY)))));
+    }
+
+    /**
+     * Parsing of CREATE TABLE with specified table options.
+     */
+    @Test
+    public void createTableWithOptions() throws SqlParseException {
+        String query = "create table my_table(id int) with" +
+            " template=\"my_template\"," +
+            " backups=2," +
+            " affinity_key=my_aff," +
+            " atomicity=atomic," +
+            " write_synchronization_mode=transactional," +
+            " cache_group=my_cache_group," +
+            " cache_name=my_cache_name," +
+            " data_region=my_data_region," +
+            " key_type=my_key_type," +
+            " value_type=my_value_type," +
+            " encrypted=true";
+
+        SqlNode node = parse(query);
+
+        assertThat(node, instanceOf(IgniteSqlCreateTable.class));
+
+        IgniteSqlCreateTable createTable = (IgniteSqlCreateTable)node;
+
+        assertThatStringOptionPresent(createTable.createOptionList().getList(), "TEMPLATE", "my_template");
+        assertThatIntegerOptionPresent(createTable.createOptionList().getList(), "BACKUPS", 2);
+        assertThatStringOptionPresent(createTable.createOptionList().getList(), "AFFINITY_KEY", "MY_AFF");
+        assertThatStringOptionPresent(createTable.createOptionList().getList(), "ATOMICITY", "ATOMIC");
+        assertThatStringOptionPresent(createTable.createOptionList().getList(), "WRITE_SYNCHRONIZATION_MODE", "TRANSACTIONAL");
+        assertThatStringOptionPresent(createTable.createOptionList().getList(), "CACHE_GROUP", "MY_CACHE_GROUP");
+        assertThatStringOptionPresent(createTable.createOptionList().getList(), "CACHE_NAME", "MY_CACHE_NAME");
+        assertThatStringOptionPresent(createTable.createOptionList().getList(), "DATA_REGION", "MY_DATA_REGION");
+        assertThatStringOptionPresent(createTable.createOptionList().getList(), "KEY_TYPE", "MY_KEY_TYPE");
+        assertThatStringOptionPresent(createTable.createOptionList().getList(), "VALUE_TYPE", "MY_VALUE_TYPE");
+        assertThatBooleanOptionPresent(createTable.createOptionList().getList(), "ENCRYPTED", true);
+    }
+
+    /**
+     * Parses a given statement and returns a resulting AST.
+     *
+     * @param stmt Statement to parse.
+     * @return An AST.
+     */
+    private static SqlNode parse(String stmt) throws SqlParseException {
+        SqlParser parser = SqlParser.create(stmt, SqlParser.config().withParserFactory(IgniteSqlParserImpl.FACTORY));
+
+        return parser.parseStmt();
+    }
+
+    /**
+     * Shortcut to verify that there is an option with a particular string value.
+     *
+     * @param optionList Option list from parsed AST.
+     * @param option An option key of interest.
+     * @param expVal Expected value.
+     */
+    private static void assertThatStringOptionPresent(List<SqlNode> optionList, String option, String expVal) {
+        assertThat(optionList, hasItem(ofTypeMatching(
+            "option " + option + "=" + expVal, IgniteSqlCreateTableOption.class,
+            opt -> opt.key().name().equals(option) && opt.value() instanceof SqlIdentifier
+                && Objects.equals(expVal, ((SqlIdentifier)opt.value()).names.get(0)))));
+    }
+
+    /**
+     * Shortcut to verify that there is an option with a particular boolean value.
+     *
+     * @param optionList Option list from parsed AST.
+     * @param option An option key of interest.
+     * @param expVal Expected value.
+     */
+    private static void assertThatBooleanOptionPresent(List<SqlNode> optionList, String option, boolean expVal) {
+        assertThat(optionList, hasItem(ofTypeMatching(
+            "option" + option + "=" + expVal, IgniteSqlCreateTableOption.class,
+            opt -> opt.key().name().equals(option) && opt.value() instanceof SqlLiteral
+                && Objects.equals(expVal, ((SqlLiteral)opt.value()).booleanValue()))));
+    }
+
+    /**
+     * Shortcut to verify that there is an option with a particular integer value.
+     *
+     * @param optionList Option list from parsed AST.
+     * @param option An option key of interest.
+     * @param expVal Expected value.
+     */
+    private static void assertThatIntegerOptionPresent(List<SqlNode> optionList, String option, int expVal) {
+        assertThat(optionList, hasItem(ofTypeMatching(
+            "option" + option + "=" + expVal, IgniteSqlCreateTableOption.class,
+            opt -> opt.key().name().equals(option) && opt.value() instanceof SqlNumericLiteral
+                && ((SqlNumericLiteral)opt.value()).isInteger()
+                && Objects.equals(expVal, ((SqlLiteral)opt.value()).intValue(true)))));
+    }
+
+    /**
+     * Matcher to verify name in the column declaration.
+     *
+     * @param name Expected name.
+     * @return {@code true} in case name in the column declaration equals to the expected one.
+     */
+    private static <T extends SqlColumnDeclaration> Matcher<T> columnWithName(String name) {
+        return new CustomMatcher<T>("column with name=" + name) {
+            @Override public boolean matches(Object item) {
+                return item instanceof SqlColumnDeclaration
+                    && ((SqlColumnDeclaration)item).name.names.get(0).equals(name);
+            }
+        };
+    }
+
+    /**
+     * Matcher to verify that an object of the expected type and matches the given predicat.
+     *
+     * @param desc Description for this matcher.
+     * @param cls Expected class to verify the object is instance of.
+     * @param pred Addition check that would be applied to the object.
+     * @return {@code true} in case the object if instance of the given class and matches the predicat.
+     */
+    private static <T> Matcher<T> ofTypeMatching(String desc, Class<T> cls, Predicate<T> pred) {
+        return new CustomMatcher<T>(desc) {
+            @Override public boolean matches(Object item) {
+                return item != null && cls.isAssignableFrom(item.getClass()) && pred.test((T)item);
+            }
+        };
+    }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IgniteCalciteTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IgniteCalciteTestSuite.java
index 9c656ca..498178a 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IgniteCalciteTestSuite.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IgniteCalciteTestSuite.java
@@ -22,6 +22,7 @@ import org.apache.ignite.internal.processors.query.calcite.CalciteBasicSecondary
 import org.apache.ignite.internal.processors.query.calcite.CalciteErrorHandlilngIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.CalciteQueryProcessorTest;
 import org.apache.ignite.internal.processors.query.calcite.CancelTest;
+import org.apache.ignite.internal.processors.query.calcite.CreateTableIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.DateTimeTest;
 import org.apache.ignite.internal.processors.query.calcite.LimitOffsetTest;
 import org.apache.ignite.internal.processors.query.calcite.MetadataIntegrationTest;
@@ -33,6 +34,7 @@ import org.apache.ignite.internal.processors.query.calcite.exec.rel.ContinuousEx
 import org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcQueryTest;
 import org.apache.ignite.internal.processors.query.calcite.rules.OrToUnionRuleTest;
 import org.apache.ignite.internal.processors.query.calcite.rules.ProjectScanMergeRuleTest;
+import org.apache.ignite.internal.processors.query.calcite.sql.SqlDdlParserTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 
@@ -59,6 +61,9 @@ import org.junit.runners.Suite;
     AggregatesIntegrationTest.class,
     MetadataIntegrationTest.class,
     SortAggregateIntegrationTest.class,
+
+    SqlDdlParserTest.class,
+    CreateTableIntegrationTest.class,
 })
 public class IgniteCalciteTestSuite {
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/IgniteQueryErrorCode.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/IgniteQueryErrorCode.java
index 705be74..9a98419 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/IgniteQueryErrorCode.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/IgniteQueryErrorCode.java
@@ -83,6 +83,9 @@ public final class IgniteQueryErrorCode {
     /** Query canceled. */
     public static final int QUERY_CANCELED = 3014;
 
+    /** Required schema not found. */
+    public static final int SCHEMA_NOT_FOUND = 3015;
+
     /* 4xxx - cache related runtime errors */
 
     /** Attempt to INSERT a key that is already in cache. */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryTypeDescriptor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryTypeDescriptor.java
index 722cf31..b112ed2 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryTypeDescriptor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryTypeDescriptor.java
@@ -216,4 +216,16 @@ public interface GridQueryTypeDescriptor {
      * @param keys Primary keys.
      */
     public void primaryKeyFields(Set<String> keys);
+
+    /**
+     * Gets whether a primary key should be autogenerated.
+     */
+    public boolean implicitPk();
+
+    /**
+     * Sets whether a primary key should be autogenerated.
+     *
+     * @param implicitPk Whether a PK should be autogenerated.
+     */
+    public void implicitPk(boolean implicitPk);
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityEx.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityEx.java
index 655cf52..c43bc3a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityEx.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityEx.java
@@ -34,6 +34,9 @@ public class QueryEntityEx extends QueryEntity {
     /** Fields that must have non-null value. */
     private Set<String> notNullFields;
 
+    /** Whether a primary key should be autocreated or not. */
+    private boolean implicitPk;
+
     /**
      * Default constructor.
      */
@@ -53,6 +56,7 @@ public class QueryEntityEx extends QueryEntity {
             QueryEntityEx other0 = (QueryEntityEx)other;
 
             notNullFields = other0.notNullFields != null ? new HashSet<>(other0.notNullFields) : null;
+            implicitPk = other0.implicitPk;
         }
     }
 
@@ -68,6 +72,18 @@ public class QueryEntityEx extends QueryEntity {
         return this;
     }
 
+    /** */
+    public boolean implicitPk() {
+        return implicitPk;
+    }
+
+    /** */
+    public QueryEntity implicitPk(boolean implicitPk) {
+        this.implicitPk = implicitPk;
+
+        return this;
+    }
+
     /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (this == o)
@@ -78,7 +94,8 @@ public class QueryEntityEx extends QueryEntity {
 
         QueryEntityEx entity = (QueryEntityEx)o;
 
-        return super.equals(entity) && F.eq(notNullFields, entity.notNullFields);
+        return super.equals(entity) && F.eq(notNullFields, entity.notNullFields)
+            && implicitPk == entity.implicitPk;
     }
 
     /** {@inheritDoc} */
@@ -86,6 +103,7 @@ public class QueryEntityEx extends QueryEntity {
         int res = super.hashCode();
 
         res = 31 * res + (notNullFields != null ? notNullFields.hashCode() : 0);
+        res = 31 * res + (implicitPk ? 1 : 0);
 
         return res;
     }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java
index 9a0c260..305b91a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java
@@ -135,6 +135,9 @@ public class QueryTypeDescriptorImpl implements GridQueryTypeDescriptor {
     /** Primary key fields. */
     private Set<String> pkFields;
 
+    /** */
+    private boolean implicitPk;
+
     /**
      * Constructor.
      *
@@ -746,4 +749,14 @@ public class QueryTypeDescriptorImpl implements GridQueryTypeDescriptor {
     @Override public void primaryKeyFields(Set<String> keys) {
         pkFields = keys;
     }
+
+    /** {@inheritDoc} */
+    @Override public boolean implicitPk() {
+        return implicitPk;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void implicitPk(boolean implicitPk) {
+        this.implicitPk = implicitPk;
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryUtils.java
index c561a5e..30da8b5 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryUtils.java
@@ -38,6 +38,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
+
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteSystemProperties;
@@ -633,6 +634,9 @@ public class QueryUtils {
         if (!isKeyClsSqlType)
             d.primaryKeyFields(keyFields);
 
+        if (qryEntity instanceof QueryEntityEx)
+            d.implicitPk(((QueryEntityEx)qryEntity).implicitPk());
+
         // Sql-typed key/value doesn't have field property, but they may have precision and scale constraints.
         // Also if fields are not set then _KEY and _VAL will be created as visible,
         // so we have to add binary properties for them
@@ -1221,7 +1225,7 @@ public class QueryUtils {
      */
     public static SchemaOperationException checkQueryEntityConflicts(CacheConfiguration<?, ?> ccfg,
         Collection<DynamicCacheDescriptor> descs) {
-        String schema = QueryUtils.normalizeSchemaName(ccfg.getName(), ccfg.getSqlSchema());
+        String schema = normalizeSchemaName(ccfg.getName(), ccfg.getSqlSchema());
 
         Set<String> idxNames = new HashSet<>();
 
@@ -1231,7 +1235,7 @@ public class QueryUtils {
             if (F.eq(ccfg.getName(), desc.cacheName()))
                 continue;
 
-            String descSchema = QueryUtils.normalizeSchemaName(desc.cacheName(),
+            String descSchema = normalizeSchemaName(desc.cacheName(),
                 desc.cacheConfiguration().getSqlSchema());
 
             if (!F.eq(schema, descSchema))
@@ -1583,6 +1587,66 @@ public class QueryUtils {
     }
 
     /**
+     * @return {@link IgniteSQLException} with the message same as of {@code this}'s and
+     */
+    public static IgniteSQLException convert(SchemaOperationException e) {
+        int sqlCode;
+
+        switch (e.code()) {
+            case SchemaOperationException.CODE_CACHE_NOT_FOUND:
+                sqlCode = IgniteQueryErrorCode.CACHE_NOT_FOUND;
+
+                break;
+
+            case SchemaOperationException.CODE_TABLE_NOT_FOUND:
+                sqlCode = IgniteQueryErrorCode.TABLE_NOT_FOUND;
+
+                break;
+
+            case SchemaOperationException.CODE_TABLE_EXISTS:
+                sqlCode = IgniteQueryErrorCode.TABLE_ALREADY_EXISTS;
+
+                break;
+
+            case SchemaOperationException.CODE_COLUMN_NOT_FOUND:
+                sqlCode = IgniteQueryErrorCode.COLUMN_NOT_FOUND;
+
+                break;
+
+            case SchemaOperationException.CODE_COLUMN_EXISTS:
+                sqlCode = IgniteQueryErrorCode.COLUMN_ALREADY_EXISTS;
+
+                break;
+
+            case SchemaOperationException.CODE_INDEX_NOT_FOUND:
+                sqlCode = IgniteQueryErrorCode.INDEX_NOT_FOUND;
+
+                break;
+
+            case SchemaOperationException.CODE_INDEX_EXISTS:
+                sqlCode = IgniteQueryErrorCode.INDEX_ALREADY_EXISTS;
+
+                break;
+
+            default:
+                sqlCode = IgniteQueryErrorCode.UNKNOWN;
+        }
+
+        return new IgniteSQLException(e.getMessage(), sqlCode, e);
+    }
+
+    /**
+     * Check if schema supports DDL statement.
+     *
+     * @param schemaName Schema name.
+     */
+    public static void isDdlOnSchemaSupported(String schemaName) {
+        if (F.eq(SCHEMA_SYS, schemaName))
+            throw new IgniteSQLException("DDL statements are not supported on " + schemaName + " schema",
+                IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
+    }
+
+    /**
      * Remove field by alias.
      *
      * @param entity Query entity.
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java b/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java
index 0b19056..d7e528b 100644
--- a/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/GridTestUtils.java
@@ -119,6 +119,8 @@ import org.apache.ignite.spi.discovery.DiscoverySpiListener;
 import org.apache.ignite.ssl.SslContextFactory;
 import org.apache.ignite.testframework.config.GridTestProperties;
 import org.apache.ignite.testframework.junits.GridAbstractTest;
+import org.hamcrest.CustomMatcher;
+import org.hamcrest.Matcher;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -2525,4 +2527,18 @@ public final class GridTestUtils {
     public static void suppressException(RunnableX runnableX) {
         runnableX.run();
     }
+
+    /**
+     * Matcher to verify size of the collection.
+     *
+     * @param size Required size.
+     * @return {@code true} in case collection is not null and has an exactly the same size.
+     */
+    public static <T extends Collection<?>> Matcher<T> hasSize(int size) {
+        return new CustomMatcher<T>("should be non empty with size=" + size) {
+            @Override public boolean matches(Object item) {
+                return item instanceof Collection && ((Collection<?>)item).size() == size;
+            }
+        };
+    }
 }
diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java
index fb314e6..a577530 100644
--- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java
+++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java
@@ -32,6 +32,7 @@ import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
+
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteCluster;
 import org.apache.ignite.IgniteDataStreamer;
@@ -135,6 +136,8 @@ import org.jetbrains.annotations.Nullable;
 import static org.apache.ignite.internal.processors.cache.mvcc.MvccUtils.mvccEnabled;
 import static org.apache.ignite.internal.processors.cache.mvcc.MvccUtils.tx;
 import static org.apache.ignite.internal.processors.cache.mvcc.MvccUtils.txStart;
+import static org.apache.ignite.internal.processors.query.QueryUtils.convert;
+import static org.apache.ignite.internal.processors.query.QueryUtils.isDdlOnSchemaSupported;
 import static org.apache.ignite.internal.processors.query.h2.sql.GridSqlQueryParser.PARAM_WRAP_VALUE;
 
 /**
@@ -966,17 +969,6 @@ public class CommandProcessor {
     }
 
     /**
-     * Check if schema supports DDL statement.
-     *
-     * @param schemaName Schema name.
-     */
-    private static void isDdlOnSchemaSupported(String schemaName) {
-        if (F.eq(QueryUtils.SCHEMA_SYS, schemaName))
-            throw new IgniteSQLException("DDL statements are not supported on " + schemaName + " schema",
-                IgniteQueryErrorCode.UNSUPPORTED_OPERATION);
-    }
-
-    /**
      * Check if table supports DDL statement.
      *
      * @param tbl Table.
@@ -1006,55 +998,6 @@ public class CommandProcessor {
     }
 
     /**
-     * @return {@link IgniteSQLException} with the message same as of {@code this}'s and
-     */
-    private IgniteSQLException convert(SchemaOperationException e) {
-        int sqlCode;
-
-        switch (e.code()) {
-            case SchemaOperationException.CODE_CACHE_NOT_FOUND:
-                sqlCode = IgniteQueryErrorCode.CACHE_NOT_FOUND;
-
-                break;
-
-            case SchemaOperationException.CODE_TABLE_NOT_FOUND:
-                sqlCode = IgniteQueryErrorCode.TABLE_NOT_FOUND;
-
-                break;
-
-            case SchemaOperationException.CODE_TABLE_EXISTS:
-                sqlCode = IgniteQueryErrorCode.TABLE_ALREADY_EXISTS;
-
-                break;
-
-            case SchemaOperationException.CODE_COLUMN_NOT_FOUND:
-                sqlCode = IgniteQueryErrorCode.COLUMN_NOT_FOUND;
-
-                break;
-
-            case SchemaOperationException.CODE_COLUMN_EXISTS:
-                sqlCode = IgniteQueryErrorCode.COLUMN_ALREADY_EXISTS;
-
-                break;
-
-            case SchemaOperationException.CODE_INDEX_NOT_FOUND:
-                sqlCode = IgniteQueryErrorCode.INDEX_NOT_FOUND;
-
-                break;
-
-            case SchemaOperationException.CODE_INDEX_EXISTS:
-                sqlCode = IgniteQueryErrorCode.INDEX_ALREADY_EXISTS;
-
-                break;
-
-            default:
-                sqlCode = IgniteQueryErrorCode.UNKNOWN;
-        }
-
-        return new IgniteSQLException(e.getMessage(), sqlCode, e);
-    }
-
-    /**
      * Convert this statement to query entity and do Ignite specific sanity checks on the way.
      * @return Query entity mimicking this SQL statement.
      */