IGNITE-15188 JDBC driver for 3.0: Database metadata (#339)

diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/JdbcQueryEventHandler.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/JdbcQueryEventHandler.java
index 5f90229..f2f1750 100644
--- a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/JdbcQueryEventHandler.java
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/JdbcQueryEventHandler.java
@@ -19,6 +19,14 @@
 
 import org.apache.ignite.client.proto.query.event.BatchExecuteRequest;
 import org.apache.ignite.client.proto.query.event.BatchExecuteResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaColumnsRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaColumnsResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaPrimaryKeysRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaPrimaryKeysResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaSchemasRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaSchemasResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaTablesRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaTablesResult;
 import org.apache.ignite.client.proto.query.event.QueryCloseRequest;
 import org.apache.ignite.client.proto.query.event.QueryCloseResult;
 import org.apache.ignite.client.proto.query.event.QueryExecuteRequest;
@@ -61,4 +69,36 @@
      * @return Result.
      */
     QueryCloseResult close(QueryCloseRequest req);
+
+    /**
+     * {@link JdbcMetaTablesRequest} command handler.
+     *
+     * @param req Jdbc tables metadata request.
+     * @return Result.
+     */
+    JdbcMetaTablesResult tablesMeta(JdbcMetaTablesRequest req);
+
+    /**
+     * {@link JdbcMetaColumnsRequest} command handler.
+     *
+     * @param req Jdbc columns metadata request.
+     * @return Result.
+     */
+    JdbcMetaColumnsResult columnsMeta(JdbcMetaColumnsRequest req);
+
+    /**
+     * {@link JdbcMetaSchemasRequest} command handler.
+     *
+     * @param req Jdbc schemas metadata request.
+     * @return Result.
+     */
+    JdbcMetaSchemasResult schemasMeta(JdbcMetaSchemasRequest req);
+
+    /**
+     * {@link JdbcMetaPrimaryKeysRequest} command handler.
+     *
+     * @param req Jdbc primary keys metadata request.
+     * @return Result.
+     */
+    JdbcMetaPrimaryKeysResult primaryKeysMeta(JdbcMetaPrimaryKeysRequest req);
 }
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/ClientMessageUtils.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/ClientMessageUtils.java
new file mode 100644
index 0000000..d836f25
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/ClientMessageUtils.java
@@ -0,0 +1,52 @@
+/*
+ * 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.client.proto.query.event;
+
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+
+/**
+ * Client message utils class.
+ */
+public class ClientMessageUtils {
+    /**
+     * Packs a string or null if string is null.
+     *
+     * @param packer Message packer.
+     * @param str String to serialize.
+     */
+    public static void writeStringNullable(ClientMessagePacker packer, String str) {
+        if (str == null)
+            packer.packNil();
+        else
+            packer.packString(str);
+    }
+
+    /**
+     * Unpack a string or null if no string is presented.
+     *
+     * @param unpacker Message unpacker.
+     * @return String or null.
+     */
+    public static String readStringNullable(ClientMessageUnpacker unpacker) {
+        if (!unpacker.tryUnpackNil())
+            return unpacker.unpackString();
+
+        return null;
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcColumnMeta.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcColumnMeta.java
new file mode 100644
index 0000000..54ef323
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcColumnMeta.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.client.proto.query.event;
+
+import java.math.BigDecimal;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.Date;
+import java.util.Objects;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.BINARY;
+import static java.sql.Types.BOOLEAN;
+import static java.sql.Types.DATE;
+import static java.sql.Types.DECIMAL;
+import static java.sql.Types.DOUBLE;
+import static java.sql.Types.FLOAT;
+import static java.sql.Types.INTEGER;
+import static java.sql.Types.OTHER;
+import static java.sql.Types.SMALLINT;
+import static java.sql.Types.TIME;
+import static java.sql.Types.TIMESTAMP;
+import static java.sql.Types.TINYINT;
+import static java.sql.Types.VARCHAR;
+
+/**
+ * JDBC column metadata.
+ */
+public class JdbcColumnMeta extends Response {
+    /** Nullable. */
+    private boolean nullable;
+
+    /** Schema name. */
+    private String schemaName;
+
+    /** Table name. */
+    private String tblName;
+
+    /** Column name. */
+    private String colName;
+
+    /** Data type. */
+    private int dataType;
+
+    /** Data type name. */
+    private String dataTypeName;
+
+    /** Precision. */
+    private int precision;
+
+    /** Scale. */
+    private int scale;
+
+    /**
+     * Default constructor is used for serialization.
+     */
+    public JdbcColumnMeta() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param schemaName Schema.
+     * @param tblName Table.
+     * @param colName Column.
+     * @param cls Type.
+     */
+    public JdbcColumnMeta(String schemaName, String tblName, String colName, Class<?> cls) {
+        this(schemaName, tblName, colName, cls, -1, -1, true);
+    }
+
+    /**
+     * Constructor with nullable flag.
+     *
+     * @param schemaName Schema.
+     * @param tblName Table.
+     * @param colName Column.
+     * @param cls Type.
+     * @param nullable Nullable flag.
+     * @param precision Column precision.
+     * @param scale Column scale.
+     */
+    public JdbcColumnMeta(String schemaName, String tblName, String colName, Class<?> cls, int precision, int scale,
+        boolean nullable) {
+        this.schemaName = schemaName;
+        this.tblName = tblName;
+        this.colName = colName;
+        this.nullable = nullable;
+
+        this.dataType = type(cls.getName());
+        this.dataTypeName = typeName(cls.getName());
+        this.precision = precision;
+        this.scale = scale;
+
+        hasResults = true;
+    }
+
+    /**
+     * Gets schema name.
+     *
+     * @return Schema name.
+     */
+    public String schemaName() {
+        return schemaName;
+    }
+
+    /**
+     * Gets table name.
+     *
+     * @return Table name.
+     */
+    public String tableName() {
+        return tblName;
+    }
+
+    /**
+     * Gets column name.
+     *
+     * @return Column name.
+     */
+    public String columnName() {
+        return colName;
+    }
+
+    /**
+     * Gets data type id.
+     *
+     * @return Column's data type.
+     */
+    public int dataType() {
+        return dataType;
+    }
+
+    /**
+     * Gets data type name.
+     *
+     * @return Column's data type name.
+     */
+    public String dataTypeName() {
+        return dataTypeName;
+    }
+
+    /**
+     * Gets default value.
+     *
+     * @return Column's default value.
+     */
+    public String defaultValue() {
+        return null;
+    }
+
+    /**
+     * Gets column precision.
+     *
+     * @return Column's precision.
+     */
+    public int precision() {
+        return precision;
+    }
+
+    /**
+     * Gets column scale.
+     *
+     * @return Column's scale.
+     */
+    public int scale() {
+        return scale;
+    }
+
+    /**
+     * Gets nullable flag.
+     *
+     * @return {@code true} in case the column allows null values. Otherwise returns {@code false}
+     */
+    public boolean isNullable() {
+        return nullable;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        super.writeBinary(packer);
+
+        if (!hasResults)
+            return;
+
+        packer.packString(schemaName);
+        packer.packString(tblName);
+        packer.packString(colName);
+
+        packer.packInt(dataType);
+        packer.packString(dataTypeName);
+        packer.packBoolean(nullable);
+        packer.packInt(precision);
+        packer.packInt(scale);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        super.readBinary(unpacker);
+
+        if (!hasResults)
+            return;
+
+        schemaName = unpacker.unpackString();
+        tblName = unpacker.unpackString();
+        colName = unpacker.unpackString();
+
+        dataType = unpacker.unpackInt();
+        dataTypeName = unpacker.unpackString();
+        nullable = unpacker.unpackBoolean();
+        precision = unpacker.unpackInt();
+        scale = unpacker.unpackInt();
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        JdbcColumnMeta meta = (JdbcColumnMeta)o;
+        return nullable == meta.nullable
+            && dataType == meta.dataType
+            && precision == meta.precision
+            && scale == meta.scale
+            && Objects.equals(schemaName, meta.schemaName)
+            && Objects.equals(tblName, meta.tblName)
+            && Objects.equals(colName, meta.colName)
+            && Objects.equals(dataTypeName, meta.dataTypeName);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int result = (nullable ? 1 : 0);
+        result = 31 * result + (schemaName != null ? schemaName.hashCode() : 0);
+        result = 31 * result + (tblName != null ? tblName.hashCode() : 0);
+        result = 31 * result + (colName != null ? colName.hashCode() : 0);
+        result = 31 * result + dataType;
+        result = 31 * result + (dataTypeName != null ? dataTypeName.hashCode() : 0);
+        result = 31 * result + precision;
+        result = 31 * result + scale;
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcColumnMeta.class, this);
+    }
+
+    /**
+     * Converts Java class name to type from {@link Types}.
+     *
+     * @param cls Java class name.
+     * @return Type from {@link Types}.
+     */
+    private static int type(String cls) {
+        if (Boolean.class.getName().equals(cls) || boolean.class.getName().equals(cls))
+            return BOOLEAN;
+        else if (Byte.class.getName().equals(cls) || byte.class.getName().equals(cls))
+            return TINYINT;
+        else if (Short.class.getName().equals(cls) || short.class.getName().equals(cls))
+            return SMALLINT;
+        else if (Integer.class.getName().equals(cls) || int.class.getName().equals(cls))
+            return INTEGER;
+        else if (Long.class.getName().equals(cls) || long.class.getName().equals(cls))
+            return BIGINT;
+        else if (Float.class.getName().equals(cls) || float.class.getName().equals(cls))
+            return FLOAT;
+        else if (Double.class.getName().equals(cls) || double.class.getName().equals(cls))
+            return DOUBLE;
+        else if (String.class.getName().equals(cls))
+            return VARCHAR;
+        else if (byte[].class.getName().equals(cls))
+            return BINARY;
+        else if (Time.class.getName().equals(cls))
+            return TIME;
+        else if (Timestamp.class.getName().equals(cls))
+            return TIMESTAMP;
+        else if (Date.class.getName().equals(cls) || java.sql.Date.class.getName().equals(cls))
+            return DATE;
+        else if (BigDecimal.class.getName().equals(cls))
+            return DECIMAL;
+        else
+            return OTHER;
+    }
+
+    /**
+     * Converts Java class name to SQL type name.
+     *
+     * @param cls Java class name.
+     * @return SQL type name.
+     */
+    private static String typeName(String cls) {
+        if (Boolean.class.getName().equals(cls) || boolean.class.getName().equals(cls))
+            return "BOOLEAN";
+        else if (Byte.class.getName().equals(cls) || byte.class.getName().equals(cls))
+            return "TINYINT";
+        else if (Short.class.getName().equals(cls) || short.class.getName().equals(cls))
+            return "SMALLINT";
+        else if (Integer.class.getName().equals(cls) || int.class.getName().equals(cls))
+            return "INTEGER";
+        else if (Long.class.getName().equals(cls) || long.class.getName().equals(cls))
+            return "BIGINT";
+        else if (Float.class.getName().equals(cls) || float.class.getName().equals(cls))
+            return "FLOAT";
+        else if (Double.class.getName().equals(cls) || double.class.getName().equals(cls))
+            return "DOUBLE";
+        else if (String.class.getName().equals(cls))
+            return "VARCHAR";
+        else if (byte[].class.getName().equals(cls))
+            return "BINARY";
+        else if (Time.class.getName().equals(cls))
+            return "TIME";
+        else if (Timestamp.class.getName().equals(cls))
+            return "TIMESTAMP";
+        else if (Date.class.getName().equals(cls) || java.sql.Date.class.getName().equals(cls))
+            return "DATE";
+        else if (BigDecimal.class.getName().equals(cls))
+            return "DECIMAL";
+        else
+            return "OTHER";
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaColumnsRequest.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaColumnsRequest.java
new file mode 100644
index 0000000..c87f7a6
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaColumnsRequest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.client.proto.query.event;
+
+import org.apache.ignite.client.proto.query.ClientMessage;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * JDBC get columns metadata request.
+ */
+public class JdbcMetaColumnsRequest implements ClientMessage {
+    /** Schema name pattern. */
+    private String schemaName;
+
+    /** Table name pattern. */
+    private String tblName;
+
+    /** Column name pattern. */
+    private String colName;
+
+    /**
+     * Default constructor is used for deserialization.
+     */
+    public JdbcMetaColumnsRequest() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param schemaName Schema name.
+     * @param tblName Table name.
+     * @param colName Column name.
+     */
+    public JdbcMetaColumnsRequest(String schemaName, String tblName, String colName) {
+        this.schemaName = schemaName;
+        this.tblName = tblName;
+        this.colName = colName;
+    }
+
+    /**
+     * Gets schema name sql pattern.
+     *
+     * @return Schema name pattern.
+     */
+    public String schemaName() {
+        return schemaName;
+    }
+
+    /**
+     * Gets table name sql pattern.
+     *
+     * @return Table name pattern.
+     */
+    public String tableName() {
+        return tblName;
+    }
+
+    /**
+     * Gets column name sql pattern.
+     *
+     * @return Column name pattern.
+     */
+    public String columnName() {
+        return colName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        packer.packString(schemaName);
+        packer.packString(tblName);
+        packer.packString(colName);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        schemaName = unpacker.unpackString();
+        tblName = unpacker.unpackString();
+        colName = unpacker.unpackString();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcMetaColumnsRequest.class, this);
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaColumnsResult.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaColumnsResult.java
new file mode 100644
index 0000000..fed2195
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaColumnsResult.java
@@ -0,0 +1,108 @@
+/*
+ * 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.client.proto.query.event;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * JDBC columns metadata result.
+ */
+public class JdbcMetaColumnsResult extends Response {
+    /** Columns metadata. */
+    private List<JdbcColumnMeta> meta;
+
+    /**
+     * Default constructor is used for deserialization.
+     */
+    public JdbcMetaColumnsResult() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param meta Columns metadata.
+     */
+    public JdbcMetaColumnsResult(Collection<JdbcColumnMeta> meta) {
+        Objects.requireNonNull(meta);
+
+        this.meta = new ArrayList<>(meta);
+
+        this.hasResults = true;
+    }
+
+    /**
+     * Gets column metadata.
+     *
+     * @return Columns metadata.
+     */
+    public List<JdbcColumnMeta> meta() {
+        return meta;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        super.writeBinary(packer);
+
+        if (!hasResults)
+            return;
+
+        packer.packArrayHeader(meta.size());
+
+        for (JdbcColumnMeta m : meta)
+            m.writeBinary(packer);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        super.readBinary(unpacker);
+
+        if (!hasResults)
+            return;
+
+        int size = unpacker.unpackArrayHeader();
+
+        if (size == 0) {
+            meta = Collections.emptyList();
+
+            return;
+        }
+
+        meta = new ArrayList<>(size);
+
+        for (int i = 0; i < size; ++i) {
+            var m = new JdbcColumnMeta();
+
+            m.readBinary(unpacker);
+
+            meta.add(m);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcMetaColumnsResult.class, this);
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaPrimaryKeysRequest.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaPrimaryKeysRequest.java
new file mode 100644
index 0000000..3535c96
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaPrimaryKeysRequest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.client.proto.query.event;
+
+import org.apache.ignite.client.proto.query.ClientMessage;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * JDBC get primary keys metadata request.
+ */
+public class JdbcMetaPrimaryKeysRequest implements ClientMessage {
+    /** Schema name pattern. */
+    private String schemaName;
+
+    /** Table name pattern. */
+    private String tblName;
+
+    /**
+     * Default constructor is used for deserialization.
+     */
+    public JdbcMetaPrimaryKeysRequest() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param schemaName Schema name.
+     * @param tblName Table name.
+     */
+    public JdbcMetaPrimaryKeysRequest(String schemaName, String tblName) {
+        this.schemaName = schemaName;
+        this.tblName = tblName;
+    }
+
+    /**
+     * Gets schema name sql pattern.
+     *
+     * @return Schema name pattern.
+     */
+    public String schemaName() {
+        return schemaName;
+    }
+
+    /**
+     * Gets table name sql pattern.
+     *
+     * @return Table name pattern.
+     */
+    public String tableName() {
+        return tblName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        schemaName = ClientMessageUtils.readStringNullable(unpacker);
+        tblName = ClientMessageUtils.readStringNullable(unpacker);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        ClientMessageUtils.writeStringNullable(packer, schemaName);
+        ClientMessageUtils.writeStringNullable(packer, tblName);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcMetaPrimaryKeysRequest.class, this);
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaPrimaryKeysResult.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaPrimaryKeysResult.java
new file mode 100644
index 0000000..15423b4
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaPrimaryKeysResult.java
@@ -0,0 +1,114 @@
+/*
+ * 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.client.proto.query.event;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * JDBC primary keys metadata result.
+ */
+public class JdbcMetaPrimaryKeysResult extends Response {
+    /** Primary keys meta. */
+    private List<JdbcPrimaryKeyMeta> meta;
+
+    /**
+     * Default constructor is used for deserialization.
+     */
+    public JdbcMetaPrimaryKeysResult() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param meta Column metadata.
+     */
+    public JdbcMetaPrimaryKeysResult(Collection<JdbcPrimaryKeyMeta> meta) {
+        Objects.requireNonNull(meta);
+
+        this.meta = new ArrayList<>(meta);
+
+        this.hasResults = true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        super.writeBinary(packer);
+
+        if (!hasResults)
+            return;
+
+        if (meta == null || meta.isEmpty()) {
+            packer.packNil();
+
+            return;
+        }
+
+        packer.packArrayHeader(meta.size());
+
+        for (JdbcPrimaryKeyMeta keyMeta : meta) {
+            keyMeta.writeBinary(packer);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        super.readBinary(unpacker);
+
+        if (!hasResults)
+            return;
+
+        if (unpacker.tryUnpackNil()) {
+            meta = Collections.emptyList();
+
+            return;
+        }
+
+        int size = unpacker.unpackArrayHeader();
+
+        meta = new ArrayList<>(size);
+
+        for (int i = 0; i < size; ++i) {
+            JdbcPrimaryKeyMeta m = new JdbcPrimaryKeyMeta();
+
+            m.readBinary(unpacker);
+
+            meta.add(m);
+        }
+    }
+
+    /**
+     * Gets primary key metadata.
+     *
+     * @return Primary keys metadata.
+     */
+    public List<JdbcPrimaryKeyMeta> meta() {
+        return meta;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcMetaPrimaryKeysResult.class, this);
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaSchemasRequest.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaSchemasRequest.java
new file mode 100644
index 0000000..de83408
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaSchemasRequest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.client.proto.query.event;
+
+import org.apache.ignite.client.proto.query.ClientMessage;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * JDBC schemas request.
+ */
+public class JdbcMetaSchemasRequest implements ClientMessage {
+    /** Schema search pattern. */
+    private String schemaName;
+
+    /**
+     * Default constructor is used for deserialization.
+     */
+    public JdbcMetaSchemasRequest() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param schemaName Schema search pattern.
+     */
+    public JdbcMetaSchemasRequest(String schemaName) {
+        this.schemaName = schemaName;
+    }
+
+    /**
+     * Gets schema name sql pattern.
+     *
+     * @return Schema search pattern.
+     */
+    public String schemaName() {
+        return schemaName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        ClientMessageUtils.writeStringNullable(packer, schemaName);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        schemaName = ClientMessageUtils.readStringNullable(unpacker);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcMetaSchemasRequest.class, this);
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaSchemasResult.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaSchemasResult.java
new file mode 100644
index 0000000..c742462
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaSchemasResult.java
@@ -0,0 +1,94 @@
+/*
+ * 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.client.proto.query.event;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Objects;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * JDBC schemas result.
+ */
+public class JdbcMetaSchemasResult extends Response {
+    /** Found schemas. */
+    private Collection<String> schemas;
+
+    /**
+     * Default constructor is used for deserialization.
+     */
+    public JdbcMetaSchemasResult() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param schemas Found schemas.
+     */
+    public JdbcMetaSchemasResult(Collection<String> schemas) {
+        Objects.requireNonNull(schemas);
+
+        this.schemas = schemas;
+
+        this.hasResults = true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        super.writeBinary(packer);
+
+        if (!hasResults)
+            return;
+
+        packer.packArrayHeader(schemas.size());
+
+        for (String schema : schemas)
+            packer.packString(schema);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        super.readBinary(unpacker);
+
+        if (!hasResults)
+            return;
+
+        int size = unpacker.unpackArrayHeader();
+
+        schemas = new ArrayList<>(size);
+
+        for (int i = 0; i < size; i++)
+            schemas.add(unpacker.unpackString());
+    }
+
+    /**
+     * Gets found table schemas.
+     *
+     * @return Found schemas.
+     */
+    public Collection<String> schemas() {
+        return schemas;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcMetaSchemasResult.class, this);
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaTablesRequest.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaTablesRequest.java
new file mode 100644
index 0000000..4182954
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaTablesRequest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.client.proto.query.event;
+
+import org.apache.ignite.client.proto.query.ClientMessage;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * JDBC tables metadata request.
+ */
+public class JdbcMetaTablesRequest implements ClientMessage {
+    /** Schema search pattern. */
+    private String schemaName;
+
+    /** Table search pattern. */
+    private String tblName;
+
+    /** Table types. */
+    private String[] tblTypes;
+
+    /**
+     * Default constructor is used for deserialization.
+     */
+    public JdbcMetaTablesRequest() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param schemaName Schema search pattern.
+     * @param tblName Table search pattern.
+     * @param tblTypes Table types.
+     */
+    public JdbcMetaTablesRequest(String schemaName, String tblName, String[] tblTypes) {
+        this.schemaName = schemaName;
+        this.tblName = tblName;
+        this.tblTypes = tblTypes;
+    }
+
+    /**
+     * Gets schema name pattern.
+     *
+     * @return Schema search pattern.
+     */
+    public String schemaName() {
+        return schemaName;
+    }
+
+    /**
+     * Gets table name pattern.
+     *
+     * @return Table search pattern.
+     */
+    public String tableName() {
+        return tblName;
+    }
+
+    /**
+     * Gets allowed table types.
+     *
+     * @return Table types.
+     */
+    public String[] tableTypes() {
+        return tblTypes;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        ClientMessageUtils.writeStringNullable(packer, schemaName);
+        ClientMessageUtils.writeStringNullable(packer, tblName);
+
+        if (tblTypes == null) {
+            packer.packNil();
+
+            return;
+        }
+
+        packer.packArrayHeader(tblTypes.length);
+
+        for (String type : tblTypes)
+            packer.packString(type);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        schemaName = ClientMessageUtils.readStringNullable(unpacker);
+        tblName = ClientMessageUtils.readStringNullable(unpacker);
+
+        if (unpacker.tryUnpackNil())
+            return;
+
+        int size = unpacker.unpackArrayHeader();
+
+        tblTypes = new String[size];
+
+        for (int i = 0; i < size; i++)
+            tblTypes[i] = unpacker.unpackString();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcMetaTablesRequest.class, this);
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaTablesResult.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaTablesResult.java
new file mode 100644
index 0000000..2fa57ff
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcMetaTablesResult.java
@@ -0,0 +1,99 @@
+/*
+ * 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.client.proto.query.event;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * JDBC tables metadata result.
+ */
+public class JdbcMetaTablesResult extends Response {
+    /** Tables metadata. */
+    private List<JdbcTableMeta> meta;
+
+    /**
+     * Default constructor is used for deserialization.
+     */
+    public JdbcMetaTablesResult() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param meta Tables metadata.
+     */
+    public JdbcMetaTablesResult(List<JdbcTableMeta> meta) {
+        Objects.requireNonNull(meta);
+
+        this.meta = meta;
+
+        this.hasResults = true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        super.writeBinary(packer);
+
+        if (!hasResults)
+            return;
+
+        packer.packArrayHeader(meta.size());
+
+        for (JdbcTableMeta m : meta)
+            m.writeBinary(packer);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        super.readBinary(unpacker);
+
+        if (!hasResults)
+            return;
+
+        int size = unpacker.unpackArrayHeader();
+
+        meta = new ArrayList<>(size);
+
+        for (int i = 0; i < size; ++i) {
+            JdbcTableMeta m = new JdbcTableMeta();
+
+            m.readBinary(unpacker);
+
+            meta.add(m);
+        }
+    }
+
+    /**
+     * Gets table metadata.
+     *
+     * @return Tables metadata.
+     */
+    public List<JdbcTableMeta> meta() {
+        return meta;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcMetaTablesResult.class, this);
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcPrimaryKeyMeta.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcPrimaryKeyMeta.java
new file mode 100644
index 0000000..2c85c63
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcPrimaryKeyMeta.java
@@ -0,0 +1,167 @@
+/*
+ * 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.client.proto.query.event;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import org.apache.ignite.client.proto.query.ClientMessage;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * JDBC primary key metadata.
+ */
+public class JdbcPrimaryKeyMeta implements ClientMessage {
+    /** Schema name. */
+    private String schemaName;
+
+    /** Table name. */
+    private String tblName;
+
+    /** Primary key name. */
+    private String name;
+
+    /** Primary key fields. */
+    private List<String> fields;
+
+    /**
+     * Default constructor is used for binary serialization.
+     */
+    public JdbcPrimaryKeyMeta() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param schemaName Schema.
+     * @param tblName Table.
+     * @param name Primary key name.
+     * @param fields Primary key fields.
+     */
+    public JdbcPrimaryKeyMeta(String schemaName, String tblName, String name, List<String> fields) {
+        this.schemaName = schemaName;
+        this.tblName = tblName;
+        this.name = name;
+        this.fields = fields;
+    }
+
+    /**
+     * Gets schema name.
+     *
+     * @return Schema name.
+     */
+    public String schemaName() {
+        return schemaName;
+    }
+
+    /**
+     * Gets table name.
+     *
+     * @return Table name.
+     */
+    public String tableName() {
+        return tblName;
+    }
+
+    /**
+     * Gets primary key name.
+     *
+     * @return Primary key name.
+     */
+    public String name() {
+        return name;
+    }
+
+    /**
+     * Gets key field names.
+     *
+     * @return Key fields.
+     */
+    public List<String> fields() {
+        return fields;
+    }
+
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        packer.packString(schemaName);
+        packer.packString(tblName);
+        packer.packString(name);
+
+        if (fields == null || fields.isEmpty())
+            packer.packNil();
+
+        packer.packArrayHeader(fields.size());
+
+        for (String field : fields) {
+            packer.packString(field);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        schemaName = unpacker.unpackString();
+        tblName = unpacker.unpackString();
+        name = unpacker.unpackString();
+
+        if (unpacker.tryUnpackNil()) {
+            fields = Collections.emptyList();
+
+            return;
+        }
+
+        int size = unpacker.unpackArrayHeader();
+
+        fields = new ArrayList<>(size);
+
+        for (int i = 0; i < size; i++)
+            fields.add(unpacker.unpackString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        JdbcPrimaryKeyMeta meta = (JdbcPrimaryKeyMeta)o;
+
+        return Objects.equals(schemaName, meta.schemaName)
+            && Objects.equals(tblName, meta.tblName)
+            && Objects.equals(name, meta.name)
+            && Objects.equals(fields, meta.fields);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int result = schemaName.hashCode();
+        result = 31 * result + tblName.hashCode();
+        result = 31 * result + name.hashCode();
+        result = 31 * result + (fields != null ? fields.hashCode() : 0);
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcPrimaryKeyMeta.class, this);
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcTableMeta.java b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcTableMeta.java
new file mode 100644
index 0000000..06c7557
--- /dev/null
+++ b/modules/client-common/src/main/java/org/apache/ignite/client/proto/query/event/JdbcTableMeta.java
@@ -0,0 +1,138 @@
+/*
+ * 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.client.proto.query.event;
+
+import java.util.Objects;
+
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+import org.apache.ignite.internal.tostring.S;
+
+/**
+ * JDBC table metadata.
+ */
+public class JdbcTableMeta extends Response {
+    /** Schema name. */
+    private String schemaName;
+
+    /** Table name. */
+    private String tblName;
+
+    /** Table type. */
+    private String tblType;
+
+    /**
+     * Default constructor is used for deserialization.
+     */
+    public JdbcTableMeta() {
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param schemaName Schema name.
+     * @param tblName Table name.
+     * @param tblType Table type.
+     */
+    public JdbcTableMeta(String schemaName, String tblName, String tblType) {
+        this.schemaName = schemaName;
+        this.tblName = tblName;
+        this.tblType = tblType;
+
+        this.hasResults = true;
+    }
+
+    /**
+     * Gets schema name.
+     *
+     * @return Schema name.
+     */
+    public String schemaName() {
+        return schemaName;
+    }
+
+    /**
+     * Gets table name.
+     *
+     * @return Table name.
+     */
+    public String tableName() {
+        return tblName;
+    }
+
+    /**
+     * Gets table type.
+     *
+     * @return Table type.
+     */
+    public String tableType() {
+        return tblType;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(ClientMessagePacker packer) {
+        super.writeBinary(packer);
+
+        if (!hasResults)
+            return;
+
+        packer.packString(schemaName);
+        packer.packString(tblName);
+        packer.packString(tblType);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(ClientMessageUnpacker unpacker) {
+        super.readBinary(unpacker);
+
+        if (!hasResults)
+            return;
+
+        schemaName = unpacker.unpackString();
+        tblName = unpacker.unpackString();
+        tblType = unpacker.unpackString();
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        JdbcTableMeta meta = (JdbcTableMeta)o;
+
+        return Objects.equals(schemaName, meta.schemaName)
+            && Objects.equals(tblName, meta.tblName)
+            && Objects.equals(tblType, meta.tblType);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int result = schemaName.hashCode();
+        result = 31 * result + tblName.hashCode();
+        result = 31 * result + tblType.hashCode();
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(JdbcTableMeta.class, this);
+    }
+}
diff --git a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientOp.java b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientOp.java
index fb23495..87b44ad 100644
--- a/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientOp.java
+++ b/modules/client-common/src/main/java/org/apache/ignite/internal/client/proto/ClientOp.java
@@ -119,4 +119,16 @@
 
     /** Close query cursor. */
     public static final int SQL_CURSOR_CLOSE = 37;
+
+    /** Get table metadata. */
+    public static final int SQL_TABLE_META = 38;
+
+    /** Get column metadata. */
+    public static final int SQL_COLUMN_META = 39;
+
+    /** Get schemas list. */
+    public static final int SQL_SCHEMAS_META = 40;
+
+    /** Get primary key metadata. */
+    public static final int SQL_PK_META = 41;
 }
diff --git a/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/QueryEventHandlerTest.java b/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/QueryEventHandlerTest.java
deleted file mode 100644
index d74bc1f..0000000
--- a/modules/client-handler/src/integrationTest/java/org/apache/ignite/client/handler/QueryEventHandlerTest.java
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- * 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.client.handler;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import org.apache.ignite.client.proto.query.JdbcQueryEventHandler;
-import org.apache.ignite.client.proto.query.event.BatchExecuteRequest;
-import org.apache.ignite.client.proto.query.event.BatchExecuteResult;
-import org.apache.ignite.client.proto.query.event.Query;
-import org.apache.ignite.client.proto.query.event.QueryCloseRequest;
-import org.apache.ignite.client.proto.query.event.QueryCloseResult;
-import org.apache.ignite.client.proto.query.event.QueryExecuteRequest;
-import org.apache.ignite.client.proto.query.event.QueryExecuteResult;
-import org.apache.ignite.client.proto.query.event.QueryFetchRequest;
-import org.apache.ignite.client.proto.query.event.QueryFetchResult;
-import org.apache.ignite.client.proto.query.event.QuerySingleResult;
-import org.apache.ignite.internal.processors.query.calcite.QueryProcessor;
-import org.apache.ignite.internal.processors.query.calcite.SqlCursor;
-import org.apache.ignite.internal.processors.query.calcite.SqlQueryType;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import static org.apache.ignite.client.proto.query.IgniteQueryErrorCode.UNSUPPORTED_OPERATION;
-import static org.apache.ignite.client.proto.query.event.Response.STATUS_FAILED;
-import static org.apache.ignite.client.proto.query.event.Response.STATUS_SUCCESS;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.when;
-
-/**
- * Test QueryEventHandler implementation with various request types.
- * */
-@ExtendWith(MockitoExtension.class)
-public class QueryEventHandlerTest {
-    /**
-     * Mocked query processor.
-     */
-    @Mock
-    private QueryProcessor processor;
-
-    /**
-     * Mocked sql cursor.
-     */
-    @Mock
-    private SqlCursor<List<?>> cursor;
-
-    /**
-     * Test multiple select query request.
-     */
-    @Test
-    public void testSelectQueryRequestMultipleStatements() {
-        int cursorSize = 10;
-
-        JdbcQueryEventHandler hnd = prepareHandlerForMultiState(cursorSize);
-
-        QueryExecuteRequest qryReq = getJdbcQueryExecuteRequest(10);
-
-        QueryExecuteResult res = hnd.query(qryReq);
-
-        assertEquals(res.status(), STATUS_SUCCESS);
-        assertNull(res.err());
-
-        assertEquals(res.results().size(), cursorSize);
-
-        for (int i = 0; i < res.results().size(); i++) {
-            QuerySingleResult singleRes = res.results().get(i);
-            assertTrue(singleRes.isQuery());
-            assertFalse(singleRes.last());
-            assertEquals(singleRes.cursorId(), i);
-
-            assertEquals(singleRes.items().size(), 10);
-            assertEquals(singleRes.items().get(0).size(), 1);
-
-            assertEquals(singleRes.items().get(0).get(0), "42");
-        }
-    }
-
-    /**
-     * Prepare cursors and processor for multiple select query request.
-     *
-     * @param cursorSize Size of the cursors array.
-     * @return Query event handler.
-     */
-    private JdbcQueryEventHandler prepareHandlerForMultiState(int cursorSize) {
-        when(cursor.getQueryType()).thenReturn(SqlQueryType.QUERY);
-        when(cursor.hasNext()).thenReturn(true);
-
-        doReturn(Collections.singletonList("42")).when(cursor).next();
-
-        List<SqlCursor<List<?>>> cursors = new ArrayList<>(cursorSize);
-
-        for (int i = 0; i < cursorSize; i++)
-            cursors.add(cursor);
-
-        when(processor.query(anyString(), anyString(), any())).thenReturn(cursors);
-
-        return new JdbcQueryEventHandlerImpl(processor);
-    }
-
-    /**
-     * Test fetch query request.
-     */
-    @Test
-    public void testFetchQueryRequest() {
-        JdbcQueryEventHandler hnd = getHandler(SqlQueryType.QUERY, "42");
-
-        QueryExecuteRequest qryReq = getJdbcQueryExecuteRequest(10);
-
-        QueryExecuteResult qryRes = hnd.query(qryReq);
-
-        var fetchReq = new QueryFetchRequest(qryRes.results().get(0).cursorId(), 10);
-
-        QueryFetchResult fetchRes = hnd.fetch(fetchReq);
-
-        assertEquals(fetchRes.status(), STATUS_SUCCESS);
-        assertNull(fetchRes.err());
-
-        assertEquals(fetchRes.items().size(), 10);
-
-        assertEquals(fetchRes.items().get(0).get(0), "42");
-    }
-
-    /**
-     * Test dml query request.
-     */
-    @Test
-    public void testDMLQuery() {
-        JdbcQueryEventHandler hnd = getHandler(SqlQueryType.DML, 1L);
-
-        when(cursor.hasNext()).thenReturn(true).thenReturn(false);
-
-        QueryExecuteRequest qryReq = getJdbcQueryExecuteRequest(10);
-
-        QueryExecuteResult res = hnd.query(qryReq);
-
-        assertEquals(res.status(), STATUS_SUCCESS);
-        assertNull(res.err());
-
-        assertEquals(res.results().size(), 1);
-
-        QuerySingleResult singleRes = res.results().get(0);
-
-        assertEquals(singleRes.updateCount(), 1L);
-        assertFalse(singleRes.isQuery());
-        assertTrue(singleRes.last());
-    }
-
-    /**
-     * Test batch query request.
-     */
-    @Test
-    public void testBatchQuery() {
-        JdbcQueryEventHandler hnd = new JdbcQueryEventHandlerImpl(processor);
-
-        var req = new BatchExecuteRequest(
-            "PUBLIC",
-            Collections.singletonList(new Query("INSERT INTRO test VALUES (1);", null)),
-            false);
-
-        BatchExecuteResult batch = hnd.batch(req);
-
-        assertEquals(batch.status(), UNSUPPORTED_OPERATION);
-        assertNotNull(batch.err());
-    }
-
-    /**
-     * Test error cases for select query request.
-     */
-    @Test
-    public void testSelectQueryBadRequest() {
-        JdbcQueryEventHandler hnd = new JdbcQueryEventHandlerImpl(processor);
-
-        QueryExecuteRequest qryReq = getJdbcQueryExecuteRequest(10);
-
-        QueryExecuteResult res1 = hnd.query(qryReq);
-
-        assertEquals(res1.status(), STATUS_FAILED);
-        assertNotNull(res1.err());
-
-        QueryExecuteRequest req2 = getJdbcQueryExecuteRequest(10);
-
-        when(processor.query(anyString(), anyString(), any())).thenReturn(Collections.emptyList());
-
-        QueryExecuteResult res2 = hnd.query(req2);
-
-        assertEquals(res2.status(), STATUS_FAILED);
-        assertNotNull(res2.err());
-
-        when(cursor.hasNext()).thenReturn(true);
-        when(cursor.next()).thenThrow(RuntimeException.class);
-        when(processor.query(anyString(), anyString(), any())).thenReturn(Collections.singletonList(cursor));
-
-        QueryExecuteResult res3 = hnd.query(req2);
-
-        assertEquals(res3.status(), STATUS_FAILED);
-        assertNotNull(res3.err());
-    }
-
-    /**
-     * Test error cases for fetch query request.
-     */
-    @Test
-    public void testFetchQueryBadRequests() {
-        JdbcQueryEventHandler hnd = getHandler(SqlQueryType.QUERY, "42");
-
-        QueryExecuteRequest qryReq = getJdbcQueryExecuteRequest(1);
-
-        QueryExecuteResult qryRes = hnd.query(qryReq);
-
-        var fetchReq = new QueryFetchRequest(qryRes.results().get(0).cursorId(), -1);
-
-        QueryFetchResult fetchRes = hnd.fetch(fetchReq);
-
-        assertEquals(fetchRes.status(), STATUS_FAILED);
-        assertNotNull(fetchRes.err());
-
-        fetchReq = new QueryFetchRequest(Integer.MAX_VALUE, 1);
-
-        fetchRes = hnd.fetch(fetchReq);
-
-        assertEquals(fetchRes.status(), STATUS_FAILED);
-        assertNotNull(fetchRes.err());
-    }
-
-    /**
-     * Test close cursor request.
-     */
-    @Test
-    public void testCloseRequest() {
-        JdbcQueryEventHandler hnd = getHandler(SqlQueryType.QUERY, "42");
-
-        QueryExecuteRequest qryReq = getJdbcQueryExecuteRequest(1);
-
-        QueryExecuteResult qryRes = hnd.query(qryReq);
-
-        var closeReq = new QueryCloseRequest(qryRes.results().get(0).cursorId());
-
-        QueryCloseResult closeRes = hnd.close(closeReq);
-
-        assertEquals(closeRes.status(), STATUS_SUCCESS);
-
-        closeRes = hnd.close(closeReq);
-
-        assertEquals(closeRes.status(), STATUS_FAILED);
-        assertNotNull(closeRes.err());
-    }
-
-    /**
-     * Prepare getJdbcQueryExecuteRequest.
-     *
-     * @param pageSize Size of result set in response.
-     * @return JdbcQueryExecuteRequest.
-     */
-    private QueryExecuteRequest getJdbcQueryExecuteRequest(int pageSize) {
-        return new QueryExecuteRequest("PUBLIC", pageSize, 3, "SELECT * FROM Test;", null);
-    }
-
-    /**
-     * Prepare cursor and processor for multiple select query request.
-     *
-     * @param type Expected sql query type.
-     * @param val Value in result set.
-     * @return Query event handler.
-     */
-    private JdbcQueryEventHandler getHandler(SqlQueryType type, Object val) {
-        when(cursor.getQueryType()).thenReturn(type);
-        when(cursor.hasNext()).thenReturn(true);
-
-        doReturn(Collections.singletonList(val)).when(cursor).next();
-
-        when(processor.query(anyString(), anyString(), any())).thenReturn(Collections.singletonList(cursor));
-
-        return new JdbcQueryEventHandlerImpl(processor);
-    }
-}
diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
index d6d22e3..47173c8 100644
--- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
+++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/ClientInboundMessageHandler.java
@@ -25,9 +25,14 @@
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
 import org.apache.ignite.client.handler.requests.sql.ClientSqlCloseRequest;
+import org.apache.ignite.client.handler.requests.sql.ClientSqlColumnMetadataRequest;
 import org.apache.ignite.client.handler.requests.sql.ClientSqlExecuteBatchRequest;
 import org.apache.ignite.client.handler.requests.sql.ClientSqlExecuteRequest;
 import org.apache.ignite.client.handler.requests.sql.ClientSqlFetchRequest;
+import org.apache.ignite.client.handler.requests.sql.ClientSqlPrimaryKeyMetadataRequest;
+import org.apache.ignite.client.handler.requests.sql.ClientSqlSchemasMetadataRequest;
+import org.apache.ignite.client.handler.requests.sql.ClientSqlTableMetadataRequest;
+import org.apache.ignite.client.handler.requests.sql.JdbcMetadataCatalog;
 import org.apache.ignite.client.handler.requests.table.ClientSchemasGetRequest;
 import org.apache.ignite.client.handler.requests.table.ClientTableDropRequest;
 import org.apache.ignite.client.handler.requests.table.ClientTableGetRequest;
@@ -98,7 +103,7 @@
 
         this.igniteTables = igniteTables;
 
-        this.handler = new JdbcQueryEventHandlerImpl(processor);
+        this.handler = new JdbcQueryEventHandlerImpl(processor, new JdbcMetadataCatalog(igniteTables));
     }
 
     /** {@inheritDoc} */
@@ -351,6 +356,18 @@
             case ClientOp.SQL_CURSOR_CLOSE:
                 return ClientSqlCloseRequest.process(in, out, handler);
 
+            case ClientOp.SQL_TABLE_META:
+                return ClientSqlTableMetadataRequest.process(in, out, handler);
+
+            case ClientOp.SQL_COLUMN_META:
+                return ClientSqlColumnMetadataRequest.process(in, out, handler);
+
+            case ClientOp.SQL_SCHEMAS_META:
+                return ClientSqlSchemasMetadataRequest.process(in, out, handler);
+
+            case ClientOp.SQL_PK_META:
+                return ClientSqlPrimaryKeyMetadataRequest.process(in, out, handler);
+
             default:
                 throw new IgniteException("Unexpected operation code: " + opCode);
         }
diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java
index 782d385..c6001ea 100644
--- a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java
+++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/JdbcQueryEventHandlerImpl.java
@@ -20,12 +20,25 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicLong;
+import org.apache.ignite.client.handler.requests.sql.JdbcMetadataCatalog;
 import org.apache.ignite.client.proto.query.JdbcQueryEventHandler;
 import org.apache.ignite.client.proto.query.event.BatchExecuteRequest;
 import org.apache.ignite.client.proto.query.event.BatchExecuteResult;
+import org.apache.ignite.client.proto.query.event.JdbcColumnMeta;
+import org.apache.ignite.client.proto.query.event.JdbcMetaColumnsRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaColumnsResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaPrimaryKeysRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaPrimaryKeysResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaSchemasRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaSchemasResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaTablesRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaTablesResult;
+import org.apache.ignite.client.proto.query.event.JdbcPrimaryKeyMeta;
+import org.apache.ignite.client.proto.query.event.JdbcTableMeta;
 import org.apache.ignite.client.proto.query.event.QueryCloseRequest;
 import org.apache.ignite.client.proto.query.event.QueryCloseResult;
 import org.apache.ignite.client.proto.query.event.QueryExecuteRequest;
@@ -53,13 +66,18 @@
     /** Sql query processor. */
     private final QueryProcessor processor;
 
+    /** Jdbc metadata info. */
+    private final JdbcMetadataCatalog meta;
+
     /**
      * Constructor.
      *
      * @param processor Processor.
+     * @param meta JdbcMetadataInfo.
      */
-    public JdbcQueryEventHandlerImpl(QueryProcessor processor) {
+    public JdbcQueryEventHandlerImpl(QueryProcessor processor, JdbcMetadataCatalog meta) {
         this.processor = processor;
+        this.meta = meta;
     }
 
     /** {@inheritDoc} */
@@ -154,6 +172,34 @@
         return new QueryCloseResult();
     }
 
+    /** {@inheritDoc} */
+    @Override public JdbcMetaTablesResult tablesMeta(JdbcMetaTablesRequest req) {
+        List<JdbcTableMeta> tblsMeta = meta.getTablesMeta(req.schemaName(), req.tableName(), req.tableTypes());
+
+        return new JdbcMetaTablesResult(tblsMeta);
+    }
+
+    /** {@inheritDoc} */
+    @Override public JdbcMetaColumnsResult columnsMeta(JdbcMetaColumnsRequest req) {
+        Collection<JdbcColumnMeta> tblsMeta = meta.getColumnsMeta(req.schemaName(), req.tableName(), req.columnName());
+
+        return new JdbcMetaColumnsResult(tblsMeta);
+    }
+
+    /** {@inheritDoc} */
+    @Override public JdbcMetaSchemasResult schemasMeta(JdbcMetaSchemasRequest req) {
+        Collection<String> tblsMeta = meta.getSchemasMeta(req.schemaName());
+
+        return new JdbcMetaSchemasResult(tblsMeta);
+    }
+
+    /** {@inheritDoc} */
+    @Override public JdbcMetaPrimaryKeysResult primaryKeysMeta(JdbcMetaPrimaryKeysRequest req) {
+        Collection<JdbcPrimaryKeyMeta> tblsMeta = meta.getPrimaryKeys(req.schemaName(), req.tableName());
+
+        return new JdbcMetaPrimaryKeysResult(tblsMeta);
+    }
+
     /**
      * Serializes the stack trace of given exception for further sending to the client.
      *
diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlColumnMetadataRequest.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlColumnMetadataRequest.java
new file mode 100644
index 0000000..a245162
--- /dev/null
+++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlColumnMetadataRequest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.client.handler.requests.sql;
+
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.client.proto.query.JdbcQueryEventHandler;
+import org.apache.ignite.client.proto.query.event.JdbcMetaColumnsRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaColumnsResult;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+
+/**
+ * Client sql column metadata request handler.
+ */
+public class ClientSqlColumnMetadataRequest {
+    /**
+     * Processes remote {@code JdbcMetaColumnsRequest}.
+     *
+     * @param in Client message unpacker.
+     * @param out Client message packer.
+     * @param handler Query event handler.
+     * @return null value indicates synchronous operation.
+     */
+    public static CompletableFuture<Void> process(
+        ClientMessageUnpacker in,
+        ClientMessagePacker out,
+        JdbcQueryEventHandler handler
+    ) {
+        var req = new JdbcMetaColumnsRequest();
+
+        req.readBinary(in);
+
+        JdbcMetaColumnsResult res = handler.columnsMeta(req);
+
+        res.writeBinary(out);
+
+        return null;
+    }
+}
diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlPrimaryKeyMetadataRequest.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlPrimaryKeyMetadataRequest.java
new file mode 100644
index 0000000..d2774bc
--- /dev/null
+++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlPrimaryKeyMetadataRequest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.client.handler.requests.sql;
+
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.client.proto.query.JdbcQueryEventHandler;
+import org.apache.ignite.client.proto.query.event.JdbcMetaPrimaryKeysRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaPrimaryKeysResult;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+
+/**
+ * Client sql primary key metadata request handler.
+ */
+public class ClientSqlPrimaryKeyMetadataRequest {
+    /**
+     * Processes remote {@code JdbcMetaPrimaryKeysRequest}.
+     *
+     * @param in Client message unpacker.
+     * @param out Client message packer.
+     * @param handler Query event handler.
+     * @return null value indicates synchronous operation.
+     */
+    public static CompletableFuture<Void> process(
+        ClientMessageUnpacker in,
+        ClientMessagePacker out,
+        JdbcQueryEventHandler handler
+    ) {
+        var req = new JdbcMetaPrimaryKeysRequest();
+
+        req.readBinary(in);
+
+        JdbcMetaPrimaryKeysResult res = handler.primaryKeysMeta(req);
+
+        res.writeBinary(out);
+
+        return null;
+    }
+}
diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlSchemasMetadataRequest.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlSchemasMetadataRequest.java
new file mode 100644
index 0000000..12090c4
--- /dev/null
+++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlSchemasMetadataRequest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.client.handler.requests.sql;
+
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.client.proto.query.JdbcQueryEventHandler;
+import org.apache.ignite.client.proto.query.event.JdbcMetaSchemasRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaSchemasResult;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+
+/**
+ * Client sql schema metadata request handler.
+ */
+public class ClientSqlSchemasMetadataRequest {
+    /**
+     * Processes remote {@code JdbcMetaSchemasRequest}.
+     *
+     * @param in Client message unpacker.
+     * @param out Client message packer.
+     * @param handler Query event handler.
+     * @return null value indicates synchronous operation.
+     */
+    public static CompletableFuture<Void> process(
+        ClientMessageUnpacker in,
+        ClientMessagePacker out,
+        JdbcQueryEventHandler handler
+    ) {
+        var req = new JdbcMetaSchemasRequest();
+
+        req.readBinary(in);
+
+        JdbcMetaSchemasResult res = handler.schemasMeta(req);
+
+        res.writeBinary(out);
+
+        return null;
+    }
+}
diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlTableMetadataRequest.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlTableMetadataRequest.java
new file mode 100644
index 0000000..c072889
--- /dev/null
+++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/ClientSqlTableMetadataRequest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.client.handler.requests.sql;
+
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.client.proto.query.JdbcQueryEventHandler;
+import org.apache.ignite.client.proto.query.event.JdbcMetaTablesRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaTablesResult;
+import org.apache.ignite.internal.client.proto.ClientMessagePacker;
+import org.apache.ignite.internal.client.proto.ClientMessageUnpacker;
+
+/**
+ * Client sql table metadata request handler.
+ */
+public class ClientSqlTableMetadataRequest {
+    /**
+     * Processes remote {@code JdbcMetaTablesRequest}.
+     *
+     * @param in Client message unpacker.
+     * @param out Client message packer.
+     * @param handler Query event handler.
+     * @return null value indicates synchronous operation.
+     */
+    public static CompletableFuture<Void> process(
+        ClientMessageUnpacker in,
+        ClientMessagePacker out,
+        JdbcQueryEventHandler handler
+    ) {
+        var req = new JdbcMetaTablesRequest();
+
+        req.readBinary(in);
+
+        JdbcMetaTablesResult res = handler.tablesMeta(req);
+
+        res.writeBinary(out);
+
+        return null;
+    }
+}
diff --git a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/JdbcMetadataCatalog.java b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/JdbcMetadataCatalog.java
new file mode 100644
index 0000000..f66f3fb
--- /dev/null
+++ b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/sql/JdbcMetadataCatalog.java
@@ -0,0 +1,335 @@
+/*
+ * 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.client.handler.requests.sql;
+
+import java.sql.DatabaseMetaData;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.ignite.client.proto.query.event.JdbcColumnMeta;
+import org.apache.ignite.client.proto.query.event.JdbcPrimaryKeyMeta;
+import org.apache.ignite.client.proto.query.event.JdbcTableMeta;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+import org.apache.ignite.internal.schema.Column;
+import org.apache.ignite.internal.schema.DecimalNativeType;
+import org.apache.ignite.internal.schema.NativeType;
+import org.apache.ignite.internal.schema.NativeTypeSpec;
+import org.apache.ignite.internal.schema.NumberNativeType;
+import org.apache.ignite.internal.schema.SchemaDescriptor;
+import org.apache.ignite.internal.schema.SchemaRegistry;
+import org.apache.ignite.internal.table.TableImpl;
+import org.apache.ignite.internal.util.Pair;
+import org.apache.ignite.table.Table;
+import org.apache.ignite.table.manager.IgniteTables;
+
+//TODO IGNITE-15525 Filter by table type must be added after 'view' type will appear.
+/**
+ * Facade over {@link IgniteTables} to get information about database entities in terms of JDBC.
+ */
+public class JdbcMetadataCatalog {
+    /** Table name separator. */
+    private static final String TABLE_NAME_SEPARATOR = "\\.";
+
+    /** Table schema. */
+    private static final int TABLE_SCHEMA = 0;
+
+    /** Table name. */
+    private static final int TABLE_NAME = 1;
+
+    /** Primary key identifier. */
+    private static final String PK = "PK_";
+
+    /** Table type. */
+    private static final String TBL_TYPE = "TABLE";
+
+    /** Default schema name. */
+    private static final String DEFAULT_SCHEMA_NAME = "PUBLIC";
+
+    /** Ignite tables interface. Used to get all the database metadata. */
+    private final IgniteTables tables;
+
+    /** Comparator for {@link Column} by schema then table name then column order. */
+    private static final Comparator<Pair<String, Column>> bySchemaThenTabNameThenColOrder
+        = Comparator.comparing((Function<Pair<String, Column>, String>)Pair::getFirst)
+        .thenComparingInt(o -> o.getSecond().schemaIndex());
+
+    /** Comparator for {@link JdbcTableMeta} by table type then schema then table name. */
+    private static final Comparator<Table> byTblTypeThenSchemaThenTblName = Comparator.comparing(Table::tableName);
+
+    /**
+     * Initializes info.
+     *
+     * @param tables IgniteTables.
+     */
+    public JdbcMetadataCatalog(IgniteTables tables) {
+        this.tables = tables;
+    }
+
+    /**
+     * See {@link DatabaseMetaData#getPrimaryKeys(String, String, String)} for details.
+     *
+     * Ignite has only one possible CATALOG_NAME, it is handled on the client (driver) side.
+     *
+     * @param schemaNamePtrn Sql pattern for schema name.
+     * @param tblNamePtrn Sql pattern for table name.
+     * @return Collection of primary keys information for tables that matches specified schema and table name patterns.
+     */
+    public Collection<JdbcPrimaryKeyMeta> getPrimaryKeys(String schemaNamePtrn, String tblNamePtrn) {
+        Collection<JdbcPrimaryKeyMeta> metaSet = new HashSet<>();
+
+        String schemaNameRegex = translateSqlWildcardsToRegex(schemaNamePtrn);
+        String tlbNameRegex = translateSqlWildcardsToRegex(tblNamePtrn);
+
+        tables.tables().stream()
+            .filter(t -> matches(getTblSchema(t.tableName()), schemaNameRegex))
+            .filter(t -> matches(getTblName(t.tableName()), tlbNameRegex))
+            .forEach(tbl -> {
+                JdbcPrimaryKeyMeta meta = createPrimaryKeyMeta(tbl);
+
+                metaSet.add(meta);
+            });
+
+        return metaSet;
+    }
+
+    /**
+     * See {@link DatabaseMetaData#getTables(String, String, String, String[])} for details.
+     *
+     * Ignite has only one possible value for CATALOG_NAME and has only one table type so these parameters are handled
+     * on the client (driver) side.
+     *
+     * Result is ordered by (schema name, table name).
+     *
+     * @param schemaNamePtrn Sql pattern for schema name.
+     * @param tblNamePtrn Sql pattern for table name.
+     * @param tblTypes Requested table types.
+     * @return List of metadatas of tables that matches.
+     */
+    public List<JdbcTableMeta> getTablesMeta(String schemaNamePtrn, String tblNamePtrn, String[] tblTypes) {
+        String schemaNameRegex = translateSqlWildcardsToRegex(schemaNamePtrn);
+        String tlbNameRegex = translateSqlWildcardsToRegex(tblNamePtrn);
+
+        List<Table> tblsMeta = tables.tables().stream()
+            .filter(t -> matches(getTblSchema(t.tableName()), schemaNameRegex))
+            .filter(t -> matches(getTblName(t.tableName()), tlbNameRegex))
+            .collect(Collectors.toList());
+
+        return tblsMeta.stream()
+            .sorted(byTblTypeThenSchemaThenTblName)
+            .map(t -> new JdbcTableMeta(getTblSchema(t.tableName()), getTblName(t.tableName()), TBL_TYPE))
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * See {@link DatabaseMetaData#getColumns(String, String, String, String)} for details.
+     *
+     * Ignite has only one possible CATALOG_NAME, it is handled on the client (driver) side.
+     *
+     * @param schemaNamePtrn Schema name java regex pattern.
+     * @param tblNamePtrn Table name java regex pattern.
+     * @param colNamePtrn Column name java regex pattern.
+     * @return List of metadatas about columns that match specified schema/tablename/columnname criterias.
+     */
+    public Collection<JdbcColumnMeta> getColumnsMeta(String schemaNamePtrn, String tblNamePtrn, String colNamePtrn) {
+        Collection<JdbcColumnMeta> metas = new LinkedHashSet<>();
+
+        String schemaNameRegex = translateSqlWildcardsToRegex(schemaNamePtrn);
+        String tlbNameRegex = translateSqlWildcardsToRegex(tblNamePtrn);
+        String colNameRegex = translateSqlWildcardsToRegex(colNamePtrn);
+
+        tables.tables().stream()
+            .filter(t -> matches(getTblSchema(t.tableName()), schemaNameRegex))
+            .filter(t -> matches(getTblName(t.tableName()), tlbNameRegex))
+            .flatMap(
+                tbl -> {
+                    SchemaDescriptor schema = ((TableImpl)tbl).schemaView().schema();
+
+                    List<Pair<String, Column>> tblColPairs = new ArrayList<>();
+
+                    for (Column column : schema.keyColumns().columns())
+                        tblColPairs.add(new Pair<>(tbl.tableName(), column));
+
+                    for (Column column : schema.valueColumns().columns())
+                        tblColPairs.add(new Pair<>(tbl.tableName(), column));
+
+                    return tblColPairs.stream();
+            })
+            .filter(e -> matches(e.getSecond().name(), colNameRegex))
+            .sorted(bySchemaThenTabNameThenColOrder)
+            .forEachOrdered(pair -> {
+                JdbcColumnMeta colMeta = createColumnMeta(pair.getFirst(), pair.getSecond());
+
+                if (!metas.contains(colMeta))
+                    metas.add(colMeta);
+            });
+
+        return metas;
+    }
+
+    /**
+     * See {@link DatabaseMetaData#getSchemas(String, String)} for details.
+     *
+     * Ignite has only one possible CATALOG_NAME, it is handled on the client (driver) side.
+     *
+     * @param schemaNamePtrn Sql pattern for schema name filter.
+     * @return schema names that matches provided pattern.
+     */
+    public Collection<String> getSchemasMeta(String schemaNamePtrn) {
+        SortedSet<String> schemas = new TreeSet<>(); // to have values sorted.
+
+        String schemaNameRegex = translateSqlWildcardsToRegex(schemaNamePtrn);
+
+        if (matches(DEFAULT_SCHEMA_NAME, schemaNameRegex))
+            schemas.add(DEFAULT_SCHEMA_NAME);
+
+        tables.tables().stream()
+            .map(tbl -> getTblSchema(tbl.tableName()))
+            .filter(schema -> matches(schema, schemaNameRegex))
+            .forEach(schemas::add);
+
+        return schemas;
+    }
+
+    /**
+     * Creates primary key metadata from table object.
+     *
+     * @param tbl Table.
+     * @return Jdbc primary key metadata.
+     */
+    private JdbcPrimaryKeyMeta createPrimaryKeyMeta(Table tbl) {
+        String schemaName = getTblSchema(tbl.tableName());
+        String tblName = getTblName(tbl.tableName());
+
+        final String keyName = PK + tblName;
+
+        SchemaRegistry registry = ((TableImpl)tbl).schemaView();
+
+        List<String> keyColNames = Arrays.stream(registry.schema().keyColumns().columns())
+            .map(Column::name)
+            .collect(Collectors.toList());
+
+        return new JdbcPrimaryKeyMeta(schemaName, tblName, keyName, keyColNames);
+    }
+
+    /**
+     * Creates column metadata from column and table name.
+     *
+     * @param tblName Table name.
+     * @param col Column.
+     * @return Column metadata.
+     */
+    private JdbcColumnMeta createColumnMeta(String tblName, Column col) {
+        NativeType type = col.type();
+
+        int precision = -1;
+        int scale = -1;
+
+        if (type.spec() == NativeTypeSpec.NUMBER)
+            precision = ((NumberNativeType)type).precision();
+        else if (type.spec() == NativeTypeSpec.DECIMAL) {
+            precision = ((DecimalNativeType)type).precision();
+            scale = ((DecimalNativeType)type).scale();
+        }
+
+        return new JdbcColumnMeta(
+            getTblSchema(tblName),
+            getTblName(tblName),
+            col.name(),
+            Commons.nativeTypeToClass(col.type()),
+            precision,
+            scale,
+            col.nullable()
+        );
+    }
+
+    /**
+     * Splits the tableName into schema and table name and returns the table name.
+     *
+     * @param tblName Table name.
+     * @return Table name string.
+     */
+    private String getTblName(String tblName) {
+        return tblName.split(TABLE_NAME_SEPARATOR)[TABLE_NAME];
+    }
+
+    /**
+     * Splits the tableName into schema and table name and returns the table schema.
+     *
+     * @param tblName Table name.
+     * @return Table schema string.
+     */
+    private String getTblSchema(String tblName) {
+        return tblName.split(TABLE_NAME_SEPARATOR)[TABLE_SCHEMA];
+    }
+
+    /**
+     * Checks whether string matches SQL pattern.
+     *
+     * @param str String.
+     * @param sqlPtrn Pattern.
+     * @return Whether string matches pattern.
+     */
+    private static boolean matches(String str, String sqlPtrn) {
+        if (str == null)
+            return false;
+
+        if (sqlPtrn == null)
+            return true;
+
+        return str.matches(sqlPtrn);
+    }
+
+    /**
+     * <p>Converts sql pattern wildcards into java regex wildcards.</p>
+     * <p>Translates "_" to "." and "%" to ".*" if those are not escaped with "\" ("\_" or "\%").</p>
+     * <p>All other characters are considered normal and will be escaped if necessary.</p>
+     * <pre>
+     * Example:
+     *      som_    -->     som.
+     *      so%     -->     so.*
+     *      s[om]e  -->     so\[om\]e
+     *      so\_me  -->     so_me
+     *      some?   -->     some\?
+     *      som\e   -->     som\\e
+     * </pre>
+     *
+     * @param sqlPtrn Sql pattern.
+     * @return Java regex pattern.
+     */
+    private static String translateSqlWildcardsToRegex(String sqlPtrn) {
+        if (sqlPtrn == null || sqlPtrn.isEmpty())
+            return sqlPtrn;
+
+        String toRegex = ' ' + sqlPtrn;
+
+        toRegex = toRegex.replaceAll("([\\[\\]{}()*+?.\\\\\\\\^$|])", "\\\\$1");
+        toRegex = toRegex.replaceAll("([^\\\\\\\\])((?:\\\\\\\\\\\\\\\\)*)%", "$1$2.*");
+        toRegex = toRegex.replaceAll("([^\\\\\\\\])((?:\\\\\\\\\\\\\\\\)*)_", "$1$2.");
+        toRegex = toRegex.replaceAll("([^\\\\\\\\])(\\\\\\\\(?>\\\\\\\\\\\\\\\\)*\\\\\\\\)*\\\\\\\\([_|%])", "$1$2$3");
+
+        return toRegex.substring(1);
+    }
+}
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpIgniteClient.java b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpIgniteClient.java
index 88680be7..0ffb12b 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpIgniteClient.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpIgniteClient.java
@@ -117,11 +117,11 @@
     }
 
     /**
-     * Send JdbcClientMessage request to server size and reads JdbcClientMessage result.
+     * Send ClientMessage request to server size and reads ClientMessage result.
      *
      * @param opCode Operation code.
-     * @param req JdbcClientMessage request.
-     * @param res JdbcClientMessage result.
+     * @param req ClientMessage request.
+     * @param res ClientMessage result.
      */
     public void sendRequest(int opCode, ClientMessage req, ClientMessage res) {
         ch.serviceAsync(opCode, w -> req.writeBinary(w.out()), p -> {
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/client/query/JdbcClientQueryEventHandler.java b/modules/client/src/main/java/org/apache/ignite/internal/client/query/JdbcClientQueryEventHandler.java
index a02b766..441776c 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/client/query/JdbcClientQueryEventHandler.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/client/query/JdbcClientQueryEventHandler.java
@@ -20,6 +20,14 @@
 import org.apache.ignite.client.proto.query.JdbcQueryEventHandler;
 import org.apache.ignite.client.proto.query.event.BatchExecuteRequest;
 import org.apache.ignite.client.proto.query.event.BatchExecuteResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaColumnsRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaColumnsResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaPrimaryKeysRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaPrimaryKeysResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaSchemasRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaSchemasResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaTablesRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaTablesResult;
 import org.apache.ignite.client.proto.query.event.QueryCloseRequest;
 import org.apache.ignite.client.proto.query.event.QueryCloseResult;
 import org.apache.ignite.client.proto.query.event.QueryExecuteRequest;
@@ -78,4 +86,40 @@
 
         return res;
     }
+
+    /** {@inheritDoc} */
+    @Override public JdbcMetaTablesResult tablesMeta(JdbcMetaTablesRequest req) {
+        JdbcMetaTablesResult res = new JdbcMetaTablesResult();
+
+        client.sendRequest(ClientOp.SQL_TABLE_META, req, res);
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public JdbcMetaColumnsResult columnsMeta(JdbcMetaColumnsRequest req) {
+        JdbcMetaColumnsResult res = new JdbcMetaColumnsResult();
+
+        client.sendRequest(ClientOp.SQL_COLUMN_META, req, res);
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public JdbcMetaSchemasResult schemasMeta(JdbcMetaSchemasRequest req) {
+        JdbcMetaSchemasResult res = new JdbcMetaSchemasResult();
+
+        client.sendRequest(ClientOp.SQL_SCHEMAS_META, req, res);
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public JdbcMetaPrimaryKeysResult primaryKeysMeta(JdbcMetaPrimaryKeysRequest req) {
+        JdbcMetaPrimaryKeysResult res = new JdbcMetaPrimaryKeysResult();
+
+        client.sendRequest(ClientOp.SQL_PK_META, req, res);
+
+        return res;
+    }
 }
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java b/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
index f8effd4..fc48d0c 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcConnection.java
@@ -104,6 +104,9 @@
     /** Ignite remote client. */
     private final TcpIgniteClient client;
 
+    /** Jdbc metadata. Cache the JDBC object on the first access */
+    private JdbcDatabaseMetadata metadata;
+
     /**
      * Constructor.
      *
@@ -255,7 +258,7 @@
         if (autoCommit)
             throw new SQLException("Transaction cannot be committed explicitly in auto-commit mode.");
 
-         doCommit();
+        doCommit();
     }
 
     /** {@inheritDoc} */
@@ -311,7 +314,10 @@
     @Override public DatabaseMetaData getMetaData() throws SQLException {
         ensureNotClosed();
 
-        throw new SQLFeatureNotSupportedException("DatabaseMetaData is not supported.");
+        if (metadata == null)
+            metadata = new JdbcDatabaseMetadata(this);
+
+        return metadata;
     }
 
     /** {@inheritDoc} */
@@ -774,4 +780,13 @@
     public ConnectionProperties connectionProperties() {
         return connProps;
     }
+
+    /**
+     * Gets connection url.
+     *
+     * @return Connection URL.
+     */
+    public String url() {
+        return connProps.getUrl();
+    }
 }
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcDatabaseMetadata.java b/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcDatabaseMetadata.java
new file mode 100644
index 0000000..27ee6b2
--- /dev/null
+++ b/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcDatabaseMetadata.java
@@ -0,0 +1,1531 @@
+/*
+ * 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.jdbc;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.RowIdLifetime;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import org.apache.ignite.client.proto.query.IgniteQueryErrorCode;
+import org.apache.ignite.client.proto.query.event.JdbcColumnMeta;
+import org.apache.ignite.client.proto.query.event.JdbcMetaColumnsRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaColumnsResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaPrimaryKeysRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaPrimaryKeysResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaSchemasRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaSchemasResult;
+import org.apache.ignite.client.proto.query.event.JdbcMetaTablesRequest;
+import org.apache.ignite.client.proto.query.event.JdbcMetaTablesResult;
+import org.apache.ignite.client.proto.query.event.JdbcPrimaryKeyMeta;
+import org.apache.ignite.client.proto.query.event.JdbcTableMeta;
+import org.apache.ignite.internal.client.proto.ProtocolVersion;
+
+import static java.sql.Connection.TRANSACTION_NONE;
+import static java.sql.ResultSet.CONCUR_READ_ONLY;
+import static java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT;
+import static java.sql.ResultSet.TYPE_FORWARD_ONLY;
+import static java.sql.RowIdLifetime.ROWID_UNSUPPORTED;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+/**
+ * JDBC database metadata implementation.
+ */
+public class JdbcDatabaseMetadata implements DatabaseMetaData {
+    /** Driver name. */
+    public static final String DRIVER_NAME = "Apache Ignite JDBC Driver";
+
+    /** The only possible name for catalog. */
+    public static final String CATALOG_NAME = "IGNITE";
+
+    /** Name of TABLE type. */
+    public static final String TYPE_TABLE = "TABLE";
+
+    /** Connection. */
+    private final JdbcConnection conn;
+
+    /**
+     * Constructor.
+     *
+     * @param conn Connection.
+     */
+    JdbcDatabaseMetadata(JdbcConnection conn) {
+        this.conn = conn;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean allProceduresAreCallable() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean allTablesAreSelectable() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getURL() {
+        return conn.url();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getUserName() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isReadOnly() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean nullsAreSortedHigh() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean nullsAreSortedLow() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean nullsAreSortedAtStart() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean nullsAreSortedAtEnd() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getDatabaseProductName() {
+        return "Apache Ignite";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getDatabaseProductVersion() {
+        return ProtocolVersion.LATEST_VER.toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getDriverName() {
+        return DRIVER_NAME;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getDriverVersion() {
+        return ProtocolVersion.LATEST_VER.toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getDriverMajorVersion() {
+        return ProtocolVersion.LATEST_VER.major();
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getDriverMinorVersion() {
+        return ProtocolVersion.LATEST_VER.minor();
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean usesLocalFiles() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean usesLocalFilePerTable() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMixedCaseIdentifiers() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesUpperCaseIdentifiers() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesLowerCaseIdentifiers() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesMixedCaseIdentifiers() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMixedCaseQuotedIdentifiers() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesUpperCaseQuotedIdentifiers() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesLowerCaseQuotedIdentifiers() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesMixedCaseQuotedIdentifiers() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getIdentifierQuoteString() {
+        return "\"";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getSQLKeywords() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getNumericFunctions() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getStringFunctions() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getSystemFunctions() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getTimeDateFunctions() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getSearchStringEscape() {
+        return "\\";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getExtraNameCharacters() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsAlterTableWithAddColumn() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsAlterTableWithDropColumn() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsColumnAliasing() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean nullPlusNonNullIsNull() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsConvert() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsConvert(int fromType, int toType) {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsTableCorrelationNames() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsDifferentTableCorrelationNames() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsExpressionsInOrderBy() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOrderByUnrelated() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsGroupBy() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsGroupByUnrelated() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsGroupByBeyondSelect() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsLikeEscapeClause() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMultipleResultSets() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMultipleTransactions() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsNonNullableColumns() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMinimumSQLGrammar() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCoreSQLGrammar() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsExtendedSQLGrammar() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsANSI92EntryLevelSQL() {
+        //TODO IGNITE-15527
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsANSI92IntermediateSQL() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsANSI92FullSQL() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsIntegrityEnhancementFacility() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOuterJoins() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsFullOuterJoins() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsLimitedOuterJoins() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getSchemaTerm() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getProcedureTerm() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getCatalogTerm() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isCatalogAtStart() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getCatalogSeparator() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSchemasInDataManipulation() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSchemasInProcedureCalls() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSchemasInTableDefinitions() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSchemasInIndexDefinitions() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSchemasInPrivilegeDefinitions() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCatalogsInDataManipulation() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCatalogsInProcedureCalls() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCatalogsInTableDefinitions() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCatalogsInIndexDefinitions() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCatalogsInPrivilegeDefinitions() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsPositionedDelete() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsPositionedUpdate() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSelectForUpdate() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsStoredProcedures() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSubqueriesInComparisons() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSubqueriesInExists() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSubqueriesInIns() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSubqueriesInQuantifieds() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCorrelatedSubqueries() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsUnion() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsUnionAll() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOpenCursorsAcrossCommit() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOpenCursorsAcrossRollback() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOpenStatementsAcrossCommit() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOpenStatementsAcrossRollback() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxBinaryLiteralLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxCharLiteralLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnNameLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnsInGroupBy() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnsInIndex() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnsInOrderBy() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnsInSelect() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnsInTable() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxConnections() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxCursorNameLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxIndexLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxSchemaNameLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxProcedureNameLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxCatalogNameLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxRowSize() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean doesMaxRowSizeIncludeBlobs() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxStatementLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxStatements() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxTableNameLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxTablesInSelect() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxUserNameLength() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getDefaultTransactionIsolation() {
+        return TRANSACTION_NONE;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsTransactions() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsTransactionIsolationLevel(int level) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsDataDefinitionAndDataManipulationTransactions() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsDataManipulationTransactionsOnly() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean dataDefinitionCausesTransactionCommit() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean dataDefinitionIgnoredInTransactions() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getProcedures(String catalog, String schemaPtrn,
+        String procedureNamePtrn) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "PROCEDURE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "PROCEDURE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "PROCEDURE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "REMARKS", String.class),
+            new JdbcColumnMeta(null, null, "PROCEDURE_TYPE", String.class),
+            new JdbcColumnMeta(null, null, "SPECIFIC_NAME", String.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getProcedureColumns(String catalog, String schemaPtrn, String procedureNamePtrn,
+        String colNamePtrn) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "PROCEDURE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "PROCEDURE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "PROCEDURE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_TYPE", Short.class),
+            new JdbcColumnMeta(null, null, "COLUMN_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "PRECISION", Integer.class),
+            new JdbcColumnMeta(null, null, "LENGTH", Integer.class),
+            new JdbcColumnMeta(null, null, "SCALE", Short.class),
+            new JdbcColumnMeta(null, null, "RADIX", Short.class),
+            new JdbcColumnMeta(null, null, "NULLABLE", Short.class),
+            new JdbcColumnMeta(null, null, "REMARKS", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_DEF", String.class),
+            new JdbcColumnMeta(null, null, "SQL_DATA_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "SQL_DATETIME_SUB", Integer.class),
+            new JdbcColumnMeta(null, null, "CHAR_OCTET_LENGTH", Integer.class),
+            new JdbcColumnMeta(null, null, "ORDINAL_POSITION", Integer.class),
+            new JdbcColumnMeta(null, null, "IS_NULLABLE", String.class),
+            new JdbcColumnMeta(null, null, "SPECIFIC_NAME", String.class)
+            ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getTables(String catalog, String schemaPtrn, String tblNamePtrn, String[] tblTypes)
+        throws SQLException {
+        conn.ensureNotClosed();
+
+        final List<JdbcColumnMeta> meta = asList(
+            new JdbcColumnMeta(null, null, "TABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_TYPE", String.class),
+            new JdbcColumnMeta(null, null, "REMARKS", String.class),
+            new JdbcColumnMeta(null, null, "TYPE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TYPE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "SELF_REFERENCING_COL_NAME", String.class),
+            new JdbcColumnMeta(null, null, "REF_GENERATION", String.class));
+
+        boolean tblTypeMatch = false;
+
+        if (tblTypes == null)
+            tblTypeMatch = true;
+        else {
+            for (String type : tblTypes) {
+                if (TYPE_TABLE.equals(type)) {
+                    tblTypeMatch = true;
+
+                    break;
+                }
+            }
+        }
+
+        if (!isValidCatalog(catalog) || !tblTypeMatch)
+            return new JdbcResultSet(Collections.emptyList(), meta);
+
+        JdbcMetaTablesResult res
+            = conn.handler().tablesMeta(new JdbcMetaTablesRequest(schemaPtrn, tblNamePtrn, tblTypes));
+
+        if (!res.hasResults())
+            throw IgniteQueryErrorCode.createJdbcSqlException(res.err(), res.status());
+
+        List<List<Object>> rows = new LinkedList<>();
+
+        for (JdbcTableMeta tblMeta : res.meta())
+            rows.add(tableRow(tblMeta));
+
+        return new JdbcResultSet(rows, meta);
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getSchemas() throws SQLException {
+        return getSchemas(null, "%");
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+    @Override public ResultSet getCatalogs() {
+        return new JdbcResultSet(singletonList(singletonList(CATALOG_NAME)),
+            asList(new JdbcColumnMeta(null, null, "TABLE_CAT", String.class)));
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+    @Override public ResultSet getTableTypes() {
+        return new JdbcResultSet(
+            asList(singletonList(TYPE_TABLE)),
+            asList(new JdbcColumnMeta(null, null, "TABLE_TYPE", String.class)));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getColumns(String catalog, String schemaPtrn, String tblNamePtrn, String colNamePtrn) throws SQLException {
+        conn.ensureNotClosed();
+
+        final List<JdbcColumnMeta> meta = asList(
+            new JdbcColumnMeta(null, null, "TABLE_CAT", String.class),      // 1
+            new JdbcColumnMeta(null, null, "TABLE_SCHEM", String.class),    // 2
+            new JdbcColumnMeta(null, null, "TABLE_NAME", String.class),     // 3
+            new JdbcColumnMeta(null, null, "COLUMN_NAME", String.class),    // 4
+            new JdbcColumnMeta(null, null, "DATA_TYPE", Short.class),       // 5
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),      // 6
+            new JdbcColumnMeta(null, null, "COLUMN_SIZE", Integer.class),   // 7
+            new JdbcColumnMeta(null, null, "BUFFER_LENGTH", Integer.class), // 8
+            new JdbcColumnMeta(null, null, "DECIMAL_DIGITS", Integer.class), // 9
+            new JdbcColumnMeta(null, null, "NUM_PREC_RADIX", Short.class),  // 10
+            new JdbcColumnMeta(null, null, "NULLABLE", Short.class),        // 11
+            new JdbcColumnMeta(null, null, "REMARKS", String.class),        // 12
+            new JdbcColumnMeta(null, null, "COLUMN_DEF", String.class),     // 13
+            new JdbcColumnMeta(null, null, "SQL_DATA_TYPE", Integer.class), // 14
+            new JdbcColumnMeta(null, null, "SQL_DATETIME_SUB", Integer.class), // 15
+            new JdbcColumnMeta(null, null, "CHAR_OCTET_LENGTH", Integer.class), // 16
+            new JdbcColumnMeta(null, null, "ORDINAL_POSITION", Integer.class), // 17
+            new JdbcColumnMeta(null, null, "IS_NULLABLE", String.class),    // 18
+            new JdbcColumnMeta(null, null, "SCOPE_CATLOG", String.class),   // 19
+            new JdbcColumnMeta(null, null, "SCOPE_SCHEMA", String.class),   // 20
+            new JdbcColumnMeta(null, null, "SCOPE_TABLE", String.class),    // 21
+            new JdbcColumnMeta(null, null, "SOURCE_DATA_TYPE", Short.class), // 22
+            new JdbcColumnMeta(null, null, "IS_AUTOINCREMENT", String.class), // 23
+            new JdbcColumnMeta(null, null, "IS_GENERATEDCOLUMN", String.class) // 24
+        );
+
+        if (!isValidCatalog(catalog))
+            return new JdbcResultSet(Collections.emptyList(), meta);
+
+        JdbcMetaColumnsResult res = conn.handler().columnsMeta(new JdbcMetaColumnsRequest(schemaPtrn, tblNamePtrn, colNamePtrn));
+
+        if (!res.hasResults())
+            throw IgniteQueryErrorCode.createJdbcSqlException(res.err(), res.status());
+
+        List<List<Object>> rows = new LinkedList<>();
+
+        for (int i = 0; i < res.meta().size(); ++i)
+            rows.add(columnRow(res.meta().get(i), i + 1));
+
+        return new JdbcResultSet(rows, meta);
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getColumnPrivileges(String catalog, String schema, String tbl,
+        String colNamePtrn) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "TABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "GRANTOR", String.class),
+            new JdbcColumnMeta(null, null, "GRANTEE", String.class),
+            new JdbcColumnMeta(null, null, "PRIVILEGE", String.class),
+            new JdbcColumnMeta(null, null, "IS_GRANTABLE", String.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getTablePrivileges(String catalog, String schemaPtrn,
+        String tblNamePtrn) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "TABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "GRANTOR", String.class),
+            new JdbcColumnMeta(null, null, "GRANTEE", String.class),
+            new JdbcColumnMeta(null, null, "PRIVILEGE", String.class),
+            new JdbcColumnMeta(null, null, "IS_GRANTABLE", String.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getBestRowIdentifier(String catalog, String schema, String tbl, int scope,
+        boolean nullable) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "SCOPE", Short.class),
+            new JdbcColumnMeta(null, null, "COLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "DATA_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_SIZE", Integer.class),
+            new JdbcColumnMeta(null, null, "BUFFER_LENGTH", Integer.class),
+            new JdbcColumnMeta(null, null, "DECIMAL_DIGITS", Short.class),
+            new JdbcColumnMeta(null, null, "PSEUDO_COLUMN", Short.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getVersionColumns(String catalog, String schema, String tbl) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "SCOPE", Short.class),
+            new JdbcColumnMeta(null, null, "COLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "DATA_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_SIZE", Integer.class),
+            new JdbcColumnMeta(null, null, "BUFFER_LENGTH", Integer.class),
+            new JdbcColumnMeta(null, null, "DECIMAL_DIGITS", Short.class),
+            new JdbcColumnMeta(null, null, "PSEUDO_COLUMN", Short.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getPrimaryKeys(String catalog, String schema, String tbl) throws SQLException {
+        conn.ensureNotClosed();
+
+        final List<JdbcColumnMeta> meta = asList(
+            new JdbcColumnMeta(null, null, "TABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "KEY_SEQ", Short.class),
+            new JdbcColumnMeta(null, null, "PK_NAME", String.class));
+
+        if (!isValidCatalog(catalog))
+            return new JdbcResultSet(Collections.emptyList(), meta);
+
+        JdbcMetaPrimaryKeysResult res = conn.handler().primaryKeysMeta(new JdbcMetaPrimaryKeysRequest(schema, tbl));
+
+        if (!res.hasResults())
+            throw IgniteQueryErrorCode.createJdbcSqlException(res.err(), res.status());
+
+        List<List<Object>> rows = new LinkedList<>();
+
+        for (JdbcPrimaryKeyMeta pkMeta : res.meta())
+            rows.addAll(primaryKeyRows(pkMeta));
+
+        return new JdbcResultSet(rows, meta);
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getImportedKeys(String catalog, String schema, String tbl) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "PKTABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "PKTABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "PKTABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "PKCOLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "FKTABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "FKTABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "FKTABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "FKCOLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "KEY_SEQ", Short.class),
+            new JdbcColumnMeta(null, null, "UPDATE_RULE", Short.class),
+            new JdbcColumnMeta(null, null, "DELETE_RULE", Short.class),
+            new JdbcColumnMeta(null, null, "FK_NAME", String.class),
+            new JdbcColumnMeta(null, null, "PK_NAME", String.class),
+            new JdbcColumnMeta(null, null, "DEFERRABILITY", Short.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getExportedKeys(String catalog, String schema, String tbl) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "PKTABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "PKTABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "PKTABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "PKCOLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "FKTABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "FKTABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "FKTABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "FKCOLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "KEY_SEQ", Short.class),
+            new JdbcColumnMeta(null, null, "UPDATE_RULE", Short.class),
+            new JdbcColumnMeta(null, null, "DELETE_RULE", Short.class),
+            new JdbcColumnMeta(null, null, "FK_NAME", String.class),
+            new JdbcColumnMeta(null, null, "PK_NAME", String.class),
+            new JdbcColumnMeta(null, null, "DEFERRABILITY", Short.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTbl,
+        String foreignCatalog, String foreignSchema, String foreignTbl) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "PKTABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "PKTABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "PKTABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "PKCOLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "FKTABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "FKTABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "FKTABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "FKCOLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "KEY_SEQ", Short.class),
+            new JdbcColumnMeta(null, null, "UPDATE_RULE", Short.class),
+            new JdbcColumnMeta(null, null, "DELETE_RULE", Short.class),
+            new JdbcColumnMeta(null, null, "FK_NAME", String.class),
+            new JdbcColumnMeta(null, null, "PK_NAME", String.class),
+            new JdbcColumnMeta(null, null, "DEFERRABILITY", Short.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getTypeInfo() {
+        List<List<Object>> types = new ArrayList<>(21);
+
+        types.add(Arrays.asList("BOOLEAN", Types.BOOLEAN, 1, null, null, null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "BOOLEAN", 0, 0,
+            Types.BOOLEAN, 0, 10));
+
+        types.add(Arrays.asList("TINYINT", Types.TINYINT, 3, null, null, null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "TINYINT", 0, 0,
+            Types.TINYINT, 0, 10));
+
+        types.add(Arrays.asList("SMALLINT", Types.SMALLINT, 5, null, null, null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "SMALLINT", 0, 0,
+            Types.SMALLINT, 0, 10));
+
+        types.add(Arrays.asList("INTEGER", Types.INTEGER, 10, null, null, null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "INTEGER", 0, 0,
+            Types.INTEGER, 0, 10));
+
+        types.add(Arrays.asList("BIGINT", Types.BIGINT, 19, null, null, null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "BIGINT", 0, 0,
+            Types.BIGINT, 0, 10));
+
+        types.add(Arrays.asList("FLOAT", Types.FLOAT, 17, null, null, null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "FLOAT", 0, 0,
+            Types.FLOAT, 0, 10));
+
+        types.add(Arrays.asList("REAL", Types.REAL, 7, null, null, null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "REAL", 0, 0,
+            Types.REAL, 0, 10));
+
+        types.add(Arrays.asList("DOUBLE", Types.DOUBLE, 17, null, null, null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "DOUBLE", 0, 0,
+            Types.DOUBLE, 0, 10));
+
+        types.add(Arrays.asList("NUMERIC", Types.NUMERIC, Integer.MAX_VALUE, null, null, "PRECISION,SCALE",
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "NUMERIC", 0, 0,
+            Types.NUMERIC, 0, 10));
+
+        types.add(Arrays.asList("DECIMAL", Types.DECIMAL, Integer.MAX_VALUE, null, null, "PRECISION,SCALE",
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "DECIMAL", 0, 0,
+            Types.DECIMAL, 0, 10));
+
+        types.add(Arrays.asList("DATE", Types.DATE, 8, "DATE '", "'", null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "DATE", 0, 0,
+            Types.DATE, 0, null));
+
+        types.add(Arrays.asList("TIME", Types.TIME, 6, "TIME '", "'", null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "TIME", 0, 0,
+            Types.TIME, 0, null));
+
+        types.add(Arrays.asList("TIMESTAMP", Types.TIMESTAMP, 23, "TIMESTAMP '", "'", null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "TIMESTAMP", 0, 10,
+            Types.TIMESTAMP, 0, null));
+
+        types.add(Arrays.asList("CHAR", Types.CHAR, Integer.MAX_VALUE, "'", "'", "LENGTH",
+            (short)typeNullable, true, (short)typeSearchable, false, false, false, "CHAR", 0, 0,
+            Types.CHAR, 0, null));
+
+        types.add(Arrays.asList("VARCHAR", Types.VARCHAR, Integer.MAX_VALUE, "'", "'", "LENGTH",
+            (short)typeNullable, true, (short)typeSearchable, false, false, false, "VARCHAR", 0, 0,
+            Types.VARCHAR, 0, null));
+
+        types.add(Arrays.asList("LONGVARCHAR", Types.LONGVARCHAR, Integer.MAX_VALUE, "'", "'", "LENGTH",
+            (short)typeNullable, true, (short)typeSearchable, false, false, false, "LONGVARCHAR", 0, 0,
+            Types.LONGVARCHAR, 0, null));
+
+        types.add(Arrays.asList("BINARY", Types.BINARY, Integer.MAX_VALUE, "'", "'", "LENGTH",
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "BINARY", 0, 0,
+            Types.BINARY, 0, null));
+
+        types.add(Arrays.asList("VARBINARY", Types.VARBINARY, Integer.MAX_VALUE, "'", "'", "LENGTH",
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "VARBINARY", 0, 0,
+            Types.VARBINARY, 0, null));
+
+        types.add(Arrays.asList("LONGVARBINARY", Types.LONGVARBINARY, Integer.MAX_VALUE, "'", "'", "LENGTH",
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "LONGVARBINARY", 0, 0,
+            Types.LONGVARBINARY, 0, null));
+
+        types.add(Arrays.asList("OTHER", Types.OTHER, Integer.MAX_VALUE, "'", "'", "LENGTH",
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "OTHER", 0, 0,
+            Types.OTHER, 0, null));
+
+        types.add(Arrays.asList("ARRAY", Types.ARRAY, 0, "(", "')", null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "ARRAY", 0, 0,
+            Types.ARRAY, 0, null));
+
+        return new JdbcResultSet(types, asList(
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "DATA_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "PRECISION", Integer.class),
+            new JdbcColumnMeta(null, null, "LITERAL_PREFIX", String.class),
+            new JdbcColumnMeta(null, null, "LITERAL_SUFFIX", String.class),
+            new JdbcColumnMeta(null, null, "CREATE_PARAMS", String.class),
+            new JdbcColumnMeta(null, null, "NULLABLE", Short.class),
+            new JdbcColumnMeta(null, null, "CASE_SENSITIVE", Boolean.class),
+            new JdbcColumnMeta(null, null, "SEARCHABLE", Short.class),
+            new JdbcColumnMeta(null, null, "UNSIGNED_ATTRIBUTE", Boolean.class),
+            new JdbcColumnMeta(null, null, "FIXED_PREC_SCALE", Boolean.class),
+            new JdbcColumnMeta(null, null, "AUTO_INCREMENT", Boolean.class),
+            new JdbcColumnMeta(null, null, "LOCAL_TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "MINIMUM_SCALE", Short.class),
+            new JdbcColumnMeta(null, null, "MAXIMUM_SCALE", Short.class),
+            new JdbcColumnMeta(null, null, "SQL_DATA_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "SQL_DATETIME_SUB", Integer.class),
+            new JdbcColumnMeta(null, null, "NUM_PREC_RADIX", Integer.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getIndexInfo(String catalog, String schema, String tbl, boolean unique,
+        boolean approximate) throws SQLException {
+        conn.ensureNotClosed();
+
+        final List<JdbcColumnMeta> meta = asList(
+            new JdbcColumnMeta(null, null, "TABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "NON_UNIQUE", Boolean.class),
+            new JdbcColumnMeta(null, null, "INDEX_QUALIFIER", String.class),
+            new JdbcColumnMeta(null, null, "INDEX_NAME", String.class),
+            new JdbcColumnMeta(null, null, "TYPE", Short.class),
+            new JdbcColumnMeta(null, null, "ORDINAL_POSITION", Short.class),
+            new JdbcColumnMeta(null, null, "COLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "ASC_OR_DESC", String.class),
+            new JdbcColumnMeta(null, null, "CARDINALITY", Integer.class),
+            new JdbcColumnMeta(null, null, "PAGES", Integer.class),
+            new JdbcColumnMeta(null, null, "FILTER_CONDITION", String.class));
+
+        if (!isValidCatalog(catalog))
+            return new JdbcResultSet(Collections.emptyList(), meta);
+
+        throw new UnsupportedOperationException("Index info is not supported yet.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsResultSetType(int type) {
+        return type == TYPE_FORWARD_ONLY;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsResultSetConcurrency(int type, int concurrency) {
+        return supportsResultSetType(type) && concurrency == CONCUR_READ_ONLY;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean ownUpdatesAreVisible(int type) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean ownDeletesAreVisible(int type) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean ownInsertsAreVisible(int type) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean othersUpdatesAreVisible(int type) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean othersDeletesAreVisible(int type) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean othersInsertsAreVisible(int type) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean updatesAreDetected(int type) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean deletesAreDetected(int type) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean insertsAreDetected(int type) {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsBatchUpdates() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getUDTs(String catalog, String schemaPtrn, String typeNamePtrn,
+        int[] types) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "TYPE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TYPE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "CLASS_NAME", String.class),
+            new JdbcColumnMeta(null, null, "DATA_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "REMARKS", String.class),
+            new JdbcColumnMeta(null, null, "BASE_TYPE", Short.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public Connection getConnection() {
+        return conn;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSavepoints() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsNamedParameters() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMultipleOpenResults() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsGetGeneratedKeys() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getSuperTypes(String catalog, String schemaPtrn,
+        String typeNamePtrn) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "TYPE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TYPE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "SUPERTYPE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "SUPERTYPE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "SUPERTYPE_NAME", String.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getSuperTables(String catalog, String schemaPtrn,
+        String tblNamePtrn) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "TABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "SUPERTABLE_NAME", String.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getAttributes(String catalog, String schemaPtrn, String typeNamePtrn,
+        String attributeNamePtrn) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "TYPE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TYPE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "ATTR_NAME", String.class),
+            new JdbcColumnMeta(null, null, "DATA_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "ATTR_TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "ATTR_SIZE", Integer.class),
+            new JdbcColumnMeta(null, null, "DECIMAL_DIGITS", Integer.class),
+            new JdbcColumnMeta(null, null, "NUM_PREC_RADIX", Integer.class),
+            new JdbcColumnMeta(null, null, "NULLABLE", Integer.class),
+            new JdbcColumnMeta(null, null, "REMARKS", String.class),
+            new JdbcColumnMeta(null, null, "ATTR_DEF", String.class),
+            new JdbcColumnMeta(null, null, "SQL_DATA_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "SQL_DATETIME_SUB", Integer.class),
+            new JdbcColumnMeta(null, null, "CHAR_OCTET_LENGTH", Integer.class),
+            new JdbcColumnMeta(null, null, "ORDINAL_POSITION", Integer.class),
+            new JdbcColumnMeta(null, null, "IS_NULLABLE", String.class),
+            new JdbcColumnMeta(null, null, "SCOPE_CATALOG", String.class),
+            new JdbcColumnMeta(null, null, "SCOPE_SCHEMA", String.class),
+            new JdbcColumnMeta(null, null, "SCOPE_TABLE", String.class),
+            new JdbcColumnMeta(null, null, "SOURCE_DATA_TYPE", Short.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsResultSetHoldability(int holdability) {
+        return holdability == HOLD_CURSORS_OVER_COMMIT;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getResultSetHoldability() {
+        return HOLD_CURSORS_OVER_COMMIT;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getDatabaseMajorVersion() {
+        return ProtocolVersion.LATEST_VER.major();
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getDatabaseMinorVersion() {
+        return ProtocolVersion.LATEST_VER.minor();
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getJDBCMajorVersion() {
+        return 4;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getJDBCMinorVersion() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getSQLStateType() {
+        return DatabaseMetaData.sqlStateSQL99;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean locatorsUpdateCopy() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsStatementPooling() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public RowIdLifetime getRowIdLifetime() {
+        return ROWID_UNSUPPORTED;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getSchemas(String catalog, String schemaPtrn) throws SQLException {
+        conn.ensureNotClosed();
+
+        final List<JdbcColumnMeta> meta = asList(
+            new JdbcColumnMeta(null, null, "TABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_CATALOG", String.class)
+        );
+
+        if (!isValidCatalog(catalog))
+            return new JdbcResultSet(Collections.emptyList(), meta);
+
+        JdbcMetaSchemasResult res = conn.handler().schemasMeta(new JdbcMetaSchemasRequest(schemaPtrn));
+
+        if (!res.hasResults())
+            throw IgniteQueryErrorCode.createJdbcSqlException(res.err(), res.status());
+
+        List<List<Object>> rows = new LinkedList<>();
+
+        for (String schema : res.schemas()) {
+            List<Object> row = new ArrayList<>(2);
+
+            row.add(schema);
+            row.add(CATALOG_NAME);
+
+            rows.add(row);
+        }
+
+        return new JdbcResultSet(rows, meta);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsStoredFunctionsUsingCallSyntax() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean autoCommitFailureClosesAllResultSets() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getClientInfoProperties() {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "NAME", String.class),
+            new JdbcColumnMeta(null, null, "MAX_LEN", Integer.class),
+            new JdbcColumnMeta(null, null, "DEFAULT_VALUE", String.class),
+            new JdbcColumnMeta(null, null, "DESCRIPTION", String.class)
+        ));
+    }
+
+    //TODO IGNITE-15529 List all supported functions
+    /** {@inheritDoc} */
+    @Override public ResultSet getFunctions(String catalog, String schemaPtrn,
+        String functionNamePtrn) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "FUNCTION_CAT", String.class),
+            new JdbcColumnMeta(null, null, "FUNCTION_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "FUNCTION_NAME", String.class),
+            new JdbcColumnMeta(null, null, "REMARKS", String.class),
+            new JdbcColumnMeta(null, null, "FUNCTION_TYPE", String.class),
+            new JdbcColumnMeta(null, null, "SPECIFIC_NAME", String.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getFunctionColumns(String catalog, String schemaPtrn, String functionNamePtrn,
+        String colNamePtrn) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "FUNCTION_CAT", String.class),
+            new JdbcColumnMeta(null, null, "FUNCTION_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "FUNCTION_NAME", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_TYPE", Short.class),
+            new JdbcColumnMeta(null, null, "DATA_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "PRECISION", Integer.class),
+            new JdbcColumnMeta(null, null, "LENGTH", Integer.class),
+            new JdbcColumnMeta(null, null, "SCALE", Short.class),
+            new JdbcColumnMeta(null, null, "RADIX", Short.class),
+            new JdbcColumnMeta(null, null, "NULLABLE", Short.class),
+            new JdbcColumnMeta(null, null, "REMARKS", String.class),
+            new JdbcColumnMeta(null, null, "CHAR_OCTET_LENGTH", Integer.class),
+            new JdbcColumnMeta(null, null, "ORDINAL_POSITION", Integer.class),
+            new JdbcColumnMeta(null, null, "IS_NULLABLE", String.class),
+            new JdbcColumnMeta(null, null, "SPECIFIC_NAME", String.class)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T> T unwrap(Class<T> iface) throws SQLException {
+        if (!isWrapperFor(iface))
+            throw new SQLException("Database meta data is not a wrapper for " + iface.getName());
+
+        return (T)this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isWrapperFor(Class<?> iface) {
+        return iface != null && iface.isAssignableFrom(JdbcDatabaseMetadata.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getPseudoColumns(String catalog, String schemaPtrn, String tblNamePtrn,
+        String colNamePtrn) {
+        return new JdbcResultSet(Collections.emptyList(), asList(
+            new JdbcColumnMeta(null, null, "TABLE_CAT", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_NAME", String.class),
+            new JdbcColumnMeta(null, null, "DATA_TYPE", Integer.class),
+            new JdbcColumnMeta(null, null, "COLUMN_SIZE", Integer.class),
+            new JdbcColumnMeta(null, null, "DECIMAL_DIGITS", Integer.class),
+            new JdbcColumnMeta(null, null, "NUM_PREC_RADIX", Integer.class),
+            new JdbcColumnMeta(null, null, "COLUMN_USAGE", Integer.class),
+            new JdbcColumnMeta(null, null, "REMARKS", String.class),
+            new JdbcColumnMeta(null, null, "CHAR_OCTET_LENGTH", Integer.class),
+            new JdbcColumnMeta(null, null, "IS_NULLABLE", String.class)
+        ));
+    }
+
+    /**
+     * Checks if specified catalog matches the only possible catalog value. See {@link JdbcDatabaseMetadata#CATALOG_NAME}.
+     *
+     * @param catalog Catalog name or {@code null}.
+     * @return {@code true} If catalog equal ignoring case to {@link JdbcDatabaseMetadata#CATALOG_NAME}
+     * or null (which means any catalog).
+     *  Otherwise returns {@code false}.
+     */
+    private static boolean isValidCatalog(String catalog) {
+        return catalog == null || catalog.equalsIgnoreCase(CATALOG_NAME);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean generatedKeyAlwaysReturned() {
+        return false;
+    }
+
+    /**
+     * Constructs a list of rows in jdbc format for a given table metadata.
+     *
+     * @param tblMeta Table metadata.
+     * @return Table metadata row.
+     */
+    private List<Object> tableRow(JdbcTableMeta tblMeta) {
+        List<Object> row = new ArrayList<>(10);
+
+        row.add(CATALOG_NAME);
+        row.add(tblMeta.schemaName());
+        row.add(tblMeta.tableName());
+        row.add(tblMeta.tableType());
+        row.add(null);
+        row.add(null);
+        row.add(null);
+        row.add(null);
+        row.add(null);
+        row.add(null);
+
+        return row;
+    }
+
+    /**
+     * Constructs a list of rows in jdbc format for a given column metadata.
+     *
+     * @param colMeta Column metadata.
+     * @param pos Ordinal position.
+     * @return Column metadata row.
+     */
+    public static List<Object> columnRow(JdbcColumnMeta colMeta, int pos) {
+        List<Object> row = new ArrayList<>(24);
+
+        row.add(CATALOG_NAME);                  // 1. TABLE_CAT
+        row.add(colMeta.schemaName());          // 2. TABLE_SCHEM
+        row.add(colMeta.tableName());           // 3. TABLE_NAME
+        row.add(colMeta.columnName());          // 4. COLUMN_NAME
+        row.add(colMeta.dataType());            // 5. DATA_TYPE
+        row.add(colMeta.dataTypeName());        // 6. TYPE_NAME
+        row.add(colMeta.precision() == -1 ? null : colMeta.precision()); // 7. COLUMN_SIZE
+        row.add((Integer)null);                 // 8. BUFFER_LENGTH
+        row.add(colMeta.scale() == -1 ? null : colMeta.scale());           // 9. DECIMAL_DIGITS
+        row.add(10);                            // 10. NUM_PREC_RADIX
+        row.add(colMeta.isNullable() ? columnNullable : columnNoNulls);  // 11. NULLABLE
+        row.add((String)null);                  // 12. REMARKS
+        row.add(colMeta.defaultValue());        // 13. COLUMN_DEF
+        row.add(colMeta.dataType());            // 14. SQL_DATA_TYPE
+        row.add((Integer)null);                 // 15. SQL_DATETIME_SUB
+        row.add(Integer.MAX_VALUE);             // 16. CHAR_OCTET_LENGTH
+        row.add(pos);                           // 17. ORDINAL_POSITION
+        row.add(colMeta.isNullable() ? "YES" : "NO"); // 18. IS_NULLABLE
+        row.add((String)null);                  // 19. SCOPE_CATALOG
+        row.add((String)null);                  // 20. SCOPE_SCHEMA
+        row.add((String)null);                  // 21. SCOPE_TABLE
+        row.add((Short)null);                   // 22. SOURCE_DATA_TYPE
+        row.add("NO");                          // 23. IS_AUTOINCREMENT
+        row.add("NO");                          // 23. IS_GENERATEDCOLUMN
+
+        return row;
+    }
+
+    /**
+     * Constructs a list of rows in jdbc format for a given primary key metadata.
+     *
+     * @param pkMeta Primary key metadata.
+     * @return Result set rows for primary key.
+     */
+    private static List<List<Object>> primaryKeyRows(JdbcPrimaryKeyMeta pkMeta) {
+        List<List<Object>> rows = new ArrayList<>(pkMeta.fields().size());
+
+        for (int i = 0; i < pkMeta.fields().size(); ++i) {
+            List<Object> row = new ArrayList<>(6);
+
+            row.add(CATALOG_NAME); // table catalog
+            row.add(pkMeta.schemaName());
+            row.add(pkMeta.tableName());
+            row.add(pkMeta.fields().get(i));
+            row.add(i + 1); // sequence number
+            row.add(pkMeta.name());
+
+            rows.add(row);
+        }
+
+        return rows;
+    }
+}
diff --git a/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcResultSet.java b/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcResultSet.java
index eeab095..8d4a61a 100644
--- a/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcResultSet.java
+++ b/modules/client/src/main/java/org/apache/ignite/internal/jdbc/JdbcResultSet.java
@@ -47,6 +47,7 @@
 import java.time.LocalDate;
 import java.time.LocalTime;
 import java.util.Calendar;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -55,6 +56,7 @@
 import org.apache.ignite.client.proto.query.IgniteQueryErrorCode;
 import org.apache.ignite.client.proto.query.JdbcQueryEventHandler;
 import org.apache.ignite.client.proto.query.SqlStateCode;
+import org.apache.ignite.client.proto.query.event.JdbcColumnMeta;
 import org.apache.ignite.client.proto.query.event.QueryCloseRequest;
 import org.apache.ignite.client.proto.query.event.QueryCloseResult;
 import org.apache.ignite.client.proto.query.event.QueryFetchRequest;
@@ -87,6 +89,15 @@
     /** Cursor ID. */
     private final Long cursorId;
 
+    /** Jdbc column metadata. */
+    private List<JdbcColumnMeta> meta;
+
+    /** Metadata initialization flag. */
+    private boolean metaInit;
+
+    /** Column order map. */
+    private Map<String, Integer> colOrder;
+
     /** Rows. */
     private List<List<Object>> rows;
 
@@ -163,6 +174,27 @@
             this.updCnt = updCnt;
     }
 
+    /**
+     * Creates new result set.
+     *
+     * @param rows Rows.
+     * @param meta Column metadata.
+     */
+    public JdbcResultSet(List<List<Object>> rows, List<JdbcColumnMeta> meta) {
+        stmt = null;
+        cursorId = null;
+
+        finished = true;
+        isQuery = true;
+
+        this.rows = rows;
+        this.rowsIter = rows.iterator();
+        this.meta = meta;
+        this.metaInit = true;
+
+        initColumnOrder();
+    }
+
     /** {@inheritDoc} */
     @Override public boolean next() throws SQLException {
         ensureNotClosed();
@@ -690,7 +722,6 @@
         ensureNotClosed();
 
         throw new SQLFeatureNotSupportedException("ResultSetMetaData are not supported.");
-
     }
 
     /** {@inheritDoc} */
@@ -699,8 +730,17 @@
 
         Objects.requireNonNull(colLb);
 
-        throw new SQLFeatureNotSupportedException("FindColumn by column label are not supported.");
+        if (!metaInit)
+            throw new SQLFeatureNotSupportedException("FindColumn by column label are not supported.");
 
+        Integer order = columnOrder().get(colLb.toUpperCase());
+
+        if (order == null)
+            throw new SQLException("Column not found: " + colLb, SqlStateCode.PARSING_EXCEPTION);
+
+        assert order >= 0;
+
+        return order + 1;
     }
 
     /** {@inheritDoc} */
@@ -1888,4 +1928,49 @@
                     SqlStateCode.CONVERSION_FAILED);
         }
     }
+
+    /**
+     * Init if needed and return column order.
+     *
+     * @return Column order map.
+     * @throws SQLException On error.
+     */
+    private Map<String, Integer> columnOrder() throws SQLException {
+        if (colOrder != null)
+            return colOrder;
+
+        if (!metaInit)
+            meta();
+
+        initColumnOrder();
+
+        return colOrder;
+    }
+
+    /**
+     * Init column order map.
+     */
+    private void initColumnOrder() {
+        colOrder = new HashMap<>(meta.size());
+
+        for (int i = 0; i < meta.size(); ++i) {
+            String colName = meta.get(i).columnName().toUpperCase();
+
+            if (!colOrder.containsKey(colName))
+                colOrder.put(colName, i);
+        }
+    }
+
+    /**
+     * Returns columns metadata list.
+     *
+     * @return Results metadata.
+     * @throws SQLException On error.
+     */
+    private List<JdbcColumnMeta> meta() throws SQLException {
+        if (finished && (!isQuery || autoClose))
+            throw new SQLException("Server cursor is already closed.", SqlStateCode.INVALID_CURSOR_STATE);
+
+        throw new SQLFeatureNotSupportedException("ResultSetMetaData are not supported.");
+    }
 }
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/JdbcConnectionSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/JdbcConnectionSelfTest.java
index 8427299..afe3bf2 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/JdbcConnectionSelfTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/JdbcConnectionSelfTest.java
@@ -82,7 +82,7 @@
      */
     @SuppressWarnings({"EmptyTryBlock", "unused"})
     @Test
-    @Disabled
+    @Disabled("ITDS-1887")
     public void testDefaultsIPv6() throws Exception {
         var url = "jdbc:ignite:thin://[::1]:10800";
 
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/JdbcMetadataSelfTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/JdbcMetadataSelfTest.java
new file mode 100644
index 0000000..7818ec7
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/jdbc/JdbcMetadataSelfTest.java
@@ -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.
+ */
+
+package org.apache.ignite.internal.runner.app.jdbc;
+
+import java.math.BigDecimal;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.DriverManager;
+import java.sql.ParameterMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.ignite.app.Ignite;
+import org.apache.ignite.app.IgnitionManager;
+import org.apache.ignite.internal.client.proto.ProtocolVersion;
+import org.apache.ignite.internal.schema.configuration.SchemaConfigurationConverter;
+import org.apache.ignite.jdbc.IgniteJdbcDriver;
+import org.apache.ignite.schema.ColumnType;
+import org.apache.ignite.schema.SchemaBuilders;
+import org.apache.ignite.schema.SchemaTable;
+import org.apache.ignite.table.Table;
+import org.apache.ignite.table.Tuple;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static java.sql.Types.DATE;
+import static java.sql.Types.DECIMAL;
+import static java.sql.Types.INTEGER;
+import static java.sql.Types.VARCHAR;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Metadata tests.
+ */
+public class JdbcMetadataSelfTest {
+    /** URL. */
+    protected static final String URL = "jdbc:ignite:thin://127.0.1.1:10800";
+
+    /** Nodes bootstrap configuration. */
+    private static final Map<String, String> nodesBootstrapCfg = new LinkedHashMap<>() {{
+        put("node2", "{\n" +
+            "  \"node\": {\n" +
+            "    \"metastorageNodes\":[ \"node2\" ]\n" +
+            "  }\n" +
+            "}");
+    }};
+
+    /** Cluster nodes. */
+    protected static final List<Ignite> clusterNodes = new ArrayList<>();
+
+    /**
+     * Creates a cluster of three nodes.
+     *
+     * @param temp Temporal directory.
+     */
+    @BeforeAll
+    public static void beforeAll(@TempDir Path temp) {
+        IgniteJdbcDriver.register();
+
+        nodesBootstrapCfg.forEach((nodeName, configStr) ->
+            clusterNodes.add(IgnitionManager.start(nodeName, configStr, temp.resolve(nodeName)))
+        );
+    }
+
+    /**
+     * Close all cluster nodes.
+     *
+     * @throws Exception if failed.
+     */
+    @AfterAll
+    public static void afterAll() throws Exception {
+        for (Ignite clusterNode : clusterNodes) {
+            clusterNode.close();
+        }
+    }
+
+    /**
+     * Create the connection ant statement.
+     *
+     * @throws Exception if failed.
+     */
+    @BeforeEach
+    public void beforeTest() {
+        // Create table on node 0.
+        SchemaTable perTbl = SchemaBuilders.tableBuilder("PUBLIC", "PERSON").columns(
+            SchemaBuilders.column("NAME", ColumnType.string()).asNullable().build(),
+            SchemaBuilders.column("AGE", ColumnType.INT32).asNullable().build(),
+            SchemaBuilders.column("ORGID", ColumnType.INT32).asNonNull().build()
+        ).withPrimaryKey("ORGID").build();
+
+        SchemaTable orgTbl = SchemaBuilders.tableBuilder("PUBLIC", "ORGANIZATION").columns(
+            SchemaBuilders.column("ID", ColumnType.INT32).asNonNull().build(),
+            SchemaBuilders.column("NAME", ColumnType.string()).asNullable().build(),
+            SchemaBuilders.column("BIGDATA", ColumnType.decimalOf(20, 10)).asNullable().build()
+        ).withPrimaryKey("ID").build();
+
+        if (clusterNodes.get(0).tables().table(perTbl.canonicalName()) != null)
+            return;
+
+        clusterNodes.get(0).tables().createTable(perTbl.canonicalName(), tblCh ->
+            SchemaConfigurationConverter.convert(perTbl, tblCh)
+                .changeReplicas(1)
+                .changePartitions(10)
+        );
+
+        clusterNodes.get(0).tables().createTable(orgTbl.canonicalName(), tblCh ->
+            SchemaConfigurationConverter.convert(orgTbl, tblCh)
+                .changeReplicas(1)
+                .changePartitions(10)
+        );
+
+        Table tbl1 = clusterNodes.get(0).tables().table(perTbl.canonicalName());
+        Table tbl2 = clusterNodes.get(0).tables().table(orgTbl.canonicalName());
+
+        tbl1.insert(Tuple.create().set("ORGID", 1).set("NAME", "111").set("AGE", 111));
+        tbl2.insert(Tuple.create().set("ID", 1).set("NAME", "AAA").set("BIGDATA", BigDecimal.valueOf(10)));
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    @Disabled("IGNITE-15187")
+    public void testResultSetMetaData() throws Exception {
+        Statement stmt = DriverManager.getConnection(URL).createStatement();
+
+        ResultSet rs = stmt.executeQuery(
+            "select p.name, o.id as orgId, p.age from PERSON p, ORGANIZATION o where p.orgId = o.id");
+
+        assertNotNull(rs);
+
+        ResultSetMetaData meta = rs.getMetaData();
+
+        assertNotNull(meta);
+
+        assertEquals(3, meta.getColumnCount());
+
+        assertEquals("Person".toUpperCase(), meta.getTableName(1).toUpperCase());
+        assertEquals("name".toUpperCase(), meta.getColumnName(1).toUpperCase());
+        assertEquals("name".toUpperCase(), meta.getColumnLabel(1).toUpperCase());
+        assertEquals(VARCHAR, meta.getColumnType(1));
+        assertEquals(meta.getColumnTypeName(1), "VARCHAR");
+        assertEquals(meta.getColumnClassName(1), "java.lang.String");
+
+        assertEquals("Organization".toUpperCase(), meta.getTableName(2).toUpperCase());
+        assertEquals("orgId".toUpperCase(), meta.getColumnName(2).toUpperCase());
+        assertEquals("orgId".toUpperCase(), meta.getColumnLabel(2).toUpperCase());
+        assertEquals(INTEGER, meta.getColumnType(2));
+        assertEquals(meta.getColumnTypeName(2), "INTEGER");
+        assertEquals(meta.getColumnClassName(2), "java.lang.Integer");
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    @Disabled
+    public void testDecimalAndDateTypeMetaData() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            Statement stmt = conn.createStatement();
+
+            ResultSet rs = stmt.executeQuery(
+                    "select t.decimal, t.date from \"metaTest\".MetaTest as t");
+
+            assertNotNull(rs);
+
+            ResultSetMetaData meta = rs.getMetaData();
+
+            assertNotNull(meta);
+
+            assertEquals(2, meta.getColumnCount());
+
+            assertEquals("METATEST", meta.getTableName(1).toUpperCase());
+            assertEquals("DECIMAL", meta.getColumnName(1).toUpperCase());
+            assertEquals("DECIMAL", meta.getColumnLabel(1).toUpperCase());
+            assertEquals(DECIMAL, meta.getColumnType(1));
+            assertEquals(meta.getColumnTypeName(1), "DECIMAL");
+            assertEquals(meta.getColumnClassName(1), "java.math.BigDecimal");
+
+            assertEquals("METATEST", meta.getTableName(2).toUpperCase());
+            assertEquals("DATE", meta.getColumnName(2).toUpperCase());
+            assertEquals("DATE", meta.getColumnLabel(2).toUpperCase());
+            assertEquals(DATE, meta.getColumnType(2));
+            assertEquals(meta.getColumnTypeName(2), "DATE");
+            assertEquals(meta.getColumnClassName(2), "java.sql.Date");
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testGetTables() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            DatabaseMetaData meta = conn.getMetaData();
+
+            ResultSet rs = meta.getTables("IGNITE", "PUBLIC", "%", new String[]{"TABLE"});
+            assertNotNull(rs);
+            assertTrue(rs.next());
+            assertEquals("TABLE", rs.getString("TABLE_TYPE"));
+            assertEquals("ORGANIZATION", rs.getString("TABLE_NAME"));
+            assertTrue(rs.next());
+            assertEquals("TABLE", rs.getString("TABLE_TYPE"));
+            assertEquals("PERSON", rs.getString("TABLE_NAME"));
+
+            rs = meta.getTables("IGNITE", "PUBLIC", "%", null);
+            assertNotNull(rs);
+            assertTrue(rs.next());
+            assertEquals("TABLE", rs.getString("TABLE_TYPE"));
+            assertEquals("ORGANIZATION", rs.getString("TABLE_NAME"));
+
+            rs = meta.getTables("IGNITE", "PUBLIC", "", new String[]{"WRONG"});
+            assertFalse(rs.next());
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testGetColumns() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            DatabaseMetaData meta = conn.getMetaData();
+
+            ResultSet rs = meta.getColumns("IGNITE", "PUBLIC", "PERSON", "%");
+
+            assertNotNull(rs);
+
+            Collection<String> names = new ArrayList<>(2);
+
+            names.add("NAME");
+            names.add("AGE");
+            names.add("ORGID");
+
+            int cnt = 0;
+
+            while (rs.next()) {
+                String name = rs.getString("COLUMN_NAME");
+
+                assertTrue(names.remove(name));
+
+                if ("NAME".equals(name)) {
+                    assertEquals(VARCHAR, rs.getInt("DATA_TYPE"));
+                    assertEquals(rs.getString("TYPE_NAME"), "VARCHAR");
+                    assertEquals(1, rs.getInt("NULLABLE"));
+                } else if ("AGE".equals(name)) {
+                    assertEquals(INTEGER, rs.getInt("DATA_TYPE"));
+                    assertEquals(rs.getString("TYPE_NAME"), "INTEGER");
+                    assertEquals(1, rs.getInt("NULLABLE"));
+                } else if ("ORGID".equals(name)) {
+                    assertEquals(INTEGER, rs.getInt("DATA_TYPE"));
+                    assertEquals(rs.getString("TYPE_NAME"), "INTEGER");
+                    assertEquals(0, rs.getInt("NULLABLE"));
+
+                }
+                cnt++;
+            }
+
+            assertTrue(names.isEmpty());
+            assertEquals(3, cnt);
+
+            rs = meta.getColumns("IGNITE", "PUBLIC", "ORGANIZATION", "%");
+
+            assertNotNull(rs);
+
+            names.add("ID");
+            names.add("NAME");
+            names.add("BIGDATA");
+
+            cnt = 0;
+
+            while (rs.next()) {
+                String name = rs.getString("COLUMN_NAME");
+
+                assertTrue(names.remove(name));
+
+                if ("ID".equals(name)) {
+                    assertEquals(INTEGER, rs.getInt("DATA_TYPE"));
+                    assertEquals(rs.getString("TYPE_NAME"), "INTEGER");
+                    assertEquals(0, rs.getInt("NULLABLE"));
+                } else if ("NAME".equals(name)) {
+                    assertEquals(VARCHAR, rs.getInt("DATA_TYPE"));
+                    assertEquals(rs.getString("TYPE_NAME"), "VARCHAR");
+                    assertEquals(1, rs.getInt("NULLABLE"));
+                } else if ("BIGDATA".equals(name)) {
+                    assertEquals(DECIMAL, rs.getInt("DATA_TYPE"));
+                    assertEquals(rs.getString("TYPE_NAME"), "DECIMAL");
+                    assertEquals(1, rs.getInt("NULLABLE"));
+                    assertEquals(10, rs.getInt("DECIMAL_DIGITS"));
+                    assertEquals(20, rs.getInt("COLUMN_SIZE"));
+                }
+
+                cnt++;
+            }
+
+            assertTrue(names.isEmpty());
+            assertEquals(3, cnt);
+        }
+    }
+
+    /**
+     * Check JDBC support flags.
+     */
+    @Test
+    public void testCheckSupports() throws SQLException {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            DatabaseMetaData meta = conn.getMetaData();
+
+            assertTrue(meta.supportsANSI92EntryLevelSQL());
+            assertTrue(meta.supportsAlterTableWithAddColumn());
+            assertTrue(meta.supportsAlterTableWithDropColumn());
+            assertTrue(meta.nullPlusNonNullIsNull());
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testVersions() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            assertEquals(conn.getMetaData().getDatabaseProductVersion(), ProtocolVersion.LATEST_VER.toString(),
+                "Unexpected ignite database product version.");
+            assertEquals(conn.getMetaData().getDriverVersion(), ProtocolVersion.LATEST_VER.toString(),
+                "Unexpected ignite driver version.");
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testSchemasMetadata() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            ResultSet rs = conn.getMetaData().getSchemas();
+
+            Set<String> expectedSchemas = new HashSet<>(Arrays.asList("PUBLIC", "PUBLIC"));
+
+            Set<String> schemas = new HashSet<>();
+
+            while (rs.next())
+                schemas.add(rs.getString(1));
+
+            assertEquals(schemas, expectedSchemas);
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testEmptySchemasMetadata() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            ResultSet rs = conn.getMetaData().getSchemas(null, "qqq");
+
+            assertFalse(rs.next(), "Empty result set is expected");
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testPrimaryKeyMetadata() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL);
+             ResultSet rs = conn.getMetaData().getPrimaryKeys(null, "PUBLIC", "PERSON")) {
+
+            int cnt = 0;
+
+            while (rs.next()) {
+                assertEquals(rs.getString("COLUMN_NAME"), "ORGID");
+
+                cnt++;
+            }
+
+            assertEquals(1, cnt);
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testGetAllPrimaryKeys() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            ResultSet rs = conn.getMetaData().getPrimaryKeys(null, null, null);
+
+            Set<String> expectedPks = new HashSet<>(Arrays.asList(
+                "PUBLIC.ORGANIZATION.PK_ORGANIZATION.ID",
+                "PUBLIC.PERSON.PK_PERSON.ORGID"));
+
+            Set<String> actualPks = new HashSet<>(expectedPks.size());
+
+            while (rs.next()) {
+                actualPks.add(rs.getString("TABLE_SCHEM") +
+                    '.' + rs.getString("TABLE_NAME") +
+                    '.' + rs.getString("PK_NAME") +
+                    '.' + rs.getString("COLUMN_NAME"));
+            }
+
+            assertEquals(expectedPks, actualPks, "Metadata contains unexpected primary keys info.");
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testInvalidCatalog() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            DatabaseMetaData meta = conn.getMetaData();
+
+            ResultSet rs = meta.getSchemas("q", null);
+
+            assertFalse(rs.next(), "Results must be empty");
+
+            rs = meta.getTables("q", null, null, null);
+
+            assertFalse(rs.next(), "Results must be empty");
+
+            rs = meta.getColumns("q", null, null, null);
+
+            assertFalse(rs.next(), "Results must be empty");
+
+            rs = meta.getIndexInfo("q", null, null, false, false);
+
+            assertFalse(rs.next(), "Results must be empty");
+
+            rs = meta.getPrimaryKeys("q", null, null);
+
+            assertFalse(rs.next(), "Results must be empty");
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testGetTableTypes() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            DatabaseMetaData meta = conn.getMetaData();
+
+            ResultSet rs = meta.getTableTypes();
+
+            assertTrue(rs.next());
+
+            assertEquals("TABLE", rs.getString("TABLE_TYPE"));
+
+            assertFalse(rs.next());
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    @Disabled
+    public void testParametersMetadata() throws Exception {
+        // Perform checks few times due to query/plan caching.
+        for (int i = 0; i < 3; i++) {
+            // No parameters statement.
+            try (Connection conn = DriverManager.getConnection(URL)) {
+                conn.setSchema("\"pers\"");
+
+                PreparedStatement noParams = conn.prepareStatement("select * from Person;");
+                ParameterMetaData params = noParams.getParameterMetaData();
+
+                assertEquals(0, params.getParameterCount(), "Parameters should be empty.");
+            }
+
+            // Selects.
+            try (Connection conn = DriverManager.getConnection(URL)) {
+                conn.setSchema("\"pers\"");
+
+                PreparedStatement selectStmt = conn.prepareStatement("select orgId from Person p where p.name > ? and p.orgId > ?");
+
+                ParameterMetaData meta = selectStmt.getParameterMetaData();
+
+                assertNotNull(meta);
+
+                assertEquals(2, meta.getParameterCount());
+
+                assertEquals(VARCHAR, meta.getParameterType(1));
+                assertEquals(ParameterMetaData.parameterNullableUnknown, meta.isNullable(1));
+                assertEquals(Integer.MAX_VALUE, meta.getPrecision(1));
+
+                assertEquals(INTEGER, meta.getParameterType(2));
+                assertEquals(ParameterMetaData.parameterNullableUnknown, meta.isNullable(2));
+            }
+
+            // Updates.
+            try (Connection conn = DriverManager.getConnection(URL)) {
+                conn.setSchema("\"pers\"");
+
+                PreparedStatement updateStmt = conn.prepareStatement("update Person p set orgId = 42 where p.name > ? and p.orgId > ?");
+
+                ParameterMetaData meta = updateStmt.getParameterMetaData();
+
+                assertNotNull(meta);
+
+                assertEquals(2, meta.getParameterCount());
+
+                assertEquals(VARCHAR, meta.getParameterType(1));
+                assertEquals(ParameterMetaData.parameterNullableUnknown, meta.isNullable(1));
+                assertEquals(Integer.MAX_VALUE, meta.getPrecision(1));
+
+                assertEquals(INTEGER, meta.getParameterType(2));
+                assertEquals(ParameterMetaData.parameterNullableUnknown, meta.isNullable(2));
+            }
+
+            // Multistatement
+            try (Connection conn = DriverManager.getConnection(URL)) {
+                conn.setSchema("\"pers\"");
+
+                PreparedStatement updateStmt = conn.prepareStatement(
+                    "update Person p set orgId = 42 where p.name > ? and p.orgId > ?;" +
+                        "select orgId from Person p where p.name > ? and p.orgId > ?");
+
+                ParameterMetaData meta = updateStmt.getParameterMetaData();
+
+                assertNotNull(meta);
+
+                assertEquals(4, meta.getParameterCount());
+
+                assertEquals(VARCHAR, meta.getParameterType(1));
+                assertEquals(ParameterMetaData.parameterNullableUnknown, meta.isNullable(1));
+                assertEquals(Integer.MAX_VALUE, meta.getPrecision(1));
+
+                assertEquals(INTEGER, meta.getParameterType(2));
+                assertEquals(ParameterMetaData.parameterNullableUnknown, meta.isNullable(2));
+
+                assertEquals(VARCHAR, meta.getParameterType(3));
+                assertEquals(ParameterMetaData.parameterNullableUnknown, meta.isNullable(3));
+                assertEquals(Integer.MAX_VALUE, meta.getPrecision(3));
+
+                assertEquals(INTEGER, meta.getParameterType(4));
+                assertEquals(ParameterMetaData.parameterNullableUnknown, meta.isNullable(4));
+            }
+        }
+    }
+
+    /**
+     * Check that parameters metadata throws correct exception on non-parsable statement.
+     */
+    @Test
+    public void testParametersMetadataNegative() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            conn.setSchema("\"pers\"");
+
+            PreparedStatement notCorrect = conn.prepareStatement("select * from NotExistingTable;");
+
+            assertThrows(SQLException.class, notCorrect::getParameterMetaData, "Table \"NOTEXISTINGTABLE\" not found");
+        }
+    }
+
+    /**
+     * Negative scenarios for catalog name.
+     * Perform metadata lookups, that use incorrect catalog names.
+     */
+    @Test
+    public void testCatalogWithNotExistingName() throws SQLException {
+        checkNoEntitiesFoundForCatalog("");
+        checkNoEntitiesFoundForCatalog("NOT_EXISTING_CATALOG");
+    }
+
+    /**
+     * Check that lookup in the metadata have been performed using specified catalog name (that is neither {@code null}
+     * nor correct catalog name), empty result set is returned.
+     *
+     * @param invalidCat catalog name that is not either
+     */
+    private void checkNoEntitiesFoundForCatalog(String invalidCat) throws SQLException {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            DatabaseMetaData meta = conn.getMetaData();
+
+            // Intention: we set the other arguments that way, the values to have as many results as possible.
+            assertIsEmpty(meta.getTables(invalidCat, null, "%", new String[] {"TABLE"}));
+            assertIsEmpty(meta.getColumns(invalidCat, null, "%", "%"));
+            assertIsEmpty(meta.getColumnPrivileges(invalidCat, "pers", "PERSON", "%"));
+            assertIsEmpty(meta.getTablePrivileges(invalidCat, null, "%"));
+            assertIsEmpty(meta.getPrimaryKeys(invalidCat, "pers", "PERSON"));
+            assertIsEmpty(meta.getImportedKeys(invalidCat, "pers", "PERSON"));
+            assertIsEmpty(meta.getExportedKeys(invalidCat, "pers", "PERSON"));
+            // meta.getCrossReference(...) doesn't make sense because we don't have FK constraint.
+            assertIsEmpty(meta.getIndexInfo(invalidCat, null, "%", false, true));
+            assertIsEmpty(meta.getSuperTables(invalidCat, "%", "%"));
+            assertIsEmpty(meta.getSchemas(invalidCat, null));
+            assertIsEmpty(meta.getPseudoColumns(invalidCat, null, "%", ""));
+        }
+    }
+
+    /**
+     * Assert that specified ResultSet contains no rows.
+     *
+     * @param rs result set to check.
+     * @throws SQLException on error.
+     */
+    private static void assertIsEmpty(ResultSet rs) throws SQLException {
+        try {
+            boolean empty = !rs.next();
+
+            assertTrue(empty, "Result should be empty because invalid catalog is specified.");
+        }
+        finally {
+            rs.close();
+        }
+    }
+}