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

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


The following commit(s) were added to refs/heads/main by this push:
     new 53b6524d85b IGNITE-26156 Drop cached SQL plans through CLI and REST 
(#6533)
53b6524d85b is described below

commit 53b6524d85b48478f5316b40b7bc6848e6a06557
Author: Andrew V. Mashenkov <[email protected]>
AuthorDate: Mon Sep 15 14:15:25 2025 +0300

    IGNITE-26156 Drop cached SQL plans through CLI and REST (#6533)
---
 .../sql/planner/ItSqlPlannerCommandTest.java       |  54 +++++
 .../sql/planner/ItSqlPlannerReplCommandTest.java}  |  19 +-
 .../cli/call/sql/InvalidateCacheCallInput.java     |  65 ++++++
 .../cli/call/sql/InvalidatePlannerCacheCall.java   |  55 +++++
 .../internal/cli/commands/sql/SqlCommand.java      |  98 +++-----
 .../sql/{SqlCommand.java => SqlExecCommand.java}   |   6 +-
 ...SqlReplCommand.java => SqlExecReplCommand.java} |   2 +-
 .../internal/cli/commands/sql/SqlReplCommand.java  | 250 +++------------------
 .../sql/planner/InvalidateCacheCommand.java        |  56 +++++
 .../sql/planner/InvalidateCacheReplCommand.java    |  62 +++++
 .../commands/sql/planner/SqlPlannerCommand.java}   |  21 +-
 .../sql/planner/SqlPlannerReplCommand.java}        |  21 +-
 .../internal/cli/commands/sql/SqlCommandTest.java  |   5 +-
 modules/client-handler/build.gradle                |   3 +-
 modules/client/build.gradle                        |   2 +-
 .../org/apache/ignite/internal/sql/SqlCommon.java  |   3 +
 modules/partition-replicator/build.gradle          |   1 +
 .../ignite/internal/rest/api/sql/SqlQueryApi.java  |  20 ++
 .../internal/rest/sql/SqlQueryController.java      |  14 ++
 modules/sql-engine-api/build.gradle                |   3 +
 .../ignite/internal/sql/engine/AsyncSqlCursor.java |   0
 .../ignite/internal/sql/engine/InternalSqlRow.java |   0
 .../ignite/internal/sql/engine/QueryProcessor.java |  11 +
 .../ignite/internal/sql/engine/SqlProperties.java  |   9 +-
 .../ignite/internal/sql/engine/SqlQueryType.java   |   0
 .../internal/sql/engine/exec/AsyncDataCursor.java  |   0
 .../sql/engine/prepare/ParameterMetadata.java      |   0
 .../internal/sql/engine/prepare/ParameterType.java |  16 --
 .../internal/sql/engine/prepare/QueryMetadata.java |   0
 .../prepare/partitionawareness/DirectTxMode.java   |   0
 .../PartitionAwarenessMetadata.java                |   2 -
 modules/sql-engine/build.gradle                    |   1 +
 .../ignite/internal/sql/api/IgniteSqlImpl.java     |  12 +-
 .../sql/api/PublicApiThreadingIgniteSql.java       |   3 +-
 .../internal/sql/engine/SqlQueryProcessor.java     |  12 +-
 .../sql/engine/prepare/PrepareService.java         |  12 +
 .../sql/engine/prepare/PrepareServiceImpl.java     | 133 ++++++++---
 .../ignite/internal/sql/engine/util/TypeUtils.java |  16 ++
 .../sql/engine/exec/ExecutionServiceImplTest.java  |   3 +-
 .../sql/engine/exec/RuntimeSortedIndexTest.java    |   4 +-
 .../sql/engine/exec/rel/AbstractExecutionTest.java |   4 +-
 .../sql/engine/framework/TestBuilders.java         |   3 +-
 .../internal/sql/engine/framework/TestNode.java    |   3 +-
 .../sql/engine/prepare/PrepareServiceImplTest.java | 137 +++++++++--
 .../internal/sql/engine/util/QueryCheckerImpl.java |   3 +-
 45 files changed, 739 insertions(+), 405 deletions(-)

diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/planner/ItSqlPlannerCommandTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/planner/ItSqlPlannerCommandTest.java
new file mode 100644
index 00000000000..1ca0c8a7be2
--- /dev/null
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/planner/ItSqlPlannerCommandTest.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.commands.sql.planner;
+
+import static 
org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_URL_OPTION;
+
+import org.apache.ignite.internal.cli.commands.sql.CliSqlCommandTestBase;
+import org.apache.ignite.internal.util.ArrayUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link SqlPlannerCommand}.
+ */
+class ItSqlPlannerCommandTest extends CliSqlCommandTestBase {
+    private static final String[] INVALIDATE_CACHE_COMMAND = 
{"invalidate-cache"};
+
+    @Override
+    protected Class<?> getCommandClass() {
+        return SqlPlannerCommand.class;
+    }
+
+    @Test
+    void clearCache() {
+        execute(ArrayUtils.concat(INVALIDATE_CACHE_COMMAND, 
CLUSTER_URL_OPTION, NODE_URL));
+
+        assertErrOutputIsEmpty();
+        assertExitCodeIs(0);
+        assertOutputContains("Successfully cleared SQL query plan cache.");
+    }
+
+    @Test
+    void clearCacheFiltered() {
+        execute(ArrayUtils.concat(INVALIDATE_CACHE_COMMAND, 
CLUSTER_URL_OPTION, NODE_URL, "--tables", "PUBLIC.\"test\""));
+
+        assertErrOutputIsEmpty();
+        assertExitCodeIs(0);
+        assertOutputContains("Successfully cleared SQL query plan cache.");
+    }
+}
diff --git a/modules/sql-engine-api/build.gradle 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/planner/ItSqlPlannerReplCommandTest.java
similarity index 68%
copy from modules/sql-engine-api/build.gradle
copy to 
modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/planner/ItSqlPlannerReplCommandTest.java
index bc8420b1d7f..d14a18a280d 100644
--- a/modules/sql-engine-api/build.gradle
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/sql/planner/ItSqlPlannerReplCommandTest.java
@@ -15,15 +15,14 @@
  * limitations under the License.
  */
 
-apply from: "$rootDir/buildscripts/java-core.gradle"
-apply from: "$rootDir/buildscripts/publishing.gradle"
-apply from: "$rootDir/buildscripts/java-junit5.gradle"
-apply from: "$rootDir/buildscripts/java-test-fixtures.gradle"
+package org.apache.ignite.internal.cli.commands.sql.planner;
 
-dependencies {
-    implementation project(':ignite-core')
-
-    implementation libs.jetbrains.annotations
+/**
+ * Tests for {@link SqlPlannerReplCommand}.
+ */
+public class ItSqlPlannerReplCommandTest extends ItSqlPlannerCommandTest {
+    @Override
+    protected Class<?> getCommandClass() {
+        return SqlPlannerReplCommand.class;
+    }
 }
-
-description = 'ignite-sql-engine-api'
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/sql/InvalidateCacheCallInput.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/sql/InvalidateCacheCallInput.java
new file mode 100644
index 00000000000..b7397ea7d9e
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/sql/InvalidateCacheCallInput.java
@@ -0,0 +1,65 @@
+/*
+ * 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.cli.call.sql;
+
+import java.util.List;
+import org.apache.ignite.internal.cli.core.call.CallInput;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Input for {@link InvalidatePlannerCacheCall}.
+ */
+public class InvalidateCacheCallInput implements CallInput {
+    /** Cluster url. */
+    private final String clusterUrl;
+
+    /** Tables filter. */
+    @Nullable
+    private final List<String> tables;
+
+    @Nullable
+    private final List<String> targetNodes;
+
+    /** Returns {@link InvalidateCacheCallInput} with specified arguments. */
+    public static InvalidateCacheCallInput of(String clusterUrl, @Nullable 
List<String> tables, @Nullable List<String> targetNodes) {
+        return new InvalidateCacheCallInput(clusterUrl, tables, targetNodes);
+    }
+
+    private InvalidateCacheCallInput(String clusterUrl, @Nullable List<String> 
tables, @Nullable List<String> targetNodes) {
+        this.clusterUrl = clusterUrl;
+        this.tables = tables;
+        this.targetNodes = targetNodes;
+    }
+
+    /** Cluster url. */
+    public String clusterUrl() {
+        return clusterUrl;
+    }
+
+    /** Returns names specifying nodes to restart partitions. Empty/null means 
"all nodes". */
+    @Nullable
+    public List<String> targetNodes() {
+        return targetNodes;
+    }
+
+    /** Return tables that SQL plans contains any of these tables should be 
invalidated. */
+    @Nullable
+    public List<String> getTables() {
+        return tables;
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/sql/InvalidatePlannerCacheCall.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/sql/InvalidatePlannerCacheCall.java
new file mode 100644
index 00000000000..0885127c40d
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/sql/InvalidatePlannerCacheCall.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.call.sql;
+
+import jakarta.inject.Singleton;
+import org.apache.ignite.internal.cli.core.call.Call;
+import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
+import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
+import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
+import org.apache.ignite.rest.client.api.SqlApi;
+import org.apache.ignite.rest.client.invoker.ApiException;
+
+/**
+ * Shows node configuration from ignite cluster.
+ */
+@Singleton
+public class InvalidatePlannerCacheCall implements 
Call<InvalidateCacheCallInput, String> {
+    private final ApiClientFactory clientFactory;
+
+    public InvalidatePlannerCacheCall(ApiClientFactory clientFactory) {
+        this.clientFactory = clientFactory;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public DefaultCallOutput<String> execute(InvalidateCacheCallInput input) {
+        SqlApi client = createApiClient(input);
+
+        try {
+            client.clearCache(input.getTables());
+            return DefaultCallOutput.success("Successfully cleared SQL query 
plan cache.");
+        } catch (ApiException e) {
+            return DefaultCallOutput.failure(new IgniteCliApiException(e, 
input.clusterUrl()));
+        }
+    }
+
+    private SqlApi createApiClient(InvalidateCacheCallInput input) {
+        return new SqlApi(clientFactory.getClient(input.clusterUrl()));
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
index 0c23597b4db..9ab52fdd34b 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
@@ -17,79 +17,47 @@
 
 package org.apache.ignite.internal.cli.commands.sql;
 
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_KEY;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_OPTION;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_OPTION_DESC;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OPTION;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OPTION_DESC;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION_DESC;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.sql.SQLException;
 import java.util.concurrent.Callable;
-import org.apache.ignite.internal.cli.call.sql.SqlQueryCall;
 import org.apache.ignite.internal.cli.commands.BaseCommand;
-import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
-import org.apache.ignite.internal.cli.core.call.StringCallInput;
-import org.apache.ignite.internal.cli.core.exception.ExceptionWriter;
-import org.apache.ignite.internal.cli.core.exception.IgniteCliException;
-import 
org.apache.ignite.internal.cli.core.exception.handler.SqlExceptionHandler;
-import org.apache.ignite.internal.cli.decorators.SqlQueryResultDecorator;
-import org.apache.ignite.internal.cli.sql.SqlManager;
-import picocli.CommandLine.ArgGroup;
+import org.apache.ignite.internal.cli.commands.sql.planner.SqlPlannerCommand;
+import org.apache.ignite.internal.util.ArrayUtils;
+import picocli.CommandLine;
 import picocli.CommandLine.Command;
-import picocli.CommandLine.Option;
-import picocli.CommandLine.Parameters;
+import picocli.CommandLine.IFactory;
+import picocli.CommandLine.Unmatched;
 
 /**
- * Command for sql execution.
+ * Command for sql operations.
+ *
+ * <p>The class describes subcommands and redirect calls to default subcommand 
{@link SqlExecCommand} if no subcommand was specified.
+ *
+ * @see SqlExecCommand
  */
-@Command(name = "sql", description = "Executes SQL query")
+@Command(name = "sql",
+        subcommands = {
+                SqlPlannerCommand.class
+        },
+        description = "SQL query engine operations."
+)
 public class SqlCommand extends BaseCommand implements Callable<Integer> {
-    @Option(names = JDBC_URL_OPTION, required = true, descriptionKey = 
JDBC_URL_KEY, description = JDBC_URL_OPTION_DESC)
-    private String jdbc;
-
-    @Option(names = PLAIN_OPTION, description = PLAIN_OPTION_DESC)
-    private boolean plain;
-
-    @ArgGroup(multiplicity = "1")
-    private ExecOptions execOptions;
-
-    private static class ExecOptions {
-        @Parameters(index = "0", description = "SQL query to execute")
-        private String command;
-
-        @Option(names = SCRIPT_FILE_OPTION, description = 
SCRIPT_FILE_OPTION_DESC)
-        private File file;
-    }
-
-    private static String extract(File file) {
-        try {
-            return String.join("\n", Files.readAllLines(file.toPath(), 
StandardCharsets.UTF_8));
-        } catch (IOException e) {
-            throw new IgniteCliException("File [" + file.getAbsolutePath() + 
"] not found");
-        }
-    }
+    @Unmatched
+    private String[] args;
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
-    public Integer call() {
-        try (SqlManager sqlManager = new SqlManager(jdbc)) {
-            String executeCommand = execOptions.file != null ? 
extract(execOptions.file) : execOptions.command;
-            return runPipeline(CallExecutionPipeline.builder(new 
SqlQueryCall(sqlManager))
-                    .inputProvider(() -> new StringCallInput(executeCommand))
-                    .exceptionHandler(SqlExceptionHandler.INSTANCE)
-                    .decorator(new SqlQueryResultDecorator(plain))
-            );
-        } catch (SQLException e) {
-            ExceptionWriter exceptionWriter = 
ExceptionWriter.fromPrintWriter(spec.commandLine().getErr());
-            return SqlExceptionHandler.INSTANCE.handle(exceptionWriter, e);
-        }
+    public Integer call() throws Exception {
+        // Picocli lack flexibility parameter validation for subcommands + 
parser can't distinct positional parameter and subcommand in
+        // some cases. That leads to unexpected behavior.
+        //
+        // With RunLast strategy (see IExecutionStrategy) is used and all 
parent parameters have Scope.LOCAL,
+        // we don't expect parent command parameters be validated when running 
a subcommand (this just make no sense).
+        // To overcome this issues, we implement command in separate class 
(see SqlExecCommand) and redirect call to it.
+        IFactory factory = spec.commandLine().getFactory();
+        CommandLine commandLine = new 
CommandLine(factory.create(SqlExecCommand.class), factory)
+                .setErr(spec.commandLine().getErr())
+                .setOut(spec.commandLine().getOut())
+                .setDefaultValueProvider(spec.defaultValueProvider())
+                
.setExecutionExceptionHandler(spec.commandLine().getExecutionExceptionHandler());
+
+        return commandLine.execute(args == null ?  
ArrayUtils.STRING_EMPTY_ARRAY : args);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecCommand.java
similarity index 97%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecCommand.java
index 0c23597b4db..9f0144497bf 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecCommand.java
@@ -49,7 +49,7 @@ import picocli.CommandLine.Parameters;
  * Command for sql execution.
  */
 @Command(name = "sql", description = "Executes SQL query")
-public class SqlCommand extends BaseCommand implements Callable<Integer> {
+public class SqlExecCommand extends BaseCommand implements Callable<Integer> {
     @Option(names = JDBC_URL_OPTION, required = true, descriptionKey = 
JDBC_URL_KEY, description = JDBC_URL_OPTION_DESC)
     private String jdbc;
 
@@ -75,9 +75,7 @@ public class SqlCommand extends BaseCommand implements 
Callable<Integer> {
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
+    /** {@inheritDoc} */
     @Override
     public Integer call() {
         try (SqlManager sqlManager = new SqlManager(jdbc)) {
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecReplCommand.java
similarity index 99%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecReplCommand.java
index d15ee44cd86..244d2eb1c9d 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlExecReplCommand.java
@@ -80,7 +80,7 @@ import picocli.CommandLine.Parameters;
  * Command for sql execution in REPL mode.
  */
 @Command(name = "sql", description = "Executes SQL query")
-public class SqlReplCommand extends BaseCommand implements Runnable {
+public class SqlExecReplCommand extends BaseCommand implements Runnable {
     @Option(names = JDBC_URL_OPTION, required = true, descriptionKey = 
JDBC_URL_KEY, description = JDBC_URL_OPTION_DESC)
     private String jdbc;
 
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
index d15ee44cd86..539a475894a 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/SqlReplCommand.java
@@ -17,227 +17,47 @@
 
 package org.apache.ignite.internal.cli.commands.sql;
 
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_KEY;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_OPTION;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.JDBC_URL_OPTION_DESC;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OPTION;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.PLAIN_OPTION_DESC;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION;
-import static 
org.apache.ignite.internal.cli.commands.Options.Constants.SCRIPT_FILE_OPTION_DESC;
-import static org.apache.ignite.internal.cli.core.style.AnsiStringSupport.ansi;
-import static org.apache.ignite.internal.cli.core.style.AnsiStringSupport.fg;
-
-import jakarta.inject.Inject;
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.sql.SQLException;
-import java.util.regex.Pattern;
-import org.apache.ignite.internal.cli.call.sql.SqlQueryCall;
+import java.util.concurrent.Callable;
 import org.apache.ignite.internal.cli.commands.BaseCommand;
-import 
org.apache.ignite.internal.cli.commands.sql.help.IgniteSqlCommandCompleter;
-import 
org.apache.ignite.internal.cli.commands.treesitter.highlighter.SqlAttributedStringHighlighter;
-import org.apache.ignite.internal.cli.config.CliConfigKeys;
-import org.apache.ignite.internal.cli.config.ConfigManagerProvider;
-import org.apache.ignite.internal.cli.core.CallExecutionPipelineProvider;
-import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
-import org.apache.ignite.internal.cli.core.call.StringCallInput;
-import org.apache.ignite.internal.cli.core.exception.ExceptionHandlers;
-import org.apache.ignite.internal.cli.core.exception.ExceptionWriter;
-import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
-import org.apache.ignite.internal.cli.core.exception.IgniteCliException;
-import 
org.apache.ignite.internal.cli.core.exception.handler.ClusterNotInitializedExceptionHandler;
-import 
org.apache.ignite.internal.cli.core.exception.handler.SqlExceptionHandler;
-import org.apache.ignite.internal.cli.core.repl.Repl;
-import org.apache.ignite.internal.cli.core.repl.Session;
-import 
org.apache.ignite.internal.cli.core.repl.executor.RegistryCommandExecutor;
-import org.apache.ignite.internal.cli.core.repl.executor.ReplExecutorProvider;
-import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
-import org.apache.ignite.internal.cli.core.style.AnsiStringSupport.Color;
-import org.apache.ignite.internal.cli.decorators.SqlQueryResultDecorator;
-import org.apache.ignite.internal.cli.sql.SqlManager;
-import org.apache.ignite.internal.cli.sql.SqlSchemaProvider;
-import org.apache.ignite.internal.util.StringUtils;
-import org.apache.ignite.rest.client.api.ClusterManagementApi;
-import org.apache.ignite.rest.client.invoker.ApiException;
-import org.jline.reader.EOFError;
-import org.jline.reader.Highlighter;
-import org.jline.reader.LineReader;
-import org.jline.reader.ParsedLine;
-import org.jline.reader.Parser;
-import org.jline.reader.SyntaxError;
-import org.jline.reader.impl.DefaultHighlighter;
-import org.jline.reader.impl.DefaultParser;
-import org.jline.reader.impl.completer.AggregateCompleter;
-import org.jline.utils.AttributedString;
-import picocli.CommandLine.ArgGroup;
+import 
org.apache.ignite.internal.cli.commands.sql.planner.SqlPlannerReplCommand;
+import org.apache.ignite.internal.util.ArrayUtils;
+import picocli.CommandLine;
 import picocli.CommandLine.Command;
-import picocli.CommandLine.Option;
-import picocli.CommandLine.Parameters;
+import picocli.CommandLine.IFactory;
+import picocli.CommandLine.Unmatched;
 
 /**
- * Command for sql execution in REPL mode.
+ * Command for sql operations in REPL mode.
+ *
+ * <p>The class describes subcommands and redirect calls to default subcommand 
{@link SqlExecReplCommand} if no subcommand was specified.
+ *
+ * @see SqlExecReplCommand
  */
-@Command(name = "sql", description = "Executes SQL query")
-public class SqlReplCommand extends BaseCommand implements Runnable {
-    @Option(names = JDBC_URL_OPTION, required = true, descriptionKey = 
JDBC_URL_KEY, description = JDBC_URL_OPTION_DESC)
-    private String jdbc;
-
-    @Option(names = PLAIN_OPTION, description = PLAIN_OPTION_DESC)
-    private boolean plain;
-
-    @ArgGroup
-    private ExecOptions execOptions;
-
-    private static class ExecOptions {
-        @Parameters(index = "0", description = "SQL query to execute", 
defaultValue = Option.NULL_VALUE)
-        private String command;
-
-        @Option(names = SCRIPT_FILE_OPTION, description = 
SCRIPT_FILE_OPTION_DESC, defaultValue = Option.NULL_VALUE)
-        private File file;
-    }
-
-    @Inject
-    private ReplExecutorProvider replExecutorProvider;
-
-    @Inject
-    private ConfigManagerProvider configManagerProvider;
+@Command(name = "sql",
+        subcommands = {
+                SqlPlannerReplCommand.class,
+        },
+        description = "SQL query engine operations."
+)
+public class SqlReplCommand extends BaseCommand implements Callable<Integer> {
+    @Unmatched
+    private String[] args;
 
-    @Inject
-    private Session session;
-
-    @Inject
-    private ApiClientFactory clientFactory;
-
-    private static String extract(File file) {
-        try {
-            return String.join("\n", Files.readAllLines(file.toPath(), 
StandardCharsets.UTF_8));
-        } catch (IOException e) {
-            throw new IgniteCliException("File [" + file.getAbsolutePath() + 
"] not found");
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
     @Override
-    public void run() {
-        try (SqlManager sqlManager = new SqlManager(jdbc)) {
-            // When passing white space to this command, picocli will treat it 
as a positional argument
-            if (execOptions == null || 
(StringUtils.nullOrBlank(execOptions.command) && execOptions.file == null)) {
-                SqlSchemaProvider schemaProvider = new 
SqlSchemaProvider(sqlManager::getMetadata);
-                schemaProvider.initStateAsync();
-
-                SqlCompleter sqlCompleter = new SqlCompleter(schemaProvider);
-                IgniteSqlCommandCompleter sqlCommandCompleter = new 
IgniteSqlCommandCompleter();
-
-                replExecutorProvider.get().execute(Repl.builder()
-                        .withPromptProvider(() -> 
ansi(fg(Color.GREEN).mark("sql-cli> ")))
-                        .withCompleter(new 
AggregateCompleter(sqlCommandCompleter, sqlCompleter))
-                        .withCommandClass(SqlReplTopLevelCliCommand.class)
-                        
.withCallExecutionPipelineProvider(provider(sqlManager))
-                        .withHistoryFileName("sqlhistory")
-                        .withAutosuggestionsWidgets()
-                        .withHighlighter(highlightingEnabled() ? new 
HighlighterImpl() : new DefaultHighlighter())
-                        .withParser(multilineSupported() ? new 
MultilineParser() : new DefaultParser())
-                        .build());
-            } else {
-                String executeCommand = execOptions.file != null ? 
extract(execOptions.file) : execOptions.command;
-                createSqlExecPipeline(sqlManager, 
executeCommand).runPipeline();
-            }
-        } catch (SQLException e) {
-            String url = session.info() == null ? null : 
session.info().nodeUrl();
-
-            ExceptionWriter exceptionWriter = 
ExceptionWriter.fromPrintWriter(spec.commandLine().getErr());
-            try {
-                if (url != null) {
-                    new 
ClusterManagementApi(clientFactory.getClient(url)).clusterState();
-                }
-
-                SqlExceptionHandler.INSTANCE.handle(exceptionWriter, e);
-            } catch (ApiException apiE) {
-                new ClusterNotInitializedExceptionHandler("Failed to start sql 
repl mode", "cluster init")
-                        .handle(exceptionWriter, new 
IgniteCliApiException(apiE, url));
-            }
-        }
-    }
-
-    private boolean multilineSupported() {
-        return 
Boolean.parseBoolean(configManagerProvider.get().getCurrentProperty(CliConfigKeys.SQL_MULTILINE.value()));
-    }
-
-    private boolean highlightingEnabled() {
-        return 
Boolean.parseBoolean(configManagerProvider.get().getCurrentProperty(CliConfigKeys.SYNTAX_HIGHLIGHTING.value()));
-    }
-
-    /**
-     * Multiline parser, expects ";" at the end of the line.
-     */
-    private static final class MultilineParser implements Parser {
-
-        private static final Parser DEFAULT_PARSER = new DefaultParser();
-
-        @Override
-        public ParsedLine parse(String line, int cursor, Parser.ParseContext 
context) throws SyntaxError {
-            if ((ParseContext.UNSPECIFIED == context || 
ParseContext.ACCEPT_LINE == context)
-                    && !line.trim().endsWith(";")) {
-                throw new EOFError(-1, cursor, "Missing semicolon (;)");
-            }
-
-            return DEFAULT_PARSER.parse(line, cursor, context);
-        }
-    }
-
-    private static class HighlighterImpl implements Highlighter {
-
-        @Override
-        public AttributedString highlight(LineReader lineReader, String s) {
-            return SqlAttributedStringHighlighter.highlight(s);
-        }
-
-        @Override
-        public void setErrorPattern(Pattern pattern) {
-        }
-
-        @Override
-        public void setErrorIndex(int i) {
-        }
-    }
-
-    private CallExecutionPipelineProvider provider(SqlManager sqlManager) {
-        return (executor, exceptionHandlers, line) -> 
executor.hasCommand(dropSemicolon(line))
-                ? createInternalCommandPipeline(executor, exceptionHandlers, 
line)
-                : createSqlExecPipeline(sqlManager, line);
-    }
-
-    private CallExecutionPipeline<?, ?> createSqlExecPipeline(SqlManager 
sqlManager, String line) {
-        return CallExecutionPipeline.builder(new SqlQueryCall(sqlManager))
-                .inputProvider(() -> new StringCallInput(line))
-                .output(spec.commandLine().getOut())
-                .errOutput(spec.commandLine().getErr())
-                .decorator(new SqlQueryResultDecorator(plain))
-                .verbose(verbose)
-                .exceptionHandler(SqlExceptionHandler.INSTANCE)
-                .build();
-    }
-
-    private CallExecutionPipeline<?, ?> 
createInternalCommandPipeline(RegistryCommandExecutor call,
-            ExceptionHandlers exceptionHandlers,
-            String line) {
-        return CallExecutionPipeline.builder(call)
-                .inputProvider(() -> new StringCallInput(dropSemicolon(line)))
-                .output(spec.commandLine().getOut())
-                .errOutput(spec.commandLine().getErr())
-                .exceptionHandlers(exceptionHandlers)
-                .verbose(verbose)
-                .build();
-    }
-
-    private static String dropSemicolon(String line) {
-        if (line.trim().endsWith(";")) {
-            line = line.substring(0, line.length() - 1);
-        }
-        return line;
+    public Integer call() throws Exception {
+        // Picocli lack flexibility parameter validation for subcommands + 
parser can't distinct positional parameter and subcommand in
+        // some cases. That leads to unexpected behavior.
+        //
+        // With RunLast strategy (see IExecutionStrategy) is used and all 
parent parameters have Scope.LOCAL,
+        // we don't expect parent command parameters be validated when running 
a subcommand (this just make no sense).
+        // To overcome this issues, we implement command in separate class 
(see SqlExecReplCommand) and redirect call to it.
+        IFactory factory = spec.commandLine().getFactory();
+        CommandLine commandLine = new 
CommandLine(factory.create(SqlExecReplCommand.class), factory)
+                .setErr(spec.commandLine().getErr())
+                .setOut(spec.commandLine().getOut())
+                .setDefaultValueProvider(spec.defaultValueProvider())
+                
.setExecutionExceptionHandler(spec.commandLine().getExecutionExceptionHandler());
+
+        return commandLine.execute(args == null ?  
ArrayUtils.STRING_EMPTY_ARRAY : args);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/InvalidateCacheCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/InvalidateCacheCommand.java
new file mode 100644
index 00000000000..3c9d3a2fa5a
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/InvalidateCacheCommand.java
@@ -0,0 +1,56 @@
+/*
+ * 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.cli.commands.sql.planner;
+
+import jakarta.inject.Inject;
+import java.util.List;
+import java.util.concurrent.Callable;
+import org.apache.ignite.internal.cli.call.sql.InvalidateCacheCallInput;
+import org.apache.ignite.internal.cli.call.sql.InvalidatePlannerCacheCall;
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import org.apache.ignite.internal.cli.commands.cluster.ClusterUrlProfileMixin;
+import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
+import 
org.apache.ignite.internal.cli.core.exception.handler.ClusterNotInitializedExceptionHandler;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+import picocli.CommandLine.Option;
+
+/**
+ * Sql planner cache invalidation command.
+ */
+@Command(name = "invalidate-cache", description = "Invalidates SQL planner 
cache")
+public class InvalidateCacheCommand extends BaseCommand implements 
Callable<Integer> {
+    /** Cluster endpoint URL option. */
+    @Mixin
+    private ClusterUrlProfileMixin clusterUrl;
+
+    @Inject
+    private InvalidatePlannerCacheCall call;
+
+    @Option(names = "--tables", description = "Tables filter", split = ",")
+    private List<String> tables;
+
+    /** {@inheritDoc} */
+    @Override
+    public Integer call() {
+        return runPipeline(CallExecutionPipeline.builder(call)
+                .inputProvider(() -> 
InvalidateCacheCallInput.of(clusterUrl.getClusterUrl(), tables, List.of()))
+                
.exceptionHandler(ClusterNotInitializedExceptionHandler.createHandler("Failed 
to invalidate SQL planner cache"))
+        );
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/InvalidateCacheReplCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/InvalidateCacheReplCommand.java
new file mode 100644
index 00000000000..eff8db3aa8e
--- /dev/null
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/InvalidateCacheReplCommand.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cli.commands.sql.planner;
+
+import jakarta.inject.Inject;
+import java.util.List;
+import org.apache.ignite.internal.cli.call.sql.InvalidateCacheCallInput;
+import org.apache.ignite.internal.cli.call.sql.InvalidatePlannerCacheCall;
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import org.apache.ignite.internal.cli.commands.cluster.ClusterUrlMixin;
+import 
org.apache.ignite.internal.cli.commands.questions.ConnectToClusterQuestion;
+import 
org.apache.ignite.internal.cli.core.exception.handler.ClusterNotInitializedExceptionHandler;
+import org.apache.ignite.internal.cli.core.flow.builder.Flows;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Mixin;
+import picocli.CommandLine.Option;
+
+/**
+ * Sql planner cache invalidation command in REPL mode.
+ */
+@Command(name = "invalidate-cache", description = "Invalidates SQL planner 
cache")
+public class InvalidateCacheReplCommand extends BaseCommand implements 
Runnable {
+    /** Cluster endpoint URL option. */
+    @Mixin
+    private ClusterUrlMixin clusterUrl;
+
+    @Inject
+    private ConnectToClusterQuestion question;
+
+    @Inject
+    private InvalidatePlannerCacheCall call;
+
+    @Option(names = "--tables", description = "Tables filter", split = ",")
+    private List<String> tables;
+
+    /** {@inheritDoc} */
+    @Override
+    public void run() {
+        runFlow(question.askQuestionIfNotConnected(clusterUrl.getClusterUrl())
+                .map(url -> InvalidateCacheCallInput.of(url, tables, 
List.of()))
+
+                .then(Flows.fromCall(call))
+                
.exceptionHandler(ClusterNotInitializedExceptionHandler.createHandler("Failed 
to invalidate SQL planner cache"))
+                .print()
+        );
+    }
+}
diff --git a/modules/sql-engine-api/build.gradle 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/SqlPlannerCommand.java
similarity index 66%
copy from modules/sql-engine-api/build.gradle
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/SqlPlannerCommand.java
index bc8420b1d7f..ade0bcece2e 100644
--- a/modules/sql-engine-api/build.gradle
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/SqlPlannerCommand.java
@@ -15,15 +15,18 @@
  * limitations under the License.
  */
 
-apply from: "$rootDir/buildscripts/java-core.gradle"
-apply from: "$rootDir/buildscripts/publishing.gradle"
-apply from: "$rootDir/buildscripts/java-junit5.gradle"
-apply from: "$rootDir/buildscripts/java-test-fixtures.gradle"
+package org.apache.ignite.internal.cli.commands.sql.planner;
 
-dependencies {
-    implementation project(':ignite-core')
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import picocli.CommandLine.Command;
 
-    implementation libs.jetbrains.annotations
+/**
+ * SQL planner command.
+ */
+@Command(name = "planner",
+        subcommands = {
+                InvalidateCacheCommand.class
+        },
+        description = "SQL planner operations.")
+public class SqlPlannerCommand extends BaseCommand {
 }
-
-description = 'ignite-sql-engine-api'
diff --git a/modules/sql-engine-api/build.gradle 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/SqlPlannerReplCommand.java
similarity index 65%
copy from modules/sql-engine-api/build.gradle
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/SqlPlannerReplCommand.java
index bc8420b1d7f..8427365617a 100644
--- a/modules/sql-engine-api/build.gradle
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/sql/planner/SqlPlannerReplCommand.java
@@ -15,15 +15,18 @@
  * limitations under the License.
  */
 
-apply from: "$rootDir/buildscripts/java-core.gradle"
-apply from: "$rootDir/buildscripts/publishing.gradle"
-apply from: "$rootDir/buildscripts/java-junit5.gradle"
-apply from: "$rootDir/buildscripts/java-test-fixtures.gradle"
+package org.apache.ignite.internal.cli.commands.sql.planner;
 
-dependencies {
-    implementation project(':ignite-core')
+import org.apache.ignite.internal.cli.commands.BaseCommand;
+import picocli.CommandLine.Command;
 
-    implementation libs.jetbrains.annotations
+/**
+ * SQL planner command in REPL mode.
+ */
+@Command(name = "planner",
+        subcommands = {
+                InvalidateCacheReplCommand.class
+        },
+        description = "SQL planner operations.")
+public class SqlPlannerReplCommand extends BaseCommand {
 }
-
-description = 'ignite-sql-engine-api'
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java
 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java
index 30a82bfc6c0..22602dc1de3 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java
+++ 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/sql/SqlCommandTest.java
@@ -23,7 +23,10 @@ import 
org.apache.ignite.internal.cli.commands.CliCommandTestBase;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
-class SqlCommandTest extends CliCommandTestBase {
+/**
+ * Tests for {@link SqlCommandTest}.
+ */
+public class SqlCommandTest extends CliCommandTestBase {
 
     @Override
     protected Class<?> getCommandClass() {
diff --git a/modules/client-handler/build.gradle 
b/modules/client-handler/build.gradle
index d6b6bfbe84d..905900bb826 100644
--- a/modules/client-handler/build.gradle
+++ b/modules/client-handler/build.gradle
@@ -30,6 +30,7 @@ dependencies {
     implementation project(':ignite-configuration-root')
     implementation project(':ignite-api')
     implementation project(':ignite-table')
+    implementation project(':ignite-sql-engine-api')
     implementation project(':ignite-sql-engine')
     implementation project(':ignite-network')
     implementation project(':ignite-core')
@@ -66,7 +67,7 @@ dependencies {
     integrationTestImplementation project(':ignite-api')
     integrationTestImplementation project(':ignite-network')
     integrationTestImplementation project(':ignite-network-api')
-    integrationTestImplementation project(':ignite-sql-engine')
+    integrationTestImplementation project(':ignite-sql-engine-api')
     integrationTestImplementation project(':ignite-schema')
     integrationTestImplementation project(':ignite-table')
     integrationTestImplementation project(':ignite-metrics')
diff --git a/modules/client/build.gradle b/modules/client/build.gradle
index e43aec28a61..0a1067fac6f 100644
--- a/modules/client/build.gradle
+++ b/modules/client/build.gradle
@@ -43,7 +43,7 @@ dependencies {
     testImplementation project(':ignite-client-handler')
     testImplementation project(':ignite-configuration')
     testImplementation project(':ignite-configuration-root')
-    testImplementation project(':ignite-sql-engine')
+    testImplementation project(':ignite-sql-engine-api')
     testImplementation project(':ignite-schema')
     testImplementation project(':ignite-table')
     testImplementation project(':ignite-network')
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/sql/SqlCommon.java 
b/modules/core/src/main/java/org/apache/ignite/internal/sql/SqlCommon.java
index 6466aa1ce8a..2d398d15741 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/sql/SqlCommon.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/sql/SqlCommon.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.sql;
 
+import java.time.ZoneId;
 import org.apache.ignite.lang.util.IgniteNameUtils;
 import org.apache.ignite.table.QualifiedName;
 
@@ -29,4 +30,6 @@ public final class SqlCommon {
 
     /** Default page size. */
     public static final int DEFAULT_PAGE_SIZE = 1024;
+    /** Default time-zone ID. */
+    public static final ZoneId DEFAULT_TIME_ZONE_ID = ZoneId.of("UTC");
 }
diff --git a/modules/partition-replicator/build.gradle 
b/modules/partition-replicator/build.gradle
index 68f85521ba4..24fc89780b2 100644
--- a/modules/partition-replicator/build.gradle
+++ b/modules/partition-replicator/build.gradle
@@ -100,6 +100,7 @@ dependencies {
     integrationTestImplementation project(':ignite-runner')
     integrationTestImplementation project(':ignite-system-disaster-recovery')
     integrationTestImplementation project(':ignite-configuration-storage')
+    integrationTestImplementation project(':ignite-sql-engine-api') // TODO: 
IGNITE-22522 - remove.
     integrationTestImplementation project(':ignite-sql-engine') // TODO: 
IGNITE-22522 - remove.
     integrationTestImplementation project(':ignite-system-view-api') // TODO: 
IGNITE-22522 - remove.
     integrationTestImplementation project(':ignite-system-view') // TODO: 
IGNITE-22522 - remove.
diff --git 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/sql/SqlQueryApi.java
 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/sql/SqlQueryApi.java
index 9425219c522..d62784ad6d8 100644
--- 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/sql/SqlQueryApi.java
+++ 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/sql/SqlQueryApi.java
@@ -23,6 +23,7 @@ import static 
org.apache.ignite.internal.rest.constants.MediaType.APPLICATION_JS
 import io.micronaut.http.annotation.Controller;
 import io.micronaut.http.annotation.Delete;
 import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.QueryValue;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.media.ArraySchema;
 import io.swagger.v3.oas.annotations.media.Content;
@@ -30,6 +31,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.rest.api.Problem;
@@ -104,4 +107,21 @@ public interface SqlQueryApi {
     CompletableFuture<Void> killQuery(
             @Schema(name = "queryId", description = "The unique identifier of 
the sql query.", requiredMode = REQUIRED) UUID queryId
     );
+
+    /**
+     * Invalidates SQL query planner cache.
+     *
+     * @return The result of the operation.
+     */
+    @Operation(
+            summary = "Invalidates SQL planner cache.",
+            description = "Invalidates SQL planner cache records on node that 
related to provided table names.")
+    @ApiResponse(responseCode = "200", description = "Successfully cleared SQL 
query plan cache.")
+    @Get("plan/clear-cache")
+    CompletableFuture<Void> clearCache(
+            @QueryValue
+            @Schema(description = "SQL query plans, which are related to given 
tables, will be evicted from cache. Case-sensitive, "
+                    + "cache will be reset if empty.")
+            Optional<Set<String>> tableNames
+    );
 }
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/sql/SqlQueryController.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/sql/SqlQueryController.java
index 14a1bb42c1a..89eb702dc4c 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/sql/SqlQueryController.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/sql/SqlQueryController.java
@@ -25,6 +25,8 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Optional;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.logger.IgniteLogger;
@@ -34,8 +36,10 @@ import org.apache.ignite.internal.rest.api.sql.SqlQueryApi;
 import org.apache.ignite.internal.rest.api.sql.SqlQueryInfo;
 import org.apache.ignite.internal.rest.sql.exception.SqlQueryKillException;
 import org.apache.ignite.internal.rest.sql.exception.SqlQueryNotFoundException;
+import org.apache.ignite.internal.sql.engine.QueryProcessor;
 import org.apache.ignite.internal.sql.engine.api.kill.CancellableOperationType;
 import org.apache.ignite.internal.sql.engine.api.kill.KillHandlerRegistry;
+import org.apache.ignite.internal.wrapper.Wrappers;
 import org.apache.ignite.sql.IgniteSql;
 import org.apache.ignite.sql.SqlRow;
 import org.apache.ignite.sql.Statement;
@@ -87,6 +91,16 @@ public class SqlQueryController implements SqlQueryApi, 
ResourceHolder {
         }
     }
 
+    @Override
+    public CompletableFuture<Void> clearCache(Optional<Set<String>> 
tableNames) {
+        try {
+            return Wrappers.unwrap(igniteSql, 
QueryProcessor.class).invalidatePlannerCache(tableNames.orElse(Set.of()));
+        } catch (Exception e) {
+            LOG.error("Failed to invalidate SQL planner cache.", e);
+            return failedFuture(e);
+        }
+    }
+
     private static Void handleOperationResult(UUID queryId, @Nullable Boolean 
result) {
         if (result != null && !result) {
             throw new SqlQueryNotFoundException(queryId.toString());
diff --git a/modules/sql-engine-api/build.gradle 
b/modules/sql-engine-api/build.gradle
index bc8420b1d7f..b4da92fecc2 100644
--- a/modules/sql-engine-api/build.gradle
+++ b/modules/sql-engine-api/build.gradle
@@ -21,7 +21,10 @@ apply from: "$rootDir/buildscripts/java-junit5.gradle"
 apply from: "$rootDir/buildscripts/java-test-fixtures.gradle"
 
 dependencies {
+    implementation project(':ignite-api')
     implementation project(':ignite-core')
+    implementation project(':ignite-schema')
+    implementation project(':ignite-transactions')
 
     implementation libs.jetbrains.annotations
 }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/AsyncSqlCursor.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/AsyncSqlCursor.java
similarity index 100%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/AsyncSqlCursor.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/AsyncSqlCursor.java
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/InternalSqlRow.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/InternalSqlRow.java
similarity index 100%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/InternalSqlRow.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/InternalSqlRow.java
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProcessor.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/QueryProcessor.java
similarity index 85%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProcessor.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/QueryProcessor.java
index 29e497a3dfa..7c49859bed1 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProcessor.java
+++ 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/QueryProcessor.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.sql.engine;
 
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.hlc.HybridTimestampTracker;
 import org.apache.ignite.internal.manager.IgniteComponent;
@@ -70,4 +71,14 @@ public interface QueryProcessor extends IgniteComponent {
             String qry,
             Object... params
     );
+
+    /**
+     * Invalidates planner cache if {@code tableNames} is empty, otherwise 
invalidates only plans, which refers to the provided tables.
+     *
+     * @param tableNames Table names.
+     * @return Operation completion future.
+     */
+    default CompletableFuture<Void> invalidatePlannerCache(Set<String> 
tableNames) {
+        return CompletableFuture.failedFuture(new 
UnsupportedOperationException("Planner implementation doesn't support cache."));
+    }
 }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlProperties.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/SqlProperties.java
similarity index 93%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlProperties.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/SqlProperties.java
index 00858271de7..f0ebe73eac1 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlProperties.java
+++ 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/SqlProperties.java
@@ -17,12 +17,10 @@
 
 package org.apache.ignite.internal.sql.engine;
 
-import static 
org.apache.ignite.internal.sql.engine.SqlQueryProcessor.DEFAULT_TIME_ZONE_ID;
-
 import java.time.ZoneId;
 import java.util.Set;
-import javax.annotation.Nullable;
 import org.apache.ignite.internal.sql.SqlCommon;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * An object that keeps values of the properties.
@@ -32,8 +30,9 @@ public class SqlProperties {
     private Set<SqlQueryType> allowedQueryTypes = SqlQueryType.ALL;
     private boolean allowMultiStatement = true;
     private String defaultSchema = SqlCommon.DEFAULT_SCHEMA_NAME;
-    private ZoneId timeZoneId = DEFAULT_TIME_ZONE_ID;
-    @Nullable private String userName;
+    private ZoneId timeZoneId = SqlCommon.DEFAULT_TIME_ZONE_ID;
+    @Nullable
+    private String userName;
 
     public SqlProperties() {
     }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryType.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryType.java
similarity index 100%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryType.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryType.java
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/AsyncDataCursor.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/exec/AsyncDataCursor.java
similarity index 100%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/AsyncDataCursor.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/exec/AsyncDataCursor.java
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterMetadata.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterMetadata.java
similarity index 100%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterMetadata.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterMetadata.java
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterType.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterType.java
similarity index 75%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterType.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterType.java
index 8afff2cf535..e8aa8416e1e 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterType.java
+++ 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterType.java
@@ -17,8 +17,6 @@
 
 package org.apache.ignite.internal.sql.engine.prepare;
 
-import org.apache.calcite.rel.type.RelDataType;
-import org.apache.ignite.internal.sql.engine.util.TypeUtils;
 import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.sql.ColumnMetadata;
 import org.apache.ignite.sql.ColumnType;
@@ -79,18 +77,4 @@ public final class ParameterType {
     public String toString() {
         return S.toString(this);
     }
-
-    /** Creates parameter metadata from the given logical type. */
-    static ParameterType fromRelDataType(RelDataType type) {
-        ColumnType columnType = TypeUtils.columnType(type);
-        assert columnType != null : "No column type for " + type;
-
-        int precision = columnType.lengthAllowed() || 
columnType.precisionAllowed()
-                ? type.getPrecision()
-                : ColumnMetadata.UNDEFINED_PRECISION;
-
-        int scale = columnType.scaleAllowed() ? type.getScale() : 
ColumnMetadata.UNDEFINED_SCALE;
-
-        return new ParameterType(columnType, precision, scale, 
type.isNullable());
-    }
 }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryMetadata.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryMetadata.java
similarity index 100%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryMetadata.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryMetadata.java
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/partitionawareness/DirectTxMode.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/partitionawareness/DirectTxMode.java
similarity index 100%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/partitionawareness/DirectTxMode.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/partitionawareness/DirectTxMode.java
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/partitionawareness/PartitionAwarenessMetadata.java
 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/partitionawareness/PartitionAwarenessMetadata.java
similarity index 98%
rename from 
modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/partitionawareness/PartitionAwarenessMetadata.java
rename to 
modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/partitionawareness/PartitionAwarenessMetadata.java
index bb9c6512105..bee2e9b3e92 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/partitionawareness/PartitionAwarenessMetadata.java
+++ 
b/modules/sql-engine-api/src/main/java/org/apache/ignite/internal/sql/engine/prepare/partitionawareness/PartitionAwarenessMetadata.java
@@ -38,8 +38,6 @@ import org.apache.ignite.internal.tostring.S;
  *   indexes[i] >= 0         => use dynamicParam[indexes[i]]
  *   indexes[i] < 0          => use hash[-(indexes[i] + 1)]
  * </pre>
- *
- * @see PartitionAwarenessMetadataExtractor
  */
 public final class PartitionAwarenessMetadata {
 
diff --git a/modules/sql-engine/build.gradle b/modules/sql-engine/build.gradle
index 0f9d3efad2a..c7b730cedfe 100644
--- a/modules/sql-engine/build.gradle
+++ b/modules/sql-engine/build.gradle
@@ -149,6 +149,7 @@ dependencies {
     testFixturesImplementation project(':ignite-core')
     testFixturesImplementation project(':ignite-api')
     testFixturesImplementation project(':ignite-schema')
+    testFixturesImplementation project(':ignite-sql-engine-api')
     testFixturesImplementation project(':ignite-transactions')
     testFixturesImplementation project(':ignite-system-view-api')
     testFixturesImplementation project(':ignite-system-view')
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/IgniteSqlImpl.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/IgniteSqlImpl.java
index bb2705e43b3..958a2b301f3 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/IgniteSqlImpl.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/IgniteSqlImpl.java
@@ -62,6 +62,7 @@ import org.apache.ignite.internal.util.AsyncCursor;
 import org.apache.ignite.internal.util.ExceptionUtils;
 import org.apache.ignite.internal.util.IgniteSpinBusyLock;
 import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.wrapper.Wrapper;
 import org.apache.ignite.lang.CancellationToken;
 import org.apache.ignite.lang.TraceableException;
 import org.apache.ignite.lang.util.IgniteNameUtils;
@@ -83,7 +84,7 @@ import org.jetbrains.annotations.TestOnly;
  * Embedded implementation of the Ignite SQL query facade.
  */
 @SuppressWarnings("rawtypes")
-public class IgniteSqlImpl implements IgniteSql, IgniteComponent {
+public class IgniteSqlImpl implements IgniteSql, IgniteComponent, Wrapper {
     private static final IgniteLogger LOG = 
Loggers.forClass(IgniteSqlImpl.class);
 
     private static final int AWAIT_CURSOR_CLOSE_ON_STOP_IN_SECONDS = 10;
@@ -690,6 +691,15 @@ public class IgniteSqlImpl implements IgniteSql, 
IgniteComponent {
         return IgniteUtils.getInterruptibly(future);
     }
 
+    @Override
+    public <T> T unwrap(Class<T> classToUnwrap) {
+        if (classToUnwrap.isAssignableFrom(QueryProcessor.class)) {
+            return classToUnwrap.cast(queryProcessor);
+        }
+
+        return classToUnwrap.cast(this);
+    }
+
     private static class ScriptHandler {
         private final CompletableFuture<Void> resFut;
         private final List<Throwable> cursorCloseErrors = 
Collections.synchronizedList(new ArrayList<>());
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/PublicApiThreadingIgniteSql.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/PublicApiThreadingIgniteSql.java
index e552380777a..323230338eb 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/PublicApiThreadingIgniteSql.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/api/PublicApiThreadingIgniteSql.java
@@ -25,6 +25,7 @@ import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 import org.apache.ignite.internal.thread.PublicApiThreading;
 import org.apache.ignite.internal.wrapper.Wrapper;
+import org.apache.ignite.internal.wrapper.Wrappers;
 import org.apache.ignite.lang.CancellationToken;
 import org.apache.ignite.sql.BatchedArguments;
 import org.apache.ignite.sql.IgniteSql;
@@ -220,6 +221,6 @@ public class PublicApiThreadingIgniteSql implements 
IgniteSql, Wrapper {
 
     @Override
     public <T> T unwrap(Class<T> classToUnwrap) {
-        return classToUnwrap.cast(sql);
+        return Wrappers.unwrap(sql, classToUnwrap);
     }
 }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java
index 8e76a785cdd..0bdcb44fad5 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java
@@ -27,11 +27,11 @@ import static 
org.apache.ignite.internal.util.IgniteUtils.closeAll;
 import static org.apache.ignite.lang.ErrorGroups.Common.NODE_STOPPING_ERR;
 import static org.apache.ignite.lang.ErrorGroups.Sql.EXECUTION_CANCELLED_ERR;
 
-import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledExecutorService;
@@ -57,6 +57,7 @@ import 
org.apache.ignite.internal.placementdriver.event.PrimaryReplicaEvent;
 import org.apache.ignite.internal.replicator.ReplicaService;
 import org.apache.ignite.internal.schema.SchemaManager;
 import org.apache.ignite.internal.schema.SchemaSyncService;
+import org.apache.ignite.internal.sql.SqlCommon;
 import 
org.apache.ignite.internal.sql.configuration.distributed.SqlDistributedConfiguration;
 import 
org.apache.ignite.internal.sql.configuration.local.SqlLocalConfiguration;
 import org.apache.ignite.internal.sql.engine.api.kill.CancellableOperationType;
@@ -122,8 +123,6 @@ import org.jetbrains.annotations.TestOnly;
  *  Main implementation of {@link QueryProcessor}.
  */
 public class SqlQueryProcessor implements QueryProcessor, SystemViewProvider {
-    /** Default time-zone ID. */
-    public static final ZoneId DEFAULT_TIME_ZONE_ID = ZoneId.of("UTC");
 
     private static final int PARSED_RESULT_CACHE_SIZE = 10_000;
 
@@ -541,7 +540,7 @@ public class SqlQueryProcessor implements QueryProcessor, 
SystemViewProvider {
                             .queryId(UUID.randomUUID())
                             // time zone is used in execution phase,
                             // so we may use any time zone for preparation only
-                            .timeZoneId(DEFAULT_TIME_ZONE_ID)
+                            .timeZoneId(SqlCommon.DEFAULT_TIME_ZONE_ID)
                             .defaultSchemaName(schemaName)
                             .operationTime(timestamp)
                             .cancel(queryCancel)
@@ -606,6 +605,11 @@ public class SqlQueryProcessor implements QueryProcessor, 
SystemViewProvider {
         return List.of(queriesViewProvider.get());
     }
 
+    @Override
+    public CompletableFuture<Void> invalidatePlannerCache(Set<String> 
tableNames) {
+        return prepareSvc.invalidateCache(tableNames);
+    }
+
     /** Completes the provided future when the callback is called. */
     public static class PrefetchCallback implements QueryPrefetchCallback {
         private final CompletableFuture<Void> prefetchFuture = new 
CompletableFuture<>();
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareService.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareService.java
index 0cd83ea8e90..b4dbab49fcf 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareService.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareService.java
@@ -17,10 +17,12 @@
 
 package org.apache.ignite.internal.sql.engine.prepare;
 
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.sql.engine.SqlOperationContext;
 import org.apache.ignite.internal.sql.engine.exec.LifecycleAware;
 import org.apache.ignite.internal.sql.engine.sql.ParsedResult;
+import org.apache.ignite.internal.util.CompletableFutures;
 
 /**
  * Preparation service that accepts an AST of the query and returns a prepared 
query plan.
@@ -36,4 +38,14 @@ public interface PrepareService extends LifecycleAware {
      * @return Future that contains prepared query plan when completes.
      */
     CompletableFuture<QueryPlan> prepareAsync(ParsedResult parsedResult, 
SqlOperationContext ctx);
+
+    /**
+     * Invalidates planner cache if {@code tableNames} is empty, otherwise 
invalidates only plans, which refers to the provided tables.
+     *
+     * @param tableNames Table names.
+     * @return Operation completion future.
+     */
+    default CompletableFuture<Void> invalidateCache(Set<String> tableNames) {
+        return CompletableFutures.nullCompletedFuture();
+    }
 }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java
index 16a7cfcce28..b7ddf7608a0 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java
@@ -30,6 +30,7 @@ import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
@@ -37,6 +38,8 @@ import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.schema.SchemaPlus;
@@ -95,6 +98,8 @@ import org.apache.ignite.sql.ColumnMetadata;
 import org.apache.ignite.sql.ColumnType;
 import org.apache.ignite.sql.ResultSetMetadata;
 import org.apache.ignite.sql.SqlException;
+import org.apache.ignite.table.QualifiedName;
+import org.apache.ignite.table.QualifiedNameHelper;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -248,7 +253,7 @@ public class PrepareServiceImpl implements PrepareService {
         boolean explicitTx = operationContext.txContext() != null && 
operationContext.txContext().explicitTx() != null;
 
         long timestamp = operationContext.operationTime().longValue();
-        int catalogVersion = schemaManager.catalogVersion(timestamp); 
+        int catalogVersion = schemaManager.catalogVersion(timestamp);
 
         CacheKey key = createCacheKey(parsedResult, catalogVersion, 
schemaName, operationContext.parameters());
 
@@ -305,6 +310,32 @@ public class PrepareServiceImpl implements PrepareService {
         );
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public CompletableFuture<Void> invalidateCache(Set<String> tableNames) {
+        return CompletableFuture.supplyAsync(() -> {
+            if (tableNames.isEmpty()) {
+                cache.clear();
+            } else {
+                Set<QualifiedName> qualifiedNames = 
tableNames.stream().map(QualifiedName::parse).collect(Collectors.toSet());
+                cache.removeIfValue(p -> p.isDone() && planMatches(p.join(), 
qualifiedNames::contains));
+            }
+
+            return null;
+        }, planningPool);
+    }
+
+    /** Check if the given query plan matches the given predicate. */
+    public static boolean planMatches(QueryPlan plan, Predicate<QualifiedName> 
predicate) {
+        assert plan instanceof ExplainablePlan;
+
+        MatchingShuttle shuttle = new MatchingShuttle(predicate);
+
+        ((ExplainablePlan) plan).getRel().accept(shuttle);
+
+        return shuttle.matches();
+    }
+
     private CompletableFuture<QueryPlan> prepareAsync0(
             ParsedResult parsedResult,
             PlanningContext planningContext
@@ -455,25 +486,25 @@ public class PrepareServiceImpl implements PrepareService 
{
                     IgniteKeyValueGet kvGet = (IgniteKeyValueGet) optimizedRel;
 
                     return new KeyValueGetPlan(
-                            nextPlanId(), 
-                            catalogVersion, 
-                            kvGet, 
+                            nextPlanId(),
+                            catalogVersion,
+                            kvGet,
                             resultSetMetadata,
                             parameterMetadata,
-                            relWithMetadata.paMetadata, 
+                            relWithMetadata.paMetadata,
                             relWithMetadata.ppMetadata
                     );
                 }
 
                 var plan = new MultiStepPlan(
-                        nextPlanId(), 
-                        SqlQueryType.QUERY, 
-                        optimizedRel, 
-                        resultSetMetadata, 
-                        parameterMetadata, 
-                        catalogVersion, 
-                        relWithMetadata.numSources, 
-                        fastPlan, 
+                        nextPlanId(),
+                        SqlQueryType.QUERY,
+                        optimizedRel,
+                        resultSetMetadata,
+                        parameterMetadata,
+                        catalogVersion,
+                        relWithMetadata.numSources,
+                        fastPlan,
                         relWithMetadata.paMetadata,
                         relWithMetadata.ppMetadata
                 );
@@ -539,10 +570,10 @@ public class PrepareServiceImpl implements PrepareService 
{
         if (optimizedRel instanceof IgniteKeyValueModify) {
             plan = new KeyValueModifyPlan(
                     nextPlanId(),
-                    ctx.catalogVersion(), 
+                    ctx.catalogVersion(),
                     (IgniteKeyValueModify) optimizedRel,
                     DML_METADATA,
-                    parameterMetadata, 
+                    parameterMetadata,
                     relWithMetadata.paMetadata,
                     relWithMetadata.ppMetadata
             );
@@ -552,9 +583,9 @@ public class PrepareServiceImpl implements PrepareService {
                     SqlQueryType.DML,
                     optimizedRel, DML_METADATA,
                     parameterMetadata,
-                    ctx.catalogVersion(), 
-                    relWithMetadata.numSources, 
-                    null, 
+                    ctx.catalogVersion(),
+                    relWithMetadata.numSources,
+                    null,
                     relWithMetadata.paMetadata,
                     relWithMetadata.ppMetadata
             );
@@ -611,25 +642,25 @@ public class PrepareServiceImpl implements PrepareService 
{
                     IgniteKeyValueModify kvModify = (IgniteKeyValueModify) 
optimizedRel;
 
                     plan = new KeyValueModifyPlan(
-                            nextPlanId(), 
+                            nextPlanId(),
                             catalogVersion,
                             kvModify,
                             DML_METADATA,
-                            parameterMetadata, 
-                            relWithMetadata.paMetadata, 
+                            parameterMetadata,
+                            relWithMetadata.paMetadata,
                             relWithMetadata.ppMetadata
                     );
                 } else {
                     plan = new MultiStepPlan(
-                            nextPlanId(), 
+                            nextPlanId(),
                             SqlQueryType.DML,
                             optimizedRel,
-                            DML_METADATA, 
-                            parameterMetadata, 
+                            DML_METADATA,
+                            parameterMetadata,
                             catalogVersion,
-                            relWithMetadata.numSources, 
-                            null, 
-                            relWithMetadata.paMetadata, 
+                            relWithMetadata.numSources,
+                            null,
+                            relWithMetadata.paMetadata,
                             relWithMetadata.ppMetadata
                     );
                 }
@@ -767,9 +798,9 @@ public class PrepareServiceImpl implements PrepareService {
     }
 
     private RelWithMetadata doOptimize(
-            PlanningContext ctx, 
-            SqlNode validatedNode, 
-            IgnitePlanner planner, 
+            PlanningContext ctx,
+            SqlNode validatedNode,
+            IgnitePlanner planner,
             @Nullable Runnable onTimeoutAction
     ) {
         // Convert to Relational operators graph
@@ -819,7 +850,7 @@ public class PrepareServiceImpl implements PrepareService {
 
         for (int i = 0; i < parameterRowType.getFieldCount(); i++) {
             RelDataTypeField field = parameterRowType.getFieldList().get(i);
-            ParameterType parameterType = 
ParameterType.fromRelDataType(field.getType());
+            ParameterType parameterType = 
TypeUtils.fromRelDataType(field.getType());
 
             parameterTypes.add(parameterType);
         }
@@ -906,7 +937,7 @@ public class PrepareServiceImpl implements PrepareService {
         RelWithMetadata(
                 IgniteRel rel,
                 int numSources,
-                @Nullable PartitionAwarenessMetadata paMetadata, 
+                @Nullable PartitionAwarenessMetadata paMetadata,
                 @Nullable PartitionPruningMetadata ppMetadata
         ) {
             this.rel = rel;
@@ -915,4 +946,42 @@ public class PrepareServiceImpl implements PrepareService {
             this.ppMetadata = ppMetadata;
         }
     }
+
+    private static class MatchingShuttle extends IgniteRelShuttle {
+        private final Predicate<QualifiedName> tableNamePredicate;
+        private boolean matches;
+
+        private MatchingShuttle(Predicate<QualifiedName> tableNamePredicate) {
+            this.tableNamePredicate = tableNamePredicate;
+            matches = false;
+        }
+
+        /**
+         * Visits all children of a parent.
+         */
+        @Override
+        protected IgniteRel processNode(IgniteRel rel) {
+            if (!matches && rel.getTable() != null) {
+                List<String> tableName = rel.getTable().getQualifiedName();
+                assert tableName.size() == 2 : "Qualified table name 
expected.";
+                matches = 
tableNamePredicate.test(QualifiedNameHelper.fromNormalized(tableName.get(0), 
tableName.get(1)));
+            }
+
+            if (matches) {
+                return rel;
+            }
+
+            List<IgniteRel> inputs = Commons.cast(rel.getInputs());
+
+            for (int i = 0; i < inputs.size() && !matches; i++) {
+                visit(inputs.get(i));
+            }
+
+            return rel;
+        }
+
+        boolean matches() {
+            return matches;
+        }
+    }
 }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
index 25d4a97b0a0..368ef3d4033 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
@@ -66,6 +66,7 @@ import 
org.apache.ignite.internal.sql.engine.exec.row.RowSchema;
 import org.apache.ignite.internal.sql.engine.exec.row.RowSchemaTypes;
 import org.apache.ignite.internal.sql.engine.exec.row.RowType;
 import org.apache.ignite.internal.sql.engine.exec.row.TypeSpec;
+import org.apache.ignite.internal.sql.engine.prepare.ParameterType;
 import org.apache.ignite.internal.sql.engine.type.IgniteCustomType;
 import 
org.apache.ignite.internal.sql.engine.type.IgniteCustomTypeCoercionRules;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
@@ -74,6 +75,7 @@ import org.apache.ignite.internal.type.NativeType;
 import org.apache.ignite.internal.type.NativeTypes;
 import org.apache.ignite.internal.type.TemporalNativeType;
 import org.apache.ignite.internal.type.VarlenNativeType;
+import org.apache.ignite.sql.ColumnMetadata;
 import org.apache.ignite.sql.ColumnType;
 import org.apache.ignite.sql.SqlException;
 import org.jetbrains.annotations.Nullable;
@@ -152,6 +154,20 @@ public class TypeUtils {
         }
     }
 
+    /** Creates parameter metadata from the given logical type. */
+    public static ParameterType fromRelDataType(RelDataType type) {
+        ColumnType columnType = columnType(type);
+        assert columnType != null : "No column type for " + type;
+
+        int precision = columnType.lengthAllowed() || 
columnType.precisionAllowed()
+                ? type.getPrecision()
+                : ColumnMetadata.UNDEFINED_PRECISION;
+
+        int scale = columnType.scaleAllowed() ? type.getScale() : 
ColumnMetadata.UNDEFINED_SCALE;
+
+        return new ParameterType(columnType, precision, scale, 
type.isNullable());
+    }
+
     private static class SupportedParamClassesHolder {
         // TODO: https://issues.apache.org/jira/browse/IGNITE-17373
         static final Set<ColumnType> UNSUPPORTED_COLUMN_TYPES_AS_PARAMETERS = 
Set.of(ColumnType.PERIOD, ColumnType.DURATION);
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
index 92411d3dfe4..ce1731eb294 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
@@ -98,7 +98,6 @@ import 
org.apache.ignite.internal.sql.engine.NodeLeftException;
 import org.apache.ignite.internal.sql.engine.QueryCancel;
 import org.apache.ignite.internal.sql.engine.QueryCancelledException;
 import org.apache.ignite.internal.sql.engine.SqlOperationContext;
-import org.apache.ignite.internal.sql.engine.SqlQueryProcessor;
 import 
org.apache.ignite.internal.sql.engine.exec.ExecutionServiceImplTest.TestCluster.TestNode;
 import org.apache.ignite.internal.sql.engine.exec.ddl.DdlCommandHandler;
 import org.apache.ignite.internal.sql.engine.exec.exp.ExpressionFactoryImpl;
@@ -1213,7 +1212,7 @@ public class ExecutionServiceImplTest extends 
BaseIgniteAbstractTest {
                 .cancel(new QueryCancel())
                 .operationTime(new HybridClockImpl().now())
                 .defaultSchemaName(SqlCommon.DEFAULT_SCHEMA_NAME)
-                .timeZoneId(SqlQueryProcessor.DEFAULT_TIME_ZONE_ID)
+                .timeZoneId(SqlCommon.DEFAULT_TIME_ZONE_ID)
                 .txContext(ExplicitTxContext.fromTx(new 
NoOpTransaction(nodeNames.get(0), false)));
     }
 
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/RuntimeSortedIndexTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/RuntimeSortedIndexTest.java
index 1fd333efa8b..1a335c42500 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/RuntimeSortedIndexTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/RuntimeSortedIndexTest.java
@@ -36,7 +36,7 @@ import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.util.ImmutableIntList;
 import org.apache.calcite.util.Pair;
 import org.apache.ignite.internal.network.ClusterNodeImpl;
-import org.apache.ignite.internal.sql.engine.SqlQueryProcessor;
+import org.apache.ignite.internal.sql.SqlCommon;
 import org.apache.ignite.internal.sql.engine.exec.exp.ExpressionFactoryImpl;
 import org.apache.ignite.internal.sql.engine.framework.ArrayRowHandler;
 import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
@@ -128,7 +128,7 @@ public class RuntimeSortedIndexTest extends 
IgniteAbstractTest {
                         ArrayRowHandler.INSTANCE,
                         Map.of(),
                         null,
-                        SqlQueryProcessor.DEFAULT_TIME_ZONE_ID,
+                        SqlCommon.DEFAULT_TIME_ZONE_ID,
                         -1,
                         Clock.systemUTC(),
                         null
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractExecutionTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractExecutionTest.java
index 6e8c8c31434..ed8a167f6e2 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractExecutionTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractExecutionTest.java
@@ -53,7 +53,7 @@ import org.apache.ignite.internal.network.InternalClusterNode;
 import org.apache.ignite.internal.schema.BinaryRowConverter;
 import org.apache.ignite.internal.schema.BinaryTuple;
 import org.apache.ignite.internal.schema.BinaryTupleSchema;
-import org.apache.ignite.internal.sql.engine.SqlQueryProcessor;
+import org.apache.ignite.internal.sql.SqlCommon;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionContext;
 import org.apache.ignite.internal.sql.engine.exec.ExecutionId;
 import org.apache.ignite.internal.sql.engine.exec.QueryTaskExecutorImpl;
@@ -155,7 +155,7 @@ public abstract class AbstractExecutionTest<T> extends 
IgniteAbstractTest {
                 rowHandler(),
                 Map.of(),
                 TxAttributes.fromTx(new NoOpTransaction("fake-test-node", 
false)),
-                SqlQueryProcessor.DEFAULT_TIME_ZONE_ID,
+                SqlCommon.DEFAULT_TIME_ZONE_ID,
                 bufferSize,
                 Clock.systemUTC(),
                 null
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java
index a44252aabfa..ab6b18ae8ff 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestBuilders.java
@@ -98,7 +98,6 @@ import 
org.apache.ignite.internal.partitiondistribution.Assignment;
 import org.apache.ignite.internal.partitiondistribution.TokenizedAssignments;
 import 
org.apache.ignite.internal.partitiondistribution.TokenizedAssignmentsImpl;
 import org.apache.ignite.internal.sql.SqlCommon;
-import org.apache.ignite.internal.sql.engine.SqlQueryProcessor;
 import org.apache.ignite.internal.sql.engine.api.kill.OperationKillHandler;
 import org.apache.ignite.internal.sql.engine.exec.ExecutableTable;
 import org.apache.ignite.internal.sql.engine.exec.ExecutableTableRegistry;
@@ -565,7 +564,7 @@ public class TestBuilders {
         private QueryTaskExecutor executor = null;
         private InternalClusterNode node = null;
         private Object[] dynamicParams = ArrayUtils.OBJECT_EMPTY_ARRAY;
-        private ZoneId zoneId = SqlQueryProcessor.DEFAULT_TIME_ZONE_ID;
+        private ZoneId zoneId = SqlCommon.DEFAULT_TIME_ZONE_ID;
         private Clock clock = Clock.systemUTC();
 
         /** {@inheritDoc} */
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestNode.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestNode.java
index 978b57951db..abb91cf4029 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestNode.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/framework/TestNode.java
@@ -55,7 +55,6 @@ import org.apache.ignite.internal.sql.engine.InternalSqlRow;
 import org.apache.ignite.internal.sql.engine.QueryCancel;
 import org.apache.ignite.internal.sql.engine.SqlOperationContext;
 import org.apache.ignite.internal.sql.engine.SqlProperties;
-import org.apache.ignite.internal.sql.engine.SqlQueryProcessor;
 import org.apache.ignite.internal.sql.engine.api.kill.OperationKillHandler;
 import org.apache.ignite.internal.sql.engine.exec.ExchangeService;
 import org.apache.ignite.internal.sql.engine.exec.ExchangeServiceImpl;
@@ -422,7 +421,7 @@ public class TestNode implements LifecycleAware {
                 .cancel(new QueryCancel())
                 .operationTime(clock.now())
                 .defaultSchemaName(SqlCommon.DEFAULT_SCHEMA_NAME)
-                .timeZoneId(SqlQueryProcessor.DEFAULT_TIME_ZONE_ID)
+                .timeZoneId(SqlCommon.DEFAULT_TIME_ZONE_ID)
                 .txContext(ImplicitTxContext.create())
                 .parameters();
     }
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java
index b6a7b9038fc..e21d918b9d6 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java
@@ -22,6 +22,7 @@ import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.assertThr
 import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.sameInstance;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@@ -38,6 +39,7 @@ import java.time.Duration;
 import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
@@ -311,22 +313,7 @@ public class PrepareServiceImplTest extends 
BaseIgniteAbstractTest {
         IgniteSchema schema = new IgniteSchema("PUBLIC", 0, 
List.of(igniteTable));
         Cache<Object, Object> cache = 
CaffeineCacheFactory.INSTANCE.create(100);
 
-        CacheFactory cacheFactory = new CacheFactory() {
-            @Override
-            public <K, V> Cache<K, V> create(int size) {
-                return (Cache<K, V>) cache;
-            }
-
-            @Override
-            public <K, V> Cache<K, V> create(int size, StatsCounter 
statCounter) {
-                return (Cache<K, V>) cache;
-            }
-
-            @Override
-            public <K, V> Cache<K, V> create(int size, StatsCounter 
statCounter, Duration expireAfterAccess) {
-                return (Cache<K, V>) cache;
-            }
-        };
+        CacheFactory cacheFactory = new DummyCacheFactory(cache);
 
         PrepareServiceImpl service = createPlannerService(schema, 
cacheFactory, 100);
 
@@ -410,6 +397,101 @@ public class PrepareServiceImplTest extends 
BaseIgniteAbstractTest {
         });
     }
 
+    @Test
+    public void invalidatePlannerCache() {
+        IgniteSchema schema = new IgniteSchema("PUBLIC", 0, List.of(
+                TestBuilders.table().name("T1").addColumn("C", 
NativeTypes.INT32).distribution(IgniteDistributions.single()).build(),
+                TestBuilders.table().name("T2").addColumn("C", 
NativeTypes.INT32).distribution(IgniteDistributions.single()).build()
+        ));
+
+        Cache<Object, Object> cache = 
CaffeineCacheFactory.INSTANCE.create(100);
+
+        PrepareServiceImpl service = createPlannerService(schema, new 
DummyCacheFactory(cache), 1000);
+
+        await(service.prepareAsync(parse("SELECT * FROM t1"),  
createContext()));
+        await(service.prepareAsync(parse("SELECT * FROM t1 WHERE C > 0"),  
createContext()));
+        await(service.prepareAsync(parse("SELECT * FROM t2"),  
createContext()));
+
+        assertThat(cache.size(), is(3));
+
+        // Invalidate
+        await(service.invalidateCache(Set.of()));
+
+        assertThat(cache.size(), is(0));
+    }
+
+    @Test
+    public void invalidateQueryPlans() {
+        IgniteSchema schema = new IgniteSchema("PUBLIC", 0, List.of(
+                TestBuilders.table().name("T1").addColumn("C", 
NativeTypes.INT32).distribution(IgniteDistributions.single()).build(),
+                TestBuilders.table().name("t2").addColumn("C", 
NativeTypes.INT32).distribution(IgniteDistributions.single()).build()
+        ));
+
+        Cache<Object, Object> cache = 
CaffeineCacheFactory.INSTANCE.create(100);
+
+        PrepareServiceImpl service = createPlannerService(schema, new 
DummyCacheFactory(cache), 1000);
+
+        { // Simple name.
+            await(service.prepareAsync(parse("SELECT * FROM t1"), 
createContext()));
+            await(service.prepareAsync(parse("SELECT * FROM t1 WHERE C > 0"), 
createContext()));
+            QueryPlan queryPlan = await(service.prepareAsync(parse("SELECT * 
FROM \"t2\""), createContext()));
+
+            assertThat(cache.size(), is(3));
+
+            // Case, when no plan matches.
+            await(service.invalidateCache(Set.of("t")));
+            assertThat(cache.size(), is(3));
+
+            await(service.invalidateCache(Set.of("t2")));
+            assertThat(cache.size(), is(3));
+
+            // Found and invalidate related plan.
+            await(service.invalidateCache(Set.of("t1")));
+            assertThat(cache.size(), is(1));
+
+            QueryPlan explainPlan = await(service.prepareAsync(
+                    parse("explain plan for select * from \"t2\""),
+                    createContext()
+            ));
+
+            ExplainPlan plan = (ExplainPlan) explainPlan;
+            assertThat(plan.plan(), sameInstance(queryPlan));
+
+            await(service.invalidateCache(Set.of("\"t2\"")));
+            assertThat(cache.size(), is(0));
+        }
+
+        { // Qualified name.
+            await(service.prepareAsync(parse("SELECT * FROM t1"), 
createContext()));
+            await(service.prepareAsync(parse("SELECT * FROM t1 WHERE C > 0"), 
createContext()));
+            QueryPlan queryPlan = await(service.prepareAsync(parse("SELECT * 
FROM \"t2\""), createContext()));
+
+            assertThat(cache.size(), is(3));
+
+            // Case, when no plan matches.
+            await(service.invalidateCache(Set.of("PUBLIC.t2")));
+            assertThat(cache.size(), is(3));
+
+            await(service.invalidateCache(Set.of("MYSCHEMA.t1")));
+            assertThat(cache.size(), is(3));
+
+            // Found and invalidate related plan.
+            await(service.invalidateCache(Set.of("PUBLIC.t1")));
+            assertThat(cache.size(), is(1));
+
+            QueryPlan explainPlan = await(service.prepareAsync(
+                    parse("explain plan for select * from \"t2\""),
+                    createContext()
+            ));
+
+            ExplainPlan plan = (ExplainPlan) explainPlan;
+            assertThat(plan.plan(), sameInstance(queryPlan));
+
+            await(service.invalidateCache(Set.of("PUBLIC.\"t2\"")));
+            assertThat(cache.size(), is(0));
+        }
+    }
+
     private static Stream<Arguments> parameterTypes() {
         int noScale = ColumnMetadata.UNDEFINED_SCALE;
         int noPrecision = ColumnMetadata.UNDEFINED_PRECISION;
@@ -491,4 +573,27 @@ public class PrepareServiceImplTest extends 
BaseIgniteAbstractTest {
 
         return service;
     }
+
+    private static class DummyCacheFactory implements CacheFactory {
+        private final Cache<Object, Object> cache;
+
+        DummyCacheFactory(Cache<Object, Object> cache) {
+            this.cache = cache;
+        }
+
+        @Override
+        public <K, V> Cache<K, V> create(int size) {
+            return (Cache<K, V>) cache;
+        }
+
+        @Override
+        public <K, V> Cache<K, V> create(int size, StatsCounter statCounter) {
+            return (Cache<K, V>) cache;
+        }
+
+        @Override
+        public <K, V> Cache<K, V> create(int size, StatsCounter statCounter, 
Duration expireAfterAccess) {
+            return (Cache<K, V>) cache;
+        }
+    }
 }
diff --git 
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java
 
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java
index c48e6a9a8b3..a78abc16249 100644
--- 
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java
+++ 
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java
@@ -52,7 +52,6 @@ import org.apache.ignite.internal.sql.engine.AsyncSqlCursor;
 import org.apache.ignite.internal.sql.engine.InternalSqlRow;
 import org.apache.ignite.internal.sql.engine.QueryProcessor;
 import org.apache.ignite.internal.sql.engine.SqlProperties;
-import org.apache.ignite.internal.sql.engine.SqlQueryProcessor;
 import org.apache.ignite.internal.sql.engine.SqlQueryType;
 import org.apache.ignite.internal.sql.engine.hint.IgniteHint;
 import org.apache.ignite.internal.sql.engine.prepare.QueryMetadata;
@@ -88,7 +87,7 @@ abstract class QueryCheckerImpl implements QueryChecker {
 
     private Object[] params = OBJECT_EMPTY_ARRAY;
 
-    private ZoneId timeZoneId = SqlQueryProcessor.DEFAULT_TIME_ZONE_ID;
+    private ZoneId timeZoneId = SqlCommon.DEFAULT_TIME_ZONE_ID;
 
     private String defaultSchema = SqlCommon.DEFAULT_SCHEMA_NAME;
 

Reply via email to