You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by vo...@apache.org on 2017/08/17 10:41:49 UTC

[3/3] ignite git commit: IGNITE-5233: JDBC thind driver: implemented metadata methods. This closes #2079.

IGNITE-5233: JDBC thind driver: implemented metadata methods. This closes #2079.


Project: http://git-wip-us.apache.org/repos/asf/ignite/repo
Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/0e803144
Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/0e803144
Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/0e803144

Branch: refs/heads/master
Commit: 0e8031444b2f0d68fd3fb5a9ba03ca4d6a0c4e2d
Parents: 37e58ba
Author: tledkov-gridgain <tl...@gridgain.com>
Authored: Thu Aug 17 13:41:37 2017 +0300
Committer: devozerov <vo...@gridgain.com>
Committed: Thu Aug 17 13:41:37 2017 +0300

----------------------------------------------------------------------
 .../jdbc/thin/JdbcThinMetadataSelfTest.java     |  337 +++-
 .../internal/jdbc/thin/JdbcThinConnection.java  |   20 +-
 .../jdbc/thin/JdbcThinDatabaseMetadata.java     | 1587 ++++++++++++++++++
 .../jdbc/thin/JdbcThinParameterMetadata.java    |  115 ++
 .../jdbc/thin/JdbcThinPreparedStatement.java    |   27 +-
 .../internal/jdbc/thin/JdbcThinResultSet.java   |   47 +-
 .../internal/jdbc/thin/JdbcThinStatement.java   |    4 +-
 .../internal/jdbc/thin/JdbcThinTcpIo.java       |  117 +-
 .../processors/odbc/SqlListenerNioListener.java |    9 +-
 .../odbc/SqlListenerRequestHandler.java         |    9 +
 .../odbc/jdbc/JdbcBatchExecuteRequest.java      |   20 +-
 .../odbc/jdbc/JdbcBatchExecuteResult.java       |    6 +
 .../processors/odbc/jdbc/JdbcColumnMeta.java    |   75 +-
 .../processors/odbc/jdbc/JdbcIndexMeta.java     |  192 +++
 .../odbc/jdbc/JdbcMetaColumnsRequest.java       |  102 ++
 .../odbc/jdbc/JdbcMetaColumnsResult.java        |   99 ++
 .../odbc/jdbc/JdbcMetaIndexesRequest.java       |   88 +
 .../odbc/jdbc/JdbcMetaIndexesResult.java        |   98 ++
 .../odbc/jdbc/JdbcMetaParamsRequest.java        |   87 +
 .../odbc/jdbc/JdbcMetaParamsResult.java         |   97 ++
 .../odbc/jdbc/JdbcMetaPrimaryKeysRequest.java   |   88 +
 .../odbc/jdbc/JdbcMetaPrimaryKeysResult.java    |   99 ++
 .../odbc/jdbc/JdbcMetaSchemasRequest.java       |   73 +
 .../odbc/jdbc/JdbcMetaSchemasResult.java        |   73 +
 .../odbc/jdbc/JdbcMetaTablesRequest.java        |   87 +
 .../odbc/jdbc/JdbcMetaTablesResult.java         |   97 ++
 .../processors/odbc/jdbc/JdbcParameterMeta.java |  165 ++
 .../odbc/jdbc/JdbcPrimaryKeyMeta.java           |  131 ++
 .../odbc/jdbc/JdbcQueryCloseRequest.java        |    4 +-
 .../odbc/jdbc/JdbcQueryExecuteRequest.java      |    8 +-
 .../odbc/jdbc/JdbcQueryExecuteResult.java       |   12 +-
 .../odbc/jdbc/JdbcQueryFetchRequest.java        |    4 +-
 .../odbc/jdbc/JdbcQueryFetchResult.java         |   12 +-
 .../odbc/jdbc/JdbcQueryMetadataRequest.java     |   18 +-
 .../odbc/jdbc/JdbcQueryMetadataResult.java      |   14 +-
 .../processors/odbc/jdbc/JdbcRequest.java       |   67 +-
 .../odbc/jdbc/JdbcRequestHandler.java           |  273 ++-
 .../processors/odbc/jdbc/JdbcResult.java        |   58 +-
 .../processors/odbc/jdbc/JdbcTableMeta.java     |   82 +
 .../processors/odbc/jdbc/JdbcUtils.java         |   37 +-
 .../odbc/odbc/OdbcRequestHandler.java           |    6 +
 .../processors/query/GridQueryProcessor.java    |    2 +-
 .../query/GridQueryTypeDescriptor.java          |    7 +
 .../query/QueryTypeDescriptorImpl.java          |   15 +
 .../internal/processors/query/QueryUtils.java   |    7 +-
 .../h2/GridIndexingSpiAbstractSelfTest.java     |   18 +-
 46 files changed, 4472 insertions(+), 121 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ignite/blob/0e803144/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMetadataSelfTest.java
----------------------------------------------------------------------
diff --git a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMetadataSelfTest.java b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMetadataSelfTest.java
index 2dae107..01b2e8a 100644
--- a/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMetadataSelfTest.java
+++ b/modules/clients/src/test/java/org/apache/ignite/jdbc/thin/JdbcThinMetadataSelfTest.java
@@ -21,16 +21,27 @@ import java.io.Serializable;
 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.Statement;
+import java.sql.Types;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Set;
 import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.QueryEntity;
+import org.apache.ignite.cache.QueryIndex;
 import org.apache.ignite.cache.affinity.AffinityKey;
-import org.apache.ignite.cache.query.annotations.QuerySqlField;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteVersionUtils;
+import org.apache.ignite.internal.binary.BinaryMarshaller;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
 import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder;
@@ -66,15 +77,18 @@ public class JdbcThinMetadataSelfTest extends JdbcThinAbstractSelfTest {
     }
 
     /**
+     * @param qryEntity Query entity.
      * @return Cache configuration.
      */
-    protected CacheConfiguration cacheConfiguration() {
+    protected CacheConfiguration cacheConfiguration(QueryEntity qryEntity) {
         CacheConfiguration<?,?> cache = defaultCacheConfiguration();
 
         cache.setCacheMode(PARTITIONED);
         cache.setBackups(1);
         cache.setWriteSynchronizationMode(FULL_SYNC);
 
+        cache.setQueryEntities(Collections.singletonList(qryEntity));
+
         return cache;
     }
 
@@ -84,22 +98,49 @@ public class JdbcThinMetadataSelfTest extends JdbcThinAbstractSelfTest {
 
         startGridsMultiThreaded(3);
 
-        IgniteCache<String, Organization> orgCache = jcache(grid(0), cacheConfiguration(), "org",
-            String.class, Organization.class);
+        IgniteCache<String, Organization> orgCache = jcache(grid(0),
+            cacheConfiguration(new QueryEntity(String.class.getName(), Organization.class.getName())
+                .addQueryField("id", Integer.class.getName(), null)
+                .addQueryField("name", String.class.getName(), null)
+                .setIndexes(Arrays.asList(
+                    new QueryIndex("id"),
+                    new QueryIndex("name", false, "org_name_index")
+                ))), "org");
 
         assert orgCache != null;
 
         orgCache.put("o1", new Organization(1, "A"));
         orgCache.put("o2", new Organization(2, "B"));
 
-        IgniteCache<AffinityKey, Person> personCache = jcache(grid(0), cacheConfiguration(), "pers",
-            AffinityKey.class, Person.class);
+        LinkedHashMap<String, Boolean> persFields = new LinkedHashMap<>();
+
+        persFields.put("name", true);
+        persFields.put("age", false);
+
+        IgniteCache<AffinityKey, Person> personCache = jcache(grid(0), cacheConfiguration(
+            new QueryEntity(AffinityKey.class.getName(), Person.class.getName())
+                .addQueryField("name", String.class.getName(), null)
+                .addQueryField("age", Integer.class.getName(), null)
+                .addQueryField("orgId", Integer.class.getName(), null)
+                .setIndexes(Arrays.asList(
+                    new QueryIndex("orgId"),
+                    new QueryIndex().setFields(persFields)))
+            ), "pers");
 
         assert personCache != null;
 
         personCache.put(new AffinityKey<>("p1", "o1"), new Person("John White", 25, 1));
         personCache.put(new AffinityKey<>("p2", "o1"), new Person("Joe Black", 35, 1));
         personCache.put(new AffinityKey<>("p3", "o2"), new Person("Mike Green", 40, 2));
+
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            Statement stmt = conn.createStatement();
+
+            stmt.execute("CREATE TABLE TEST (ID INT, NAME VARCHAR(50), VAL VARCHAR(50), PRIMARY KEY (ID, NAME))");
+            stmt.execute("CREATE TABLE \"Quoted\" (\"Id\" INT primary key, \"Name\" VARCHAR(50))");
+            stmt.execute("CREATE INDEX \"MyTestIndex quoted\" on \"Quoted\" (\"Id\" DESC)");
+            stmt.execute("CREATE INDEX IDX ON TEST (ID ASC)");
+        }
     }
 
     /** {@inheritDoc} */
@@ -147,8 +188,6 @@ public class JdbcThinMetadataSelfTest extends JdbcThinAbstractSelfTest {
      * @throws Exception If failed.
      */
     public void testGetTables() throws Exception {
-        fail("https://issues.apache.org/jira/browse/IGNITE-5233");
-
         try (Connection conn = DriverManager.getConnection(URL)) {
             DatabaseMetaData meta = conn.getMetaData();
 
@@ -184,15 +223,43 @@ public class JdbcThinMetadataSelfTest extends JdbcThinAbstractSelfTest {
     /**
      * @throws Exception If failed.
      */
+    public void testGetAllTables() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            DatabaseMetaData meta = conn.getMetaData();
+
+            ResultSet rs = meta.getTables(null, null, null, null);
+
+            Set<String> expectedTbls = new HashSet<>(Arrays.asList(
+                "org.ORGANIZATION",
+                "pers.PERSON",
+                "PUBLIC.TEST",
+                "PUBLIC.Quoted"));
+
+            Set<String> actualTbls = new HashSet<>(expectedTbls.size());
+
+            while(rs.next()) {
+                actualTbls.add(rs.getString("TABLE_SCHEM") + '.'
+                    + rs.getString("TABLE_NAME"));
+            }
+
+            assert expectedTbls.equals(actualTbls) : "expectedTbls=" + expectedTbls +
+                ", actualTbls" + actualTbls;
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
     public void testGetColumns() throws Exception {
-        fail("https://issues.apache.org/jira/browse/IGNITE-5233");
+        final boolean primitivesInformationIsLostAfterStore = ignite(0).configuration().getMarshaller()
+            instanceof BinaryMarshaller;
 
         try (Connection conn = DriverManager.getConnection(URL)) {
             conn.setSchema("pers");
 
             DatabaseMetaData meta = conn.getMetaData();
 
-            ResultSet rs = meta.getColumns("", "pers", "Person", "%");
+            ResultSet rs = meta.getColumns("", "pers", "PERSON", "%");
 
             assert rs != null;
 
@@ -216,7 +283,7 @@ public class JdbcThinMetadataSelfTest extends JdbcThinAbstractSelfTest {
                 } else if ("AGE".equals(name) || "ORGID".equals(name)) {
                     assert rs.getInt("DATA_TYPE") == INTEGER;
                     assert "INTEGER".equals(rs.getString("TYPE_NAME"));
-                    assert rs.getInt("NULLABLE") == 0;
+                    assertEquals(primitivesInformationIsLostAfterStore ? 1 : 0, rs.getInt("NULLABLE"));
                 }
                 if ("_KEY".equals(name)) {
                     assert rs.getInt("DATA_TYPE") == OTHER;
@@ -235,7 +302,7 @@ public class JdbcThinMetadataSelfTest extends JdbcThinAbstractSelfTest {
             assert names.isEmpty();
             assert cnt == 3;
 
-            rs = meta.getColumns("", "org", "Organization", "%");
+            rs = meta.getColumns("", "org", "ORGANIZATION", "%");
 
             assert rs != null;
 
@@ -280,22 +347,243 @@ public class JdbcThinMetadataSelfTest extends JdbcThinAbstractSelfTest {
     /**
      * @throws Exception If failed.
      */
-    public void testMetadataResultSetClose() throws Exception {
-        fail("https://issues.apache.org/jira/browse/IGNITE-5233");
+    public void testGetAllColumns() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            DatabaseMetaData meta = conn.getMetaData();
+
+            ResultSet rs = meta.getColumns(null, null, null, null);
+
+            Set<String> expectedCols = new HashSet<>(Arrays.asList(
+                "org.ORGANIZATION.ID",
+                "org.ORGANIZATION.NAME",
+                "pers.PERSON.ORGID",
+                "pers.PERSON.AGE",
+                "pers.PERSON.NAME",
+                "PUBLIC.TEST.ID",
+                "PUBLIC.TEST.NAME",
+                "PUBLIC.TEST.VAL",
+                "PUBLIC.Quoted.Id",
+                "PUBLIC.Quoted.Name"));
+
+            Set<String> actualCols = new HashSet<>(expectedCols.size());
+
+            while(rs.next()) {
+                actualCols.add(rs.getString("TABLE_SCHEM") + '.'
+                    + rs.getString("TABLE_NAME") + "."
+                    + rs.getString("COLUMN_NAME"));
+            }
+
+            assert expectedCols.equals(actualCols) : "expectedCols=" + expectedCols +
+                ", actualCols" + actualCols;
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    public void testInvalidCatalog() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            DatabaseMetaData meta = conn.getMetaData();
+
+            ResultSet rs = meta.getSchemas("q", null);
+
+            assert !rs.next() : "Results must be empty";
+
+            rs = meta.getTables("q", null, null, null);
+
+            assert !rs.next() : "Results must be empty";
+
+            rs = meta.getColumns("q", null, null, null);
+
+            assert !rs.next() : "Results must be empty";
+
+            rs = meta.getIndexInfo("q", null, null, false, false);
+
+            assert !rs.next() : "Results must be empty";
+
+            rs = meta.getPrimaryKeys("q", null, null);
+
+            assert !rs.next() : "Results must be empty";
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    public void testIndexMetadata() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL);
+             ResultSet rs = conn.getMetaData().getIndexInfo(null, "pers", "PERSON", false, false)) {
+
+            int cnt = 0;
+
+            while (rs.next()) {
+                String idxName = rs.getString("INDEX_NAME");
+                String field = rs.getString("COLUMN_NAME");
+                String ascOrDesc = rs.getString("ASC_OR_DESC");
+
+                assert rs.getShort("TYPE") == DatabaseMetaData.tableIndexOther;
+
+                if ("PERSON_ORGID_ASC_IDX".equals(idxName)) {
+                    assert "ORGID".equals(field);
+                    assert "A".equals(ascOrDesc);
+                }
+                else if ("PERSON_NAME_ASC_AGE_DESC_IDX".equals(idxName)) {
+                    if ("NAME".equals(field))
+                        assert "A".equals(ascOrDesc);
+                    else if ("AGE".equals(field))
+                        assert "D".equals(ascOrDesc);
+                    else
+                        fail("Unexpected field: " + field);
+                }
+                else
+                    fail("Unexpected index: " + idxName);
+
+                cnt++;
+            }
+
+            assert cnt == 3;
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    public void testGetAllIndexes() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            ResultSet rs = conn.getMetaData().getIndexInfo(null, null, null, false, false);
+
+            Set<String> expectedIdxs = new HashSet<>(Arrays.asList(
+                "org.ORGANIZATION.ORGANIZATION_ID_ASC_IDX",
+                "org.ORGANIZATION.ORG_NAME_INDEX",
+                "pers.PERSON.PERSON_ORGID_ASC_IDX",
+                "pers.PERSON.PERSON_NAME_ASC_AGE_DESC_IDX",
+                "PUBLIC.TEST.IDX",
+                "PUBLIC.Quoted.MyTestIndex quoted"));
+
+            Set<String> actualIdxs = new HashSet<>(expectedIdxs.size());
+
+            while(rs.next()) {
+                actualIdxs.add(rs.getString("TABLE_SCHEM") +
+                    '.' + rs.getString("TABLE_NAME") +
+                    '.' + rs.getString("INDEX_NAME"));
+            }
+
+            assert expectedIdxs.equals(actualIdxs) : "expectedIdxs=" + expectedIdxs +
+                ", actualIdxs" + actualIdxs;
+        }
+    }
 
+    /**
+     * @throws Exception If failed.
+     */
+    public void testPrimaryKeyMetadata() throws Exception {
         try (Connection conn = DriverManager.getConnection(URL);
-             ResultSet tbls = conn.getMetaData().getTables(null, null, "%", null)) {
-            int colCnt = tbls.getMetaData().getColumnCount();
+             ResultSet rs = conn.getMetaData().getPrimaryKeys(null, "pers", "PERSON")) {
+
+            int cnt = 0;
+
+            while (rs.next()) {
+                assert "_KEY".equals(rs.getString("COLUMN_NAME"));
+
+                cnt++;
+            }
+
+            assert cnt == 1;
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    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(
+                "org.ORGANIZATION.PK_org_ORGANIZATION._KEY",
+                "pers.PERSON.PK_pers_PERSON._KEY",
+                "PUBLIC.TEST.PK_PUBLIC_TEST.ID",
+                "PUBLIC.TEST.PK_PUBLIC_TEST.NAME",
+                "PUBLIC.Quoted.PK_PUBLIC_Quoted.Id"));
+
+            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"));
+            }
+
+            assert expectedPks.equals(actualPks) : "expectedPks=" + expectedPks +
+                ", actualPks" + actualPks;
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    public void testParametersMetadata() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            conn.setSchema("pers");
+
+            PreparedStatement stmt = conn.prepareStatement("select orgId from Person p where p.name > ? and p.orgId > ?");
+
+            ParameterMetaData meta = stmt.getParameterMetaData();
+
+            assert meta != null;
+
+            assert meta.getParameterCount() == 2;
+
+            assert meta.getParameterType(1) == Types.VARCHAR;
+            assert meta.isNullable(1) == ParameterMetaData.parameterNullableUnknown;
+            assert meta.getPrecision(1) == Integer.MAX_VALUE;
+
+            assert meta.getParameterType(2) == Types.INTEGER;
+            assert meta.isNullable(2) == ParameterMetaData.parameterNullableUnknown;
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    public void testSchemasMetadata() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            ResultSet rs = conn.getMetaData().getSchemas();
+
+            Set<String> expectedSchemas = new HashSet<>(Arrays.asList("PUBLIC", "pers", "org"));
 
-            while (tbls.next()) {
-                for (int i = 0; i < colCnt; i++)
-                    tbls.getObject(i + 1);
+            Set<String> schemas = new HashSet<>();
+
+            while (rs.next()) {
+                schemas.add(rs.getString(1));
+
+                assert rs.getString(2) == null;
             }
+
+            assert expectedSchemas.equals(schemas) : "Unexpected schemas: " + schemas +
+                ". Expected schemas: " + expectedSchemas;
         }
-        catch (Exception e) {
-            log.error("Unexpected exception", e);
+    }
 
-            fail();
+    /**
+     * @throws Exception If failed.
+     */
+    public void testEmptySchemasMetadata() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            ResultSet rs = conn.getMetaData().getSchemas(null, "qqq");
+
+            assert !rs.next() : "Empty result set is expected";
+        }
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    public void testVersions() throws Exception {
+        try (Connection conn = DriverManager.getConnection(URL)) {
+            assert conn.getMetaData().getDatabaseProductVersion().equals(IgniteVersionUtils.VER.toString());
+            assert conn.getMetaData().getDriverVersion().equals(IgniteVersionUtils.VER.toString());
         }
     }
 
@@ -305,15 +593,12 @@ public class JdbcThinMetadataSelfTest extends JdbcThinAbstractSelfTest {
     @SuppressWarnings("UnusedDeclaration")
     private static class Person implements Serializable {
         /** Name. */
-        @QuerySqlField(index = false)
         private final String name;
 
         /** Age. */
-        @QuerySqlField
         private final int age;
 
         /** Organization ID. */
-        @QuerySqlField
         private final int orgId;
 
         /**
@@ -338,11 +623,9 @@ public class JdbcThinMetadataSelfTest extends JdbcThinAbstractSelfTest {
     @SuppressWarnings("UnusedDeclaration")
     private static class Organization implements Serializable {
         /** ID. */
-        @QuerySqlField
         private final int id;
 
         /** Name. */
-        @QuerySqlField(index = false)
         private final String name;
 
         /**

http://git-wip-us.apache.org/repos/asf/ignite/blob/0e803144/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java
index 89ef2fc..8836cd5 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java
@@ -64,6 +64,9 @@ public class JdbcThinConnection implements Connection {
     /** Logger. */
     private static final Logger LOG = Logger.getLogger(JdbcThinConnection.class.getName());
 
+    /** Connection URL. */
+    private String url;
+
     /** Schema name. */
     private String schemaName;
 
@@ -88,6 +91,9 @@ public class JdbcThinConnection implements Connection {
     /** Ignite endpoint. */
     private JdbcThinTcpIo cliIo;
 
+    /** Jdbc metadata. Cache the JDBC object on the first access */
+    private JdbcThinDatabaseMetadata metadata;
+
     /**
      * Creates new connection.
      *
@@ -99,6 +105,8 @@ public class JdbcThinConnection implements Connection {
         assert url != null;
         assert props != null;
 
+        this.url = url;
+
         holdability = HOLD_CURSORS_OVER_COMMIT;
         autoCommit = true;
         txIsolation = Connection.TRANSACTION_NONE;
@@ -274,7 +282,10 @@ public class JdbcThinConnection implements Connection {
     @Override public DatabaseMetaData getMetaData() throws SQLException {
         ensureNotClosed();
 
-        return null;
+        if (metadata == null)
+            metadata = new JdbcThinDatabaseMetadata(this);
+
+        return metadata;
     }
 
     /** {@inheritDoc} */
@@ -665,4 +676,11 @@ public class JdbcThinConnection implements Connection {
                 ", value=" + strVal + ']');
         }
     }
+
+    /**
+     * @return Connection URL.
+     */
+    public String url() {
+        return url;
+    }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/0e803144/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinDatabaseMetadata.java
----------------------------------------------------------------------
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinDatabaseMetadata.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinDatabaseMetadata.java
new file mode 100644
index 0000000..583bcec
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinDatabaseMetadata.java
@@ -0,0 +1,1587 @@
+/*
+ * 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.thin;
+
+import java.io.IOException;
+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.IgniteCheckedException;
+import org.apache.ignite.internal.IgniteVersionUtils;
+import org.apache.ignite.internal.jdbc2.JdbcUtils;
+import org.apache.ignite.internal.processors.odbc.jdbc.JdbcColumnMeta;
+import org.apache.ignite.internal.processors.odbc.jdbc.JdbcIndexMeta;
+import org.apache.ignite.internal.processors.odbc.jdbc.JdbcMetaColumnsResult;
+import org.apache.ignite.internal.processors.odbc.jdbc.JdbcMetaIndexesResult;
+import org.apache.ignite.internal.processors.odbc.jdbc.JdbcMetaPrimaryKeysResult;
+import org.apache.ignite.internal.processors.odbc.jdbc.JdbcMetaSchemasResult;
+import org.apache.ignite.internal.processors.odbc.jdbc.JdbcMetaTablesResult;
+import org.apache.ignite.internal.processors.odbc.jdbc.JdbcPrimaryKeyMeta;
+import org.apache.ignite.internal.processors.odbc.jdbc.JdbcTableMeta;
+import org.apache.ignite.internal.util.typedef.F;
+
+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;
+
+/**
+ * JDBC database metadata implementation.
+ */
+@SuppressWarnings("RedundantCast")
+public class JdbcThinDatabaseMetadata implements DatabaseMetaData {
+    /** Connection. */
+    private final JdbcThinConnection conn;
+
+    /**
+     * @param conn Connection.
+     */
+    JdbcThinDatabaseMetadata(JdbcThinConnection conn) {
+        this.conn = conn;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean allProceduresAreCallable() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean allTablesAreSelectable() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getURL() throws SQLException {
+        return conn.url();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getUserName() throws SQLException {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isReadOnly() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean nullsAreSortedHigh() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean nullsAreSortedLow() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean nullsAreSortedAtStart() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean nullsAreSortedAtEnd() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getDatabaseProductName() throws SQLException {
+        return "Ignite";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getDatabaseProductVersion() throws SQLException {
+        return conn.io().igniteVersion().toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getDriverName() throws SQLException {
+        return "Ignite JDBC Thin Driver";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getDriverVersion() throws SQLException {
+        return IgniteVersionUtils.VER.toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getDriverMajorVersion() {
+        return IgniteVersionUtils.VER.major();
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getDriverMinorVersion() {
+        return IgniteVersionUtils.VER.minor();
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean usesLocalFiles() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean usesLocalFilePerTable() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMixedCaseIdentifiers() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesUpperCaseIdentifiers() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesLowerCaseIdentifiers() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesMixedCaseIdentifiers() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesUpperCaseQuotedIdentifiers() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesLowerCaseQuotedIdentifiers() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean storesMixedCaseQuotedIdentifiers() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getIdentifierQuoteString() throws SQLException {
+        return "\"";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getSQLKeywords() throws SQLException {
+        return "LIMIT,MINUS,ROWNUM,SYSDATE,SYSTIME,SYSTIMESTAMP,TODAY";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getNumericFunctions() throws SQLException {
+        // TODO: IGNITE-6028
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getStringFunctions() throws SQLException {
+        // TODO: IGNITE-6028
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getSystemFunctions() throws SQLException {
+        // TODO: IGNITE-6028
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getTimeDateFunctions() throws SQLException {
+        // TODO: IGNITE-6028
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getSearchStringEscape() throws SQLException {
+        return "\\";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getExtraNameCharacters() throws SQLException {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsAlterTableWithAddColumn() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsAlterTableWithDropColumn() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsColumnAliasing() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean nullPlusNonNullIsNull() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsConvert() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsConvert(int fromType, int toType) throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsTableCorrelationNames() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsDifferentTableCorrelationNames() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsExpressionsInOrderBy() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOrderByUnrelated() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsGroupBy() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsGroupByUnrelated() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsGroupByBeyondSelect() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsLikeEscapeClause() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMultipleResultSets() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMultipleTransactions() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsNonNullableColumns() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMinimumSQLGrammar() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCoreSQLGrammar() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsExtendedSQLGrammar() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsANSI92EntryLevelSQL() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsANSI92IntermediateSQL() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsANSI92FullSQL() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsIntegrityEnhancementFacility() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOuterJoins() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsFullOuterJoins() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsLimitedOuterJoins() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getSchemaTerm() throws SQLException {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getProcedureTerm() throws SQLException {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getCatalogTerm() throws SQLException {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isCatalogAtStart() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getCatalogSeparator() throws SQLException {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSchemasInDataManipulation() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSchemasInProcedureCalls() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSchemasInTableDefinitions() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSchemasInIndexDefinitions() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCatalogsInDataManipulation() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCatalogsInProcedureCalls() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCatalogsInTableDefinitions() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCatalogsInIndexDefinitions() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsPositionedDelete() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsPositionedUpdate() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSelectForUpdate() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsStoredProcedures() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSubqueriesInComparisons() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSubqueriesInExists() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSubqueriesInIns() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSubqueriesInQuantifieds() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsCorrelatedSubqueries() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsUnion() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsUnionAll() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOpenCursorsAcrossCommit() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOpenCursorsAcrossRollback() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOpenStatementsAcrossCommit() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsOpenStatementsAcrossRollback() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxBinaryLiteralLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxCharLiteralLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnNameLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnsInGroupBy() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnsInIndex() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnsInOrderBy() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnsInSelect() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxColumnsInTable() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxConnections() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxCursorNameLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxIndexLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxSchemaNameLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxProcedureNameLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxCatalogNameLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxRowSize() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean doesMaxRowSizeIncludeBlobs() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxStatementLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxStatements() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxTableNameLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxTablesInSelect() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getMaxUserNameLength() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getDefaultTransactionIsolation() throws SQLException {
+        return TRANSACTION_NONE;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsTransactions() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsTransactionIsolationLevel(int level) throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsDataManipulationTransactionsOnly() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean dataDefinitionCausesTransactionCommit() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean dataDefinitionIgnoredInTransactions() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getProcedures(String catalog, String schemaPtrn,
+        String procedureNamePtrn) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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 {
+        if (conn.isClosed())
+            throw new SQLException("Connection is closed.");
+
+        final List<JdbcColumnMeta> meta = Arrays.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 ("TABLE".equals(type)) {
+                    tblTypeMatch = true;
+
+                    break;
+                }
+            }
+        }
+
+        if (!validCatalogPattern(catalog) || !tblTypeMatch)
+            return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), meta);
+
+        try {
+            JdbcMetaTablesResult res = conn.io().tablesMeta(schemaPtrn, tblNamePtrn);
+
+            List<List<Object>> rows = new LinkedList<>();
+
+            for (JdbcTableMeta tblMeta : res.meta())
+                rows.add(tableRow(tblMeta));
+
+            return new JdbcThinResultSet(rows, meta);
+        }
+        catch (IOException e) {
+            conn.close();
+
+            throw new SQLException("Failed to query Ignite.", e);
+        }
+        catch (IgniteCheckedException e) {
+            throw new SQLException("Failed to query Ignite.", e);
+        }
+    }
+
+    /**
+     * @param tblMeta Table metadata.
+     * @return Table metadata row.
+     */
+    private List<Object> tableRow(JdbcTableMeta tblMeta) {
+        List<Object> row = new ArrayList<>(10);
+
+        row.add(null);
+        row.add(tblMeta.schemaName());
+        row.add(tblMeta.tableName());
+        row.add("TABLE");
+        row.add(null);
+        row.add(null);
+        row.add(null);
+        row.add(null);
+        row.add(null);
+        row.add(null);
+
+        return row;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getSchemas() throws SQLException {
+        return getSchemas(null, "%");
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+    @Override public ResultSet getCatalogs() throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(),
+            Arrays.asList(new JdbcColumnMeta(null, null, "TABLE_CAT", String.class)));
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+    @Override public ResultSet getTableTypes() throws SQLException {
+        return new JdbcThinResultSet(Collections.singletonList(Collections.<Object>singletonList("TABLE")),
+            Arrays.asList(new JdbcColumnMeta(null, null, "TABLE_TYPE", String.class)));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getColumns(String catalog, String schemaPtrn, String tblNamePtrn,
+        String colNamePtrn) throws SQLException {
+        if (conn.isClosed())
+            throw new SQLException("Connection is closed.");
+
+        final List<JdbcColumnMeta> meta = Arrays.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", Short.class),
+            new JdbcColumnMeta(null, null, "TYPE_NAME", String.class),
+            new JdbcColumnMeta(null, null, "COLUMN_SIZE", Integer.class),
+            new JdbcColumnMeta(null, null, "DECIMAL_DIGITS", Integer.class),
+            new JdbcColumnMeta(null, null, "NUM_PREC_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, "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_CATLOG", 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),
+            new JdbcColumnMeta(null, null, "IS_AUTOINCREMENT", String.class));
+
+        if (!validCatalogPattern(catalog))
+            return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), meta);
+
+        try {
+            JdbcMetaColumnsResult res = conn.io().columnsMeta(schemaPtrn, tblNamePtrn, colNamePtrn);
+
+            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 JdbcThinResultSet(rows, meta);
+        }
+        catch (IOException e) {
+            conn.close();
+
+            throw new SQLException("Failed to query Ignite.", e);
+        }
+        catch (IgniteCheckedException e) {
+            throw new SQLException("Failed to query Ignite.", e);
+        }
+    }
+
+    /**
+     * @param colMeta Column metadata.
+     * @param pos Ordinal position.
+     * @return Column metadata row.
+     */
+    private List<Object> columnRow(JdbcColumnMeta colMeta, int pos) {
+        List<Object> row = new ArrayList<>(20);
+
+        row.add((String)null);
+        row.add(colMeta.schemaName());
+        row.add(colMeta.tableName());
+        row.add(colMeta.columnName());
+        row.add(colMeta.dataType());
+        row.add(colMeta.dataTypeName());
+        row.add((Integer)null);
+        row.add((Integer)null);
+        row.add(10);
+        row.add(JdbcUtils.nullable(colMeta.columnName(), colMeta.dataTypeClass()) ? 1 : 0 );
+        row.add((String)null);
+        row.add((String)null);
+        row.add(Integer.MAX_VALUE);
+        row.add(pos);
+        row.add("YES");
+        row.add((String)null);
+        row.add((String)null);
+        row.add((String)null);
+        row.add((Short)null);
+        row.add("NO");
+
+        return row;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getColumnPrivileges(String catalog, String schema, String tbl,
+        String colNamePtrn) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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 {
+        if (conn.isClosed())
+            throw new SQLException("Connection is closed.");
+
+        final List<JdbcColumnMeta> meta = Arrays.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 (!validCatalogPattern(catalog))
+            return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), meta);
+
+        try {
+            JdbcMetaPrimaryKeysResult res = conn.io().primaryKeysMeta(schema, tbl);
+
+            List<List<Object>> rows = new LinkedList<>();
+
+            for (JdbcPrimaryKeyMeta pkMeta : res.meta())
+                rows.addAll(primaryKeyRows(pkMeta));
+
+            return new JdbcThinResultSet(rows, meta);
+        }
+        catch (IOException e) {
+            conn.close();
+
+            throw new SQLException("Failed to query Ignite.", e);
+        }
+        catch (IgniteCheckedException e) {
+            throw new SQLException("Failed to query Ignite.", e);
+        }
+    }
+
+    /**
+     * @param pkMeta Primary key metadata.
+     * @return Result set rows for primary key.
+     */
+    private 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((String)null); // table catalog
+            row.add(pkMeta.schemaName());
+            row.add(pkMeta.tableName());
+            row.add(pkMeta.fields().get(i));
+            row.add((Integer)i + 1); // sequence number
+            row.add(pkMeta.name());
+
+            rows.add(row);
+        }
+
+        return rows;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getImportedKeys(String catalog, String schema, String tbl) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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() throws SQLException {
+        List<List<Object>> types = new ArrayList<>(21);
+
+        types.add(Arrays.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>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.<Object>asList("ARRAY", Types.ARRAY, 0, "(", "')", null,
+            (short)typeNullable, false, (short)typeSearchable, false, false, false, "ARRAY", 0, 0,
+            Types.ARRAY, 0, null));
+
+        return new JdbcThinResultSet(types, Arrays.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 {
+        if (conn.isClosed())
+            throw new SQLException("Connection is closed.");
+
+        final List<JdbcColumnMeta> meta = Arrays.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 (!validCatalogPattern(catalog))
+            return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), meta);
+
+        try {
+            JdbcMetaIndexesResult res = conn.io().indexMeta(schema, tbl);
+
+            List<List<Object>> rows = new LinkedList<>();
+
+            for (JdbcIndexMeta idxMeta : res.meta())
+                rows.addAll(indexRows(idxMeta));
+
+            return new JdbcThinResultSet(rows, meta);
+        }
+        catch (IOException e) {
+            conn.close();
+
+            throw new SQLException("Failed to query Ignite.", e);
+        }
+        catch (IgniteCheckedException e) {
+            throw new SQLException("Failed to query Ignite.", e);
+        }
+    }
+
+    /**
+     * @param idxMeta Index metadata.
+     * @return List of result rows correspond to index.
+     */
+    private List<List<Object>> indexRows(JdbcIndexMeta idxMeta) {
+        List<List<Object>> rows = new ArrayList<>(idxMeta.fields().size());
+
+        for (int i = 0; i < idxMeta.fields().size(); ++i) {
+            List<Object> row = new ArrayList<>(13);
+
+            row.add((String)null); // table catalog
+            row.add(idxMeta.schemaName());
+            row.add(idxMeta.tableName());
+            row.add(true); // non unique
+            row.add(null); // index qualifier (index catalog)
+            row.add(idxMeta.indexName());
+            row.add((short)tableIndexOther); // type
+            row.add((Integer)i); // field ordinal position in index
+            row.add(idxMeta.fields().get(i));
+            row.add(idxMeta.fieldsAsc().get(i) ? "A" : "D");
+            row.add((Integer)0); // cardinality
+            row.add((Integer)0); // pages
+            row.add((String)null); // filer condition
+
+            rows.add(row);
+        }
+
+        return rows;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsResultSetType(int type) throws SQLException {
+        return type == TYPE_FORWARD_ONLY;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException {
+        return supportsResultSetType(type) && concurrency == CONCUR_READ_ONLY;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean ownUpdatesAreVisible(int type) throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean ownDeletesAreVisible(int type) throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean ownInsertsAreVisible(int type) throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean othersUpdatesAreVisible(int type) throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean othersDeletesAreVisible(int type) throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean othersInsertsAreVisible(int type) throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean updatesAreDetected(int type) throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean deletesAreDetected(int type) throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean insertsAreDetected(int type) throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsBatchUpdates() throws SQLException {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getUDTs(String catalog, String schemaPtrn, String typeNamePtrn,
+        int[] types) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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() throws SQLException {
+        return conn;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsSavepoints() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsNamedParameters() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsMultipleOpenResults() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsGetGeneratedKeys() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getSuperTypes(String catalog, String schemaPtrn,
+        String typeNamePtrn) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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) throws SQLException {
+        return holdability == HOLD_CURSORS_OVER_COMMIT;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getResultSetHoldability() throws SQLException {
+        return HOLD_CURSORS_OVER_COMMIT;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getDatabaseMajorVersion() throws SQLException {
+        return conn.io().igniteVersion().major();
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getDatabaseMinorVersion() throws SQLException {
+        return conn.io().igniteVersion().minor();
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getJDBCMajorVersion() throws SQLException {
+        return 4;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getJDBCMinorVersion() throws SQLException {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int getSQLStateType() throws SQLException {
+        return DatabaseMetaData.sqlStateSQL99;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean locatorsUpdateCopy() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsStatementPooling() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public RowIdLifetime getRowIdLifetime() throws SQLException {
+        return ROWID_UNSUPPORTED;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getSchemas(String catalog, String schemaPtrn) throws SQLException {
+        if (conn.isClosed())
+            throw new SQLException("Connection is closed.");
+
+        final List<JdbcColumnMeta> meta = Arrays.asList(
+            new JdbcColumnMeta(null, null, "TABLE_SCHEM", String.class),
+            new JdbcColumnMeta(null, null, "TABLE_CATALOG", String.class)
+        );
+
+        if (!validCatalogPattern(catalog))
+            return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), meta);
+
+        try {
+            JdbcMetaSchemasResult res = conn.io().schemasMeta(schemaPtrn);
+
+            List<List<Object>> rows = new LinkedList<>();
+
+            for (String schema : res.schemas()) {
+                List<Object> row = new ArrayList<>(2);
+
+                row.add(schema);
+                row.add(null);
+
+                rows.add(row);
+            }
+
+            return new JdbcThinResultSet(rows, meta);
+        }
+        catch (IOException e) {
+            conn.close();
+
+            throw new SQLException("Failed to query Ignite.", e);
+        }
+        catch (IgniteCheckedException e) {
+            throw new SQLException("Failed to query Ignite.", e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean autoCommitFailureClosesAllResultSets() throws SQLException {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getClientInfoProperties() throws SQLException {
+        // TODO: IGNITE-5425.
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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)
+        ));
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getFunctions(String catalog, String schemaPtrn,
+        String functionNamePtrn) throws SQLException {
+        // TODO: IGNITE-6028
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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) throws SQLException {
+        // TODO: IGNITE-6028
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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} */
+    @SuppressWarnings("unchecked")
+    @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) throws SQLException {
+        return iface != null && iface.isAssignableFrom(JdbcThinDatabaseMetadata.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override public ResultSet getPseudoColumns(String catalog, String schemaPtrn, String tblNamePtrn,
+        String colNamePtrn) throws SQLException {
+        return new JdbcThinResultSet(Collections.<List<Object>>emptyList(), Arrays.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)
+        ));
+    }
+
+    /**
+     * @param catalog Catalog pattern.
+     * @return {@code true} If patter is valid for Ignite (null, empty, or '%' wildcard).
+     *  Otherwise returns {@code false}.
+     */
+    private static boolean validCatalogPattern(String catalog) {
+        return F.isEmpty(catalog) || "%".equals(catalog);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean generatedKeyAlwaysReturned() throws SQLException {
+        return false;
+    }
+}
\ No newline at end of file