From c03e0e207055da5c4755d86db7483f3a44e2417b Mon Sep 17 00:00:00 2001 From: Kirill Tkalenko Date: Sat, 14 Mar 2026 13:03:19 +0300 Subject: [PATCH 1/3] IGNITE-28223 wip --- .../calcite/prepare/IgniteSqlValidator.java | 4 +- .../schema/CacheTableDescriptorImpl.java | 13 +- .../calcite/schema/SchemaHolderImpl.java | 8 +- .../calcite/schema/VirtualColumnProvider.java | 39 +++++ .../AbstractTestCacheColumnDescriptor.java | 136 +++++++++++++++++ .../schema/VirtualColumnProviderTest.java | 144 ++++++++++++++++++ .../plugin/IgnitePluginProcessor.java | 28 +++- 7 files changed, 363 insertions(+), 9 deletions(-) create mode 100644 modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/VirtualColumnProvider.java create mode 100644 modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/schema/AbstractTestCacheColumnDescriptor.java create mode 100644 modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/schema/VirtualColumnProviderTest.java diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java index c7101b05db6e9..383e79531b06d 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteSqlValidator.java @@ -542,7 +542,9 @@ private IgniteTypeFactory typeFactory() { /** */ private boolean isSystemFieldName(String alias) { return QueryUtils.KEY_FIELD_NAME.equalsIgnoreCase(alias) - || QueryUtils.VAL_FIELD_NAME.equalsIgnoreCase(alias); + || QueryUtils.VAL_FIELD_NAME.equalsIgnoreCase(alias) + // TODO: IGNITE-28223 Похоже что тут надо будет поменять + || "KEY_TO_STRING".equalsIgnoreCase(alias); } /** {@inheritDoc} */ diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java index 2cd164bc81570..07c8326b4840b 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java @@ -115,8 +115,12 @@ public class CacheTableDescriptorImpl extends NullInitializerExpressionFactory private RelDataType tableRowType; /** */ - public CacheTableDescriptorImpl(GridCacheContextInfo cacheInfo, GridQueryTypeDescriptor typeDesc, - Object affinityIdentity) { + public CacheTableDescriptorImpl( + GridCacheContextInfo cacheInfo, + GridQueryTypeDescriptor typeDesc, + Object affinityIdentity, + VirtualColumnProvider virtColProv + ) { this.cacheInfo = cacheInfo; this.typeDesc = typeDesc; this.affinityIdentity = affinityIdentity; @@ -150,6 +154,11 @@ public CacheTableDescriptorImpl(GridCacheContextInfo cacheInfo, GridQueryT int fldIdx = QueryUtils.VAL_COL + 1; + List virtCols = virtColProv.provideVirtualColumns(fldIdx); + descriptors.addAll(virtCols); + fldIdx += virtCols.size(); + virtCols.forEach(c -> virtualFields.set(c.fieldIndex())); + int keyField = QueryUtils.KEY_COL; int valField = QueryUtils.VAL_COL; diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java index b9eae82520f24..d2944d23e1a62 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java @@ -254,8 +254,12 @@ private IgniteCacheTable createTable( GridQueryTypeDescriptor typeDesc, GridCacheContextInfo cacheInfo ) { - CacheTableDescriptorImpl desc = - new CacheTableDescriptorImpl(cacheInfo, typeDesc, affinityIdentity(cacheInfo.config())); + CacheTableDescriptorImpl desc = new CacheTableDescriptorImpl( + cacheInfo, + typeDesc, + affinityIdentity(cacheInfo.config()), + ctx.plugins().createComponentOrDefault(VirtualColumnProvider.class, VirtualColumnProvider.EMPTY) + ); return new CacheTableImpl(ctx, desc); } diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/VirtualColumnProvider.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/VirtualColumnProvider.java new file mode 100644 index 0000000000000..aa947eb857438 --- /dev/null +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/VirtualColumnProvider.java @@ -0,0 +1,39 @@ +/* + * 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.schema; + +import java.util.List; +import org.apache.ignite.calcite.CalciteQueryEngineConfiguration; +import org.apache.ignite.internal.processors.plugin.IgnitePluginProcessor; +import org.apache.ignite.plugin.PluginProvider; + +/** + * Virtual table column provider from {@link PluginProvider plugin} created via {@link IgnitePluginProcessor#createComponent(Class)} for + * {@link CalciteQueryEngineConfiguration calcite engine}. + */ +@FunctionalInterface +public interface VirtualColumnProvider { + VirtualColumnProvider EMPTY = nextColumnIndex -> List.of(); + + /** + * Returns a list of virtual columns to add to the table. + * + * @param nxtColIdx Next column index. + */ + List provideVirtualColumns(int nxtColIdx); +} diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/schema/AbstractTestCacheColumnDescriptor.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/schema/AbstractTestCacheColumnDescriptor.java new file mode 100644 index 0000000000000..b2aa731083c26 --- /dev/null +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/schema/AbstractTestCacheColumnDescriptor.java @@ -0,0 +1,136 @@ +/* + * 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.schema; + +import org.apache.calcite.rel.type.RelDataType; +import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory; +import org.apache.ignite.internal.processors.query.calcite.util.TypeUtils; + +import static org.apache.calcite.rel.type.RelDataType.PRECISION_NOT_SPECIFIED; +import static org.apache.calcite.rel.type.RelDataType.SCALE_NOT_SPECIFIED; + +/** Abstract class for tests which allows to avoid redundant boilerplate code. */ +public abstract class AbstractTestCacheColumnDescriptor implements CacheColumnDescriptor { + /** */ + private final int idx; + + /** */ + private final String name; + + /** */ + private final Type type; + + /** */ + private final boolean isKey; + + /** */ + private final boolean isField; + + /** */ + private volatile RelDataType logicalType; + + /** */ + protected AbstractTestCacheColumnDescriptor(int idx, String name, Type type, boolean isKey, boolean isField) { + this.idx = idx; + this.name = name; + this.type = type; + this.isKey = isKey; + this.isField = isField; + } + + /** {@inheritDoc} */ + @Override public boolean field() { + return isField; + } + + /** {@inheritDoc} */ + @Override public boolean key() { + return isKey; + } + + /** {@inheritDoc} */ + @Override public String name() { + return name; + } + + /** {@inheritDoc} */ + @Override public int fieldIndex() { + return idx; + } + + /** {@inheritDoc} */ + @Override public RelDataType logicalType(IgniteTypeFactory f) { + if (logicalType == null) { + logicalType = TypeUtils.sqlType( + f, + type.cls, + type.precision != Type.NOT_SPECIFIED ? type.precision : PRECISION_NOT_SPECIFIED, + type.scale != Type.NOT_SPECIFIED ? type.scale : SCALE_NOT_SPECIFIED, + type.nullable + ); + } + + return logicalType; + } + + /** {@inheritDoc} */ + @Override public Class storageType() { + return type.cls; + } + + /** */ + public static class Type { + /** */ + public static final int NOT_SPECIFIED = -1; + + /** */ + private final Class cls; + + /** */ + private final int precision; + + /** */ + private final int scale; + + /** */ + private final boolean nullable; + + /** */ + private Type(Class cls, int precision, int scale, boolean nullable) { + this.cls = cls; + this.precision = precision; + this.scale = scale; + this.nullable = nullable; + } + + /** */ + public static Type nullable(Class cls) { + return new Type(cls, NOT_SPECIFIED, NOT_SPECIFIED, true); + } + + /** */ + public static Type notNull(Class cls) { + return new Type(cls, NOT_SPECIFIED, NOT_SPECIFIED, false); + } + + /** */ + public static Type of(Class cls, int precision, int scale, boolean nullable) { + return new Type(cls, precision, scale, nullable); + } + } +} diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/schema/VirtualColumnProviderTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/schema/VirtualColumnProviderTest.java new file mode 100644 index 0000000000000..dca0fcee91520 --- /dev/null +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/schema/VirtualColumnProviderTest.java @@ -0,0 +1,144 @@ +/* + * 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.schema; + +import java.util.List; +import org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.calcite.CalciteQueryEngineConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.configuration.SqlConfiguration; +import org.apache.ignite.indexing.IndexingQueryEngineConfiguration; +import org.apache.ignite.internal.processors.cache.GridCacheContext; +import org.apache.ignite.internal.processors.cache.persistence.CacheDataRow; +import org.apache.ignite.internal.processors.query.calcite.exec.ExecutionContext; +import org.apache.ignite.internal.processors.query.calcite.integration.AbstractBasicIntegrationTest; +import org.apache.ignite.plugin.AbstractTestPluginProvider; +import org.apache.ignite.plugin.PluginContext; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; + +/** For {@link VirtualColumnProvider} testing. */ +public class VirtualColumnProviderTest extends AbstractBasicIntegrationTest { + /** */ + private static final String KEY_TO_STRING_COLUMN_NAME = "KEY_TO_STRING"; + + /** {@inheritDoc} */ + @Override protected int nodeCount() { + return 1; + } + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + SqlConfiguration sqlCfg = new SqlConfiguration().setQueryEnginesConfiguration( + new CalciteQueryEngineConfiguration().setDefault(true), + new IndexingQueryEngineConfiguration() + ); + + return super.getConfiguration(igniteInstanceName) + .setSqlConfiguration(sqlCfg) + .setPluginProviders(new TestVirtualColumnPluginProvider()); + } + + /** */ + @Test + public void test() { + sql("create table PUBLIC.PERSON(id int primary key, name varchar)"); + + for (int i = 0; i < 2; i++) + sql("insert into PUBLIC.PERSON(id, name) values (?, ?)", i, "foo" + i); + + // Let's make sure that when using '*' there will be no virtual column. + assertQuery("select * from PUBLIC.PERSON order by id") + .columnNames("ID", "NAME") + .returns(0, "foo0") + .returns(1, "foo1") + .check(); + + // Let's make sure that when we specify a virtual column, we get it. + assertQuery(String.format("select id, name, %s from PUBLIC.PERSON order by id", KEY_TO_STRING_COLUMN_NAME)) + .columnNames("ID", "NAME", KEY_TO_STRING_COLUMN_NAME) + .returns(0, "foo0", "0") + .returns(1, "foo1", "1") + .check(); + + // Let's check the use of a virtual column in where. + assertQuery(String.format( + "select id, name, %1$s from PUBLIC.PERSON where %1$s = %2$s order by id", + KEY_TO_STRING_COLUMN_NAME, "1" + )) + .columnNames("ID", "NAME", KEY_TO_STRING_COLUMN_NAME) + .returns(1, "foo1", "1") + .check(); + + // Let's check the use of a virtual column in order by. + assertQuery(String.format("select id, name, %1$s from PUBLIC.PERSON order by %1$s", KEY_TO_STRING_COLUMN_NAME)) + .columnNames("ID", "NAME", KEY_TO_STRING_COLUMN_NAME) + .returns(0, "foo0", "0") + .returns(1, "foo1", "1") + .check(); + } + + /** */ + private static class TestVirtualColumnPluginProvider extends AbstractTestPluginProvider { + /** {@inheritDoc} */ + @Override public String name() { + return getClass().getSimpleName(); + } + + /** {@inheritDoc} */ + @Override public @Nullable T createComponent(PluginContext ctx, Class cls) { + if (VirtualColumnProvider.class.equals(cls)) { + return (T) (VirtualColumnProvider) nxtColIdx -> List.of(new KeyToStingVirtualColumn(nxtColIdx)); + } + + return super.createComponent(ctx, cls); + } + } + + /** */ + private static class KeyToStingVirtualColumn extends AbstractTestCacheColumnDescriptor { + /** */ + private KeyToStingVirtualColumn(int idx) { + super(idx, KEY_TO_STRING_COLUMN_NAME, Type.nullable(String.class), false, false); + } + + /** {@inheritDoc} */ + @Override public Object value( + ExecutionContext ectx, + GridCacheContext cctx, + CacheDataRow src + ) throws IgniteCheckedException { + return cctx.unwrapBinaryIfNeeded(src.key(), false, null).toString(); + } + + /** {@inheritDoc} */ + @Override public void set(Object dst, Object val) { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override public boolean hasDefaultValue() { + return false; + } + + /** {@inheritDoc} */ + @Override public Object defaultValue() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/plugin/IgnitePluginProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/plugin/IgnitePluginProcessor.java index 6a8fbf2962ebb..79fbc03bb1cd8 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/plugin/IgnitePluginProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/plugin/IgnitePluginProcessor.java @@ -128,21 +128,41 @@ public T pluginContextForProvider(PluginProvider provi } /** + * Creates an instance of a component from the {@link PluginProvider plugin} (first of + * {@link IgniteConfiguration#getPluginProviders()} that did not return {@code null}). + * * @param cls Component class. * @param Component type. * @return Component class instance or {@code null} if no one plugin override this component. */ - public T createComponent(Class cls) { - for (PluginProvider plugin : plugins.values()) { + public @Nullable T createComponent(Class cls) { + return createComponentOrDefault0(cls, null); + } + + /** + * Creates an instance of a component from the {@link PluginProvider plugin} (first of + * {@link IgniteConfiguration#getPluginProviders()} that did not return {@code null}). + * + * @param cls Component class. + * @param dflt Default component instance. + * @param Component type. + * @return Component class instance or {@code dflt} if no one plugin override this component. + */ + public T createComponentOrDefault(Class cls, T dflt) { + return createComponentOrDefault0(cls, dflt); + } + + private @Nullable T createComponentOrDefault0(Class cls, @Nullable T dflt) { + for (PluginProvider plugin : plugins.values()) { PluginContext ctx = pluginContextForProvider(plugin); - T comp = (T)plugin.createComponent(ctx, cls); + T comp = plugin.createComponent(ctx, cls); if (comp != null) return comp; } - return null; + return dflt; } /** {@inheritDoc} */ From 85bef41876389322ed7a488c492860da281ba206 Mon Sep 17 00:00:00 2001 From: Kirill Tkalenko Date: Sat, 14 Mar 2026 13:20:04 +0300 Subject: [PATCH 2/3] IGNITE-28223 Fix checkstyle --- .../ignite/internal/processors/plugin/IgnitePluginProcessor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/plugin/IgnitePluginProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/plugin/IgnitePluginProcessor.java index 79fbc03bb1cd8..31234001cf958 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/plugin/IgnitePluginProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/plugin/IgnitePluginProcessor.java @@ -152,6 +152,7 @@ public T createComponentOrDefault(Class cls, T dflt) { return createComponentOrDefault0(cls, dflt); } + /** */ private @Nullable T createComponentOrDefault0(Class cls, @Nullable T dflt) { for (PluginProvider plugin : plugins.values()) { PluginContext ctx = pluginContextForProvider(plugin); From 1d1def03403ed4c57b62249c4fc173be5dfc11f1 Mon Sep 17 00:00:00 2001 From: Kirill Tkalenko Date: Sat, 14 Mar 2026 13:27:57 +0300 Subject: [PATCH 3/3] IGNITE-28223 wip --- .../query/calcite/schema/CacheTableDescriptorImpl.java | 2 ++ .../org/apache/ignite/testsuites/IntegrationTestSuite.java | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java index 07c8326b4840b..0519f66a6346b 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/CacheTableDescriptorImpl.java @@ -154,6 +154,8 @@ public CacheTableDescriptorImpl( int fldIdx = QueryUtils.VAL_COL + 1; + // TODO: IGNITE-28223 Проверить, что не будет дублирования имен виртуальных колонок + // TODO: IGNITE-28223 Подумать, что будет если пользователь захочет создать колонку с именем виртуальной колонки List virtCols = virtColProv.provideVirtualColumns(fldIdx); descriptors.addAll(virtCols); fldIdx += virtCols.size(); 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 9326c647854b8..d3dd5b20ec571 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 @@ -90,6 +90,7 @@ import org.apache.ignite.internal.processors.query.calcite.rules.JoinOrderOptimizationTest; import org.apache.ignite.internal.processors.query.calcite.rules.OrToUnionRuleTest; import org.apache.ignite.internal.processors.query.calcite.rules.ProjectScanMergeRuleTest; +import org.apache.ignite.internal.processors.query.calcite.schema.VirtualColumnProviderTest; import org.apache.ignite.internal.processors.query.calcite.thin.MultiLineQueryTest; import org.apache.ignite.internal.processors.tx.TxWithExceptionalInterceptorTest; import org.junit.runner.RunWith; @@ -174,7 +175,8 @@ QueryEntityValueColumnAliasTest.class, CacheStoreTest.class, MultiDcQueryMappingTest.class, - TxWithExceptionalInterceptorTest.class + TxWithExceptionalInterceptorTest.class, + VirtualColumnProviderTest.class }) public class IntegrationTestSuite { }