From d6d8240a51820680dc051ece3a2783c61820e448 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Thu, 8 Oct 2026 16:32:34 +0300 Subject: [PATCH 1/3] IGNITE-29117 Calcite-only node: integration test and prerequisite fixes Co-Authored-By: Claude Fable 5.1 --- modules/calcite/pom.xml | 7 + .../query/calcite/CalciteQueryProcessor.java | 4 +- .../CalciteOnlyNodeIntegrationTest.java | 363 ++++++++++++++++++ .../testsuites/IntegrationTestSuite.java | 2 + .../jdbc/thin/JdbcThinConnection.java | 6 +- .../cache/IgniteCacheProxyImpl.java | 12 +- .../odbc/jdbc/JdbcConnectionContext.java | 21 +- .../odbc/odbc/OdbcConnectionContext.java | 21 +- .../processors/query/GridQueryProcessor.java | 39 +- .../processors/query/QueryEngine.java | 5 + 10 files changed, 429 insertions(+), 51 deletions(-) create mode 100644 modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteOnlyNodeIntegrationTest.java diff --git a/modules/calcite/pom.xml b/modules/calcite/pom.xml index e39877b84f237..1be401039596a 100644 --- a/modules/calcite/pom.xml +++ b/modules/calcite/pom.xml @@ -81,6 +81,13 @@ test + + com.thoughtworks.xstream + xstream + ${xstream.version} + test + + org.apache.calcite calcite-core diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java index ca6fbd6f390a8..2fafad476dcf7 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/CalciteQueryProcessor.java @@ -834,8 +834,8 @@ public QueryRegistry queryRegistry() { return qryReg; } - /** */ - public CalciteQueryEngineConfiguration config() { + /** {@inheritDoc} */ + @Override public CalciteQueryEngineConfiguration config() { return cfg; } diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteOnlyNodeIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteOnlyNodeIntegrationTest.java new file mode 100644 index 0000000000000..3ef9b10a891ea --- /dev/null +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteOnlyNodeIntegrationTest.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.processors.query.calcite.integration; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.cache.Cache; +import org.apache.ignite.Ignite; +import org.apache.ignite.IgniteCache; +import org.apache.ignite.Ignition; +import org.apache.ignite.cache.query.SqlFieldsQuery; +import org.apache.ignite.cache.query.SqlQuery; +import org.apache.ignite.cache.query.TextQuery; +import org.apache.ignite.calcite.CalciteQueryEngineConfiguration; +import org.apache.ignite.client.ClientCache; +import org.apache.ignite.client.ClientException; +import org.apache.ignite.client.Config; +import org.apache.ignite.client.IgniteClient; +import org.apache.ignite.compute.ComputeJob; +import org.apache.ignite.compute.ComputeJobAdapter; +import org.apache.ignite.compute.ComputeJobResult; +import org.apache.ignite.compute.ComputeTaskSplitAdapter; +import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.ClientConfiguration; +import org.apache.ignite.configuration.ClientConnectorConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.configuration.ThinClientConfiguration; +import org.apache.ignite.internal.IgniteComponentType; +import org.apache.ignite.internal.IgniteFutureTimeoutCheckedException; +import org.apache.ignite.internal.IgniteInternalFuture; +import org.apache.ignite.internal.processors.query.calcite.GridCommonAbstractWrapperTest; +import org.apache.ignite.resources.IgniteInstanceResource; +import org.apache.ignite.testframework.CallbackExecutorLogListener; +import org.apache.ignite.testframework.GridTestUtils; +import org.apache.ignite.testframework.ListeningTestLogger; +import org.apache.ignite.testframework.junits.multijvm.IgniteProcessProxy; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Checks behaviour of a node that has the Calcite engine on the classpath but not the H2 engine + * ({@code ignite-indexing}). + * This is the default node layout after the H2 module is moved to {@code libs/optional} (IGNITE-29117). + */ +@SuppressWarnings("deprecation") +public class CalciteOnlyNodeIntegrationTest extends GridCommonAbstractWrapperTest { + /** */ + private static final String JDBC_URL = "jdbc:ignite:thin://127.0.0.1"; + + /** + * Fragment that must be present in the error message of the query types that are implemented by the H2 engine only + * ({@link SqlQuery}, {@link TextQuery}), so that a user knows which module to add. + */ + private static final String H2_ONLY_FEATURE_MSG = "ignite-indexing"; + + /** Error message of the H2-only SQL commands ({@code SET STREAMING}, {@code COPY}): Calcite cannot parse them. */ + private static final String PARSE_ERR_MSG = "Failed to parse query"; + + /** {@link NodeTask} operation: is the H2 engine visible to the node. */ + private static final String OP_H2_IN_CLASSPATH = "h2InClassPath"; + + /** {@link NodeTask} operation: {@code SELECT QUERY_ENGINE()} via the cache API. */ + private static final String OP_QUERY_ENGINE = "queryEngine"; + + /** {@link NodeTask} operation: deprecated {@link SqlQuery} via the cache API, returns the matching keys. */ + private static final String OP_SQL_QUERY = "sqlQuery"; + + /** {@link NodeTask} operation: {@link TextQuery} via the cache API, returns the matching keys. */ + private static final String OP_TEXT_QUERY = "textQuery"; + + /** Prefix of a {@link NodeTask} result when the operation has failed; the messages of the cause chain follow. */ + private static final String ERR_PREFIX = "error: "; + + /** {@inheritDoc} */ + @BeforeAll + @Override protected void beforeTestsStarted() throws Exception { + super.beforeTestsStarted(); + + startCalciteOnlyNode(); + + assertEquals("H2 engine must not be visible to the Calcite-only node", "false", nodeOp(OP_H2_IN_CLASSPATH)); + + try (IgniteClient cli = client()) { + ClientCache cache = cli.cache(DEFAULT_CACHE_NAME); + + cache.put(1, "v1"); + cache.put(2, "v2"); + } + } + + /** {@inheritDoc} */ + @AfterAll + @Override protected void afterTestsStopped() throws Exception { + IgniteProcessProxy.killAll(); + + super.afterTestsStopped(); + } + + /** + * Starts a node in a separate JVM with {@code ignite-indexing}, H2 and Lucene removed from the classpath. + * No engine is configured explicitly: the node must pick the only available one. + */ + private void startCalciteOnlyNode() throws Exception { + IgniteConfiguration cfg = optimize(getConfiguration("calcite-only")) + .setClientConnectorConfiguration(new ClientConnectorConfiguration() + .setThinClientConfiguration(new ThinClientConfiguration().setMaxActiveComputeTasksPerConnection(1))) + .setCacheConfiguration( + new CacheConfiguration(DEFAULT_CACHE_NAME).setIndexedTypes(Integer.class, String.class)); + + CountDownLatch started = new CountDownLatch(1); + + ListeningTestLogger lsnrLog = new ListeningTestLogger(log); + + lsnrLog.registerListener(new CallbackExecutorLogListener(".*Topology snapshot \\[ver=1,.*", started::countDown)); + + new IgniteProcessProxy(cfg, lsnrLog, null, false) { + @Override protected Collection filteredJvmArgs() throws Exception { + Collection args = super.filteredJvmArgs(); + + args.add("-cp"); + args.add(Stream.of(System.getProperty("java.class.path"), System.getProperty("surefire.test.class.path")) + .filter(Objects::nonNull) + .flatMap(s -> Arrays.stream(s.split(File.pathSeparator))) + .filter(e -> !isH2Entry(e)) + .collect(Collectors.joining(File.pathSeparator))); + + return args; + } + }; + + assertTrue("Calcite-only node has not started", started.await(getTestTimeout(), TimeUnit.MILLISECONDS)); + } + + /** Baseline: {@link SqlFieldsQuery}, DDL and DML work on a Calcite-only node, Calcite is the default engine. */ + @Test + public void testSqlFieldsQuery() throws Exception { + assertEquals(CalciteQueryEngineConfiguration.ENGINE_NAME, nodeOp(OP_QUERY_ENGINE)); + + try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) { + stmt.executeUpdate("CREATE TABLE t(id INT PRIMARY KEY, val VARCHAR) WITH \"template=replicated\""); + stmt.executeUpdate("INSERT INTO t VALUES (1, 'a')"); + + try (ResultSet rs = stmt.executeQuery("SELECT val FROM t WHERE id = 1")) { + assertTrue(rs.next()); + assertEquals("a", rs.getString(1)); + } + + stmt.executeUpdate("DROP TABLE t"); + } + } + + /** + * Deprecated {@link SqlQuery} is implemented by the H2 engine only and is not supported by the Calcite engine: + * the error must name the missing module. + */ + @Test + public void testSqlQuery() throws Exception { + String res = nodeOp(OP_SQL_QUERY); + + assertTrue(res, res.startsWith(ERR_PREFIX) && res.contains(H2_ONLY_FEATURE_MSG)); + } + + /** Deprecated {@link SqlQuery} sent by the Java thin client fails on the server node with the same error. */ + @Test + @SuppressWarnings("ThrowableNotThrown") + public void testThinClientSqlQuery() { + try (IgniteClient cli = client()) { + ClientCache cache = cli.cache(DEFAULT_CACHE_NAME); + + GridTestUtils.assertThrows(log, + () -> cache.query(new SqlQuery(String.class, "_val = ?").setArgs("v2")).getAll(), + ClientException.class, H2_ONLY_FEATURE_MSG); + } + } + + /** {@link TextQuery} is implemented by the H2 engine only: the error must name the missing module. */ + @Test + public void testTextQuery() throws Exception { + String res = nodeOp(OP_TEXT_QUERY); + + assertTrue(res, res.startsWith(ERR_PREFIX) && res.contains(H2_ONLY_FEATURE_MSG)); + } + + /** + * {@code SET STREAMING} is implemented by the H2 engine only and is rejected by the Calcite parser. A rejected + * {@code SET STREAMING ON} must not leave the thin JDBC connection in a state where {@code close()} hangs. + */ + @Test + public void testSetStreaming() throws Exception { + Connection conn = DriverManager.getConnection(JDBC_URL); + + try (Statement stmt = conn.createStatement()) { + GridTestUtils.assertThrowsAnyCause(log, () -> stmt.executeUpdate("SET STREAMING ON"), + SQLException.class, PARSE_ERR_MSG); + } + finally { + IgniteInternalFuture closeFut = GridTestUtils.runAsync(conn::close); + + try { + closeFut.get(10_000); + } + catch (IgniteFutureTimeoutCheckedException e) { + fail("Connection close hangs after a rejected SET STREAMING ON"); + } + } + } + + /** {@code COPY} (bulk load) is implemented by the H2 engine only and is rejected by the Calcite parser. */ + @Test + public void testCopy() throws Exception { + try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) { + GridTestUtils.assertThrowsAnyCause(log, + () -> stmt.executeUpdate("COPY FROM '/nonexistent.csv' INTO \"test\".String(_key, _val) FORMAT CSV"), + SQLException.class, PARSE_ERR_MSG); + } + } + + /** The only available engine can be selected by name via the JDBC property without an explicit configuration. */ + @Test + public void testJdbcQueryEngineProperty() throws Exception { + try (Connection conn = DriverManager.getConnection( + JDBC_URL + "?queryEngine=" + CalciteQueryEngineConfiguration.ENGINE_NAME); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT QUERY_ENGINE()")) { + assertTrue(rs.next()); + assertEquals(CalciteQueryEngineConfiguration.ENGINE_NAME, rs.getString(1)); + } + } + + /** The only available engine can be selected by the query hint without an explicit configuration. */ + @Test + public void testQueryEngineHint() throws Exception { + try (Connection conn = DriverManager.getConnection(JDBC_URL); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT /*+ QUERY_ENGINE('" + CalciteQueryEngineConfiguration.ENGINE_NAME + "') */ QUERY_ENGINE()")) { + assertTrue(rs.next()); + assertEquals(CalciteQueryEngineConfiguration.ENGINE_NAME, rs.getString(1)); + } + } + + /** @return {@code True} if the classpath entry belongs to the H2 engine. */ + private static boolean isH2Entry(String entry) { + String name = new File(entry).getName(); + + return name.startsWith("ignite-indexing") + || name.startsWith("h2-") + || name.startsWith("lucene-") + || entry.replace('\\', '/').contains("/modules/indexing/target/"); + } + + /** */ + private static IgniteClient client() { + return Ignition.startClient(new ClientConfiguration().setAddresses(Config.SERVER)); + } + + /** + * Runs a cache API operation inside the Calcite-only node. + * + * @param op One of the {@code OP_*} constants. + * @return Operation result, or {@link #ERR_PREFIX} followed by the error messages if the operation has failed. + */ + private static String nodeOp(String op) throws Exception { + try (IgniteClient cli = client()) { + return cli.compute().execute(NodeTask.class.getName(), op); + } + } + + /** Runs a cache API operation inside the Calcite-only node, see {@link #nodeOp(String)}. */ + public static class NodeTask extends ComputeTaskSplitAdapter { + /** {@inheritDoc} */ + @Override protected Collection split(int gridSize, String op) { + return Collections.singleton(new NodeJob(op)); + } + + /** {@inheritDoc} */ + @Override public String reduce(List results) { + return results.get(0).getData(); + } + } + + /** */ + private static class NodeJob extends ComputeJobAdapter { + /** */ + @IgniteInstanceResource + private Ignite ignite; + + /** */ + private final String op; + + /** */ + private NodeJob(String op) { + this.op = op; + } + + /** {@inheritDoc} */ + @Override public Object execute() { + try { + IgniteCache cache = ignite.cache(DEFAULT_CACHE_NAME); + + switch (op) { + case OP_H2_IN_CLASSPATH: + return String.valueOf(IgniteComponentType.INDEXING.inClassPath()); + + case OP_QUERY_ENGINE: + return String.valueOf(cache.query(new SqlFieldsQuery("SELECT QUERY_ENGINE()")).getAll().get(0).get(0)); + + case OP_SQL_QUERY: + return keys(cache.query(new SqlQuery(String.class, "_val = ?").setArgs("v1")).getAll()); + + case OP_TEXT_QUERY: + return keys(cache.query(new TextQuery(String.class, "v1")).getAll()); + + default: + throw new IllegalArgumentException("Unknown operation: " + op); + } + } + catch (Throwable e) { + StringBuilder sb = new StringBuilder(ERR_PREFIX); + + for (Throwable t = e; t != null; t = t.getCause()) + sb.append(t.getMessage()).append(t.getCause() == null ? "" : " <- "); + + return sb.toString(); + } + } + + /** @return Sorted, comma separated keys of the entries. */ + private static String keys(List> entries) { + return entries.stream().map(Cache.Entry::getKey).sorted().map(String::valueOf) + .collect(Collectors.joining(",")); + } + } +} diff --git a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java index 833729b5e0b60..6cfa81743f9cb 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java +++ b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java @@ -32,6 +32,7 @@ import org.apache.ignite.internal.processors.query.calcite.integration.CacheWithInterceptorIntegrationTest; import org.apache.ignite.internal.processors.query.calcite.integration.CalciteBasicSecondaryIndexIntegrationTest; import org.apache.ignite.internal.processors.query.calcite.integration.CalciteErrorHandlilngIntegrationTest; +import org.apache.ignite.internal.processors.query.calcite.integration.CalciteOnlyNodeIntegrationTest; import org.apache.ignite.internal.processors.query.calcite.integration.CalcitePlanningDumpTest; import org.apache.ignite.internal.processors.query.calcite.integration.CollectIntegrationTest; import org.apache.ignite.internal.processors.query.calcite.integration.CorrelatesIntegrationTest; @@ -182,6 +183,7 @@ TpchScale010Test.class, TpchScale100Test.class, UnnestIntegrationTest.class, + CalciteOnlyNodeIntegrationTest.class, CalcitePlanningDumpTest.class, KeyClassChangeIntegrationTest.class, QueryEntityValueColumnAliasTest.class, diff --git a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java index 60a6a4921e628..deae742d32906 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/jdbc/thin/JdbcThinConnection.java @@ -472,12 +472,14 @@ void executeNative(String sql, SqlCommand cmd, JdbcThinStatement stmt) throws SQ + cliIo.igniteVersion() + ']', INTERNAL_ERROR); } - streamState = new StreamState((SqlSetStreamingCommand)cmd, cliIo); + StreamState streamState0 = new StreamState(cmd0, cliIo); sendRequest(new JdbcQueryExecuteRequest(JdbcStatementType.ANY_STATEMENT_TYPE, schema, 1, 1, autoCommit, stmt.explicitTimeout, sql, null, NONE_TX), stmt, cliIo); - streamState.start(); + streamState = streamState0; + + streamState0.start(); } } else diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/IgniteCacheProxyImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/IgniteCacheProxyImpl.java index 2ab4e3d990a7c..ec86be411348a 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/IgniteCacheProxyImpl.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/IgniteCacheProxyImpl.java @@ -891,10 +891,16 @@ private void validate(Query qry) { throw new CacheException("Indexing is disabled for cache: " + cacheName + ". Use setIndexedTypes or setTypeMetadata methods on CacheConfiguration to enable."); - if (!ctx.kernalContext().query().moduleEnabled() && - (qry instanceof SqlQuery || qry instanceof SqlFieldsQuery || qry instanceof TextQuery)) + if (!ctx.kernalContext().query().moduleEnabled() && qry instanceof SqlFieldsQuery) { throw new CacheException("Failed to execute query. Add module 'ignite-indexing' to the classpath " + - "of all Ignite nodes or configure any query engine."); + "of all Ignite nodes or configure any query engine."); + } + + if ((qry instanceof SqlQuery || qry instanceof TextQuery) && !ctx.kernalContext().query().indexingEnabled()) { + throw new CacheException("Failed to execute query. " + qry.getClass().getSimpleName() + " is supported " + + "by the H2 query engine only, add module 'ignite-indexing' to the classpath of all Ignite nodes" + + (qry instanceof SqlQuery ? " or use SqlFieldsQuery instead." : ".")); + } } /** {@inheritDoc} */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcConnectionContext.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcConnectionContext.java index 1ecfdd02f0076..a1478ae43c246 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcConnectionContext.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/jdbc/JdbcConnectionContext.java @@ -23,7 +23,6 @@ import java.util.concurrent.atomic.AtomicReference; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteLogger; -import org.apache.ignite.configuration.QueryEngineConfiguration; import org.apache.ignite.internal.GridKernalContext; import org.apache.ignite.internal.binary.BinaryReaderEx; import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion; @@ -36,7 +35,6 @@ import org.apache.ignite.internal.processors.odbc.ClientListenerResponseSender; import org.apache.ignite.internal.processors.platform.client.tx.ClientTxContext; import org.apache.ignite.internal.processors.query.IgniteSQLException; -import org.apache.ignite.internal.processors.query.QueryEngineConfigurationEx; import org.apache.ignite.internal.util.GridSpinBusyLock; import org.apache.ignite.internal.util.nio.GridNioSession; import org.apache.ignite.internal.util.typedef.F; @@ -217,23 +215,8 @@ public JdbcConnectionContext(GridKernalContext ctx, GridNioSession ses, GridSpin if (ver.compareTo(VER_2_13_0) >= 0) { qryEngine = reader.readString(); - if (qryEngine != null) { - QueryEngineConfiguration[] cfgs = ctx.config().getSqlConfiguration().getQueryEnginesConfiguration(); - - boolean found = false; - - if (cfgs != null) { - for (int i = 0; i < cfgs.length; i++) { - if (qryEngine.equalsIgnoreCase(((QueryEngineConfigurationEx)cfgs[i]).engineName())) { - found = true; - break; - } - } - } - - if (!found) - throw new IgniteCheckedException("Not found configuration for query engine: " + qryEngine); - } + if (qryEngine != null && !ctx.query().queryEngineConfigured(qryEngine)) + throw new IgniteCheckedException("Not found configuration for query engine: " + qryEngine); } TransactionConcurrency concurrency = null; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcConnectionContext.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcConnectionContext.java index 626ccf5d63b58..6b641f7fcdad5 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcConnectionContext.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/odbc/odbc/OdbcConnectionContext.java @@ -21,7 +21,6 @@ import java.util.Set; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteLogger; -import org.apache.ignite.configuration.QueryEngineConfiguration; import org.apache.ignite.internal.GridKernalContext; import org.apache.ignite.internal.binary.BinaryReaderEx; import org.apache.ignite.internal.processors.odbc.ClientListenerAbstractConnectionContext; @@ -31,7 +30,6 @@ import org.apache.ignite.internal.processors.odbc.ClientListenerResponse; import org.apache.ignite.internal.processors.odbc.ClientListenerResponseSender; import org.apache.ignite.internal.processors.platform.client.tx.ClientTxContext; -import org.apache.ignite.internal.processors.query.QueryEngineConfigurationEx; import org.apache.ignite.internal.util.GridSpinBusyLock; import org.apache.ignite.internal.util.nio.GridNioSession; @@ -166,23 +164,8 @@ public OdbcConnectionContext(GridKernalContext ctx, GridNioSession ses, GridSpin if (ver.compareTo(VER_2_13_0) >= 0) { qryEngine = reader.readString(); - if (qryEngine != null) { - QueryEngineConfiguration[] cfgs = ctx.config().getSqlConfiguration().getQueryEnginesConfiguration(); - - boolean found = false; - - if (cfgs != null) { - for (int i = 0; i < cfgs.length; i++) { - if (qryEngine.equalsIgnoreCase(((QueryEngineConfigurationEx)cfgs[i]).engineName())) { - found = true; - break; - } - } - } - - if (!found) - throw new IgniteCheckedException("Not found configuration for query engine: " + qryEngine); - } + if (qryEngine != null && !ctx.query().queryEngineConfigured(qryEngine)) + throw new IgniteCheckedException("Not found configuration for query engine: " + qryEngine); } authenticate(ses, user, passwd); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java index f88deb1c5c424..e926a9dbd41a4 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java @@ -198,6 +198,10 @@ public class GridQueryProcessor extends GridProcessorAdapter { Pattern.compile("QUERY_ENGINE[\\s]*\\([\\s]*'([a-z0-9]+)'[\\s]*\\)", CASE_INSENSITIVE); + /** Error message for the features that require the H2 query engine when it is not on the classpath. */ + private static final String INDEXING_DISABLED_MSG = "Failed to execute query because indexing is disabled " + + "(consider adding module " + INDEXING.module() + " to classpath or moving it from 'optional' to 'libs' folder)."; + /** */ private static final ThreadLocal requestTopVer = new ThreadLocal<>(); @@ -562,9 +566,12 @@ private void initQueryEngines() throws IgniteCheckedException { // If indexing is disabled, try to find any query engine in components. if (!indexingEnabled()) { for (GridComponent cmp : ctx.components()) { - if (cmp instanceof QueryEngine) { - qryEngines = new QueryEngine[] {(QueryEngine)cmp}; - dfltQryEngine = (QueryEngine)cmp; + if (cmp instanceof QueryEngine && ((QueryEngine)cmp).config() != null) { + QueryEngine qryEngine = (QueryEngine)cmp; + + qryEngines = new QueryEngine[] {qryEngine}; + this.qryEnginesCfg = new QueryEngineConfigurationEx[] {qryEngine.config()}; + dfltQryEngine = qryEngine; } } } @@ -1019,6 +1026,23 @@ public boolean moduleEnabled() { return indexingEnabled() || dfltQryEngine != null; } + /** + * @param engineName Query engine name. + * @return {@code true} If a query engine with the given name can be selected by the {@code QUERY_ENGINE} hint or + * by the client connection property. + */ + public boolean queryEngineConfigured(String engineName) { + if (qryEnginesCfg == null) + return false; + + for (QueryEngineConfigurationEx cfg : qryEnginesCfg) { + if (engineName.equalsIgnoreCase(cfg.engineName())) + return true; + } + + return false; + } + /** * @return Indexing. * @throws IgniteException If module is not enabled. @@ -2899,7 +2923,7 @@ private QueryTypeDescriptorImpl type(String cacheName, CacheObject val) throws I */ private void checkIndexingEnabled() throws IgniteCheckedException { if (idx == null) - throw new IgniteCheckedException("Indexing is disabled."); + throw new IgniteCheckedException(INDEXING_DISABLED_MSG); } /** @@ -2907,8 +2931,7 @@ private void checkIndexingEnabled() throws IgniteCheckedException { */ private void checkxIndexingEnabled() throws IgniteException { if (idx == null) - throw new IgniteException("Failed to execute query because indexing is disabled (consider adding module " + - INDEXING.module() + " to classpath or moving it from 'optional' to 'libs' folder)."); + throw new IgniteException(INDEXING_DISABLED_MSG); } /** @@ -3301,6 +3324,8 @@ public long streamUpdateQuery(@Nullable final String cacheName, final String sch String qryInitiatorId) { assert streamer != null; + checkxIndexingEnabled(); + if (!busyLock.enterBusy()) throw new IllegalStateException("Failed to execute query (grid is stopping)."); @@ -3363,6 +3388,8 @@ public QueryCursor> querySql( final SqlQuery qry, boolean keepBinary ) { + checkxIndexingEnabled(); + // Generate. String type = qry.getType(); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEngine.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEngine.java index faf2f78ad6bc2..b21b49fce3285 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEngine.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEngine.java @@ -81,4 +81,9 @@ List>> queryBatched( String qry, List batchedParams ) throws IgniteSQLException; + + /** @return Configuration of the engine. */ + default QueryEngineConfigurationEx config() { + return null; + } } From 5423127a1a680811ce9cccb417c21afd840ac246 Mon Sep 17 00:00:00 2001 From: Nikolay Izhikov Date: Thu, 8 Oct 2026 16:55:18 +0300 Subject: [PATCH 2/3] Fixes --- .../CalciteOnlyNodeIntegrationTest.java | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteOnlyNodeIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteOnlyNodeIntegrationTest.java index 3ef9b10a891ea..c04349e5433ac 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteOnlyNodeIntegrationTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/CalciteOnlyNodeIntegrationTest.java @@ -32,7 +32,6 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.cache.Cache; import org.apache.ignite.Ignite; import org.apache.ignite.IgniteCache; import org.apache.ignite.Ignition; @@ -55,7 +54,6 @@ import org.apache.ignite.configuration.ThinClientConfiguration; import org.apache.ignite.internal.IgniteComponentType; import org.apache.ignite.internal.IgniteFutureTimeoutCheckedException; -import org.apache.ignite.internal.IgniteInternalFuture; import org.apache.ignite.internal.processors.query.calcite.GridCommonAbstractWrapperTest; import org.apache.ignite.resources.IgniteInstanceResource; import org.apache.ignite.testframework.CallbackExecutorLogListener; @@ -66,6 +64,9 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.apache.ignite.testframework.GridTestUtils.assertThrows; +import static org.apache.ignite.testframework.GridTestUtils.assertThrowsAnyCause; + /** * Checks behaviour of a node that has the Calcite engine on the classpath but not the H2 engine * ({@code ignite-indexing}). @@ -165,7 +166,8 @@ private void startCalciteOnlyNode() throws Exception { public void testSqlFieldsQuery() throws Exception { assertEquals(CalciteQueryEngineConfiguration.ENGINE_NAME, nodeOp(OP_QUERY_ENGINE)); - try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) { + try (Connection conn = DriverManager.getConnection(JDBC_URL); + Statement stmt = conn.createStatement()) { stmt.executeUpdate("CREATE TABLE t(id INT PRIMARY KEY, val VARCHAR) WITH \"template=replicated\""); stmt.executeUpdate("INSERT INTO t VALUES (1, 'a')"); @@ -178,15 +180,12 @@ public void testSqlFieldsQuery() throws Exception { } } - /** - * Deprecated {@link SqlQuery} is implemented by the H2 engine only and is not supported by the Calcite engine: - * the error must name the missing module. - */ + /** */ @Test - public void testSqlQuery() throws Exception { - String res = nodeOp(OP_SQL_QUERY); + public void testSqlQueryNotSupported() throws Exception { + String msg = nodeOp(OP_SQL_QUERY); - assertTrue(res, res.startsWith(ERR_PREFIX) && res.contains(H2_ONLY_FEATURE_MSG)); + assertTrue(msg, msg.startsWith(ERR_PREFIX) && msg.contains(H2_ONLY_FEATURE_MSG)); } /** Deprecated {@link SqlQuery} sent by the Java thin client fails on the server node with the same error. */ @@ -194,11 +193,12 @@ public void testSqlQuery() throws Exception { @SuppressWarnings("ThrowableNotThrown") public void testThinClientSqlQuery() { try (IgniteClient cli = client()) { - ClientCache cache = cli.cache(DEFAULT_CACHE_NAME); - - GridTestUtils.assertThrows(log, - () -> cache.query(new SqlQuery(String.class, "_val = ?").setArgs("v2")).getAll(), - ClientException.class, H2_ONLY_FEATURE_MSG); + assertThrows( + log, + () -> cli.cache(DEFAULT_CACHE_NAME).query(new SqlQuery(String.class, "_val = ?").setArgs("v2")).getAll(), + ClientException.class, + H2_ONLY_FEATURE_MSG + ); } } @@ -219,14 +219,16 @@ public void testSetStreaming() throws Exception { Connection conn = DriverManager.getConnection(JDBC_URL); try (Statement stmt = conn.createStatement()) { - GridTestUtils.assertThrowsAnyCause(log, () -> stmt.executeUpdate("SET STREAMING ON"), - SQLException.class, PARSE_ERR_MSG); + assertThrowsAnyCause( + log, + () -> stmt.executeUpdate("SET STREAMING ON"), + SQLException.class, + PARSE_ERR_MSG + ); } finally { - IgniteInternalFuture closeFut = GridTestUtils.runAsync(conn::close); - try { - closeFut.get(10_000); + GridTestUtils.runAsync(conn::close).get(10_000); } catch (IgniteFutureTimeoutCheckedException e) { fail("Connection close hangs after a rejected SET STREAMING ON"); @@ -238,17 +240,19 @@ public void testSetStreaming() throws Exception { @Test public void testCopy() throws Exception { try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) { - GridTestUtils.assertThrowsAnyCause(log, + assertThrowsAnyCause( + log, () -> stmt.executeUpdate("COPY FROM '/nonexistent.csv' INTO \"test\".String(_key, _val) FORMAT CSV"), - SQLException.class, PARSE_ERR_MSG); + SQLException.class, + PARSE_ERR_MSG + ); } } /** The only available engine can be selected by name via the JDBC property without an explicit configuration. */ @Test public void testJdbcQueryEngineProperty() throws Exception { - try (Connection conn = DriverManager.getConnection( - JDBC_URL + "?queryEngine=" + CalciteQueryEngineConfiguration.ENGINE_NAME); + try (Connection conn = DriverManager.getConnection(JDBC_URL + "?queryEngine=" + CalciteQueryEngineConfiguration.ENGINE_NAME); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT QUERY_ENGINE()")) { assertTrue(rs.next()); @@ -259,10 +263,11 @@ public void testJdbcQueryEngineProperty() throws Exception { /** The only available engine can be selected by the query hint without an explicit configuration. */ @Test public void testQueryEngineHint() throws Exception { + String qry = "SELECT /*+ QUERY_ENGINE('" + CalciteQueryEngineConfiguration.ENGINE_NAME + "') */ QUERY_ENGINE()"; + try (Connection conn = DriverManager.getConnection(JDBC_URL); - Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery( - "SELECT /*+ QUERY_ENGINE('" + CalciteQueryEngineConfiguration.ENGINE_NAME + "') */ QUERY_ENGINE()")) { + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(qry)) { assertTrue(rs.next()); assertEquals(CalciteQueryEngineConfiguration.ENGINE_NAME, rs.getString(1)); } @@ -327,22 +332,22 @@ private NodeJob(String op) { try { IgniteCache cache = ignite.cache(DEFAULT_CACHE_NAME); - switch (op) { - case OP_H2_IN_CLASSPATH: - return String.valueOf(IgniteComponentType.INDEXING.inClassPath()); + return switch (op) { + case OP_H2_IN_CLASSPATH -> + String.valueOf(IgniteComponentType.INDEXING.inClassPath()); - case OP_QUERY_ENGINE: - return String.valueOf(cache.query(new SqlFieldsQuery("SELECT QUERY_ENGINE()")).getAll().get(0).get(0)); + case OP_QUERY_ENGINE -> + String.valueOf(cache.query(new SqlFieldsQuery("SELECT QUERY_ENGINE()")).getAll().get(0).get(0)); - case OP_SQL_QUERY: - return keys(cache.query(new SqlQuery(String.class, "_val = ?").setArgs("v1")).getAll()); + case OP_SQL_QUERY -> + cache.query(new SqlQuery(String.class, "_val = ?").setArgs("v1")).getAll(); - case OP_TEXT_QUERY: - return keys(cache.query(new TextQuery(String.class, "v1")).getAll()); + case OP_TEXT_QUERY -> + cache.query(new TextQuery(String.class, "v1")).getAll(); - default: + default -> throw new IllegalArgumentException("Unknown operation: " + op); - } + }; } catch (Throwable e) { StringBuilder sb = new StringBuilder(ERR_PREFIX); @@ -353,11 +358,5 @@ private NodeJob(String op) { return sb.toString(); } } - - /** @return Sorted, comma separated keys of the entries. */ - private static String keys(List> entries) { - return entries.stream().map(Cache.Entry::getKey).sorted().map(String::valueOf) - .collect(Collectors.joining(",")); - } } } From 2a75b01c28f82ccc6a868fa972ca48ccca12ba81 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Sat, 10 Oct 2026 14:25:18 +0300 Subject: [PATCH 3/3] Update modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java Co-authored-by: Vladimir Steshin --- .../ignite/internal/processors/query/GridQueryProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java index e926a9dbd41a4..c1277a37fa1a0 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryProcessor.java @@ -1028,7 +1028,7 @@ public boolean moduleEnabled() { /** * @param engineName Query engine name. - * @return {@code true} If a query engine with the given name can be selected by the {@code QUERY_ENGINE} hint or + * @return {@code True} if a query engine with the given name can be selected by the {@code QUERY_ENGINE} hint or * by the client connection property. */ public boolean queryEngineConfigured(String engineName) {