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

[ignite] branch master updated: IGNITE-17598 SQL Calcite: Implement query metadata. (#10291)

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

ivandasch pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 9e64220c422 IGNITE-17598 SQL Calcite: Implement query metadata. (#10291)
9e64220c422 is described below

commit 9e64220c4229afa9b30d707ba24a9803c79a3b9e
Author: Ivan Daschinskiy <iv...@apache.org>
AuthorDate: Mon Oct 17 10:11:09 2022 +0300

    IGNITE-17598 SQL Calcite: Implement query metadata. (#10291)
---
 modules/calcite/pom.xml                            |   7 +
 .../query/calcite/CalciteQueryProcessor.java       | 159 +++++++---
 .../query/calcite/exec/ExecutionServiceImpl.java   |  12 -
 .../calcite/prepare/AbstractMultiStepPlan.java     |  16 +-
 .../calcite/prepare/DynamicParamTypeExtractor.java |  91 ++++++
 .../query/calcite/prepare/FieldsMetadata.java      |  38 +--
 .../query/calcite/prepare/FieldsMetadataImpl.java  |  45 ++-
 .../calcite/prepare/IgniteRelRexNodeShuttle.java   | 142 +++++++++
 .../query/calcite/prepare/MultiStepDmlPlan.java    |  12 +-
 .../query/calcite/prepare/MultiStepPlan.java       |   5 +
 .../query/calcite/prepare/MultiStepQueryPlan.java  |  12 +-
 .../query/calcite/prepare/PrepareServiceImpl.java  |  13 +-
 .../calcite/schema/CacheTableDescriptorImpl.java   |  32 +-
 .../integration/QueryMetadataIntegrationTest.java  | 345 +++++++++++++++++++++
 .../query/calcite/jdbc/JdbcQueryTest.java          |  49 +++
 .../query/calcite/planner/PlannerTest.java         |  17 +-
 .../ignite/testsuites/IntegrationTestSuite.java    |   2 +
 .../internal/jdbc2/JdbcPreparedStatement.java      |   4 +-
 .../processors/odbc/jdbc/JdbcParameterMeta.java    |  14 +
 .../processors/odbc/jdbc/JdbcRequestHandler.java   |   3 +-
 .../odbc/jdbc/JdbcRequestHandlerWorker.java        |   2 +-
 .../processors/odbc/odbc/OdbcRequestHandler.java   |   4 +-
 .../odbc/odbc/OdbcRequestHandlerWorker.java        |   2 +-
 .../processors/query/GridQueryProcessor.java       |  87 +++++-
 .../internal/processors/query/NoOpQueryEngine.java |  20 +-
 .../internal/processors/query/QueryEngine.java     |  30 +-
 26 files changed, 1032 insertions(+), 131 deletions(-)

diff --git a/modules/calcite/pom.xml b/modules/calcite/pom.xml
index 6721982ed6f..5bfc03b2b77 100644
--- a/modules/calcite/pom.xml
+++ b/modules/calcite/pom.xml
@@ -198,6 +198,13 @@
             <artifactId>ignite-clients</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java
index 6ff731ae2d6..4d25fcef58d 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java
@@ -22,6 +22,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import org.apache.calcite.DataContexts;
 import org.apache.calcite.config.Lex;
@@ -48,6 +49,7 @@ import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.processors.GridProcessorAdapter;
 import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
 import org.apache.ignite.internal.processors.failure.FailureProcessor;
+import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
 import org.apache.ignite.internal.processors.query.IgniteSQLException;
 import org.apache.ignite.internal.processors.query.QueryContext;
 import org.apache.ignite.internal.processors.query.QueryEngine;
@@ -70,8 +72,11 @@ import org.apache.ignite.internal.processors.query.calcite.metadata.MappingServi
 import org.apache.ignite.internal.processors.query.calcite.metadata.MappingServiceImpl;
 import org.apache.ignite.internal.processors.query.calcite.metadata.cost.IgniteCostFactory;
 import org.apache.ignite.internal.processors.query.calcite.prepare.CacheKey;
+import org.apache.ignite.internal.processors.query.calcite.prepare.ExplainPlan;
+import org.apache.ignite.internal.processors.query.calcite.prepare.FieldsMetadata;
 import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteConvertletTable;
 import org.apache.ignite.internal.processors.query.calcite.prepare.IgniteTypeCoercion;
+import org.apache.ignite.internal.processors.query.calcite.prepare.MultiStepPlan;
 import org.apache.ignite.internal.processors.query.calcite.prepare.PrepareServiceImpl;
 import org.apache.ignite.internal.processors.query.calcite.prepare.QueryPlan;
 import org.apache.ignite.internal.processors.query.calcite.prepare.QueryPlanCache;
@@ -85,6 +90,7 @@ import org.apache.ignite.internal.processors.query.calcite.sql.generated.IgniteS
 import org.apache.ignite.internal.processors.query.calcite.trait.CorrelationTraitDef;
 import org.apache.ignite.internal.processors.query.calcite.trait.DistributionTraitDef;
 import org.apache.ignite.internal.processors.query.calcite.trait.RewindabilityTraitDef;
+import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeSystem;
 import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.calcite.util.LifecycleAware;
@@ -330,39 +336,39 @@ public class CalciteQueryProcessor extends GridProcessorAdapter implements Query
         String sql,
         Object... params
     ) throws IgniteSQLException {
-        SchemaPlus schema = schemaHolder.schema(schemaName);
-
-        assert schema != null : "Schema not found: " + schemaName;
-
-        QueryPlan plan = queryPlanCache().queryPlan(new CacheKey(schema.getName(), sql));
-
-        if (plan != null) {
-            return Collections.singletonList(
-                executeQuery(qryCtx, qry -> plan, schemaName, sql, null, params)
-            );
-        }
-
-        SqlNodeList qryList = Commons.parse(sql, FRAMEWORK_CONFIG.getParserConfig());
-
-        List<FieldsQueryCursor<List<?>>> cursors = new ArrayList<>(qryList.size());
-        List<RootQuery<Object[]>> qrys = new ArrayList<>(qryList.size());
-
-        for (final SqlNode sqlNode: qryList) {
-            FieldsQueryCursor<List<?>> cursor = executeQuery(qryCtx, qry -> {
-                if (qryList.size() == 1) {
-                    return queryPlanCache().queryPlan(
-                        new CacheKey(schemaName, sql), // Use source SQL to avoid redundant parsing next time.
-                        () -> prepareSvc.prepareSingle(sqlNode, qry.planningContext())
-                    );
-                }
-                else
-                    return prepareSvc.prepareSingle(sqlNode, qry.planningContext());
-            }, schemaName, sqlNode.toString(), qrys, params);
+        return parseAndProcessQuery(qryCtx, executionSvc::executePlan, schemaName, sql, params);
+    }
 
-            cursors.add(cursor);
-        }
+    /** {@inheritDoc} */
+    @Override public List<List<GridQueryFieldMetadata>> parameterMetaData(
+        @Nullable QueryContext ctx,
+        String schemaName,
+        String sql
+    ) throws IgniteSQLException {
+        return parseAndProcessQuery(ctx, (qry, plan) -> {
+            try {
+                return fieldsMeta(plan, true);
+            }
+            finally {
+                qryReg.unregister(qry.id());
+            }
+        }, schemaName, sql);
+    }
 
-        return cursors;
+    /** {@inheritDoc} */
+    @Override public List<List<GridQueryFieldMetadata>> resultSetMetaData(
+        @Nullable QueryContext ctx,
+        String schemaName,
+        String sql
+    ) throws IgniteSQLException {
+        return parseAndProcessQuery(ctx, (qry, plan) -> {
+            try {
+                return fieldsMeta(plan, false);
+            }
+            finally {
+                qryReg.unregister(qry.id());
+            }
+        }, schemaName, sql);
     }
 
     /** {@inheritDoc} */
@@ -372,6 +378,10 @@ public class CalciteQueryProcessor extends GridProcessorAdapter implements Query
         String sql,
         List<Object[]> batchedParams
     ) throws IgniteSQLException {
+        SchemaPlus schema = schemaHolder.schema(schemaName);
+
+        assert schema != null : "Schema not found: " + schemaName;
+
         SqlNodeList qryNodeList = Commons.parse(sql, FRAMEWORK_CONFIG.getParserConfig());
 
         if (qryNodeList.size() != 1) {
@@ -396,7 +406,7 @@ public class CalciteQueryProcessor extends GridProcessorAdapter implements Query
 
             @Override public QueryPlan apply(RootQuery<Object[]> qry) {
                 if (plan == null) {
-                    plan = queryPlanCache().queryPlan(new CacheKey(schemaName, sql), () ->
+                    plan = queryPlanCache().queryPlan(new CacheKey(schema.getName(), sql), () ->
                         prepareSvc.prepareSingle(qryNode, qry.planningContext())
                     );
                 }
@@ -405,16 +415,66 @@ public class CalciteQueryProcessor extends GridProcessorAdapter implements Query
             }
         };
 
-        for (final Object[] batch: batchedParams)
-            cursors.add(executeQuery(qryCtx, planSupplier, schemaName, sql, qrys, batch));
+        for (final Object[] batch: batchedParams) {
+            FieldsQueryCursor<List<?>> cur = processQuery(qryCtx, qry ->
+                executionSvc.executePlan(qry, planSupplier.apply(qry)), schema.getName(), sql, qrys, batch);
+
+            cursors.add(cur);
+        }
 
         return cursors;
     }
 
     /** */
-    private FieldsQueryCursor<List<?>> executeQuery(
+    private <T> List<T> parseAndProcessQuery(
         @Nullable QueryContext qryCtx,
-        Function<RootQuery<Object[]>, QueryPlan> plan,
+        BiFunction<RootQuery<Object[]>, QueryPlan, T> action,
+        @Nullable String schemaName,
+        String sql,
+        Object... params
+    ) throws IgniteSQLException {
+        SchemaPlus schema = schemaHolder.schema(schemaName);
+
+        assert schema != null : "Schema not found: " + schemaName;
+
+        QueryPlan plan = queryPlanCache().queryPlan(new CacheKey(schema.getName(), sql));
+
+        if (plan != null) {
+            return Collections.singletonList(
+                processQuery(qryCtx, qry -> action.apply(qry, plan), schema.getName(), sql, null, params)
+            );
+        }
+
+        SqlNodeList qryList = Commons.parse(sql, FRAMEWORK_CONFIG.getParserConfig());
+
+        List<T> res = new ArrayList<>(qryList.size());
+        List<RootQuery<Object[]>> qrys = new ArrayList<>(qryList.size());
+
+        for (final SqlNode sqlNode: qryList) {
+            T singleRes = processQuery(qryCtx, qry -> {
+                QueryPlan plan0;
+                if (qryList.size() == 1) {
+                    plan0 = queryPlanCache().queryPlan(
+                        new CacheKey(schema.getName(), sql), // Use source SQL to avoid redundant parsing next time.
+                        () -> prepareSvc.prepareSingle(sqlNode, qry.planningContext())
+                    );
+                }
+                else
+                    plan0 = prepareSvc.prepareSingle(sqlNode, qry.planningContext());
+
+                return action.apply(qry, plan0);
+            }, schema.getName(), sqlNode.toString(), qrys, params);
+
+            res.add(singleRes);
+        }
+
+        return res;
+    }
+
+    /** */
+    private <T> T processQuery(
+        @Nullable QueryContext qryCtx,
+        Function<RootQuery<Object[]>, T> action,
         String schema,
         String sql,
         @Nullable List<RootQuery<Object[]>> qrys,
@@ -437,7 +497,7 @@ public class CalciteQueryProcessor extends GridProcessorAdapter implements Query
         qryReg.register(qry);
 
         try {
-            return executionSvc.executePlan(qry, plan.apply(qry));
+            return action.apply(qry);
         }
         catch (Throwable e) {
             boolean isCanceled = qry.isCancelled();
@@ -454,6 +514,31 @@ public class CalciteQueryProcessor extends GridProcessorAdapter implements Query
         }
     }
 
+    /**
+     * @param plan Query plan.
+     * @param isParamsMeta If {@code true}, return parameter metadata, otherwise result set metadata.
+     * @return Return query fields metadata.
+     */
+    private List<GridQueryFieldMetadata> fieldsMeta(QueryPlan plan, boolean isParamsMeta) {
+        IgniteTypeFactory typeFactory = Commons.typeFactory();
+
+        switch (plan.type()) {
+            case QUERY:
+            case DML:
+                MultiStepPlan msPlan = (MultiStepPlan)plan;
+
+                FieldsMetadata meta = isParamsMeta ? msPlan.paramsMetadata() : msPlan.fieldsMetadata();
+
+                return meta.queryFieldsMetadata(typeFactory);
+            case EXPLAIN:
+                ExplainPlan exPlan = (ExplainPlan)plan;
+
+                return isParamsMeta ? Collections.emptyList() : exPlan.fieldsMeta().queryFieldsMetadata(typeFactory);
+            default:
+                return Collections.emptyList();
+        }
+    }
+
     /** */
     private void onStart(GridKernalContext ctx, Service... services) {
         for (Service service : services) {
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionServiceImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionServiceImpl.java
index 5aa192c73d8..6263a235d7d 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionServiceImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ExecutionServiceImpl.java
@@ -28,7 +28,6 @@ import java.util.stream.Collectors;
 import org.apache.calcite.plan.Context;
 import org.apache.calcite.plan.Contexts;
 import org.apache.calcite.plan.RelOptUtil;
-import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.SqlInsert;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.tools.Frameworks;
@@ -73,13 +72,11 @@ import org.apache.ignite.internal.processors.query.calcite.prepare.BaseQueryCont
 import org.apache.ignite.internal.processors.query.calcite.prepare.CacheKey;
 import org.apache.ignite.internal.processors.query.calcite.prepare.DdlPlan;
 import org.apache.ignite.internal.processors.query.calcite.prepare.ExplainPlan;
-import org.apache.ignite.internal.processors.query.calcite.prepare.FieldsMetadata;
 import org.apache.ignite.internal.processors.query.calcite.prepare.FieldsMetadataImpl;
 import org.apache.ignite.internal.processors.query.calcite.prepare.Fragment;
 import org.apache.ignite.internal.processors.query.calcite.prepare.FragmentPlan;
 import org.apache.ignite.internal.processors.query.calcite.prepare.MappingQueryContext;
 import org.apache.ignite.internal.processors.query.calcite.prepare.MultiStepPlan;
-import org.apache.ignite.internal.processors.query.calcite.prepare.PlanningContext;
 import org.apache.ignite.internal.processors.query.calcite.prepare.PrepareServiceImpl;
 import org.apache.ignite.internal.processors.query.calcite.prepare.QueryPlan;
 import org.apache.ignite.internal.processors.query.calcite.prepare.QueryPlanCache;
@@ -90,7 +87,6 @@ import org.apache.ignite.internal.processors.query.calcite.util.AbstractService;
 import org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.calcite.util.ConvertingClosableIterator;
 import org.apache.ignite.internal.processors.query.calcite.util.ListFieldsQueryCursor;
-import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.X;
 import org.apache.ignite.internal.util.typedef.internal.U;
@@ -658,14 +654,6 @@ public class ExecutionServiceImpl<Row> extends AbstractService implements Execut
         }
     }
 
-    /** */
-    private FieldsMetadata queryFieldsMetadata(PlanningContext ctx, RelDataType sqlType,
-        @Nullable List<List<String>> origins) {
-        RelDataType resultType = TypeUtils.getResultType(
-            ctx.typeFactory(), ctx.catalogReader(), sqlType, origins);
-        return new FieldsMetadataImpl(resultType, origins);
-    }
-
     /** */
     private void onMessage(UUID nodeId, final QueryStartRequest msg) {
         assert nodeId != null && msg != null;
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/AbstractMultiStepPlan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/AbstractMultiStepPlan.java
index b86f226377d..fed7a996658 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/AbstractMultiStepPlan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/AbstractMultiStepPlan.java
@@ -29,6 +29,7 @@ import org.apache.ignite.internal.processors.query.calcite.rel.IgniteReceiver;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSender;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.U;
+import org.jetbrains.annotations.Nullable;
 
 /**
  *
@@ -37,6 +38,9 @@ public abstract class AbstractMultiStepPlan implements MultiStepPlan {
     /** */
     protected final FieldsMetadata fieldsMetadata;
 
+    /** */
+    protected final FieldsMetadata paramsMetadata;
+
     /** */
     protected final QueryTemplate queryTemplate;
 
@@ -44,9 +48,14 @@ public abstract class AbstractMultiStepPlan implements MultiStepPlan {
     protected ExecutionPlan executionPlan;
 
     /** */
-    protected AbstractMultiStepPlan(QueryTemplate queryTemplate, FieldsMetadata fieldsMetadata) {
+    protected AbstractMultiStepPlan(
+        QueryTemplate queryTemplate,
+        FieldsMetadata fieldsMetadata,
+        @Nullable FieldsMetadata paramsMetadata
+    ) {
         this.queryTemplate = queryTemplate;
         this.fieldsMetadata = fieldsMetadata;
+        this.paramsMetadata = paramsMetadata;
     }
 
     /** {@inheritDoc} */
@@ -59,6 +68,11 @@ public abstract class AbstractMultiStepPlan implements MultiStepPlan {
         return fieldsMetadata;
     }
 
+    /** {@inheritDoc} */
+    @Override public FieldsMetadata paramsMetadata() {
+        return paramsMetadata;
+    }
+
     /** {@inheritDoc} */
     @Override public FragmentMapping mapping(Fragment fragment) {
         return fragment.mapping();
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/DynamicParamTypeExtractor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/DynamicParamTypeExtractor.java
new file mode 100644
index 00000000000..a6ae77d1dff
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/DynamicParamTypeExtractor.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.calcite.prepare;
+
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexDynamicParam;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
+import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
+
+/** */
+public class DynamicParamTypeExtractor {
+    /** */
+    public static ParamsMetadata go(IgniteRel root) {
+        DynamicParamsShuttle paramsShuttle = new DynamicParamsShuttle();
+
+        new IgniteRelRexNodeShuttle(paramsShuttle).visit(root);
+
+        return new ParamsMetadata(paramsShuttle.acc.values());
+    }
+
+    /** */
+    private static final class DynamicParamsShuttle extends RexShuttle {
+        /** */
+        private final SortedMap<Integer, RexDynamicParam> acc = new TreeMap<>();
+
+        /** {@inheritDoc} */
+        @Override public RexNode visitDynamicParam(RexDynamicParam param) {
+            acc.put(param.getIndex(), param);
+
+            return super.visitDynamicParam(param);
+        }
+    }
+
+    /** */
+    private static final class ParamsMetadata implements FieldsMetadata {
+        /** */
+        private final Collection<RexDynamicParam> params;
+
+        /** */
+        ParamsMetadata(Collection<RexDynamicParam> params) {
+            this.params = params;
+        }
+
+        /** {@inheritDoc} */
+        @Override public RelDataType rowType() {
+            return null;
+        }
+
+        /** {@inheritDoc} */
+        @Override public List<GridQueryFieldMetadata> queryFieldsMetadata(IgniteTypeFactory typeFactory) {
+            return params.stream().map(param -> {
+                RelDataType paramType = param.getType();
+                Type fieldCls = typeFactory.getResultClass(paramType);
+
+                return new CalciteQueryFieldMetadata(
+                    null,
+                    null,
+                    param.getName(),
+                    fieldCls == null ? Void.class.getName() : fieldCls.getTypeName(),
+                    paramType.getPrecision(),
+                    paramType.getScale(),
+                    paramType.isNullable()
+                );
+            }).collect(Collectors.toList());
+        }
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/FieldsMetadata.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/FieldsMetadata.java
index 492926cd989..5164878d4d5 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/FieldsMetadata.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/FieldsMetadata.java
@@ -17,14 +17,10 @@
 
 package org.apache.ignite.internal.processors.query.calcite.prepare;
 
-import java.lang.reflect.Type;
 import java.util.List;
-import com.google.common.collect.ImmutableList;
 import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
 import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
-import org.apache.ignite.internal.util.typedef.F;
 
 /**
  *
@@ -35,41 +31,9 @@ public interface FieldsMetadata {
      */
     RelDataType rowType();
 
-    /**
-     * @return Result row origins (or where a field value comes from).
-     */
-    List<List<String>> origins();
-
     /**
      * @param typeFactory Type factory.
      * @return Query field descriptors collection&
      */
-    default List<GridQueryFieldMetadata> queryFieldsMetadata(IgniteTypeFactory typeFactory) {
-        RelDataType rowType = rowType();
-        List<List<String>> origins = origins();
-        List<RelDataTypeField> fields = rowType.getFieldList();
-
-        assert origins == null || fields.size() == origins.size();
-
-        ImmutableList.Builder<GridQueryFieldMetadata> b = ImmutableList.builder();
-
-        for (int i = 0; i < fields.size(); i++) {
-            List<String> origin = origins != null ? origins.get(i) : null;
-            RelDataTypeField field = fields.get(i);
-            RelDataType fieldType = field.getType();
-            Type fieldCls = typeFactory.getResultClass(fieldType);
-
-            b.add(new CalciteQueryFieldMetadata(
-                F.isEmpty(origin) ? null : origin.get(0),
-                F.isEmpty(origin) ? null : origin.get(1),
-                F.isEmpty(origin) ? field.getName() : origin.get(2),
-                fieldCls == null ? Void.class.getName() : fieldCls.getTypeName(),
-                fieldType.getPrecision(),
-                fieldType.getScale(),
-                fieldType.isNullable()
-            ));
-        }
-
-        return b.build();
-    }
+    List<GridQueryFieldMetadata> queryFieldsMetadata(IgniteTypeFactory typeFactory);
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/FieldsMetadataImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/FieldsMetadataImpl.java
index 42a7c14b565..06dea24164d 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/FieldsMetadataImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/FieldsMetadataImpl.java
@@ -17,30 +17,71 @@
 
 package org.apache.ignite.internal.processors.query.calcite.prepare;
 
+import java.lang.reflect.Type;
 import java.util.List;
+import com.google.common.collect.ImmutableList;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
+import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
+import org.apache.ignite.internal.util.typedef.F;
 
 /** */
 public class FieldsMetadataImpl implements FieldsMetadata {
     /** */
     private final RelDataType rowType;
 
+    /** */
+    private final RelDataType sqlRowType;
+
     /** */
     private final List<List<String>> origins;
 
     /** */
     public FieldsMetadataImpl(RelDataType rowType, List<List<String>> origins) {
         this.rowType = rowType;
+        sqlRowType = rowType;
+        this.origins = origins;
+    }
+
+    /** */
+    public FieldsMetadataImpl(RelDataType sqlRowType, RelDataType rowType, List<List<String>> origins) {
+        this.rowType = rowType;
+        this.sqlRowType = sqlRowType;
         this.origins = origins;
     }
 
+
     /** {@inheritDoc} */
     @Override public RelDataType rowType() {
         return rowType;
     }
 
     /** {@inheritDoc} */
-    @Override public List<List<String>> origins() {
-        return origins;
+    @Override public List<GridQueryFieldMetadata> queryFieldsMetadata(IgniteTypeFactory typeFactory) {
+        List<RelDataTypeField> fields = sqlRowType.getFieldList();
+
+        assert origins == null || fields.size() == origins.size();
+
+        ImmutableList.Builder<GridQueryFieldMetadata> b = ImmutableList.builder();
+
+        for (int i = 0; i < fields.size(); i++) {
+            List<String> origin = origins != null ? origins.get(i) : null;
+            RelDataTypeField field = fields.get(i);
+            RelDataType fieldType = field.getType();
+            Type fieldCls = typeFactory.getResultClass(fieldType);
+
+            b.add(new CalciteQueryFieldMetadata(
+                F.isEmpty(origin) ? null : origin.get(0),
+                F.isEmpty(origin) ? null : origin.get(1),
+                F.isEmpty(origin) ? field.getName() : origin.get(2),
+                fieldCls == null ? Void.class.getName() : fieldCls.getTypeName(),
+                fieldType.getPrecision(),
+                fieldType.getScale(),
+                fieldType.isNullable()
+            ));
+        }
+
+        return b.build();
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelRexNodeShuttle.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelRexNodeShuttle.java
new file mode 100644
index 00000000000..bb3741132bd
--- /dev/null
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteRelRexNodeShuttle.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.calcite.prepare;
+
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedNestedLoopJoin;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteFilter;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteHashIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteLimit;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSort;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteSortedIndexSpool;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableFunctionScan;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableModify;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan;
+
+/** */
+public class IgniteRelRexNodeShuttle extends IgniteRelShuttle {
+    /** */
+    private final RexShuttle rexShuttle;
+
+    /** */
+    public IgniteRelRexNodeShuttle(RexShuttle rexShuttle) {
+        this.rexShuttle = rexShuttle;
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteFilter rel) {
+        rexShuttle.apply(rel.getCondition());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteProject rel) {
+        rexShuttle.apply(rel.getProjects());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteNestedLoopJoin rel) {
+        rexShuttle.apply(rel.getCondition());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteCorrelatedNestedLoopJoin rel) {
+        rexShuttle.apply(rel.getCondition());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteMergeJoin rel) {
+        rexShuttle.apply(rel.getCondition());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteIndexScan rel) {
+        rexShuttle.apply(rel.projects());
+        rexShuttle.apply(rel.condition());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteTableScan rel) {
+        rexShuttle.apply(rel.projects());
+        rexShuttle.apply(rel.condition());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteSortedIndexSpool rel) {
+        rexShuttle.apply(rel.condition());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteHashIndexSpool rel) {
+        rexShuttle.apply(rel.condition());
+        rexShuttle.apply(rel.searchRow());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteTableModify rel) {
+        rexShuttle.apply(rel.getSourceExpressionList());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteLimit rel) {
+        rexShuttle.apply(rel.fetch());
+        rexShuttle.apply(rel.offset());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteSort rel) {
+        rexShuttle.apply(rel.offset);
+        rexShuttle.apply(rel.fetch);
+        rexShuttle.apply(rel.getSortExps());
+
+        return super.visit(rel);
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteRel visit(IgniteTableFunctionScan rel) {
+        rexShuttle.apply(rel.getCall());
+
+        return super.visit(rel);
+    }
+}
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepDmlPlan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepDmlPlan.java
index de2aae72483..6eaae464090 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepDmlPlan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepDmlPlan.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.processors.query.calcite.prepare;
 
+import org.jetbrains.annotations.Nullable;
+
 /**
  * Distributed dml plan.
  */
@@ -24,8 +26,12 @@ public class MultiStepDmlPlan extends AbstractMultiStepPlan {
     /**
      * @param fieldsMeta Fields metadata.
      */
-    public MultiStepDmlPlan(QueryTemplate queryTemplate, FieldsMetadata fieldsMeta) {
-        super(queryTemplate, fieldsMeta);
+    public MultiStepDmlPlan(
+        QueryTemplate queryTemplate,
+        FieldsMetadata fieldsMeta,
+        @Nullable FieldsMetadata paramsMetadata
+    ) {
+        super(queryTemplate, fieldsMeta, paramsMetadata);
     }
 
     /** {@inheritDoc} */
@@ -35,6 +41,6 @@ public class MultiStepDmlPlan extends AbstractMultiStepPlan {
 
     /** {@inheritDoc} */
     @Override public QueryPlan copy() {
-        return new MultiStepDmlPlan(queryTemplate, fieldsMetadata);
+        return new MultiStepDmlPlan(queryTemplate, fieldsMetadata, paramsMetadata);
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepPlan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepPlan.java
index fc27362da2a..1636bcc1124 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepPlan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepPlan.java
@@ -38,6 +38,11 @@ public interface MultiStepPlan extends QueryPlan {
      */
     FieldsMetadata fieldsMetadata();
 
+    /**
+     * @return Parameters metadata;
+     */
+    FieldsMetadata paramsMetadata();
+
     /**
      * @param fragment Fragment.
      * @return Mapping for a given fragment.
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepQueryPlan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepQueryPlan.java
index 866514fa936..1a18f81eb0b 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepQueryPlan.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/MultiStepQueryPlan.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.processors.query.calcite.prepare;
 
+import org.jetbrains.annotations.Nullable;
+
 /**
  * Distributed query plan.
  */
@@ -24,8 +26,12 @@ public class MultiStepQueryPlan extends AbstractMultiStepPlan {
     /**
      * @param fieldsMeta Fields metadata.
      */
-    public MultiStepQueryPlan(QueryTemplate queryTemplate, FieldsMetadata fieldsMeta) {
-        super(queryTemplate, fieldsMeta);
+    public MultiStepQueryPlan(
+        QueryTemplate queryTemplate,
+        FieldsMetadata fieldsMeta,
+        @Nullable FieldsMetadata paramsMetadata
+    ) {
+        super(queryTemplate, fieldsMeta, paramsMetadata);
     }
 
     /** {@inheritDoc} */
@@ -35,6 +41,6 @@ public class MultiStepQueryPlan extends AbstractMultiStepPlan {
 
     /** {@inheritDoc} */
     @Override public QueryPlan copy() {
-        return new MultiStepQueryPlan(queryTemplate, fieldsMetadata);
+        return new MultiStepQueryPlan(queryTemplate, fieldsMetadata, paramsMetadata);
     }
 }
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PrepareServiceImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PrepareServiceImpl.java
index 5494caf621c..922eb5953d2 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PrepareServiceImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PrepareServiceImpl.java
@@ -151,12 +151,16 @@ public class PrepareServiceImpl extends AbstractService implements PrepareServic
 
         IgniteRel igniteRel = optimize(sqlNode, planner, log);
 
+        // Extract parameters meta.
+        FieldsMetadata params = DynamicParamTypeExtractor.go(igniteRel);
+
         // Split query plan to query fragments.
         List<Fragment> fragments = new Splitter().go(igniteRel);
 
         QueryTemplate template = new QueryTemplate(fragments);
 
-        return new MultiStepQueryPlan(template, queryFieldsMetadata(ctx, validated.dataType(), validated.origins()));
+        return new MultiStepQueryPlan(template, queryFieldsMetadata(ctx, validated.dataType(), validated.origins()),
+            params);
     }
 
     /** */
@@ -169,12 +173,15 @@ public class PrepareServiceImpl extends AbstractService implements PrepareServic
         // Convert to Relational operators graph
         IgniteRel igniteRel = optimize(sqlNode, planner, log);
 
+        // Extract parameters meta.
+        FieldsMetadata params = DynamicParamTypeExtractor.go(igniteRel);
+
         // Split query plan to query fragments.
         List<Fragment> fragments = new Splitter().go(igniteRel);
 
         QueryTemplate template = new QueryTemplate(fragments);
 
-        return new MultiStepDmlPlan(template, queryFieldsMetadata(ctx, igniteRel.getRowType(), null));
+        return new MultiStepDmlPlan(template, queryFieldsMetadata(ctx, igniteRel.getRowType(), null), params);
     }
 
     /** */
@@ -183,7 +190,7 @@ public class PrepareServiceImpl extends AbstractService implements PrepareServic
         RelDataType resultType = TypeUtils.getResultType(
             ctx.typeFactory(), ctx.catalogReader(), sqlType, origins);
 
-        return new FieldsMetadataImpl(resultType, origins);
+        return new FieldsMetadataImpl(sqlType, resultType, origins);
     }
 
     /** */
diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java
index 6d23cde0a3e..2a64f93962e 100644
--- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java
+++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java
@@ -156,23 +156,23 @@ public class CacheTableDescriptorImpl extends NullInitializerExpressionFactory
         int valField = QueryUtils.VAL_COL;
 
         for (String field : fields) {
+            GridQueryProperty prop = typeDesc.property(field);
+
             if (Objects.equals(field, typeDesc.keyFieldAlias())) {
                 keyField = descriptors.size();
 
                 virtualFields.set(0);
 
-                descriptors.add(new KeyValDescriptor(typeDesc.keyFieldAlias(), typeDesc.keyClass(), true, fldIdx++));
+                descriptors.add(new KeyValDescriptor(typeDesc.keyFieldAlias(), prop, true, fldIdx++));
             }
             else if (Objects.equals(field, typeDesc.valueFieldAlias())) {
                 valField = descriptors.size();
 
                 virtualFields.set(1);
 
-                descriptors.add(new KeyValDescriptor(typeDesc.valueFieldAlias(), typeDesc.valueClass(), false, fldIdx++));
+                descriptors.add(new KeyValDescriptor(typeDesc.valueFieldAlias(), prop, false, fldIdx++));
             }
             else {
-                GridQueryProperty prop = typeDesc.property(field);
-
                 virtualFields.set(prop.key() ? 0 : 1);
 
                 descriptors.add(new FieldDescriptor(prop, fldIdx++));
@@ -638,6 +638,9 @@ public class CacheTableDescriptorImpl extends NullInitializerExpressionFactory
 
     /** */
     private static class KeyValDescriptor implements CacheColumnDescriptor {
+        /** */
+        private final GridQueryProperty desc;
+
         /** */
         private final String name;
 
@@ -658,10 +661,21 @@ public class CacheTableDescriptorImpl extends NullInitializerExpressionFactory
             this.name = name;
             this.isKey = isKey;
             this.fieldIdx = fieldIdx;
+            desc = null;
 
             storageType = type;
         }
 
+        /** */
+        private KeyValDescriptor(String name, GridQueryProperty desc, boolean isKey, int fieldIdx) {
+            this.name = name;
+            this.isKey = isKey;
+            this.fieldIdx = fieldIdx;
+            this.desc = desc;
+
+            storageType = desc.type();
+        }
+
         /** {@inheritDoc} */
         @Override public boolean field() {
             return false;
@@ -694,8 +708,14 @@ public class CacheTableDescriptorImpl extends NullInitializerExpressionFactory
 
         /** {@inheritDoc} */
         @Override public RelDataType logicalType(IgniteTypeFactory f) {
-            if (logicalType == null)
-                logicalType = TypeUtils.sqlType(f, storageType, PRECISION_NOT_SPECIFIED, SCALE_NOT_SPECIFIED);
+            if (logicalType == null) {
+                logicalType = TypeUtils.sqlType(
+                    f,
+                    storageType,
+                    desc != null && desc.precision() != -1 ? desc.precision() : PRECISION_NOT_SPECIFIED,
+                    desc != null && desc.scale() != -1 ? desc.scale() : SCALE_NOT_SPECIFIED
+                );
+            }
 
             return logicalType;
         }
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/QueryMetadataIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/QueryMetadataIntegrationTest.java
new file mode 100644
index 00000000000..168d91009fe
--- /dev/null
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/QueryMetadataIntegrationTest.java
@@ -0,0 +1,345 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.query.calcite.integration;
+
+import java.math.BigDecimal;
+import java.sql.ResultSetMetaData;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
+import org.apache.ignite.internal.processors.query.QueryEngine;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+import org.apache.ignite.internal.util.typedef.T2;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.apache.calcite.rel.type.RelDataType.PRECISION_NOT_SPECIFIED;
+import static org.apache.calcite.rel.type.RelDataType.SCALE_NOT_SPECIFIED;
+
+/**
+ * Test query metadata.
+ */
+public class QueryMetadataIntegrationTest extends AbstractBasicIntegrationTest {
+    /** */
+    @Test
+    public void testJoin() throws Exception {
+        executeSql("CREATE TABLE tbl1 (id DECIMAL(10, 2), val VARCHAR, val2 BIGINT, ts TIMESTAMP(14), PRIMARY KEY(id, val))");
+        executeSql("CREATE TABLE tbl2 (id DECIMAL(10, 2), val VARCHAR, val2 BIGINT, ts TIMESTAMP(14), PRIMARY KEY(id, val))");
+
+        checker("select * from tbl1 inner join tbl2 on (tbl1.id > tbl2.id and tbl1.id <> ?) " +
+            "where tbl1.id > ? and tbl2.val like ? or tbl1.ts = ?")
+            .addMeta(
+                builder -> builder
+                    .add("PUBLIC", "TBL1", BigDecimal.class, "ID", 10, 2, false)
+                    .add("PUBLIC", "TBL1", String.class, "VAL", true)
+                    .add("PUBLIC", "TBL1", Long.class, "VAL2", 19, 0, true)
+                    .add("PUBLIC", "TBL1", java.sql.Timestamp.class, "TS", 0, SCALE_NOT_SPECIFIED, true)
+                    .add("PUBLIC", "TBL2", BigDecimal.class, "ID", 10, 2, false)
+                    .add("PUBLIC", "TBL2", String.class, "VAL", true)
+                    .add("PUBLIC", "TBL2", Long.class, "VAL2", 19, 0, true)
+                    .add("PUBLIC", "TBL2", java.sql.Timestamp.class, "TS", 0, SCALE_NOT_SPECIFIED, true),
+                builder -> builder
+                    .add(BigDecimal.class, 10, 2)
+                    .add(BigDecimal.class, 10, 2)
+                    .add(String.class)
+                    .add(java.sql.Timestamp.class, 0, SCALE_NOT_SPECIFIED)
+            ).check();
+    }
+
+    /** */
+    @Test
+    public void testMultipleConditions() throws Exception {
+        executeSql("CREATE TABLE tbl (id BIGINT, val VARCHAR, PRIMARY KEY(id))");
+        executeSql("CREATE INDEX tbl_val_idx ON tbl(val)");
+
+        checker("select * from tbl where id in (?, ?) or (id > ? and id <= ?) or (val <> ?)")
+            .addMeta(
+                builder -> builder
+                    .add("PUBLIC", "TBL", Long.class, "ID", 19, 0, true)
+                    .add("PUBLIC", "TBL", String.class, "VAL", true),
+                builder -> builder
+                    .add(Long.class, 19, 0)
+                    .add(Long.class, 19, 0)
+                    .add(Long.class, 19, 0)
+                    .add(Long.class, 19, 0)
+                    .add(String.class)
+            ).check();
+    }
+
+    /** */
+    @Test
+    public void testMultipleQueries() throws Exception {
+        executeSql("CREATE TABLE tbl (id BIGINT, val VARCHAR, PRIMARY KEY(id))");
+        executeSql("CREATE INDEX tbl_val_idx ON tbl(val)");
+
+        checker("insert into tbl(id, val) values (?, ?); select * from tbl where id > ?")
+            .addMeta(
+                builder -> builder
+                    .add(null, null, long.class, "ROWCOUNT", 19, 0, false),
+                builder -> builder
+                    .add(Long.class, 19, 0)
+                    .add(String.class)
+            )
+            .addMeta(
+                builder -> builder
+                    .add("PUBLIC", "TBL", Long.class, "ID", 19, 0, true)
+                    .add("PUBLIC", "TBL", String.class, "VAL", true),
+                builder -> builder
+                    .add(Long.class, 19, 0)
+            )
+            .check();
+    }
+
+    /** */
+    @Test
+    public void testDml() throws Exception {
+        executeSql("CREATE TABLE tbl1 (id BIGINT, val VARCHAR, PRIMARY KEY(id))");
+        executeSql("CREATE TABLE tbl2 (id BIGINT, val VARCHAR, PRIMARY KEY(id))");
+
+        checker("insert into tbl1(id, val) values (?, ?)")
+            .addMeta(
+                builder -> builder
+                    .add(null, null, long.class, "ROWCOUNT", 19, 0, false),
+                builder -> builder
+                    .add(Long.class, 19, 0)
+                    .add(String.class)
+            )
+            .check();
+
+        checker("insert into tbl2(id) select id from tbl1 where id > ? and val in (?, ?)")
+            .addMeta(
+                builder -> builder
+                    .add(null, null, long.class, "ROWCOUNT", 19, 0, false),
+                builder -> builder
+                    .add(Long.class, 19, 0)
+                    .add(String.class)
+                    .add(String.class)
+            )
+            .check();
+
+        checker("update tbl2 set val = ? where id in (?, ?)")
+            .addMeta(
+                builder -> builder
+                    .add(null, null, long.class, "ROWCOUNT", 19, 0, false),
+                builder -> builder
+                    .add(String.class)
+                    .add(Long.class, 19, 0)
+                    .add(Long.class, 19, 0)
+            )
+            .check();
+    }
+
+    /** */
+    @Test
+    public void testDdl() throws Exception {
+        executeSql("CREATE TABLE tbl1 (id BIGINT, val VARCHAR, PRIMARY KEY(id))");
+
+        checker("CREATE TABLE tbl2 (id BIGINT, val VARCHAR, PRIMARY KEY(id))")
+            .addMeta(builder -> {}, builder -> {})
+            .check();
+
+        // Metadata of create as select is always empty, because it is impossible to validate insert
+        // query without creating table.
+        checker("CREATE TABLE tbl2 (id, val) AS SELECT id, val FROM tbl1 WHERE id > ?")
+            .addMeta(builder -> {}, builder -> {})
+            .check();
+    }
+
+    /** */
+    @Test
+    public void testExplain() throws Exception {
+        executeSql("CREATE TABLE tbl (id BIGINT, val VARCHAR, PRIMARY KEY(id))");
+
+        checker("explain plan for select * from tbl where id in (?, ?) or (id > ? and id <= ?) or (val <> ?)")
+            .addMeta(
+                builder -> builder
+                    .add(null, null, String.class, "PLAN", false),
+                builder -> {}
+            ).check();
+    }
+
+    /** */
+    private MetadataChecker checker(String sql) {
+        return new MetadataChecker(queryEngine(client), sql);
+    }
+
+    /** */
+    private static class MetadataChecker {
+        /** */
+        private final QueryEngine qryEngine;
+
+        /** */
+        private final String sql;
+
+        /** */
+        private final String schema;
+
+        /** */
+        private final List<T2<List<GridQueryFieldMetadata>, List<GridQueryFieldMetadata>>> expected = new ArrayList<>();
+
+        /** */
+        private int paramOffset;
+
+        /** */
+        public MetadataChecker(QueryEngine qryEngine, String sql) {
+            this(qryEngine, sql, "PUBLIC");
+        }
+
+        /** */
+        public MetadataChecker(QueryEngine qryEngine, String sql, String schema) {
+            this.qryEngine = qryEngine;
+            this.sql = sql;
+            this.schema = schema;
+        }
+
+        /** */
+        public MetadataChecker addMeta(Consumer<MetadataBuilder> resultMeta, Consumer<MetadataBuilder> paramMeta) {
+            MetadataBuilder rmBuilder = new MetadataBuilder();
+            resultMeta.accept(rmBuilder);
+
+            MetadataBuilder prmBuilder = new MetadataBuilder(paramOffset);
+            paramMeta.accept(prmBuilder);
+            List<GridQueryFieldMetadata> paramMetaRes = prmBuilder.build();
+            paramOffset += paramMetaRes.size();
+
+            expected.add(new T2<>(rmBuilder.build(), paramMetaRes));
+
+            return this;
+        }
+
+        /** */
+        public void check() throws Exception {
+            List<List<GridQueryFieldMetadata>> actualRsMeta = qryEngine.resultSetMetaData(null, schema, sql);
+            List<List<GridQueryFieldMetadata>> actualParamMeta = qryEngine.parameterMetaData(null, schema, sql);
+
+            assertEquals(expected.size(), actualRsMeta.size());
+            assertEquals(expected.size(), actualParamMeta.size());
+
+            for (int i = 0; i < expected.size(); ++i) {
+                checkMeta(expected.get(i).getKey(), actualRsMeta.get(i));
+                checkMeta(expected.get(i).getValue(), actualParamMeta.get(i));
+            }
+        }
+
+        /** */
+        private void checkMeta(List<GridQueryFieldMetadata> exp, List<GridQueryFieldMetadata> actual) {
+            assertEquals(exp.size(), actual.size());
+
+            for (int i = 0; i < actual.size(); ++i) {
+                GridQueryFieldMetadata actualMeta = actual.get(i);
+                GridQueryFieldMetadata expMeta = exp.get(i);
+
+                assertEquals(expMeta.schemaName(), actualMeta.schemaName());
+                assertEquals(expMeta.typeName(), actualMeta.typeName());
+                assertEquals(expMeta.fieldName(), actualMeta.fieldName());
+                assertEquals(expMeta.fieldTypeName(), actualMeta.fieldTypeName());
+                assertEquals(expMeta.nullability(), actualMeta.nullability());
+                assertEquals(expMeta.scale(), actualMeta.scale());
+                assertEquals(expMeta.precision(), actualMeta.precision());
+            }
+        }
+    }
+
+    /** */
+    private static class MetadataBuilder {
+        /** */
+        private final List<GridQueryFieldMetadata> fields = new ArrayList<>();
+
+        /** */
+        private final int fieldOffset;
+
+        /** */
+        MetadataBuilder() {
+            this(0);
+        }
+
+        /** */
+        MetadataBuilder(int fieldOffset) {
+            this.fieldOffset = fieldOffset;
+        }
+
+        /** */
+        MetadataBuilder add(Class<?> fieldClass) {
+            add(fieldClass, PRECISION_NOT_SPECIFIED, SCALE_NOT_SPECIFIED);
+            return this;
+        }
+
+        /** */
+        MetadataBuilder add(Class<?> fieldClass, int precision, int scale) {
+            add(null, null, fieldClass, "?" + (fields.size() + fieldOffset), precision, scale, true);
+            return this;
+        }
+
+        /** */
+        MetadataBuilder add(String schemaName, String typeName, Class<?> fieldClass, String fieldName) {
+            add(schemaName, typeName, fieldClass, fieldName, true);
+            return this;
+        }
+
+        /** */
+        MetadataBuilder add(
+            String schemaName,
+            String typeName,
+            Class<?> fieldClass,
+            String fieldName,
+            boolean isNullable
+        ) {
+            add(schemaName, typeName, fieldClass, fieldName, PRECISION_NOT_SPECIFIED, SCALE_NOT_SPECIFIED, isNullable);
+            return this;
+        }
+
+        /** */
+        MetadataBuilder add(
+            String schemaName,
+            String typeName,
+            Class<?> fieldClass,
+            String fieldName,
+            int precision,
+            int scale,
+            boolean isNullable
+        ) {
+            GridQueryFieldMetadata mocked = Mockito.mock(GridQueryFieldMetadata.class);
+
+            Mockito.when(mocked.schemaName()).thenReturn(schemaName);
+            Mockito.when(mocked.typeName()).thenReturn(typeName);
+            Mockito.when(mocked.fieldName()).thenReturn(fieldName);
+            Mockito.when(mocked.fieldTypeName()).thenReturn(fieldClass.getName());
+            Mockito.when(mocked.precision()).thenReturn(precision);
+            Mockito.when(mocked.scale()).thenReturn(scale);
+            Mockito.when(mocked.nullability())
+                .thenReturn(isNullable ? ResultSetMetaData.columnNullable : ResultSetMetaData.columnNoNulls);
+
+            fields.add(mocked);
+
+            return this;
+        }
+
+        /** */
+        List<GridQueryFieldMetadata> build() {
+            return Collections.unmodifiableList(fields);
+        }
+    }
+
+    /** */
+    private QueryEngine queryEngine(IgniteEx ign) {
+        return Commons.lookupComponent(ign.context(), QueryEngine.class);
+    }
+}
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java
index c5e4bc017fe..97504e94ca1 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java
@@ -18,9 +18,12 @@
 package org.apache.ignite.internal.processors.query.calcite.jdbc;
 
 import java.io.Serializable;
+import java.math.BigDecimal;
 import java.sql.BatchUpdateException;
 import java.sql.Connection;
 import java.sql.DriverManager;
+import java.sql.JDBCType;
+import java.sql.ParameterMetaData;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
@@ -346,6 +349,52 @@ public class JdbcQueryTest extends GridCommonAbstractTest {
         stmt.close();
     }
 
+    /**
+     * @throws SQLException If failed.
+     */
+    @Test
+    public void testParametersMetadata() throws Exception {
+        stmt.execute("CREATE TABLE Person(id BIGINT, PRIMARY KEY(id), name VARCHAR, amount DECIMAL(10,2))");
+
+        try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO Person VALUES (?, ?, ?)")) {
+            ParameterMetaData meta = stmt.getParameterMetaData();
+
+            assertEquals(3, meta.getParameterCount(), 3);
+
+            assertEquals(Long.class.getName(), meta.getParameterClassName(1));
+            assertEquals(String.class.getName(), meta.getParameterClassName(2));
+            assertEquals(BigDecimal.class.getName(), meta.getParameterClassName(3));
+
+            assertEquals(JDBCType.valueOf(Types.BIGINT).getName(), meta.getParameterTypeName(1));
+            assertEquals(JDBCType.valueOf(Types.VARCHAR).getName(), meta.getParameterTypeName(2));
+            assertEquals(JDBCType.valueOf(Types.DECIMAL).getName(), meta.getParameterTypeName(3));
+
+            assertEquals(Types.BIGINT, meta.getParameterType(1));
+            assertEquals(Types.VARCHAR, meta.getParameterType(2));
+            assertEquals(Types.DECIMAL, meta.getParameterType(3));
+
+            assertEquals(19, meta.getPrecision(1));
+            assertEquals(-1, meta.getPrecision(2));
+            assertEquals(10, meta.getPrecision(3));
+
+            assertEquals(0, meta.getScale(1));
+            assertEquals(Integer.MIN_VALUE, meta.getScale(2));
+            assertEquals(2, meta.getScale(3));
+
+            assertEquals(ParameterMetaData.parameterNullable, meta.isNullable(1));
+            assertEquals(ParameterMetaData.parameterNullable, meta.isNullable(2));
+            assertEquals(ParameterMetaData.parameterNullable, meta.isNullable(3));
+
+            assertEquals(ParameterMetaData.parameterModeIn, meta.getParameterMode(1));
+            assertEquals(ParameterMetaData.parameterModeIn, meta.getParameterMode(2));
+            assertEquals(ParameterMetaData.parameterModeIn, meta.getParameterMode(3));
+
+            assertTrue(meta.isSigned(1));
+            assertTrue(meta.isSigned(2));
+            assertTrue(meta.isSigned(3));
+        }
+    }
+
     /** Some object to store. */
     private static class ObjectToStore implements Serializable {
         /** */
diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/PlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/PlannerTest.java
index 19a2d8595ef..26bb1b06133 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/PlannerTest.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/PlannerTest.java
@@ -166,7 +166,7 @@ public class PlannerTest extends AbstractPlannerTest {
 
         assertNotNull(phys);
 
-        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null);
+        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null, null);
 
         assertNotNull(plan);
 
@@ -274,8 +274,7 @@ public class PlannerTest extends AbstractPlannerTest {
 
         assertNotNull(phys);
 
-        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null);
-
+        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null, null);
         assertNotNull(plan);
 
         plan.init(this::intermediateMapping, Commons.mapContext(F.first(nodes), AffinityTopologyVersion.NONE));
@@ -501,7 +500,7 @@ public class PlannerTest extends AbstractPlannerTest {
 
         assertNotNull(phys);
 
-        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null);
+        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null, null);
 
         assertNotNull(plan);
 
@@ -720,7 +719,7 @@ public class PlannerTest extends AbstractPlannerTest {
 
         assertNotNull(phys);
 
-        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null);
+        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null, null);
 
         assertNotNull(plan);
 
@@ -803,7 +802,7 @@ public class PlannerTest extends AbstractPlannerTest {
 
         assertNotNull(phys);
 
-        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null);
+        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null, null);
 
         assertNotNull(plan);
 
@@ -883,7 +882,7 @@ public class PlannerTest extends AbstractPlannerTest {
 
         assertNotNull(phys);
 
-        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null);
+        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null, null);
 
         assertNotNull(plan);
 
@@ -965,7 +964,7 @@ public class PlannerTest extends AbstractPlannerTest {
 
         assertNotNull(phys);
 
-        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null);
+        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null, null);
 
         assertNotNull(plan);
 
@@ -1041,7 +1040,7 @@ public class PlannerTest extends AbstractPlannerTest {
 
         assertNotNull(phys);
 
-        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null);
+        MultiStepPlan plan = new MultiStepQueryPlan(new QueryTemplate(new Splitter().go(phys)), null, null);
 
         assertNotNull(plan);
 
diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
index ded1524ad90..c6f061890c9 100644
--- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
+++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
@@ -41,6 +41,7 @@ import org.apache.ignite.internal.processors.query.calcite.integration.KillComma
 import org.apache.ignite.internal.processors.query.calcite.integration.KillQueryCommandDdlIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.MetadataIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.QueryEngineConfigurationIntegrationTest;
+import org.apache.ignite.internal.processors.query.calcite.integration.QueryMetadataIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.RunningQueriesIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.SearchSargOnIndexIntegrationTest;
 import org.apache.ignite.internal.processors.query.calcite.integration.ServerStatisticsIntegrationTest;
@@ -107,6 +108,7 @@ import org.junit.runners.Suite;
     QueryEngineConfigurationIntegrationTest.class,
     SearchSargOnIndexIntegrationTest.class,
     KeepBinaryIntegrationTest.class,
+    QueryMetadataIntegrationTest.class
 })
 public class IntegrationTestSuite {
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement.java
index c632bccf78b..858a1b63f8b 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc2/JdbcPreparedStatement.java
@@ -305,7 +305,7 @@ public class JdbcPreparedStatement extends JdbcStatement implements PreparedStat
         setupQuery(qry);
 
         try {
-            List<GridQueryFieldMetadata> meta = conn.ignite().context().query().getIndexing().resultMetaData(conn.schemaName(), qry);
+            List<GridQueryFieldMetadata> meta = conn.ignite().context().query().resultSetMetaData(qry, null);
 
             if (meta == null)
                 return null;
@@ -366,7 +366,7 @@ public class JdbcPreparedStatement extends JdbcStatement implements PreparedStat
         setupQuery(qry);
 
         try {
-            List<JdbcParameterMeta> params = conn.ignite().context().query().getIndexing().parameterMetaData(conn.schemaName(), qry);
+            List<JdbcParameterMeta> params = conn.ignite().context().query().parameterMetaData(qry, null);
 
             return new JdbcThinParameterMetadata(params);
         }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcParameterMeta.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcParameterMeta.java
index f1e1f915255..5f953499560 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcParameterMeta.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcParameterMeta.java
@@ -22,6 +22,8 @@ import java.sql.SQLException;
 import org.apache.ignite.binary.BinaryObjectException;
 import org.apache.ignite.internal.binary.BinaryReaderExImpl;
 import org.apache.ignite.internal.binary.BinaryWriterExImpl;
+import org.apache.ignite.internal.jdbc.thin.JdbcThinUtils;
+import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
 import org.apache.ignite.internal.util.typedef.internal.S;
 
 /**
@@ -75,6 +77,18 @@ public class JdbcParameterMeta implements JdbcRawBinarylizable {
         mode = meta.getParameterMode(order);
     }
 
+    /** */
+    public JdbcParameterMeta(GridQueryFieldMetadata meta) {
+        isNullable = meta.nullability();
+        signed = true;
+        precision = meta.precision();
+        scale = meta.scale();
+        type = JdbcThinUtils.type(meta.fieldTypeName());
+        typeName = JdbcThinUtils.typeName(meta.fieldTypeName());
+        typeClass = meta.fieldTypeName();
+        mode = ParameterMetaData.parameterModeIn;
+    }
+
     /**
      * @return Nullable mode.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandler.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandler.java
index 10937a9c393..f0be798624c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandler.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandler.java
@@ -1290,8 +1290,7 @@ public class JdbcRequestHandler implements ClientListenerRequestHandler {
         setupQuery(qry, schemaName);
 
         try {
-            List<JdbcParameterMeta> meta = connCtx.kernalContext().query().getIndexing().
-                parameterMetaData(schemaName, qry);
+            List<JdbcParameterMeta> meta = connCtx.kernalContext().query().parameterMetaData(qry, cliCtx);
 
             JdbcMetaParamsResult res = new JdbcMetaParamsResult(meta);
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandlerWorker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandlerWorker.java
index 3833992adf9..06fda201a2d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandlerWorker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcRequestHandlerWorker.java
@@ -98,7 +98,7 @@ class JdbcRequestHandlerWorker extends GridWorker {
         finally {
             // Notify indexing that this worker is being stopped.
             try {
-                ctx.query().getIndexing().onClientDisconnect();
+                ctx.query().onClientDisconnect();
             }
             catch (Exception ignored) {
                 // No-op.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcRequestHandler.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcRequestHandler.java
index 58bea940164..5415395ff7c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcRequestHandler.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcRequestHandler.java
@@ -766,7 +766,7 @@ public class OdbcRequestHandler implements ClientListenerRequestHandler {
 
             SqlFieldsQueryEx qry = makeQuery(schema, sql);
 
-            List<JdbcParameterMeta> params = ctx.query().getIndexing().parameterMetaData(schema, qry);
+            List<JdbcParameterMeta> params = ctx.query().parameterMetaData(qry, cliCtx);
 
             byte[] typeIds = new byte[params.size()];
 
@@ -801,7 +801,7 @@ public class OdbcRequestHandler implements ClientListenerRequestHandler {
 
             SqlFieldsQueryEx qry = makeQuery(schema, sql);
 
-            List<GridQueryFieldMetadata> columns = ctx.query().getIndexing().resultMetaData(schema, qry);
+            List<GridQueryFieldMetadata> columns = ctx.query().resultSetMetaData(qry, cliCtx);
             Collection<OdbcColumnMeta> meta = OdbcUtils.convertMetadata(columns);
 
             OdbcQueryGetResultsetMetaResult res = new OdbcQueryGetResultsetMetaResult(meta);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcRequestHandlerWorker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcRequestHandlerWorker.java
index a9b18682ff3..4a9b5080ea3 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcRequestHandlerWorker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcRequestHandlerWorker.java
@@ -98,7 +98,7 @@ class OdbcRequestHandlerWorker extends GridWorker {
         finally {
             // Notify indexing that this worker is being stopped.
             try {
-                ctx.query().getIndexing().onClientDisconnect();
+                ctx.query().onClientDisconnect();
             }
             catch (Exception e) {
                 // No-op.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
index fd01dc62aeb..e8f5d4d3711 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java
@@ -36,6 +36,7 @@ import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import javax.cache.Cache;
 import javax.cache.CacheException;
 import org.apache.ignite.IgniteCheckedException;
@@ -94,6 +95,7 @@ import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode;
 import org.apache.ignite.internal.processors.cache.query.IndexQueryDesc;
 import org.apache.ignite.internal.processors.cache.query.SqlFieldsQueryEx;
 import org.apache.ignite.internal.processors.cacheobject.IgniteCacheObjectProcessor;
+import org.apache.ignite.internal.processors.odbc.jdbc.JdbcParameterMeta;
 import org.apache.ignite.internal.processors.platform.PlatformContext;
 import org.apache.ignite.internal.processors.platform.PlatformProcessor;
 import org.apache.ignite.internal.processors.query.aware.IndexBuildStatusStorage;
@@ -2887,6 +2889,18 @@ public class GridQueryProcessor extends GridProcessorAdapter {
                 INDEXING.module() + " to classpath or moving it from 'optional' to 'libs' folder).");
     }
 
+    /**
+     * @throws IgniteException If indexing is disabled.
+     */
+    private void checkxModuleEnabled() throws IgniteException {
+        if (!moduleEnabled()) {
+            throw new IgniteException("Failed to execute query because indexing is disabled and no query engine is " +
+                "configured (consider adding module " + INDEXING.module() + " to classpath or moving it " +
+                "from 'optional' to 'libs' folder or configuring any query engine with " +
+                "IgniteConfiguration.SqlConfiguration.QueryEnginesConfiguration property).");
+        }
+    }
+
     /**
      * Execute update on DHT node (i.e. when it is possible to execute and update on all nodes independently).
      *
@@ -3054,12 +3068,7 @@ public class GridQueryProcessor extends GridProcessorAdapter {
         GridCacheQueryType qryType,
         @Nullable final GridQueryCancel cancel
     ) {
-        if (!moduleEnabled()) {
-            throw new IgniteException("Failed to execute query because indexing is disabled and no query engine is " +
-                "configured (consider adding module " + INDEXING.module() + " to classpath or moving it " +
-                "from 'optional' to 'libs' folder or configuring any query engine with " +
-                "IgniteConfiguration.SqlConfiguration.QueryEnginesConfiguration property).");
-        }
+        checkxModuleEnabled();
 
         if (qry.isDistributedJoins() && qry.getPartitions() != null)
             throw new CacheException("Using both partitions and distributed JOINs is not supported for the same query");
@@ -3121,6 +3130,62 @@ public class GridQueryProcessor extends GridProcessorAdapter {
         });
     }
 
+    /** */
+    public List<JdbcParameterMeta> parameterMetaData(
+        final SqlFieldsQuery qry,
+        @Nullable final SqlClientContext cliCtx
+    ) {
+        checkxModuleEnabled();
+
+        return executeQuerySafe(null, () -> {
+            final String schemaName = qry.getSchema() == null ? QueryUtils.DFLT_SCHEMA : qry.getSchema();
+
+            QueryEngine qryEngine = engineForQuery(cliCtx, qry);
+
+            if (qryEngine != null) {
+                List<List<GridQueryFieldMetadata>> meta = qryEngine.parameterMetaData(
+                    QueryContext.of(qry, cliCtx),
+                    schemaName,
+                    qry.getSql());
+
+                return meta.stream()
+                    .flatMap(m -> m.stream().map(JdbcParameterMeta::new))
+                    .collect(Collectors.toList());
+            }
+            else
+                return idx.parameterMetaData(schemaName, qry);
+
+        });
+    }
+
+    /** */
+    public List<GridQueryFieldMetadata> resultSetMetaData(
+        final SqlFieldsQuery qry,
+        @Nullable final SqlClientContext cliCtx
+    ) {
+        checkxModuleEnabled();
+
+        return executeQuerySafe(null, () -> {
+            final String schemaName = qry.getSchema() == null ? QueryUtils.DFLT_SCHEMA : qry.getSchema();
+
+            QueryEngine qryEngine = engineForQuery(cliCtx, qry);
+
+            if (qryEngine != null) {
+                List<List<GridQueryFieldMetadata>> meta = qryEngine.resultSetMetaData(
+                    QueryContext.of(qry, cliCtx),
+                    schemaName,
+                    qry.getSql());
+
+                if (meta.size() == 1)
+                    return meta.get(0);
+
+                return null;
+            }
+            else
+                return idx.resultMetaData(schemaName, qry);
+        });
+    }
+
     /**
      * @param cctx Cache context.
      * @return Schema name.
@@ -3957,6 +4022,16 @@ public class GridQueryProcessor extends GridProcessorAdapter {
         desc.validateKeyAndValue(key, val);
     }
 
+    /**
+     * Performs necessary actions on disconnect of a stateful client (say, one associated with a transaction).
+     *
+     * @throws IgniteCheckedException If failed.
+     */
+    public void onClientDisconnect() throws IgniteCheckedException {
+        if (idx != null)
+            idx.onClientDisconnect();
+    }
+
     /**
      * @param ver Version.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/NoOpQueryEngine.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/NoOpQueryEngine.java
index 4730f65bca0..51ea2abb275 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/NoOpQueryEngine.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/NoOpQueryEngine.java
@@ -45,11 +45,29 @@ public class NoOpQueryEngine extends GridProcessorAdapter implements QueryEngine
         return Collections.emptyList();
     }
 
+    /** {@inheritDoc} */
+    @Override public List<List<GridQueryFieldMetadata>> parameterMetaData(
+        @Nullable QueryContext ctx,
+        String schemaName,
+        String qry
+    ) throws IgniteSQLException {
+        return Collections.emptyList();
+    }
+
+    /** {@inheritDoc} */
+    @Override public List<List<GridQueryFieldMetadata>> resultSetMetaData(
+        @Nullable QueryContext ctx,
+        String schemaName,
+        String qry
+    ) throws IgniteSQLException {
+        return Collections.emptyList();
+    }
+
     /** {@inheritDoc} */
     @Override public List<FieldsQueryCursor<List<?>>> queryBatched(
         @Nullable QueryContext ctx,
         String schemaName,
-        String query,
+        String qry,
         List<Object[]> batchedParams
     ) throws IgniteSQLException {
         return Collections.emptyList();
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEngine.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEngine.java
index 003f37339a1..d6ffeef4507 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEngine.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEngine.java
@@ -20,7 +20,6 @@ package org.apache.ignite.internal.processors.query;
 import java.util.Collection;
 import java.util.List;
 import java.util.UUID;
-
 import org.apache.ignite.cache.query.FieldsQueryCursor;
 import org.apache.ignite.internal.processors.GridProcessor;
 import org.jetbrains.annotations.Nullable;
@@ -42,8 +41,33 @@ public interface QueryEngine extends GridProcessor {
         String schemaName,
         String qry,
         Object... params
-    )
-        throws IgniteSQLException;
+    ) throws IgniteSQLException;
+
+    /**
+     * @param ctx Query context, may be null.
+     * @param schemaName Schema name.
+     * @param qry Query.
+     * @return List of queries' parameters metadata. Size of list depends on number of distinct queries in {@code qry}.
+     * @throws IgniteSQLException If failed.
+     */
+    List<List<GridQueryFieldMetadata>> parameterMetaData(
+        @Nullable QueryContext ctx,
+        String schemaName,
+        String qry
+    ) throws IgniteSQLException;
+
+    /**
+     * @param ctx Query context, may be null.
+     * @param schemaName Schema name.
+     * @param qry Query.
+     * @return List of queries' result sets metadata. Size of list depends on number of distinct queries in {@code qry}.
+     * @throws IgniteSQLException If failed.
+     */
+    List<List<GridQueryFieldMetadata>> resultSetMetaData(
+        @Nullable QueryContext ctx,
+        String schemaName,
+        String qry
+    ) throws IgniteSQLException;
 
     /**
      * @param ctx Query context, may be null.