This is an automated email from the ASF dual-hosted git repository.
jackietien pushed a commit to branch UserDefinedTime
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/UserDefinedTime by this push:
new 1e433efcba4 fix the optimization for LAST/FIRST/LAST_BY/FIRST_BY on
non-time columns, modify the AccumulatorFactory to explicitly verify if the
sort key in aggregations(last, first, last_by, first_by) is actually the time
column (#17074)
1e433efcba4 is described below
commit 1e433efcba42cee73c65b44f30ca1618f3f7d1d1
Author: alpass163 <[email protected]>
AuthorDate: Tue Jan 27 18:01:54 2026 +0800
fix the optimization for LAST/FIRST/LAST_BY/FIRST_BY on non-time columns,
modify the AccumulatorFactory to explicitly verify if the sort key in
aggregations(last, first, last_by, first_by) is actually the time column
(#17074)
---
.../it/schema/IoTDBUserDefinedTimeIT.java | 233 +++++++++++++++++++++
.../relational/aggregation/AccumulatorFactory.java | 58 +++--
.../distribution/AggregationTableScanTest.java | 101 +++++++++
.../schema/table/TsFileTableSchemaUtil.java | 13 +-
4 files changed, 373 insertions(+), 32 deletions(-)
diff --git
a/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBUserDefinedTimeIT.java
b/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBUserDefinedTimeIT.java
new file mode 100644
index 00000000000..60cb94d5bba
--- /dev/null
+++
b/integration-test/src/test/java/org/apache/iotdb/relational/it/schema/IoTDBUserDefinedTimeIT.java
@@ -0,0 +1,233 @@
+/*
+ * 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.iotdb.relational.it.schema;
+
+import org.apache.iotdb.db.it.utils.TestUtils;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.TableClusterIT;
+import org.apache.iotdb.itbase.category.TableLocalStandaloneIT;
+import org.apache.iotdb.itbase.env.BaseEnv;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Arrays;
+
+import static org.apache.iotdb.db.it.utils.TestUtils.prepareTableData;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({TableLocalStandaloneIT.class, TableClusterIT.class})
+public class IoTDBUserDefinedTimeIT {
+
+ private static final String TABLE_DATABASE = "user_defined_time";
+ private static final String VIEW_DATABASE = "user_defined_time_for_view";
+ private static final String SELECT_DATABASE = "select_agg_function";
+ private static final String[] SQLS =
+ new String[] {
+ "CREATE DATABASE " + TABLE_DATABASE,
+ "CREATE DATABASE " + VIEW_DATABASE,
+ "CREATE DATABASE " + SELECT_DATABASE
+ };
+ private final String header = "ColumnName,DataType,Category,";
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ EnvFactory.getEnv().initClusterEnvironment();
+ prepareTableData(SQLS);
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ EnvFactory.getEnv().cleanClusterEnvironment();
+ }
+
+ @Test
+ public void testCreateTable() {
+ try (final Connection connection =
+ EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+ final Statement statement = connection.createStatement()) {
+ statement.execute("use " + TABLE_DATABASE);
+
+ // create table and do not assign the time column name
+ try {
+ statement.execute(
+ "create table default_not_assign_time(device string tag, s1 int32
field)");
+ TestUtils.assertResultSetEqual(
+ statement.executeQuery("describe default_not_assign_time"),
+ header,
+ Arrays.asList("time,TIMESTAMP,TIME,", "device,STRING,TAG,",
"s1,INT32,FIELD,"));
+ } catch (SQLException e) {
+ fail("create table without time info fails, the specific message: " +
e.getMessage());
+ }
+
+ // create table and assign the time column name
+ try {
+ statement.execute(
+ "create table time_in_first(date_time timestamp time, device
string tag, s1 int32 field)");
+ TestUtils.assertResultSetEqual(
+ statement.executeQuery("describe time_in_first"),
+ header,
+ Arrays.asList("date_time,TIMESTAMP,TIME,", "device,STRING,TAG,",
"s1,INT32,FIELD,"));
+ } catch (SQLException e) {
+ fail("assign the name of time column fails, the specific message: " +
e.getMessage());
+ }
+
+ // create table which of the time column not at the first column
+ try {
+ statement.execute(
+ "create table time_not_in_first(device string tag, date_time
timestamp time, s1 int32 field)");
+ TestUtils.assertResultSetEqual(
+ statement.executeQuery("describe time_not_in_first"),
+ header,
+ Arrays.asList("device,STRING,TAG,", "date_time,TIMESTAMP,TIME,",
"s1,INT32,FIELD,"));
+ } catch (SQLException e) {
+ fail("assign the name of time column fails, the specific message: " +
e.getMessage());
+ }
+
+ // create table with multi time-column
+ try {
+ statement.execute(
+ "create table with_multi_time(time_type timestamp time, device
string tag, date_time timestamp time, s1 int32 field)");
+ fail("Creating table is not be allowed to assign two time columns");
+ } catch (SQLException e) {
+ assertEquals("701: A table cannot have more than one time column",
e.getMessage());
+ }
+
+ // create table with time column that is not timestamp data type
+ try {
+ statement.execute(
+ "create table time_other_type(device string tag, date_time int64
time, s1 int32 field)");
+ fail("The time column has to be assigned a timestamp data type when
creating table");
+ } catch (SQLException e) {
+ assertEquals("701: The time column's type shall be 'timestamp'.",
e.getMessage());
+ }
+
+ // Columns in table shall not share the same name time when creating
table
+ try {
+ statement.execute(
+ "create table shared_time_name(device string tag, time int64
field, s1 int32 field)");
+ fail("Columns in table shall not share the same name time when
creating table");
+ } catch (SQLException e) {
+ assertEquals("701: Columns in table shall not share the same name
time.", e.getMessage());
+ }
+
+ } catch (SQLException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+
+ private void prepareTreeData() {
+ try (final Connection connection =
EnvFactory.getEnv().getConnection(BaseEnv.TREE_SQL_DIALECT);
+ final Statement statement = connection.createStatement()) {
+ statement.execute("create timeseries root.tt.device.s1 with
datatype=int32");
+ } catch (SQLException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCreateView() {
+ prepareTreeData();
+
+ try (final Connection connection =
+ EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+ final Statement statement = connection.createStatement()) {
+ statement.execute("use " + VIEW_DATABASE);
+
+ // create view and do not assign the time column name
+ try {
+ statement.execute(
+ "create view default_not_assign_time(device string tag, s1 int32
field) as root.tt.**");
+ TestUtils.assertResultSetEqual(
+ statement.executeQuery("describe default_not_assign_time"),
+ header,
+ Arrays.asList("time,TIMESTAMP,TIME,", "device,STRING,TAG,",
"s1,INT32,FIELD,"));
+ } catch (SQLException e) {
+ fail("create table without time info fails, the specific message: " +
e.getMessage());
+ }
+
+ // create view which of the time column at the first column
+ try {
+ statement.execute(
+ "create view time_in_first(date_time timestamp time, device string
tag, s1 int32 field) as root.tt.**");
+ TestUtils.assertResultSetEqual(
+ statement.executeQuery("describe time_in_first"),
+ header,
+ Arrays.asList("date_time,TIMESTAMP,TIME,", "device,STRING,TAG,",
"s1,INT32,FIELD,"));
+ } catch (SQLException e) {
+ fail("assign the name of time column fails, the specific message: " +
e.getMessage());
+ }
+
+ // create view which of the time column not at the first column
+ try {
+ statement.execute(
+ "create view time_not_in_first(device string tag, date_time
timestamp time, s1 int32 field) as root.tt.**");
+ TestUtils.assertResultSetEqual(
+ statement.executeQuery("describe time_not_in_first"),
+ header,
+ Arrays.asList("device,STRING,TAG,", "date_time,TIMESTAMP,TIME,",
"s1,INT32,FIELD,"));
+ } catch (SQLException e) {
+ fail("assign the name of time column fails, the specific message: " +
e.getMessage());
+ }
+
+ // create view with multi time-column
+ try {
+ statement.execute(
+ "create view with_multi_time(time_type timestamp time, device
string tag, date_time timestamp time, s1 int32 field) as root.tt.**");
+ fail("Creating view is not be allowed to assign two time columns");
+ } catch (SQLException e) {
+ assertEquals("701: A table cannot have more than one time column",
e.getMessage());
+ }
+
+ // create table with time column that is not timestamp data type
+ try {
+ statement.execute(
+ "create view time_other_type(device string tag, date_time int64
time, s1 int32 field) as root.tt.**");
+ fail("The time column has to be assigned a timestamp data type when
creating view");
+ } catch (SQLException e) {
+ assertEquals("701: The time column's type shall be 'timestamp'.",
e.getMessage());
+ }
+
+ // Columns in table shall not share the same name time when creating
table
+ try {
+ statement.execute(
+ "create view shared_time_time(device string tag, time int64 field,
s1 int32 field) as root.tt.**");
+ fail("Columns in view shall not share the same name time when creating
table");
+ } catch (SQLException e) {
+ assertEquals("701: Columns in table shall not share the same name
time.", e.getMessage());
+ }
+
+ } catch (SQLException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/aggregation/AccumulatorFactory.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/aggregation/AccumulatorFactory.java
index 45617de31d1..90dcb82c2f0 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/aggregation/AccumulatorFactory.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/relational/aggregation/AccumulatorFactory.java
@@ -78,6 +78,7 @@ import java.util.stream.Stream;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
+import static
org.apache.iotdb.commons.udf.builtin.relational.TableBuiltinAggregationFunction.FIRST;
import static
org.apache.iotdb.commons.udf.builtin.relational.TableBuiltinAggregationFunction.FIRST_BY;
import static
org.apache.iotdb.commons.udf.builtin.relational.TableBuiltinAggregationFunction.LAST;
import static
org.apache.iotdb.commons.udf.builtin.relational.TableBuiltinAggregationFunction.LAST_BY;
@@ -108,11 +109,14 @@ public class AccumulatorFactory {
&& inputExpressions.size() > 1) {
boolean xIsTimeColumn = isTimeColumn(inputExpressions.get(0),
timeColumnName);
boolean yIsTimeColumn = isTimeColumn(inputExpressions.get(1),
timeColumnName);
- // When used in AggTableScanOperator, we can finish calculation of
+ boolean orderKeyIsTimeColumn = isTimeColumn(inputExpressions.get(2),
timeColumnName);
+
+ // When used in AggTableScanOperator and the order column is time
column, we can finish
+ // calculation of
// LastDesc/LastByDesc/First/First_by after the result has been
initialized
if (LAST_BY.getFunctionName().equals(functionName)) {
result =
- ascending
+ ascending || !orderKeyIsTimeColumn
? new LastByAccumulator(
inputDataTypes.get(0), inputDataTypes.get(1),
xIsTimeColumn, yIsTimeColumn)
: new LastByDescAccumulator(
@@ -125,7 +129,7 @@ public class AccumulatorFactory {
isAggTableScan);
} else {
result =
- ascending
+ ascending && orderKeyIsTimeColumn
? new FirstByAccumulator(
inputDataTypes.get(0),
inputDataTypes.get(1),
@@ -136,13 +140,21 @@ public class AccumulatorFactory {
inputDataTypes.get(0), inputDataTypes.get(1),
xIsTimeColumn, yIsTimeColumn);
}
} else if (LAST.getFunctionName().equals(functionName)) {
- return ascending
- ? new LastAccumulator(inputDataTypes.get(0))
- : new LastDescAccumulator(
- inputDataTypes.get(0),
- isTimeColumn(inputExpressions.get(0), timeColumnName),
- isMeasurementColumn(inputExpressions.get(0),
measurementColumnNames),
- isAggTableScan);
+ boolean orderKeyIsTimeColumn = isTimeColumn(inputExpressions.get(1),
timeColumnName);
+ result =
+ ascending || !orderKeyIsTimeColumn
+ ? new LastAccumulator(inputDataTypes.get(0))
+ : new LastDescAccumulator(
+ inputDataTypes.get(0),
+ isTimeColumn(inputExpressions.get(0), timeColumnName),
+ isMeasurementColumn(inputExpressions.get(0),
measurementColumnNames),
+ isAggTableScan);
+ } else if (FIRST.getFunctionName().equals(functionName)) {
+ boolean orderKeyIsTimeColumn = isTimeColumn(inputExpressions.get(1),
timeColumnName);
+ result =
+ ascending && orderKeyIsTimeColumn
+ ? new FirstAccumulator(inputDataTypes.get(0), isAggTableScan)
+ : new FirstDescAccumulator(inputDataTypes.get(0));
} else {
result =
createBuiltinAccumulator(
@@ -307,34 +319,18 @@ public class AccumulatorFactory {
case SUM:
return new SumAccumulator(inputDataTypes.get(0));
case LAST:
- return ascending
- ? new LastAccumulator(inputDataTypes.get(0))
- : new LastDescAccumulator(inputDataTypes.get(0), false, false,
isAggTableScan);
+ return new LastAccumulator(inputDataTypes.get(0));
case FIRST:
- return ascending
- ? new FirstAccumulator(inputDataTypes.get(0), isAggTableScan)
- : new FirstDescAccumulator(inputDataTypes.get(0));
+ return new FirstDescAccumulator(inputDataTypes.get(0));
case MAX:
return new MaxAccumulator(inputDataTypes.get(0));
case MIN:
return new MinAccumulator(inputDataTypes.get(0));
case LAST_BY:
- return ascending
- ? new LastByAccumulator(inputDataTypes.get(0),
inputDataTypes.get(1), false, false)
- : new LastByDescAccumulator(
- inputDataTypes.get(0),
- inputDataTypes.get(1),
- false,
- false,
- false,
- false,
- isAggTableScan);
+ return new LastByAccumulator(inputDataTypes.get(0),
inputDataTypes.get(1), false, false);
case FIRST_BY:
- return ascending
- ? new FirstByAccumulator(
- inputDataTypes.get(0), inputDataTypes.get(1), false, false,
isAggTableScan)
- : new FirstByDescAccumulator(
- inputDataTypes.get(0), inputDataTypes.get(1), false, false);
+ return new FirstByDescAccumulator(
+ inputDataTypes.get(0), inputDataTypes.get(1), false, false);
case MAX_BY:
return new TableMaxByAccumulator(inputDataTypes.get(0),
inputDataTypes.get(1));
case MIN_BY:
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/distribution/AggregationTableScanTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/distribution/AggregationTableScanTest.java
new file mode 100644
index 00000000000..c03e5e9c0ce
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/distribution/AggregationTableScanTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.iotdb.db.queryengine.plan.planner.distribution;
+
+import static
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceContext.createFragmentInstanceContext;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.concurrent.ExecutorService;
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.commons.concurrent.IoTDBThreadPoolFactory;
+import org.apache.iotdb.db.protocol.session.IClientSession;
+import org.apache.iotdb.db.queryengine.common.FragmentInstanceId;
+import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.PlanFragmentId;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.common.SessionInfo;
+import org.apache.iotdb.db.queryengine.execution.fragment.DataNodeQueryContext;
+import
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceContext;
+import
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceStateMachine;
+import org.apache.iotdb.db.queryengine.plan.analyze.Analysis;
+import org.apache.iotdb.db.queryengine.plan.analyze.Analyzer;
+import org.apache.iotdb.db.queryengine.plan.analyze.FakePartitionFetcherImpl;
+import org.apache.iotdb.db.queryengine.plan.analyze.FakeSchemaFetcherImpl;
+import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator;
+import org.apache.iotdb.db.queryengine.plan.planner.LocalExecutionPlanner;
+import org.apache.iotdb.db.queryengine.plan.planner.LogicalPlanner;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.FragmentInstance;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.LogicalQueryPlan;
+import org.apache.iotdb.db.queryengine.plan.statement.Statement;
+import org.apache.iotdb.db.storageengine.dataregion.DataRegion;
+import org.apache.iotdb.db.storageengine.dataregion.IDataRegionForQuery;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class AggregationTableScanTest {
+
+ @Test
+ public void lastAggTest() {
+ final String sql = null;
+ DataNodeQueryContext dataNodeQueryContext = new DataNodeQueryContext(1);
+
+ SessionInfo sessionInfo =
+ new SessionInfo(
+ 0, "root", ZoneId.systemDefault(), "last_agg_db",
IClientSession.SqlDialect.TABLE);
+ QueryId queryId = new QueryId("test");
+ MPPQueryContext context =
+ new MPPQueryContext(
+ sql,
+ queryId,
+ sessionInfo,
+ new TEndPoint("127.0.0.1", 6667),
+ new TEndPoint("127.0.0.1", 6667));
+ Analyzer analyzer =
+ new Analyzer(context, new FakePartitionFetcherImpl(), new
FakeSchemaFetcherImpl());
+ Statement statement = StatementGenerator.createStatement(sql,
ZonedDateTime.now().getOffset());
+ Analysis analysis = analyzer.analyze(statement);
+ LogicalPlanner logicalPlanner = new LogicalPlanner(context);
+ LogicalQueryPlan logicalPlan = logicalPlanner.plan(analysis);
+ DistributionPlanner distributionPlanner = new
DistributionPlanner(analysis, logicalPlan);
+ FragmentInstance instance =
distributionPlanner.planFragments().getInstances().get(0);
+
+ LocalExecutionPlanner localExecutionPlanner =
LocalExecutionPlanner.getInstance();
+ localExecutionPlanner.plan(
+ instance.getFragment().getPlanNodeTree(),
+ instance.getFragment().getTypeProvider(),
+ mockFIContext(queryId),
+ dataNodeQueryContext);
+ }
+
+ private FragmentInstanceContext mockFIContext(QueryId queryId) {
+ ExecutorService instanceNotificationExecutor =
+ IoTDBThreadPoolFactory.newFixedThreadPool(1,
"last_agg-instance-notification");
+ FragmentInstanceId instanceId =
+ new FragmentInstanceId(new PlanFragmentId(queryId, 0),
"stub-instance");
+ FragmentInstanceStateMachine stateMachine =
+ new FragmentInstanceStateMachine(instanceId,
instanceNotificationExecutor);
+ FragmentInstanceContext instanceContext =
+ createFragmentInstanceContext(instanceId, stateMachine);
+ IDataRegionForQuery dataRegionForQuery = Mockito.mock(DataRegion.class);
+ instanceContext.setDataRegion(dataRegionForQuery);
+ return instanceContext;
+ }
+}
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/table/TsFileTableSchemaUtil.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/table/TsFileTableSchemaUtil.java
index 7a74f4be651..eaa1b6a6a9e 100644
---
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/table/TsFileTableSchemaUtil.java
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/table/TsFileTableSchemaUtil.java
@@ -34,6 +34,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import static org.apache.iotdb.commons.schema.table.TsTable.TIME_COLUMN_NAME;
+
/** Utility class for converting between TsTable and TSFile TableSchema */
public class TsFileTableSchemaUtil {
@@ -156,9 +158,18 @@ public class TsFileTableSchemaUtil {
// Directly iterate through columns and filter out TIME and ATTRIBUTE
columns
int columnIndex = 0;
- for (final TsTableColumnSchema columnSchema : tsTableColumnSchemas) {
+
+ for (int i = 0; i < tsTableColumnSchemas.size(); i++) {
+ TsTableColumnSchema columnSchema = tsTableColumnSchemas.get(i);
final TsTableColumnCategory category = columnSchema.getColumnCategory();
+ // if the time columns is named as "time" and in first position, drop it
+ if (i == 0
+ && category == TsTableColumnCategory.TIME
+ && columnSchema.getColumnName().equalsIgnoreCase(TIME_COLUMN_NAME)) {
+ continue;
+ }
+
// Skip ATTRIBUTE columns (only include TIME, TAG and FIELD)
if (category == TsTableColumnCategory.ATTRIBUTE) {
continue;