This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 3b47945b3ce Show pre-deleted table schemas and improve write errors
(#18626)
3b47945b3ce is described below
commit 3b47945b3ceb0c3c25c1951a7afd3b2424fde3d5
Author: Caideyipi <[email protected]>
AuthorDate: Mon Sep 21 11:52:09 2026 +0800
Show pre-deleted table schemas and improve write errors (#18626)
* Show pre-deleted table schemas and improve write errors
* Support rollback for pre-altered table columns
* Fix TreeViewTest metadata lease timeout
* Clarify pre-deleted column cache scope
---
.../consensus/request/ConfigPhysicalPlan.java | 4 +
.../consensus/request/ConfigPhysicalPlanType.java | 1 +
.../table/RollbackPreAlterColumnDataTypePlan.java | 61 ++++
.../manager/schema/ClusterSchemaManager.java | 40 ++-
.../persistence/executor/ConfigPlanExecutor.java | 4 +
.../persistence/schema/ClusterSchemaInfo.java | 103 +++++-
.../confignode/persistence/schema/ConfigMTree.java | 169 +++++++++-
.../table/AlterTableColumnDataTypeProcedure.java | 78 ++++-
.../impl/schema/table/CreateTableProcedure.java | 16 +-
.../request/ConfigPhysicalPlanSerDeTest.java | 15 +
.../persistence/schema/ConfigMTreeTest.java | 77 ++++-
.../persistence/schema/TablePreDeleteTest.java | 368 +++++++++++++++++++++
.../AlterTableColumnDataTypeProcedureTest.java | 171 ++++++++++
.../schema/table/CreateTableProcedureTest.java | 39 +++
.../iotdb/db/i18n/DataNodeSchemaMessages.java | 2 -
.../iotdb/db/i18n/DataNodeSchemaMessages.java | 2 -
.../config/executor/ClusterConfigTaskExecutor.java | 14 +
.../fetcher/TableHeaderSchemaValidator.java | 41 ++-
.../db/schemaengine/table/DataNodeTableCache.java | 86 ++++-
.../plan/relational/analyzer/TreeViewTest.java | 5 +
.../fetcher/TableHeaderSchemaValidatorTest.java | 251 ++++++++++++++
.../schemaengine/table/DataNodeTableCacheTest.java | 191 ++++++++++-
.../apache/iotdb/commons/i18n/CommonMessages.java | 6 +
.../apache/iotdb/commons/i18n/CommonMessages.java | 6 +
.../exception/table/ColumnInAlterException.java | 40 +++
.../exception/table/ColumnInDeletionException.java | 40 +++
.../exception/table/TableInDeletionException.java | 38 +++
27 files changed, 1819 insertions(+), 49 deletions(-)
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlan.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlan.java
index 0d9ca912571..ff765a035b0 100644
---
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlan.java
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlan.java
@@ -116,6 +116,7 @@ import
org.apache.iotdb.confignode.consensus.request.write.table.PreDeleteTableP
import
org.apache.iotdb.confignode.consensus.request.write.table.RenameTableColumnPlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RenameTablePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackCreateTablePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackPreAlterColumnDataTypePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackPreDeleteTablePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.SetTableColumnCommentPlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.SetTableCommentPlan;
@@ -463,6 +464,9 @@ public abstract class ConfigPhysicalPlan implements
IConsensusRequest {
case PreAlterColumnDataType:
plan = new PreAlterColumnDataTypePlan();
break;
+ case RollbackPreAlterColumnDataType:
+ plan = new RollbackPreAlterColumnDataTypePlan();
+ break;
case AlterColumnDataType:
plan = new AlterColumnDataTypePlan();
break;
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlanType.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlanType.java
index 1be95181414..afef9f82bfe 100644
---
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlanType.java
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlanType.java
@@ -233,6 +233,7 @@ public enum ConfigPhysicalPlanType {
AlterColumnDataType((short) 878),
PreAlterColumnDataType((short) 879),
RollbackPreDeleteTable((short) 880),
+ RollbackPreAlterColumnDataType((short) 881),
/** Deprecated types for sync, restored them for upgrade. */
@Deprecated
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/write/table/RollbackPreAlterColumnDataTypePlan.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/write/table/RollbackPreAlterColumnDataTypePlan.java
new file mode 100644
index 00000000000..bcb9fb4ca72
--- /dev/null
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/request/write/table/RollbackPreAlterColumnDataTypePlan.java
@@ -0,0 +1,61 @@
+/*
+ * 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.confignode.consensus.request.write.table;
+
+import org.apache.iotdb.confignode.consensus.request.ConfigPhysicalPlanType;
+
+import org.apache.tsfile.enums.TSDataType;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class RollbackPreAlterColumnDataTypePlan extends
AbstractTableColumnPlan {
+ private TSDataType newType;
+
+ public RollbackPreAlterColumnDataTypePlan() {
+ super(ConfigPhysicalPlanType.RollbackPreAlterColumnDataType);
+ }
+
+ public RollbackPreAlterColumnDataTypePlan(
+ final String database,
+ final String tableName,
+ final String columnName,
+ final TSDataType newType) {
+ super(ConfigPhysicalPlanType.RollbackPreAlterColumnDataType, database,
tableName, columnName);
+ this.newType = newType;
+ }
+
+ @Override
+ protected void serializeImpl(final DataOutputStream stream) throws
IOException {
+ super.serializeImpl(stream);
+ stream.write(newType.serialize());
+ }
+
+ @Override
+ protected void deserializeImpl(final ByteBuffer buffer) throws IOException {
+ super.deserializeImpl(buffer);
+ newType = TSDataType.deserializeFrom(buffer);
+ }
+
+ public TSDataType getNewType() {
+ return newType;
+ }
+}
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/schema/ClusterSchemaManager.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/schema/ClusterSchemaManager.java
index 840be42d48c..b55180e35f7 100644
---
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/schema/ClusterSchemaManager.java
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/schema/ClusterSchemaManager.java
@@ -24,6 +24,7 @@ import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation;
import org.apache.iotdb.common.rpc.thrift.TSStatus;
import org.apache.iotdb.commons.conf.CommonDescriptor;
import org.apache.iotdb.commons.exception.MetadataException;
+import org.apache.iotdb.commons.exception.table.TableInDeletionException;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.commons.path.PathPatternTree;
import org.apache.iotdb.commons.schema.SchemaConstant;
@@ -1540,13 +1541,34 @@ public class ClusterSchemaManager {
return clusterSchemaInfo.getTsTableIfExists(database, tableName);
}
+ public boolean isColumnAlterCommitted(
+ final String database,
+ final String tableName,
+ final String columnName,
+ final TSDataType dataType)
+ throws MetadataException {
+ return clusterSchemaInfo.isColumnAlterCommitted(database, tableName,
columnName, dataType);
+ }
+
+ public Optional<TSDataType> getPreAlteredColumnType(
+ final String database, final String tableName, final String columnName)
+ throws MetadataException {
+ return clusterSchemaInfo.getPreAlteredColumnType(database, tableName,
columnName);
+ }
+
public synchronized Pair<TSStatus, TsTable>
tableColumnCheckForColumnExtension(
final String database,
final String tableName,
final List<TsTableColumnSchema> columnSchemaList,
final boolean isTableView)
throws MetadataException {
- final TsTable originalTable = getTableIfExists(database,
tableName).orElse(null);
+ final TsTable originalTable =
+ clusterSchemaInfo.getTableForModification(
+ database,
+ tableName,
+ columnSchemaList.stream()
+ .map(TsTableColumnSchema::getColumnName)
+ .toArray(String[]::new));
if (Objects.isNull(originalTable)) {
return new Pair<>(
@@ -1601,7 +1623,7 @@ public class ClusterSchemaManager {
final TSDataType dataType,
final boolean isGeneratedByPipe)
throws MetadataException {
- final TsTable originalTable = getTableIfExists(database,
tableName).orElse(null);
+ final TsTable originalTable =
clusterSchemaInfo.getTableForModification(database, tableName);
if (Objects.isNull(originalTable)) {
return new Pair<>(
@@ -1638,7 +1660,8 @@ public class ClusterSchemaManager {
final String newName,
final boolean isTableView)
throws MetadataException {
- final TsTable originalTable = getTableIfExists(database,
tableName).orElse(null);
+ final TsTable originalTable =
+ clusterSchemaInfo.getTableForModification(database, tableName,
oldName, newName);
if (Objects.isNull(originalTable)) {
return new Pair<>(
@@ -1691,7 +1714,7 @@ public class ClusterSchemaManager {
final String newName,
final boolean isTableView)
throws MetadataException {
- final TsTable originalTable = getTableIfExists(database,
tableName).orElse(null);
+ final TsTable originalTable =
clusterSchemaInfo.getTableForModification(database, tableName);
if (Objects.isNull(originalTable)) {
return new Pair<>(
@@ -1707,7 +1730,12 @@ public class ClusterSchemaManager {
return result.get();
}
- if (getTableIfExists(database, newName).isPresent()) {
+ final Optional<Pair<TsTable, TableNodeStatus>> targetTable =
+ getTableAndStatusIfExists(database, newName);
+ if (targetTable.isPresent() && targetTable.get().getRight() ==
TableNodeStatus.PRE_DELETE) {
+ throw new TableInDeletionException(database, newName);
+ }
+ if (targetTable.isPresent()) {
return new Pair<>(
RpcUtils.getStatus(
TSStatusCode.TABLE_ALREADY_EXISTS,
@@ -1776,7 +1804,7 @@ public class ClusterSchemaManager {
final Map<String, String> updatedProperties,
final boolean isTableView)
throws MetadataException {
- final TsTable originalTable = getTableIfExists(database,
tableName).orElse(null);
+ final TsTable originalTable =
clusterSchemaInfo.getTableForModification(database, tableName);
if (Objects.isNull(originalTable)) {
return new Pair<>(
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/executor/ConfigPlanExecutor.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/executor/ConfigPlanExecutor.java
index 772f46baa31..f2d6ae80926 100644
---
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/executor/ConfigPlanExecutor.java
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/executor/ConfigPlanExecutor.java
@@ -134,6 +134,7 @@ import
org.apache.iotdb.confignode.consensus.request.write.table.PreDeleteTableP
import
org.apache.iotdb.confignode.consensus.request.write.table.RenameTableColumnPlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RenameTablePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackCreateTablePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackPreAlterColumnDataTypePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackPreDeleteTablePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.SetTableColumnCommentPlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.SetTableCommentPlan;
@@ -615,6 +616,9 @@ public class ConfigPlanExecutor {
return clusterSchemaInfo.dropTable((CommitDeleteTablePlan)
physicalPlan);
case PreAlterColumnDataType:
return
clusterSchemaInfo.preAlterColumnDataType((PreAlterColumnDataTypePlan)
physicalPlan);
+ case RollbackPreAlterColumnDataType:
+ return clusterSchemaInfo.rollbackPreAlterColumnDataType(
+ (RollbackPreAlterColumnDataTypePlan) physicalPlan);
case AlterColumnDataType:
return clusterSchemaInfo.commitAlterColumnDataType(
((AlterColumnDataTypePlan) physicalPlan));
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/schema/ClusterSchemaInfo.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/schema/ClusterSchemaInfo.java
index 1b9c880cd6a..dc8106a76a6 100644
---
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/schema/ClusterSchemaInfo.java
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/schema/ClusterSchemaInfo.java
@@ -27,6 +27,9 @@ import org.apache.iotdb.commons.conf.CommonDescriptor;
import org.apache.iotdb.commons.exception.IllegalPathException;
import org.apache.iotdb.commons.exception.MetadataException;
import org.apache.iotdb.commons.exception.SemanticException;
+import org.apache.iotdb.commons.exception.table.ColumnInAlterException;
+import org.apache.iotdb.commons.exception.table.ColumnInDeletionException;
+import org.apache.iotdb.commons.exception.table.TableInDeletionException;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.commons.path.PathPatternTree;
import org.apache.iotdb.commons.schema.table.TableNodeStatus;
@@ -34,6 +37,7 @@ import org.apache.iotdb.commons.schema.table.TableType;
import org.apache.iotdb.commons.schema.table.TreeViewSchema;
import org.apache.iotdb.commons.schema.table.TsTable;
import org.apache.iotdb.commons.schema.table.TsTableInternalRPCUtil;
+import org.apache.iotdb.commons.schema.table.column.TsTableColumnSchema;
import org.apache.iotdb.commons.schema.template.Template;
import org.apache.iotdb.commons.snapshot.SnapshotProcessor;
import org.apache.iotdb.commons.utils.PathUtils;
@@ -67,6 +71,7 @@ import
org.apache.iotdb.confignode.consensus.request.write.table.PreDeleteTableP
import
org.apache.iotdb.confignode.consensus.request.write.table.RenameTableColumnPlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RenameTablePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackCreateTablePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackPreAlterColumnDataTypePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackPreDeleteTablePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.SetTableColumnCommentPlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.SetTableCommentPlan;
@@ -109,6 +114,7 @@ import org.apache.iotdb.rpc.RpcUtils;
import org.apache.iotdb.rpc.TSStatusCode;
import org.apache.tsfile.annotations.TableModel;
+import org.apache.tsfile.enums.TSDataType;
import org.apache.tsfile.utils.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -1352,9 +1358,11 @@ public class ClusterSchemaInfo implements
SnapshotProcessor {
})
.collect(Collectors.toList())
: tableModelMTree
- .getAllUsingTablesUnderSpecificDatabase(
+ .getAllTablesUnderSpecificDatabase(
getQualifiedDatabasePartialPath(plan.getDatabase()))
.stream()
+ .filter(pair -> pair.getRight() !=
TableNodeStatus.PRE_CREATE)
+ .map(Pair::getLeft)
.map(
tsTable ->
new TTableInfo(
@@ -1447,7 +1455,7 @@ public class ClusterSchemaInfo implements
SnapshotProcessor {
}
return new DescTableResp(
StatusUtils.OK,
- tableModelMTree.getUsingTableSchema(databasePath,
plan.getTableName()),
+ tableModelMTree.getTableSchemaForDesc(databasePath,
plan.getTableName()),
null,
null);
} catch (final MetadataException e) {
@@ -1557,6 +1565,80 @@ public class ClusterSchemaInfo implements
SnapshotProcessor {
}
}
+ public TsTable getTableForModification(
+ final String database, final String tableName, final String...
columnNames)
+ throws MetadataException {
+ databaseReadWriteLock.readLock().lock();
+ try {
+ final PartialPath databasePath =
getQualifiedDatabasePartialPath(database);
+ final Optional<Pair<TsTable, TableNodeStatus>> tableAndStatus =
+ tableModelMTree.getTableAndStatusIfExists(databasePath, tableName);
+ if (!tableAndStatus.isPresent()) {
+ return null;
+ }
+ if (tableAndStatus.get().getRight() == TableNodeStatus.PRE_DELETE) {
+ throw new TableInDeletionException(database, tableName);
+ }
+ final TableSchemaDetails details =
+ tableModelMTree.getTableSchemaDetails(databasePath, tableName);
+ for (final String columnName : columnNames) {
+ if (details.preDeletedColumns.contains(columnName)) {
+ throw new ColumnInDeletionException(database, tableName, columnName);
+ }
+ if (details.preAlteredColumns.containsKey(columnName)) {
+ throw new ColumnInAlterException(database, tableName, columnName);
+ }
+ }
+ return tableModelMTree.getTableSchemaForDataNode(databasePath,
tableName);
+ } finally {
+ databaseReadWriteLock.readLock().unlock();
+ }
+ }
+
+ public boolean isColumnAlterCommitted(
+ final String database,
+ final String tableName,
+ final String columnName,
+ final TSDataType dataType)
+ throws MetadataException {
+ databaseReadWriteLock.readLock().lock();
+ try {
+ final PartialPath databasePath =
getQualifiedDatabasePartialPath(database);
+ final Optional<Pair<TsTable, TableNodeStatus>> tableAndStatus =
+ tableModelMTree.getTableAndStatusIfExists(databasePath, tableName);
+ if (!tableAndStatus.isPresent()) {
+ return false;
+ }
+ final TableSchemaDetails details =
+ tableModelMTree.getTableSchemaDetails(databasePath, tableName);
+ final TsTableColumnSchema columnSchema =
details.table.getColumnSchema(columnName);
+ return !details.preAlteredColumns.containsKey(columnName)
+ && columnSchema != null
+ && columnSchema.getDataType() == dataType;
+ } finally {
+ databaseReadWriteLock.readLock().unlock();
+ }
+ }
+
+ public Optional<TSDataType> getPreAlteredColumnType(
+ final String database, final String tableName, final String columnName)
+ throws MetadataException {
+ databaseReadWriteLock.readLock().lock();
+ try {
+ final PartialPath databasePath =
getQualifiedDatabasePartialPath(database);
+ if (!tableModelMTree.getTableAndStatusIfExists(databasePath,
tableName).isPresent()) {
+ return Optional.empty();
+ }
+ return Optional.ofNullable(
+ tableModelMTree
+ .getTableSchemaDetails(databasePath, tableName)
+ .preAlteredColumns
+ .get(columnName));
+ } finally {
+ databaseReadWriteLock.readLock().unlock();
+ }
+ }
+
public TSStatus addTableColumn(final AddTableColumnPlan plan) {
return executeWithLock(
() -> {
@@ -1644,6 +1726,23 @@ public class ClusterSchemaInfo implements
SnapshotProcessor {
}
}
+ public TSStatus rollbackPreAlterColumnDataType(final
RollbackPreAlterColumnDataTypePlan plan) {
+ databaseReadWriteLock.writeLock().lock();
+ try {
+ tableModelMTree.rollbackPreAlterColumnDataType(
+ getQualifiedDatabasePartialPath(plan.getDatabase()),
+ plan.getTableName(),
+ plan.getColumnName(),
+ plan.getNewType());
+ return RpcUtils.SUCCESS_STATUS;
+ } catch (final MetadataException e) {
+ LOGGER.warn(e.getMessage(), e);
+ return RpcUtils.getStatus(e.getErrorCode(), e.getMessage());
+ } finally {
+ databaseReadWriteLock.writeLock().unlock();
+ }
+ }
+
public TSStatus commitAlterColumnDataType(AlterColumnDataTypePlan plan) {
databaseReadWriteLock.writeLock().lock();
try {
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/schema/ConfigMTree.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/schema/ConfigMTree.java
index 12f6a2baaa1..53dacbd7196 100644
---
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/schema/ConfigMTree.java
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/schema/ConfigMTree.java
@@ -25,8 +25,11 @@ import
org.apache.iotdb.commons.exception.IllegalPathException;
import org.apache.iotdb.commons.exception.IoTDBException;
import org.apache.iotdb.commons.exception.MetadataException;
import org.apache.iotdb.commons.exception.SemanticException;
+import org.apache.iotdb.commons.exception.table.ColumnInAlterException;
+import org.apache.iotdb.commons.exception.table.ColumnInDeletionException;
import org.apache.iotdb.commons.exception.table.ColumnNotExistsException;
import org.apache.iotdb.commons.exception.table.TableAlreadyExistsException;
+import org.apache.iotdb.commons.exception.table.TableInDeletionException;
import org.apache.iotdb.commons.exception.table.TableNotExistsException;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.commons.path.PathPatternTree;
@@ -684,6 +687,9 @@ public class ConfigMTree {
tableNode.setTable(table);
tableNode.setStatus(TableNodeStatus.PRE_CREATE);
} else if (node instanceof ConfigTableNode) {
+ if (((ConfigTableNode) node).getStatus() == TableNodeStatus.PRE_DELETE) {
+ throw new TableInDeletionException(database.getFullPath(),
table.getTableName());
+ }
throw new TableAlreadyExistsException(
database.getFullPath().substring(ROOT.length() + 1),
table.getTableName());
} else {
@@ -697,6 +703,10 @@ public class ConfigMTree {
final IConfigMNode databaseNode =
getDatabaseNodeByDatabasePath(database).getAsMNode();
final IConfigMNode node = databaseNode.getChild(table.getTableName());
if (Objects.nonNull(node)) {
+ if (node instanceof ConfigTableNode
+ && ((ConfigTableNode) node).getStatus() ==
TableNodeStatus.PRE_DELETE) {
+ throw new TableInDeletionException(database.getFullPath(),
table.getTableName());
+ }
if (!TreeViewSchema.isTreeViewTable(((ConfigTableNode)
node).getTable())) {
throw new TableAlreadyExistsException(
database.getFullPath().substring(ROOT.length() + 1),
table.getTableName());
@@ -774,7 +784,7 @@ public class ConfigMTree {
public void renameTable(final PartialPath database, final String tableName,
final String newName)
throws MetadataException {
final IConfigMNode databaseNode =
getDatabaseNodeByDatabasePath(database).getAsMNode();
- final ConfigTableNode tableNode = (ConfigTableNode)
databaseNode.getChild(tableName);
+ final ConfigTableNode tableNode = getTableNodeForModification(database,
tableName);
store.deleteChild(databaseNode, tableName);
tableNode.setName(newName);
store.addChild(databaseNode, newName, tableNode);
@@ -786,7 +796,19 @@ public class ConfigMTree {
final String oldName,
final String newName)
throws MetadataException {
- final ConfigTableNode tableNode = getTableNode(database, tableName);
+ final ConfigTableNode tableNode = getTableNodeForModification(database,
tableName);
+ if (tableNode.getPreDeletedColumns().contains(oldName)) {
+ throw new ColumnInDeletionException(database.getFullPath(), tableName,
oldName);
+ }
+ if (tableNode.getPreDeletedColumns().contains(newName)) {
+ throw new ColumnInDeletionException(database.getFullPath(), tableName,
newName);
+ }
+ if (tableNode.getPreAlteredColumns().containsKey(oldName)) {
+ throw new ColumnInAlterException(database.getFullPath(), tableName,
oldName);
+ }
+ if (tableNode.getPreAlteredColumns().containsKey(newName)) {
+ throw new ColumnInAlterException(database.getFullPath(), tableName,
newName);
+ }
tableNode.getTable().renameColumnSchema(oldName, newName);
}
@@ -796,7 +818,7 @@ public class ConfigMTree {
final String comment,
final boolean isView)
throws MetadataException {
- final TsTable table = getTable(database, tableName);
+ final TsTable table = getTableForModification(database, tableName);
final Optional<Pair<TSStatus, TsTable>> check =
ClusterSchemaManager.checkTable4View(database.getTailNode(), table,
isView);
if (check.isPresent()) {
@@ -816,7 +838,8 @@ public class ConfigMTree {
final @Nonnull String columnName,
final @Nullable String comment)
throws MetadataException {
- final TsTable table = getTable(database, tableName);
+ final ConfigTableNode node = getTableNodeForModification(database,
tableName);
+ final TsTable table = node.getTable();
final TsTableColumnSchema columnSchema = table.getColumnSchema(columnName);
@@ -824,6 +847,12 @@ public class ConfigMTree {
throw new ColumnNotExistsException(
PathUtils.unQualifyDatabaseName(database.getFullPath()), tableName,
columnName);
}
+ if (node.getPreDeletedColumns().contains(columnName)) {
+ throw new ColumnInDeletionException(database.getFullPath(), tableName,
columnName);
+ }
+ if (node.getPreAlteredColumns().containsKey(columnName)) {
+ throw new ColumnInAlterException(database.getFullPath(), tableName,
columnName);
+ }
if (Objects.nonNull(comment)) {
columnSchema.getProps().put(TsTable.COMMENT_KEY, comment);
} else {
@@ -839,7 +868,7 @@ public class ConfigMTree {
child ->
child instanceof ConfigTableNode
&& ((ConfigTableNode)
child).getStatus().equals(TableNodeStatus.USING))
- .map(child -> ((ConfigTableNode) child).getTable())
+ .map(child -> getTableSchemaForDataNode((ConfigTableNode) child))
.collect(Collectors.toList());
}
@@ -869,7 +898,7 @@ public class ConfigMTree {
TsTable table =
((ConfigTableNode) child).getStatus() == TableNodeStatus.PRE_DELETE
? new PreDeleteTsTable(tableName)
- : ((ConfigTableNode) child).getTable();
+ : getTableSchemaForDataNode((ConfigTableNode) child);
result.put(tableName, table);
} else {
result.put(tableName, null);
@@ -939,7 +968,13 @@ public class ConfigMTree {
final String tableName,
final List<TsTableColumnSchema> columnSchemaList)
throws MetadataException {
- final TsTable table = getTable(database, tableName);
+ final TsTable table =
+ getTableForModification(
+ database,
+ tableName,
+ columnSchemaList.stream()
+ .map(TsTableColumnSchema::getColumnName)
+ .toArray(String[]::new));
columnSchemaList.forEach(table::addColumnSchema);
}
@@ -960,7 +995,8 @@ public class ConfigMTree {
throw new TableNotExistsException(
database.getFullPath().substring(ROOT.length() + 1), tableName);
}
- final TsTable table = ((ConfigTableNode)
databaseNode.getChild(tableName)).getTable();
+ final ConfigTableNode tableNode = getTableNodeForModification(database,
tableName);
+ final TsTable table = tableNode.getTable();
tableProperties.forEach(
(k, v) -> {
if (Objects.nonNull(v)) {
@@ -988,7 +1024,7 @@ public class ConfigMTree {
final String columnName,
final boolean isView)
throws MetadataException, SemanticException {
- final ConfigTableNode node = getTableNode(database, tableName);
+ final ConfigTableNode node = getTableNodeForModification(database,
tableName);
final Optional<Pair<TSStatus, TsTable>> check =
ClusterSchemaManager.checkTable4View(database.getTailNode(),
node.getTable(), isView);
if (check.isPresent()) {
@@ -1006,6 +1042,10 @@ public class ConfigMTree {
throw new
SemanticException(ConfigNodeMessages.DROPPING_TAG_OR_TIME_COLUMN_IS_NOT_SUPPORTED);
}
+ if (node.getPreAlteredColumns().containsKey(columnName)) {
+ throw new ColumnInAlterException(database.getFullPath(), tableName,
columnName);
+ }
+
node.addPreDeletedColumn(columnName);
return columnSchema.getColumnCategory() == TsTableColumnCategory.ATTRIBUTE;
}
@@ -1018,22 +1058,34 @@ public class ConfigMTree {
if (Objects.nonNull(table.getColumnSchema(columnName))) {
table.removeColumnSchema(columnName);
node.removePreDeletedColumn(columnName);
+ node.removePreAlteredColumn(columnName);
}
}
public void preAlterColumnDataType(
PartialPath database, String tableName, String columnName, TSDataType
dataType)
throws MetadataException {
- final ConfigTableNode node = getTableNode(database, tableName);
+ final ConfigTableNode node = getTableNodeForModification(database,
tableName);
final TsTableColumnSchema columnSchema =
node.getTable().getColumnSchema(columnName);
if (Objects.isNull(columnSchema)) {
throw new ColumnNotExistsException(
PathUtils.unQualifyDatabaseName(database.getFullPath()), tableName,
columnName);
}
+ if (node.getPreDeletedColumns().contains(columnName)) {
+ throw new ColumnInDeletionException(database.getFullPath(), tableName,
columnName);
+ }
if (columnSchema.getColumnCategory() != TsTableColumnCategory.FIELD) {
throw new
SemanticException(ConfigNodeMessages.CAN_ONLY_ALTER_DATATYPE_OF_FIELD_COLUMNS);
}
+ if (node.getPreAlteredColumns().containsKey(columnName)) {
+ final TSDataType currentType =
node.getPreAlteredColumns().get(columnName);
+ if (currentType == dataType) {
+ return;
+ }
+ throw new ColumnInAlterException(database.getFullPath(), tableName,
columnName);
+ }
+
if (!MetadataUtils.canAlter(columnSchema.getDataType(), dataType)) {
throw new SemanticException(
String.format(
@@ -1048,8 +1100,19 @@ public class ConfigMTree {
public void commitAlterColumnDataType(
PartialPath database, String tableName, String columnName, TSDataType
dataType)
throws MetadataException {
- final ConfigTableNode node = getTableNode(database, tableName);
- final TsTable table = getTable(database, tableName);
+ final IConfigMNode databaseNode =
getDatabaseNodeByDatabasePath(database).getAsMNode();
+ if (!databaseNode.hasChild(tableName)) {
+ return;
+ }
+ final IConfigMNode tableNode = databaseNode.getChild(tableName);
+ if (!(tableNode instanceof ConfigTableNode)) {
+ return;
+ }
+ final ConfigTableNode node = (ConfigTableNode) tableNode;
+ if (!Objects.equals(node.getPreAlteredColumns().get(columnName),
dataType)) {
+ return;
+ }
+ final TsTable table = node.getTable();
final TsTableColumnSchema columnSchema = table.getColumnSchema(columnName);
if (Objects.nonNull(columnSchema)) {
columnSchema.setDataType(dataType);
@@ -1058,20 +1121,68 @@ public class ConfigMTree {
fieldColumnSchema.setEncoding(
SchemaUtils.getDataTypeCompatibleEncoding(dataType,
fieldColumnSchema.getEncoding()));
}
+ }
+ node.removePreAlteredColumn(columnName);
+ }
+
+ public void rollbackPreAlterColumnDataType(
+ final PartialPath database,
+ final String tableName,
+ final String columnName,
+ final TSDataType dataType)
+ throws MetadataException {
+ final IConfigMNode databaseNode =
getDatabaseNodeByDatabasePath(database).getAsMNode();
+ if (!databaseNode.hasChild(tableName)) {
+ return;
+ }
+ final IConfigMNode tableNode = databaseNode.getChild(tableName);
+ if (!(tableNode instanceof ConfigTableNode)) {
+ return;
+ }
+ final ConfigTableNode node = (ConfigTableNode) tableNode;
+ if (Objects.equals(node.getPreAlteredColumns().get(columnName), dataType))
{
node.removePreAlteredColumn(columnName);
}
}
- public TsTable getUsingTableSchema(final PartialPath database, final String
tableName)
+ public TsTable getTableSchemaForDataNode(final PartialPath database, final
String tableName)
throws MetadataException {
- final ConfigTableNode node = getTableNode(database, tableName);
+ return getTableSchemaForDataNode(getTableNode(database, tableName));
+ }
+
+ private TsTable getTableSchemaForDataNode(final ConfigTableNode node) {
if (node.getPreDeletedColumns().isEmpty() &&
node.getPreAlteredColumns().isEmpty()) {
return node.getTable();
}
- final TsTable newTable = new TsTable(node.getTable());
- if (!node.getPreDeletedColumns().isEmpty()) {
- node.getPreDeletedColumns().forEach(newTable::removeColumnSchema);
+ // Cache reloads and later schema updates must not make a column writable
again while its
+ // deletion is still pending. DESC uses the complete schema separately.
+ final TsTable table = new TsTable(node.getTable());
+ node.getPreDeletedColumns().forEach(table::removeColumnSchema);
+ node.getPreAlteredColumns()
+ .forEach(
+ (columnName, dataType) -> {
+ final TsTableColumnSchema columnSchema =
table.getColumnSchema(columnName);
+ if (columnSchema == null) {
+ return;
+ }
+ columnSchema.setDataType(dataType);
+ if (columnSchema instanceof FieldColumnSchema) {
+ final FieldColumnSchema fieldColumnSchema =
(FieldColumnSchema) columnSchema;
+ fieldColumnSchema.setEncoding(
+ SchemaUtils.getDataTypeCompatibleEncoding(
+ dataType, fieldColumnSchema.getEncoding()));
+ }
+ });
+ return table;
+ }
+
+ public TsTable getTableSchemaForDesc(final PartialPath database, final
String tableName)
+ throws MetadataException {
+ final ConfigTableNode node = getTableNode(database, tableName);
+ if (node.getPreAlteredColumns().isEmpty()) {
+ return node.getTable();
}
+ final TsTable newTable = new TsTable(node.getTable());
if (!node.getPreAlteredColumns().isEmpty()) {
node.getPreAlteredColumns()
.forEach(
@@ -1110,6 +1221,30 @@ public class ConfigMTree {
return getTableNode(database, tableName).getTable();
}
+ private TsTable getTableForModification(
+ final PartialPath database, final String tableName, final String...
columnNames)
+ throws MetadataException {
+ final ConfigTableNode node = getTableNodeForModification(database,
tableName);
+ for (final String columnName : columnNames) {
+ if (node.getPreDeletedColumns().contains(columnName)) {
+ throw new ColumnInDeletionException(database.getFullPath(), tableName,
columnName);
+ }
+ if (node.getPreAlteredColumns().containsKey(columnName)) {
+ throw new ColumnInAlterException(database.getFullPath(), tableName,
columnName);
+ }
+ }
+ return node.getTable();
+ }
+
+ private ConfigTableNode getTableNodeForModification(
+ final PartialPath database, final String tableName) throws
MetadataException {
+ final ConfigTableNode node = getTableNode(database, tableName);
+ if (node.getStatus() == TableNodeStatus.PRE_DELETE) {
+ throw new TableInDeletionException(database.getFullPath(), tableName);
+ }
+ return node;
+ }
+
public Optional<Pair<TsTable, TableNodeStatus>> getTableAndStatusIfExists(
final PartialPath database, final String tableName) throws
MetadataException {
final IConfigMNode databaseNode =
getDatabaseNodeByDatabasePath(database).getAsMNode();
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedure.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedure.java
index 12491f24a48..dd63a1ecaf0 100644
---
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedure.java
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedure.java
@@ -24,9 +24,11 @@ import org.apache.iotdb.commons.exception.IoTDBException;
import org.apache.iotdb.commons.exception.MetadataException;
import org.apache.iotdb.commons.schema.table.TsTable;
import
org.apache.iotdb.confignode.consensus.request.write.table.AlterColumnDataTypePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackPreAlterColumnDataTypePlan;
import org.apache.iotdb.confignode.i18n.ProcedureMessages;
import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv;
import org.apache.iotdb.confignode.procedure.exception.ProcedureException;
+import org.apache.iotdb.confignode.procedure.impl.schema.SchemaUtils;
import
org.apache.iotdb.confignode.procedure.state.schema.AlterTableColumnDataTypeState;
import org.apache.iotdb.confignode.procedure.store.ProcedureType;
import org.apache.iotdb.rpc.TSStatusCode;
@@ -41,6 +43,7 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Objects;
+import java.util.Optional;
public class AlterTableColumnDataTypeProcedure
extends AbstractAlterOrDropTableProcedure<AlterTableColumnDataTypeState> {
@@ -156,14 +159,25 @@ public class AlterTableColumnDataTypeProcedure
new AlterColumnDataTypePlan(database, tableName, columnName,
dataType),
isGeneratedByPipe);
if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+ // A consensus write may have been applied even when the client observed
a failure (for
+ // example, a timeout after the leader committed the entry). Continue
with release when the
+ // canonical CN schema already contains the requested type; rolling back
in that case would
+ // leave DataNodes on an older schema than the CN.
+ if (isColumnAlterCommitted(env)) {
+ setNextState(AlterTableColumnDataTypeState.COMMIT_RELEASE);
+ return;
+ }
setFailure(new ProcedureException(new
IoTDBException(status.getMessage(), status.getCode())));
+ return;
}
setNextState(AlterTableColumnDataTypeState.COMMIT_RELEASE);
}
@Override
protected boolean isRollbackSupported(final AlterTableColumnDataTypeState
state) {
- return false;
+ return state == AlterTableColumnDataTypeState.CHECK_AND_INVALIDATE_COLUMN
+ || state == AlterTableColumnDataTypeState.PRE_RELEASE
+ || state == AlterTableColumnDataTypeState.ALTER_TABLE_COLUMN_DATA_TYPE;
}
@Override
@@ -171,7 +185,67 @@ public class AlterTableColumnDataTypeProcedure
final ConfigNodeProcedureEnv configNodeProcedureEnv,
final AlterTableColumnDataTypeState alterTableColumnDataTypeState)
throws IOException, InterruptedException, ProcedureException {
- // Do nothing
+ // COMMIT_RELEASE is irreversible: the CN schema has already been
committed and must not be
+ // followed by a cache rollback if the procedure is aborted while
notifying DataNodes.
+ if (alterTableColumnDataTypeState ==
AlterTableColumnDataTypeState.COMMIT_RELEASE) {
+ return;
+ }
+ final Optional<TSDataType> pendingType =
getPreAlteredColumnType(configNodeProcedureEnv);
+ if (pendingType.isPresent() && pendingType.get() != dataType) {
+ // This rollback belongs to an older procedure. Leave a newer pre-alter
marker and its
+ // DataNode cache entry untouched.
+ return;
+ }
+ final boolean ownsPendingMarker = pendingType.isPresent();
+ if (!ownsPendingMarker &&
isColumnAlterCommittedForRollback(configNodeProcedureEnv)) {
+ return;
+ }
+ final TSStatus status =
+ SchemaUtils.executeInConsensusLayer(
+ new RollbackPreAlterColumnDataTypePlan(database, tableName,
columnName, dataType),
+ configNodeProcedureEnv,
+ LOGGER);
+ if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+ throw new ProcedureException(new IoTDBException(status.getMessage(),
status.getCode()));
+ }
+ if (alterTableColumnDataTypeState !=
AlterTableColumnDataTypeState.CHECK_AND_INVALIDATE_COLUMN
+ && table != null
+ && !getPreAlteredColumnType(configNodeProcedureEnv).isPresent()
+ && (ownsPendingMarker ||
!isColumnAlterCommittedForRollback(configNodeProcedureEnv))) {
+ rollbackPreRelease(configNodeProcedureEnv);
+ }
+ }
+
+ private Optional<TSDataType> getPreAlteredColumnType(final
ConfigNodeProcedureEnv env)
+ throws ProcedureException {
+ try {
+ return env.getConfigManager()
+ .getClusterSchemaManager()
+ .getPreAlteredColumnType(database, tableName, columnName);
+ } catch (final MetadataException e) {
+ throw new ProcedureException(e);
+ }
+ }
+
+ private boolean isColumnAlterCommittedForRollback(final
ConfigNodeProcedureEnv env)
+ throws ProcedureException {
+ try {
+ return env.getConfigManager()
+ .getClusterSchemaManager()
+ .isColumnAlterCommitted(database, tableName, columnName, dataType);
+ } catch (final MetadataException e) {
+ throw new ProcedureException(e);
+ }
+ }
+
+ private boolean isColumnAlterCommitted(final ConfigNodeProcedureEnv env) {
+ try {
+ return env.getConfigManager()
+ .getClusterSchemaManager()
+ .isColumnAlterCommitted(database, tableName, columnName, dataType);
+ } catch (final MetadataException e) {
+ return false;
+ }
}
@Override
diff --git
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedure.java
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedure.java
index f2379ae1256..8c6596cfe9e 100644
---
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedure.java
+++
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedure.java
@@ -22,6 +22,8 @@ package
org.apache.iotdb.confignode.procedure.impl.schema.table;
import org.apache.iotdb.common.rpc.thrift.TSStatus;
import org.apache.iotdb.commons.exception.IoTDBException;
import org.apache.iotdb.commons.exception.MetadataException;
+import org.apache.iotdb.commons.exception.table.TableInDeletionException;
+import org.apache.iotdb.commons.schema.table.TableNodeStatus;
import org.apache.iotdb.commons.schema.table.TsTable;
import
org.apache.iotdb.confignode.consensus.request.write.pipe.payload.PipeEnrichedPlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.CommitCreateTablePlan;
@@ -40,6 +42,7 @@ import org.apache.iotdb.confignode.rpc.thrift.TDatabaseSchema;
import org.apache.iotdb.mpp.rpc.thrift.TUpdateTableReq;
import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.tsfile.utils.Pair;
import org.apache.tsfile.utils.ReadWriteIOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -49,6 +52,7 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import static org.apache.iotdb.rpc.TSStatusCode.TABLE_ALREADY_EXISTS;
@@ -117,10 +121,14 @@ public class CreateTableProcedure
protected void checkTableExistence(final ConfigNodeProcedureEnv env) {
try {
- if (env.getConfigManager()
- .getClusterSchemaManager()
- .getTableIfExists(database, table.getTableName())
- .isPresent()) {
+ final Optional<Pair<TsTable, TableNodeStatus>> existingTable =
+ env.getConfigManager()
+ .getClusterSchemaManager()
+ .getTableAndStatusIfExists(database, table.getTableName());
+ if (existingTable.isPresent()) {
+ if (existingTable.get().getRight() == TableNodeStatus.PRE_DELETE) {
+ throw new TableInDeletionException(database, table.getTableName());
+ }
setFailure(
new ProcedureException(
new IoTDBException(
diff --git
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlanSerDeTest.java
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlanSerDeTest.java
index 0e858589803..1b9f4d55029 100644
---
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlanSerDeTest.java
+++
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/consensus/request/ConfigPhysicalPlanSerDeTest.java
@@ -147,6 +147,7 @@ import
org.apache.iotdb.confignode.consensus.request.write.table.PreDeleteTableP
import
org.apache.iotdb.confignode.consensus.request.write.table.RenameTableColumnPlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RenameTablePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackCreateTablePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.RollbackPreAlterColumnDataTypePlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.SetTableColumnCommentPlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.SetTableCommentPlan;
import
org.apache.iotdb.confignode.consensus.request.write.table.SetTablePropertiesPlan;
@@ -1716,6 +1717,20 @@ public class ConfigPhysicalPlanSerDeTest {
alterColumnDataTypePlan.getNewType(),
alterColumnDataTypePlan1.getNewType());
}
+ @Test
+ public void RollbackPreAlterTableColumnDataTypePlanTest() throws IOException
{
+ final RollbackPreAlterColumnDataTypePlan rollbackPlan =
+ new RollbackPreAlterColumnDataTypePlan("database1", "table1", "field",
TSDataType.FLOAT);
+ final RollbackPreAlterColumnDataTypePlan rollbackPlan1 =
+ (RollbackPreAlterColumnDataTypePlan)
+
ConfigPhysicalPlan.Factory.create(rollbackPlan.serializeToByteBuffer());
+ Assert.assertEquals(rollbackPlan.getDatabase(),
rollbackPlan1.getDatabase());
+ Assert.assertEquals(rollbackPlan.getTableName(),
rollbackPlan1.getTableName());
+ Assert.assertEquals(rollbackPlan.getColumnName(),
rollbackPlan1.getColumnName());
+ Assert.assertEquals(rollbackPlan.getType(), rollbackPlan1.getType());
+ Assert.assertEquals(rollbackPlan.getNewType(), rollbackPlan1.getNewType());
+ }
+
@Test
public void AlterTableColumnDataTypePlanTest() throws IOException {
final AlterColumnDataTypePlan alterColumnDataTypePlan =
diff --git
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/schema/ConfigMTreeTest.java
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/schema/ConfigMTreeTest.java
index c2519dcbfae..318c957b825 100644
---
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/schema/ConfigMTreeTest.java
+++
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/schema/ConfigMTreeTest.java
@@ -49,6 +49,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -360,6 +361,10 @@ public class ConfigMTreeTest {
root.preCreateTable(pathList[i], table);
root.commitCreateTable(pathList[i], tableName);
+ if (i == 0) {
+ Assert.assertTrue(root.preDeleteColumn(pathList[i], tableName, "Attr",
false));
+ root.preAlterColumnDataType(pathList[i], tableName, "Measurement",
TSDataType.STRING);
+ }
}
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
@@ -402,8 +407,21 @@ public class ConfigMTreeTest {
final TsTable table = tables.get(0);
assertEquals("table" + i, table.getTableName());
assertEquals(1, table.getTagNum());
- // currently, only construct the TsTable would not carry the time column
- assertEquals(3, table.getColumnNum());
+ // These TsTables have no time column; the first table also hides its
pre-deleted attribute.
+ assertEquals(i == 0 ? 2 : 3, table.getColumnNum());
+ final ConfigMTree.TableSchemaDetails details =
+ newTree.getTableSchemaDetails(pathList[i], table.getTableName());
+ if (i == 0) {
+ assertEquals(Collections.singleton("Attr"), details.preDeletedColumns);
+ assertEquals(TSDataType.STRING,
details.preAlteredColumns.get("Measurement"));
+ assertEquals(TSDataType.DOUBLE,
details.table.getColumnSchema("Measurement").getDataType());
+ Assert.assertNotNull(details.table.getColumnSchema("Attr"));
+ assertEquals(TSDataType.STRING,
table.getColumnSchema("Measurement").getDataType());
+ Assert.assertNull(table.getColumnSchema("Attr"));
+ } else {
+ assertTrue(details.preDeletedColumns.isEmpty());
+ assertTrue(details.preAlteredColumns.isEmpty());
+ }
}
}
@@ -434,7 +452,7 @@ public class ConfigMTreeTest {
SchemaUtils.getDataTypeCompatibleEncoding(TSDataType.STRING,
TSEncoding.GORILLA);
Assert.assertNotEquals(TSEncoding.GORILLA, expectedEncoding);
- final TsTable preAlteredTable = root.getUsingTableSchema(database,
table.getTableName());
+ final TsTable preAlteredTable = root.getTableSchemaForDesc(database,
table.getTableName());
final FieldColumnSchema preAlteredField =
(FieldColumnSchema) preAlteredTable.getColumnSchema("measurement");
Assert.assertEquals(TSDataType.STRING, preAlteredField.getDataType());
@@ -443,7 +461,7 @@ public class ConfigMTreeTest {
root.commitAlterColumnDataType(
database, table.getTableName(), "measurement", TSDataType.STRING);
- final TsTable committedTable = root.getUsingTableSchema(database,
table.getTableName());
+ final TsTable committedTable = root.getTableSchemaForDesc(database,
table.getTableName());
final FieldColumnSchema committedField =
(FieldColumnSchema) committedTable.getColumnSchema("measurement");
Assert.assertEquals(TSDataType.STRING, committedField.getDataType());
@@ -453,6 +471,57 @@ public class ConfigMTreeTest {
root.getTableSchemaDetails(database,
table.getTableName()).preAlteredColumns.isEmpty());
}
+ @Test
+ public void testRollbackPreAlterColumnDataTypeOnlyClearsMatchingRequest()
throws Exception {
+ root = new ConfigMTree(true);
+
+ final PartialPath database = new PartialPath("root.sg");
+ root.setStorageGroup(database);
+ final IDatabaseMNode<IConfigMNode> databaseNode =
root.getDatabaseNodeByDatabasePath(database);
+ databaseNode
+ .getAsMNode()
+ .getDatabaseSchema()
+ .setName(PathUtils.unQualifyDatabaseName(database.getFullPath()));
+ databaseNode.getAsMNode().getDatabaseSchema().setIsTableModel(true);
+
+ final TsTable table = new TsTable("table1");
+ table.addColumnSchema(new TagColumnSchema("id", TSDataType.STRING));
+ table.addColumnSchema(
+ new FieldColumnSchema(
+ "measurement", TSDataType.DOUBLE, TSEncoding.GORILLA,
CompressionType.SNAPPY));
+ root.preCreateTable(database, table);
+ root.commitCreateTable(database, table.getTableName());
+
+ root.preAlterColumnDataType(database, table.getTableName(), "measurement",
TSDataType.STRING);
+ Assert.assertEquals(
+ TSDataType.STRING,
+ root.getTableSchemaForDataNode(database, table.getTableName())
+ .getColumnSchema("measurement")
+ .getDataType());
+
+ // A stale rollback must not clear a newer request for a different target
type.
+ root.rollbackPreAlterColumnDataType(
+ database, table.getTableName(), "measurement", TSDataType.FLOAT);
+ Assert.assertEquals(
+ TSDataType.STRING,
+ root.getTableSchemaDetails(database, table.getTableName())
+ .preAlteredColumns
+ .get("measurement"));
+
+ root.rollbackPreAlterColumnDataType(
+ database, table.getTableName(), "measurement", TSDataType.STRING);
+ Assert.assertTrue(
+ root.getTableSchemaDetails(database,
table.getTableName()).preAlteredColumns.isEmpty());
+ // A delayed commit from the failed procedure must not apply after the
marker was rolled back.
+ root.commitAlterColumnDataType(
+ database, table.getTableName(), "measurement", TSDataType.STRING);
+ Assert.assertEquals(
+ TSDataType.DOUBLE,
+ root.getTableSchemaForDataNode(database, table.getTableName())
+ .getColumnSchema("measurement")
+ .getDataType());
+ }
+
@Test
public void testSetTemplate() throws MetadataException {
root.setStorageGroup(new PartialPath("root.a"));
diff --git
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/schema/TablePreDeleteTest.java
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/schema/TablePreDeleteTest.java
new file mode 100644
index 00000000000..86b74ec00b6
--- /dev/null
+++
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/persistence/schema/TablePreDeleteTest.java
@@ -0,0 +1,368 @@
+/*
+ * 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.confignode.persistence.schema;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.exception.table.ColumnInAlterException;
+import org.apache.iotdb.commons.exception.table.ColumnInDeletionException;
+import org.apache.iotdb.commons.exception.table.TableInDeletionException;
+import org.apache.iotdb.commons.schema.table.TableNodeStatus;
+import org.apache.iotdb.commons.schema.table.TsTable;
+import org.apache.iotdb.commons.schema.table.TsTableInternalRPCUtil;
+import org.apache.iotdb.commons.schema.table.column.AttributeColumnSchema;
+import org.apache.iotdb.commons.schema.table.column.FieldColumnSchema;
+import org.apache.iotdb.commons.schema.table.column.TsTableColumnSchema;
+import org.apache.iotdb.confignode.consensus.request.ConfigPhysicalPlanType;
+import org.apache.iotdb.confignode.consensus.request.read.table.DescTablePlan;
+import org.apache.iotdb.confignode.consensus.request.read.table.FetchTablePlan;
+import org.apache.iotdb.confignode.consensus.request.read.table.ShowTablePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.database.DatabaseSchemaPlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.AddTableColumnPlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.AlterColumnDataTypePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.CommitCreateTablePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.CommitDeleteColumnPlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.CommitDeleteTablePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.PreAlterColumnDataTypePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.PreCreateTablePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.PreDeleteColumnPlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.PreDeleteTablePlan;
+import
org.apache.iotdb.confignode.consensus.request.write.table.SetTableColumnCommentPlan;
+import org.apache.iotdb.confignode.manager.IManager;
+import org.apache.iotdb.confignode.manager.schema.ClusterSchemaManager;
+import org.apache.iotdb.confignode.manager.schema.ClusterSchemaQuotaStatistics;
+import org.apache.iotdb.confignode.rpc.thrift.TDatabaseSchema;
+import org.apache.iotdb.confignode.rpc.thrift.TDescTableResp;
+import org.apache.iotdb.confignode.rpc.thrift.TShowTableResp;
+import org.apache.iotdb.confignode.rpc.thrift.TTableInfo;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.enums.CompressionType;
+import org.apache.tsfile.file.metadata.enums.TSEncoding;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class TablePreDeleteTest {
+ private static final String DATABASE = "root.pre_delete_test";
+ private static final String TABLE = "table1";
+ private ClusterSchemaInfo schemaInfo;
+ private ClusterSchemaManager schemaManager;
+
+ @Before
+ public void setUp() throws Exception {
+ schemaInfo = new ClusterSchemaInfo();
+ schemaManager =
+ new ClusterSchemaManager(
+ Mockito.mock(IManager.class),
+ schemaInfo,
+ Mockito.mock(ClusterSchemaQuotaStatistics.class));
+ assertSuccess(
+ schemaInfo.createDatabase(
+ new DatabaseSchemaPlan(
+ ConfigPhysicalPlanType.CreateDatabase,
+ new TDatabaseSchema(DATABASE).setIsTableModel(true))));
+ createTable(TABLE);
+ }
+
+ @After
+ public void tearDown() {
+ schemaInfo.clear();
+ }
+
+ @Test
+ public void testShowTablesIncludesPreDeleteButNotPreCreate() {
+ createTable("using_table");
+ assertSuccess(
+ schemaInfo.preCreateTable(new PreCreateTablePlan(DATABASE, new
TsTable("creating"))));
+ assertSuccess(schemaInfo.preDeleteTable(new PreDeleteTablePlan(DATABASE,
TABLE)));
+
+ final TShowTableResp basic =
+ schemaInfo.showTables(new ShowTablePlan(DATABASE,
false)).convertToTShowTableResp();
+ assertSuccess(basic.getStatus());
+ assertEquals(
+ Arrays.asList(TABLE, "using_table"),
+ basic.getTableInfoList().stream()
+ .map(TTableInfo::getTableName)
+ .sorted()
+ .collect(Collectors.toList()));
+
+ final TShowTableResp details =
+ schemaInfo.showTables(new ShowTablePlan(DATABASE,
true)).convertToTShowTableResp();
+ final Map<String, Integer> states =
+ details.getTableInfoList().stream()
+ .collect(Collectors.toMap(TTableInfo::getTableName,
TTableInfo::getState));
+ assertEquals(Integer.valueOf(TableNodeStatus.PRE_DELETE.ordinal()),
states.get(TABLE));
+ assertEquals(Integer.valueOf(TableNodeStatus.PRE_CREATE.ordinal()),
states.get("creating"));
+ assertEquals(Integer.valueOf(TableNodeStatus.USING.ordinal()),
states.get("using_table"));
+
+ assertSuccess(schemaInfo.dropTable(new CommitDeleteTablePlan(DATABASE,
TABLE)));
+ assertEquals(
+ Collections.singletonList("using_table"),
+ schemaInfo
+ .showTables(new ShowTablePlan(DATABASE, false))
+ .convertToTShowTableResp()
+ .getTableInfoList()
+ .stream()
+ .map(TTableInfo::getTableName)
+ .collect(Collectors.toList()));
+ }
+
+ @Test
+ public void testDescRetainsPreDeletedColumnsAndAlteredTypes() {
+ assertSuccess(schemaInfo.preDeleteColumn(new PreDeleteColumnPlan(DATABASE,
TABLE, "field")));
+ assertSuccess(
+ schemaInfo.preDeleteColumn(new PreDeleteColumnPlan(DATABASE, TABLE,
"attribute")));
+ assertSuccess(
+ schemaInfo.preAlterColumnDataType(
+ new PreAlterColumnDataTypePlan(DATABASE, TABLE, "live",
TSDataType.INT64)));
+
+ TDescTableResp basic = describe(false);
+ TsTable table =
TsTableInternalRPCUtil.deserializeSingleTsTable(basic.getTableInfo());
+ assertNotNull(table.getColumnSchema("field"));
+ assertNotNull(table.getColumnSchema("attribute"));
+ assertEquals(TSDataType.INT64,
table.getColumnSchema("live").getDataType());
+ assertFalse(basic.isSetPreDeletedColumns());
+
+ assertEquals(
+ TSDataType.INT64,
+
schemaInfo.getAllUsingTables().get(DATABASE).get(0).getColumnSchema("live").getDataType());
+
+ final TDescTableResp details = describe(true);
+
assertTrue(details.getPreDeletedColumns().containsAll(Arrays.asList("field",
"attribute")));
+ assertEquals(
+ Byte.valueOf(TSDataType.INT64.serialize()),
details.getPreAlteredColumns().get("live"));
+
+ assertSuccess(
+ schemaInfo.commitDeleteColumn(new CommitDeleteColumnPlan(DATABASE,
TABLE, "field")));
+ table =
TsTableInternalRPCUtil.deserializeSingleTsTable(describe(false).getTableInfo());
+ assertNull(table.getColumnSchema("field"));
+
+ assertSuccess(schemaInfo.preDeleteTable(new PreDeleteTablePlan(DATABASE,
TABLE)));
+ assertNotNull(
+
TsTableInternalRPCUtil.deserializeSingleTsTable(describe(false).getTableInfo())
+ .getColumnSchema("attribute"));
+ }
+
+ @Test
+ public void testColumnExtensionRejectsPreDeletedNames() throws Exception {
+ for (final String column : Arrays.asList("field", "attribute")) {
+ assertSuccess(schemaInfo.preDeleteColumn(new
PreDeleteColumnPlan(DATABASE, TABLE, column)));
+ final List<TsTableColumnSchema> columns =
+ new ArrayList<>(Arrays.asList(field("new_field"), field(column)));
+ final ColumnInDeletionException exception =
+ assertThrows(
+ ColumnInDeletionException.class,
+ () ->
+ schemaManager.tableColumnCheckForColumnExtension(
+ DATABASE, TABLE, columns, false));
+ assertEquals(TSStatusCode.SEMANTIC_ERROR.getStatusCode(),
exception.getErrorCode());
+ assertEquals(
+ new ColumnInDeletionException(DATABASE, TABLE, column).getMessage(),
+ exception.getMessage());
+ assertEquals(2, columns.size());
+ assertNull(
+ schemaInfo
+ .getTsTableIfExists(DATABASE, TABLE)
+ .get()
+ .getLeft()
+ .getColumnSchema("new_field"));
+ }
+ assertEquals(
+ TSStatusCode.COLUMN_ALREADY_EXISTS.getStatusCode(),
+ schemaManager
+ .tableColumnCheckForColumnExtension(
+ DATABASE, TABLE, new
ArrayList<>(Collections.singletonList(field("live"))), false)
+ .getLeft()
+ .getCode());
+
+ assertSuccess(
+ schemaInfo.commitDeleteColumn(new CommitDeleteColumnPlan(DATABASE,
TABLE, "field")));
+ assertSuccess(
+ schemaManager
+ .tableColumnCheckForColumnExtension(
+ DATABASE, TABLE, new
ArrayList<>(Collections.singletonList(field("field"))), false)
+ .getLeft());
+ }
+
+ @Test
+ public void testPreAlterRejectsConflictingColumnOperations() throws
Exception {
+ assertSuccess(
+ schemaInfo.preAlterColumnDataType(
+ new PreAlterColumnDataTypePlan(DATABASE, TABLE, "live",
TSDataType.INT64)));
+ // Retrying the same target type is allowed so a stuck procedure can be
resumed.
+ assertSuccess(
+ schemaInfo.preAlterColumnDataType(
+ new PreAlterColumnDataTypePlan(DATABASE, TABLE, "live",
TSDataType.INT64)));
+
+ final TSStatus secondAlter =
+ schemaInfo.preAlterColumnDataType(
+ new PreAlterColumnDataTypePlan(DATABASE, TABLE, "live",
TSDataType.FLOAT));
+ assertEquals(TSStatusCode.SEMANTIC_ERROR.getStatusCode(),
secondAlter.getCode());
+ assertEquals(
+ new ColumnInAlterException(DATABASE, TABLE, "live").getMessage(),
secondAlter.getMessage());
+
+ final TSStatus delete =
+ schemaInfo.preDeleteColumn(new PreDeleteColumnPlan(DATABASE, TABLE,
"live"));
+ assertEquals(TSStatusCode.SEMANTIC_ERROR.getStatusCode(),
delete.getCode());
+ assertEquals(
+ new ColumnInAlterException(DATABASE, TABLE, "live").getMessage(),
delete.getMessage());
+
+ assertThrows(
+ ColumnInAlterException.class,
+ () ->
+ schemaManager.tableColumnCheckForColumnExtension(
+ DATABASE, TABLE, new
ArrayList<>(Collections.singletonList(field("live"))), false));
+ assertEquals(
+ new ColumnInAlterException(DATABASE, TABLE, "live").getMessage(),
+ schemaInfo
+ .addTableColumn(
+ new AddTableColumnPlan(
+ DATABASE, TABLE, Collections.singletonList(field("live")),
false))
+ .getMessage());
+ assertEquals(
+ new ColumnInAlterException(DATABASE, TABLE, "live").getMessage(),
+ schemaInfo
+ .setTableColumnComment(
+ new SetTableColumnCommentPlan(DATABASE, TABLE, "live",
"comment"))
+ .getMessage());
+ assertThrows(
+ ColumnInAlterException.class,
+ () ->
+ schemaManager.tableColumnCheckForColumnRenaming(
+ DATABASE, TABLE, "live", "renamed", false));
+ }
+
+ @Test
+ public void
testSameTypePreAlterIsNotReportedAsCommittedUntilMarkerIsCleared() throws
Exception {
+ assertSuccess(
+ schemaInfo.preAlterColumnDataType(
+ new PreAlterColumnDataTypePlan(DATABASE, TABLE, "live",
TSDataType.INT32)));
+ assertFalse(schemaInfo.isColumnAlterCommitted(DATABASE, TABLE, "live",
TSDataType.INT32));
+
+ assertSuccess(
+ schemaInfo.commitAlterColumnDataType(
+ new AlterColumnDataTypePlan(DATABASE, TABLE, "live",
TSDataType.INT32)));
+ assertTrue(schemaInfo.isColumnAlterCommitted(DATABASE, TABLE, "live",
TSDataType.INT32));
+ }
+
+ @Test
+ public void testPreDeletedTableRejectsCreationAndColumnExtension() {
+ assertSuccess(schemaInfo.preDeleteTable(new PreDeleteTablePlan(DATABASE,
TABLE)));
+ final TSStatus create =
+ schemaInfo.preCreateTable(new PreCreateTablePlan(DATABASE, new
TsTable(TABLE)));
+ assertEquals(TSStatusCode.SEMANTIC_ERROR.getStatusCode(),
create.getCode());
+ assertEquals(new TableInDeletionException(DATABASE, TABLE).getMessage(),
create.getMessage());
+ final TableInDeletionException exception =
+ assertThrows(
+ TableInDeletionException.class,
+ () ->
+ schemaManager.tableColumnCheckForColumnExtension(
+ DATABASE,
+ TABLE,
+ new
ArrayList<>(Collections.singletonList(field("new_field"))),
+ false));
+ assertEquals(create.getMessage(), exception.getMessage());
+ }
+
+ @Test
+ public void testDataNodeSchemasExcludePreDeletedColumns() throws Exception {
+ assertSuccess(schemaInfo.preDeleteColumn(new PreDeleteColumnPlan(DATABASE,
TABLE, "field")));
+ assertSuccess(
+ schemaInfo.preDeleteColumn(new PreDeleteColumnPlan(DATABASE, TABLE,
"attribute")));
+
+ assertOnlyLiveColumns(schemaInfo.getAllUsingTables().get(DATABASE).get(0));
+ final TsTable fetched =
+ TsTableInternalRPCUtil.deserializeTsTableFetchResult(
+ schemaInfo
+ .fetchTables(
+ new FetchTablePlan(
+ Collections.singletonMap(DATABASE,
Collections.singleton(TABLE)),
+ Collections.singleton(TableNodeStatus.USING)))
+ .convertToTFetchTableResp()
+ .getTableInfoMap())
+ .get(DATABASE)
+ .get(TABLE);
+ assertOnlyLiveColumns(fetched);
+
+ final TsTable expanded =
+ schemaManager
+ .tableColumnCheckForColumnExtension(
+ DATABASE,
+ TABLE,
+ new ArrayList<>(Collections.singletonList(field("new_field"))),
+ false)
+ .getRight();
+ assertOnlyLiveColumns(expanded);
+ assertNotNull(expanded.getColumnSchema("new_field"));
+
+ // Preparing a cache snapshot must not change the complete schema used by
DESC.
+ final TsTable described =
+
TsTableInternalRPCUtil.deserializeSingleTsTable(describe(false).getTableInfo());
+ assertNotNull(described.getColumnSchema("field"));
+ assertNotNull(described.getColumnSchema("attribute"));
+ }
+
+ private static void assertOnlyLiveColumns(final TsTable table) {
+ assertNotNull(table.getColumnSchema("live"));
+ assertNull(table.getColumnSchema("field"));
+ assertNull(table.getColumnSchema("attribute"));
+ }
+
+ private TDescTableResp describe(final boolean details) {
+ final TDescTableResp resp =
+ schemaInfo.descTable(new DescTablePlan(DATABASE, TABLE,
details)).convertToTDescTableResp();
+ assertSuccess(resp.getStatus());
+ return resp;
+ }
+
+ private void createTable(final String name) {
+ final TsTable table = new TsTable(name);
+ table.addColumnSchema(field("field"));
+ table.addColumnSchema(field("live"));
+ table.addColumnSchema(new AttributeColumnSchema("attribute",
TSDataType.STRING));
+ assertSuccess(schemaInfo.preCreateTable(new PreCreateTablePlan(DATABASE,
table)));
+ assertSuccess(schemaInfo.commitCreateTable(new
CommitCreateTablePlan(DATABASE, name)));
+ }
+
+ private static FieldColumnSchema field(final String name) {
+ return new FieldColumnSchema(name, TSDataType.INT32, TSEncoding.RLE,
CompressionType.LZ4);
+ }
+
+ private static void assertSuccess(final TSStatus status) {
+ assertEquals(
+ status.getMessage(), TSStatusCode.SUCCESS_STATUS.getStatusCode(),
status.getCode());
+ }
+}
diff --git
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedureTest.java
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedureTest.java
new file mode 100644
index 00000000000..5634abbe6bf
--- /dev/null
+++
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedureTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.confignode.procedure.impl.schema.table;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.schema.table.TsTable;
+import org.apache.iotdb.confignode.manager.ConfigManager;
+import org.apache.iotdb.confignode.manager.consensus.ConsensusManager;
+import org.apache.iotdb.confignode.manager.schema.ClusterSchemaManager;
+import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv;
+import
org.apache.iotdb.confignode.procedure.state.schema.AlterTableColumnDataTypeState;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.lang.reflect.Method;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class AlterTableColumnDataTypeProcedureTest {
+
+ @Test
+ public void commitReleaseIsNeverRolledBack() throws Exception {
+ final AlterTableColumnDataTypeProcedure procedure =
+ new AlterTableColumnDataTypeProcedure("database", "table", "query",
"value", null, false);
+ procedure.table = new TsTable("table");
+
+ // The commit state may be present on the rollback stack when an abort
races with cache
+ // notification. It must not issue a rollback plan or touch DataNode
caches.
+ procedure.rollbackState(null,
AlterTableColumnDataTypeState.COMMIT_RELEASE);
+ }
+
+ @Test
+ public void rollbackClearsCnMarkerAfterProcedureRestartBeforeTableSnapshot()
throws Exception {
+ final ConsensusManager consensusManager =
Mockito.mock(ConsensusManager.class);
+ final ClusterSchemaManager schemaManager =
Mockito.mock(ClusterSchemaManager.class);
+ final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+ final ConfigNodeProcedureEnv env =
Mockito.mock(ConfigNodeProcedureEnv.class);
+ Mockito.when(env.getConfigManager()).thenReturn(configManager);
+
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+
Mockito.when(configManager.getClusterSchemaManager()).thenReturn(schemaManager);
+ Mockito.when(consensusManager.write(Mockito.any()))
+ .thenReturn(new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode()));
+ Mockito.when(
+ schemaManager.isColumnAlterCommitted("database", "table", "value",
TSDataType.INT64))
+ .thenReturn(false);
+
+ final AlterTableColumnDataTypeProcedure procedure =
+ new AlterTableColumnDataTypeProcedure(
+ "database", "table", "query", "value", TSDataType.INT64, false);
+
+ // The table snapshot is intentionally null, as it can be after a restart
that happened after
+ // the pre-alter consensus entry was applied but before the procedure
persisted its snapshot.
+ procedure.rollbackState(env,
AlterTableColumnDataTypeState.CHECK_AND_INVALIDATE_COLUMN);
+
+ Mockito.verify(consensusManager).write(Mockito.any());
+ }
+
+ @Test
+ public void sameTypePreAlterRollbackStillCleansDataNodeCache() throws
Exception {
+ final ConsensusManager consensusManager =
Mockito.mock(ConsensusManager.class);
+ final ClusterSchemaManager schemaManager =
Mockito.mock(ClusterSchemaManager.class);
+ final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+ final ConfigNodeProcedureEnv env =
Mockito.mock(ConfigNodeProcedureEnv.class);
+ Mockito.when(env.getConfigManager()).thenReturn(configManager);
+
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+
Mockito.when(configManager.getClusterSchemaManager()).thenReturn(schemaManager);
+
+ final AtomicBoolean markerCleared = new AtomicBoolean(false);
+ Mockito.when(schemaManager.getPreAlteredColumnType("database", "table",
"value"))
+ .thenAnswer(
+ invocation -> markerCleared.get() ? Optional.empty() :
Optional.of(TSDataType.INT64));
+ Mockito.when(
+ schemaManager.isColumnAlterCommitted("database", "table", "value",
TSDataType.INT64))
+ .thenAnswer(invocation -> markerCleared.get());
+ Mockito.when(consensusManager.write(Mockito.any()))
+ .thenAnswer(
+ invocation -> {
+ markerCleared.set(true);
+ return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
+ });
+
+ final TestAlterTableColumnDataTypeProcedure procedure =
+ new TestAlterTableColumnDataTypeProcedure();
+ procedure.table = new TsTable("table");
+ procedure.rollbackState(env, AlterTableColumnDataTypeState.PRE_RELEASE);
+
+ Assert.assertTrue(procedure.dataNodeRollbackCalled);
+ }
+
+ @Test
+ public void staleRollbackDoesNotClearNewerPreAlter() throws Exception {
+ final ConsensusManager consensusManager =
Mockito.mock(ConsensusManager.class);
+ final ClusterSchemaManager schemaManager =
Mockito.mock(ClusterSchemaManager.class);
+ final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+ final ConfigNodeProcedureEnv env =
Mockito.mock(ConfigNodeProcedureEnv.class);
+ Mockito.when(env.getConfigManager()).thenReturn(configManager);
+
Mockito.when(configManager.getConsensusManager()).thenReturn(consensusManager);
+
Mockito.when(configManager.getClusterSchemaManager()).thenReturn(schemaManager);
+ Mockito.when(schemaManager.getPreAlteredColumnType("database", "table",
"value"))
+ .thenReturn(Optional.of(TSDataType.FLOAT));
+
+ final TestAlterTableColumnDataTypeProcedure procedure =
+ new TestAlterTableColumnDataTypeProcedure();
+ procedure.table = new TsTable("table");
+ procedure.rollbackState(env, AlterTableColumnDataTypeState.PRE_RELEASE);
+
+ Mockito.verify(consensusManager, Mockito.never()).write(Mockito.any());
+ Assert.assertFalse(procedure.dataNodeRollbackCalled);
+ }
+
+ @Test
+ public void consensusFailureAfterCommitContinuesToCommitRelease() throws
Exception {
+ final ClusterSchemaManager schemaManager =
Mockito.mock(ClusterSchemaManager.class);
+ final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+ final ConfigNodeProcedureEnv env =
Mockito.mock(ConfigNodeProcedureEnv.class);
+ Mockito.when(env.getConfigManager()).thenReturn(configManager);
+
Mockito.when(configManager.getClusterSchemaManager()).thenReturn(schemaManager);
+
+ Mockito.when(schemaManager.executePlan(Mockito.any(), Mockito.eq(false)))
+ .thenReturn(new
TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode()));
+ Mockito.when(
+ schemaManager.isColumnAlterCommitted("database", "table", "value",
TSDataType.INT64))
+ .thenReturn(true);
+
+ final AlterTableColumnDataTypeProcedure procedure =
+ new AlterTableColumnDataTypeProcedure(
+ "database", "table", "query", "value", TSDataType.INT64, false);
+ final Method alterColumnDataType =
+ AlterTableColumnDataTypeProcedure.class.getDeclaredMethod(
+ "alterColumnDataType", ConfigNodeProcedureEnv.class);
+ alterColumnDataType.setAccessible(true);
+ alterColumnDataType.invoke(procedure, env);
+
+ Assert.assertFalse(procedure.isFailed());
+ }
+
+ private static class TestAlterTableColumnDataTypeProcedure
+ extends AlterTableColumnDataTypeProcedure {
+ private boolean dataNodeRollbackCalled;
+
+ private TestAlterTableColumnDataTypeProcedure() {
+ super("database", "table", "query", "value", TSDataType.INT64, false);
+ }
+
+ @Override
+ protected void rollbackPreRelease(final ConfigNodeProcedureEnv env) {
+ dataNodeRollbackCalled = true;
+ }
+ }
+}
diff --git
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedureTest.java
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedureTest.java
index fab1bfb2660..0b9a118fd1a 100644
---
a/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedureTest.java
+++
b/iotdb-core/confignode/src/test/java/org/apache/iotdb/confignode/procedure/impl/schema/table/CreateTableProcedureTest.java
@@ -20,24 +20,63 @@
package org.apache.iotdb.confignode.procedure.impl.schema.table;
import org.apache.iotdb.commons.exception.IllegalPathException;
+import org.apache.iotdb.commons.exception.IoTDBException;
+import org.apache.iotdb.commons.exception.table.TableInDeletionException;
+import org.apache.iotdb.commons.schema.table.TableNodeStatus;
import org.apache.iotdb.commons.schema.table.TsTable;
import org.apache.iotdb.commons.schema.table.column.AttributeColumnSchema;
import org.apache.iotdb.commons.schema.table.column.FieldColumnSchema;
import org.apache.iotdb.commons.schema.table.column.TagColumnSchema;
+import org.apache.iotdb.confignode.manager.ConfigManager;
+import org.apache.iotdb.confignode.manager.schema.ClusterSchemaManager;
+import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv;
import org.apache.iotdb.confignode.procedure.store.ProcedureType;
+import org.apache.iotdb.rpc.TSStatusCode;
import org.apache.tsfile.enums.TSDataType;
import org.apache.tsfile.file.metadata.enums.CompressionType;
import org.apache.tsfile.file.metadata.enums.TSEncoding;
+import org.apache.tsfile.utils.Pair;
import org.junit.Assert;
import org.junit.Test;
+import org.mockito.Mockito;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.Optional;
public class CreateTableProcedureTest {
+ @Test
+ public void testPreDeletedTableIsNotReportedAsAlreadyExisting() throws
Exception {
+ final ConfigNodeProcedureEnv env =
Mockito.mock(ConfigNodeProcedureEnv.class);
+ final ConfigManager configManager = Mockito.mock(ConfigManager.class);
+ final ClusterSchemaManager schemaManager =
Mockito.mock(ClusterSchemaManager.class);
+ Mockito.when(env.getConfigManager()).thenReturn(configManager);
+
Mockito.when(configManager.getClusterSchemaManager()).thenReturn(schemaManager);
+ final TsTable table = new TsTable("table1");
+ Mockito.when(schemaManager.getTableAndStatusIfExists("database1",
"table1"))
+ .thenReturn(Optional.of(new Pair<>(table,
TableNodeStatus.PRE_DELETE)));
+
+ final CreateTableProcedure procedure = new
CreateTableProcedure("database1", table, false);
+ procedure.checkTableExistence(env);
+
+ Assert.assertTrue(procedure.isFailed());
+ Assert.assertTrue(procedure.getException().getCause() instanceof
TableInDeletionException);
+ Assert.assertEquals(
+ TSStatusCode.SEMANTIC_ERROR.getStatusCode(),
+ ((IoTDBException) procedure.getException().getCause()).getErrorCode());
+
+ Mockito.when(schemaManager.getTableAndStatusIfExists("database1",
"table1"))
+ .thenReturn(Optional.of(new Pair<>(table, TableNodeStatus.USING)));
+ final CreateTableProcedure duplicate = new
CreateTableProcedure("database1", table, false);
+ duplicate.checkTableExistence(env);
+ Assert.assertEquals(
+ TSStatusCode.TABLE_ALREADY_EXISTS.getStatusCode(),
+ ((IoTDBException) duplicate.getException().getCause()).getErrorCode());
+ }
+
@Test
public void serializeDeserializeTest() throws IllegalPathException,
IOException {
final TsTable table = new TsTable("table1");
diff --git
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeSchemaMessages.java
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeSchemaMessages.java
index dd1c4af94f6..ef703af3f0e 100644
---
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeSchemaMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeSchemaMessages.java
@@ -584,8 +584,6 @@ public final class DataNodeSchemaMessages {
public static final String UPDATE_TABLE_BY_FETCH_WITH_DETAIL =
"Update table {}.{} by table fetch, {}";
public static final String UPDATE_TABLE_BY_FETCH = "Update table {}.{} by
table fetch.";
- public static final String THE_TABLE_IS_IN_PRE_DELETE_STATE =
- "The table %s.%s is in the pre-delete state. Please wait a few seconds.
If the table is still in this state, please drop it again.";
public static final String COMPARE_TABLE_ADDED = "Added table: ";
public static final String COMPARE_TABLE_REMOVED = "Removed table: ";
public static final String COMPARE_TABLE_NAME = "Table name: ";
diff --git
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeSchemaMessages.java
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeSchemaMessages.java
index 411044b14af..f1ceaaa0470 100644
---
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeSchemaMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeSchemaMessages.java
@@ -578,8 +578,6 @@ public final class DataNodeSchemaMessages {
"尝试获取信号量以从 ConfigNode 获取表时被中断,已忽略。";
public static final String UPDATE_TABLE_BY_FETCH_WITH_DETAIL = "获取表 {}.{}
信息, {}";
public static final String UPDATE_TABLE_BY_FETCH = "通过表拉取更新表 {}.{}";
- public static final String THE_TABLE_IS_IN_PRE_DELETE_STATE =
- "表 %s.%s 处于预删除的状态,请稍等,如之后重试还是此状态,请输入sql再次删除";
public static final String COMPARE_TABLE_ADDED = "新增表:";
public static final String COMPARE_TABLE_REMOVED = "已移除表:";
public static final String COMPARE_TABLE_NAME = "表名:";
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java
index 7a3580fe66b..d7dd2abcb1d 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/executor/ClusterConfigTaskExecutor.java
@@ -4830,6 +4830,20 @@ public class ClusterConfigTaskExecutor implements
IConfigTaskExecutor {
return future;
}
+ public Set<String> getPreDeletedColumns(final String database, final String
tableName) {
+ try (final ConfigNodeClient configNodeClient =
+
CONFIG_NODE_CLIENT_MANAGER.borrowClient(ConfigNodeInfo.CONFIG_REGION_ID)) {
+ final TDescTableResp resp = configNodeClient.describeTable(database,
tableName, true);
+ if (resp.getStatus().getCode() !=
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+ throw new IoTDBRuntimeException(
+ getTableErrorMessage(resp.getStatus(), database),
resp.getStatus().getCode());
+ }
+ return resp.isSetPreDeletedColumns() ? resp.getPreDeletedColumns() :
Collections.emptySet();
+ } catch (final ClientManagerException | TException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
@Override
public SettableFuture<ConfigTaskResult> describeTable(
final String database,
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableHeaderSchemaValidator.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableHeaderSchemaValidator.java
index 6a08427d82b..aeaf82e54f0 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableHeaderSchemaValidator.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableHeaderSchemaValidator.java
@@ -24,6 +24,7 @@ import org.apache.iotdb.commons.exception.IoTDBException;
import org.apache.iotdb.commons.exception.IoTDBRuntimeException;
import org.apache.iotdb.commons.exception.MetadataException;
import org.apache.iotdb.commons.exception.SemanticException;
+import org.apache.iotdb.commons.exception.table.ColumnInDeletionException;
import org.apache.iotdb.commons.i18n.QueryMessages;
import
org.apache.iotdb.commons.queryengine.plan.relational.metadata.ColumnSchema;
import
org.apache.iotdb.commons.queryengine.plan.relational.metadata.QualifiedObjectName;
@@ -71,6 +72,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
@@ -83,11 +85,14 @@ public class TableHeaderSchemaValidator {
private static final Logger LOGGER =
LoggerFactory.getLogger(TableHeaderSchemaValidator.class);
- private final ClusterConfigTaskExecutor configTaskExecutor =
- ClusterConfigTaskExecutor.getInstance();
+ private final ClusterConfigTaskExecutor configTaskExecutor;
private TableHeaderSchemaValidator() {
- // do nothing
+ this(ClusterConfigTaskExecutor.getInstance());
+ }
+
+ TableHeaderSchemaValidator(final ClusterConfigTaskExecutor
configTaskExecutor) {
+ this.configTaskExecutor = configTaskExecutor;
}
private static class TableHeaderSchemaValidatorHolder {
@@ -213,6 +218,10 @@ public class TableHeaderSchemaValidator {
boolean refreshed = false;
boolean noField = true;
+ // Keep this cache scoped to the validation request. ConfigNode does not
notify DataNode when
+ // DROP COLUMN commits, so a cross-request cache could reject a recreated
column as still
+ // being deleted.
+ Set<String> preDeletedColumns = null;
for (final ColumnSchema columnSchema : inputColumnList) {
TsTableColumnSchema existingColumn =
table.getColumnSchema(columnSchema.getName());
if (Objects.isNull(existingColumn)) {
@@ -225,6 +234,12 @@ public class TableHeaderSchemaValidator {
existingColumn = table.getColumnSchema(columnSchema.getName());
}
if (Objects.isNull(existingColumn)) {
+ if (preDeletedColumns == null) {
+ preDeletedColumns =
+ configTaskExecutor.getPreDeletedColumns(database,
tableSchema.getTableName());
+ }
+ checkColumnNotPreDeleted(
+ database, tableSchema.getTableName(), columnSchema.getName(),
preDeletedColumns);
// check arguments for column auto creation
if (columnSchema.getColumnCategory() == null) {
throw new SemanticException(
@@ -397,6 +412,10 @@ public class TableHeaderSchemaValidator {
boolean refreshed = false;
boolean noField = true;
boolean hasAttribute = false;
+ // Keep this cache scoped to the validation request. ConfigNode does not
notify DataNode when
+ // DROP COLUMN commits, so a cross-request cache could reject a recreated
column as still
+ // being deleted.
+ Set<String> preDeletedColumns = null;
// Track TAG column measurement indices for batch processing after
validation loop
// LinkedHashMap maintains insertion order, key is column name, value is
measurement index
@@ -432,6 +451,12 @@ public class TableHeaderSchemaValidator {
}
if (Objects.isNull(existingColumn)) {
+ if (preDeletedColumns == null) {
+ preDeletedColumns =
+ configTaskExecutor.getPreDeletedColumns(database,
measurementInfo.getTableName());
+ }
+ checkColumnNotPreDeleted(
+ database, measurementInfo.getTableName(), measurementName,
preDeletedColumns);
// Check arguments for column auto creation
if (category == null) {
throw new SemanticException(
@@ -539,6 +564,16 @@ public class TableHeaderSchemaValidator {
}
}
+ private static void checkColumnNotPreDeleted(
+ final String database,
+ final String tableName,
+ final String columnName,
+ final Set<String> preDeletedColumns) {
+ if (preDeletedColumns.contains(columnName)) {
+ throw new SemanticException(new ColumnInDeletionException(database,
tableName, columnName));
+ }
+ }
+
private void autoCreateTableFromMeasurementInfo(
final MPPQueryContext context,
final String database,
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/table/DataNodeTableCache.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/table/DataNodeTableCache.java
index 23daf268206..39520259da1 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/table/DataNodeTableCache.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/table/DataNodeTableCache.java
@@ -26,6 +26,7 @@ import org.apache.iotdb.commons.consensus.ConfigRegionId;
import org.apache.iotdb.commons.exception.IoTDBRuntimeException;
import
org.apache.iotdb.commons.exception.MetadataLeaseFencedException.LeaseFencedRetryPolicy;
import org.apache.iotdb.commons.exception.SemanticException;
+import org.apache.iotdb.commons.exception.table.TableInDeletionException;
import org.apache.iotdb.commons.schema.table.NonCommittableTsTable;
import org.apache.iotdb.commons.schema.table.PreDeleteTsTable;
import org.apache.iotdb.commons.schema.table.TableNodeStatus;
@@ -87,6 +88,13 @@ public class DataNodeTableCache implements ITableCache {
private final Map<String, Map<String, Pair<TsTable, Long>>> specialStatusMap
=
new ConcurrentHashMap<>();
+ /**
+ * The cache entry replaced by the latest pre-update. It lets a rollback
restore a schema that may
+ * already have been promoted from {@code specialStatusMap} by a concurrent
fetch. A {@link
+ * NonCommittableTsTable} value means that the previous schema is
unavailable after a restart.
+ */
+ private final Map<String, Map<String, TsTable>> previousTableMap = new
ConcurrentHashMap<>();
+
private final ReentrantReadWriteLock readWriteLock = new
ReentrantReadWriteLock();
private final Semaphore fetchTableSemaphore =
new Semaphore(
@@ -119,6 +127,7 @@ public class DataNodeTableCache implements ITableCache {
TsTableInternalRPCUtil.deserializeTableInitializationInfo(tableInitializationBytes);
final Map<String, List<TsTable>> usingMap = tableInfo.left;
final Map<String, List<TsTable>> specialStatusMap = tableInfo.right;
+ previousTableMap.clear();
usingMap.forEach(
(key, value) ->
databaseTableMap.put(
@@ -141,6 +150,19 @@ public class DataNodeTableCache implements ITableCache {
table -> new Pair<>(table, 0L),
(v1, v2) -> v2,
ConcurrentHashMap::new))));
+ specialStatusMap.forEach(
+ (key, value) ->
+ value.stream()
+ .filter(NonCommittableTsTable.class::isInstance)
+ .forEach(
+ table ->
+ previousTableMap
+ .computeIfAbsent(
+ PathUtils.unQualifyDatabaseName(key),
+ database -> new ConcurrentHashMap<>())
+ .put(
+ table.getTableName(),
+ new
NonCommittableTsTable(table.getTableName()))));
LOGGER.info(DataNodeSchemaMessages.INIT_TABLE_CACHE_SUCCESS);
} finally {
readWriteLock.writeLock().unlock();
@@ -183,6 +205,14 @@ public class DataNodeTableCache implements ITableCache {
readWriteLock.writeLock().lock();
try {
failIfMetadataLeaseFenced(LeaseFencedRetryPolicy.RETRY_UNTIL_SUCCESS);
+ if (oldName == null && !(table instanceof PreDeleteTsTable)) {
+ final TsTable previousTable = getTableFromCache(database,
table.getTableName());
+ if (previousTable != null) {
+ previousTableMap
+ .computeIfAbsent(database, k -> new ConcurrentHashMap<>())
+ .putIfAbsent(table.getTableName(), new TsTable(previousTable));
+ }
+ }
specialStatusMap
.computeIfAbsent(database, k -> new ConcurrentHashMap<>())
.compute(
@@ -239,13 +269,47 @@ public class DataNodeTableCache implements ITableCache {
failIfMetadataLeaseFenced(LeaseFencedRetryPolicy.RETRY_UNTIL_SUCCESS);
// if rollback the drop table procedure, do nothing,
// wait for triggering the action of pull table from CN
- final TsTable table = getTableFromSpecialStatusMap(database, tableName);
+ final Map<String, Pair<TsTable, Long>> databaseSpecialStatusMap =
+ specialStatusMap.get(database);
+ final Pair<TsTable, Long> tableStatusPair =
+ databaseSpecialStatusMap == null ? null :
databaseSpecialStatusMap.get(tableName);
+ final TsTable table = tableStatusPair == null ? null :
tableStatusPair.getLeft();
+ final TsTable previousTable =
+ previousTableMap.containsKey(database)
+ ? previousTableMap.get(database).get(tableName)
+ : null;
if (table instanceof PreDeleteTsTable) {
+ removePreviousTable(database, tableName);
+ return;
+ }
+ // A null pending value with no saved previous schema means commit
already consumed this
+ // update. A delayed rollback must not evict the committed table.
+ if (Objects.isNull(oldName) && table == null && previousTable == null) {
return;
}
removeTableFromSpecialStatusMap(database, tableName);
+ removePreviousTable(database, tableName);
LOGGER.info(DataNodeSchemaMessages.ROLLBACK_UPDATE_TABLE_SUCCESS,
database, tableName);
+ // A table fetched while the update was pending can already be in
databaseTableMap and the
+ // special entry can consequently have a null left value. Restore the
snapshot captured at
+ // PRE_UPDATE time, or evict the entry so the next lookup must fetch the
canonical CN schema.
+ if (Objects.isNull(oldName) && tableStatusPair != null) {
+ if (previousTable != null && !(previousTable instanceof
NonCommittableTsTable)) {
+ databaseTableMap
+ .computeIfAbsent(database, k -> new ConcurrentHashMap<>())
+ .put(tableName, previousTable);
+ } else if (databaseTableMap.containsKey(database)) {
+ databaseTableMap.get(database).remove(tableName);
+ }
+ if (previousTable == null || previousTable instanceof
NonCommittableTsTable) {
+ // The previous schema is unavailable after a restart or when this
was a newly-created
+ // table. Keep a non-committable marker so getTable() fetches the
canonical CN state
+ // instead of serving a stale entry or permanently treating the
cache as already handled.
+ tableStatusPair.setLeft(new NonCommittableTsTable(tableName));
+ }
+ }
+
// If rename table
if (Objects.nonNull(oldName)) {
// Equals to commit update
@@ -313,6 +377,15 @@ public class DataNodeTableCache implements ITableCache {
});
}
+ private void removePreviousTable(final String database, final String
tableName) {
+ previousTableMap.computeIfPresent(
+ database,
+ (k, v) -> {
+ v.remove(tableName);
+ return v.isEmpty() ? null : v;
+ });
+ }
+
@Override
public void commitUpdateTable(
String database, final String tableName, final @Nullable String oldName)
{
@@ -330,6 +403,7 @@ public class DataNodeTableCache implements ITableCache {
if (Objects.nonNull(oldName)) {
removeTableFromSpecialStatusMap(database, oldName);
}
+ removePreviousTable(database, tableName);
return;
}
// Cannot be committed, consider:
@@ -359,6 +433,7 @@ public class DataNodeTableCache implements ITableCache {
LOGGER.info(DataNodeSchemaMessages.COMMIT_UPDATE_TABLE_SUCCESS,
database, tableName);
}
removeTableFromSpecialStatusMap(database, tableName);
+ removePreviousTable(database, tableName);
if (Objects.nonNull(oldName)) {
removeTableFromSpecialStatusMap(database, oldName);
LOGGER.info(DataNodeSchemaMessages.RENAME_OLD_TABLE_SUCCESS, database,
oldName);
@@ -374,6 +449,7 @@ public class DataNodeTableCache implements ITableCache {
databaseTableMap.get(database).remove(tableName);
}
removeTableFromSpecialStatusMap(database, tableName);
+ removePreviousTable(database, tableName);
LOGGER.info(DataNodeSchemaMessages.COMMIT_DELETE_TABLE_SUCCESS, database,
tableName);
}
@@ -384,6 +460,7 @@ public class DataNodeTableCache implements ITableCache {
try {
databaseTableMap.remove(database);
specialStatusMap.remove(database);
+ previousTableMap.remove(database);
instanceVersion.incrementAndGet();
} finally {
readWriteLock.writeLock().unlock();
@@ -401,6 +478,7 @@ public class DataNodeTableCache implements ITableCache {
try {
databaseTableMap.clear();
specialStatusMap.clear();
+ previousTableMap.clear();
instanceVersion.incrementAndGet();
} finally {
readWriteLock.writeLock().unlock();
@@ -708,11 +786,7 @@ public class DataNodeTableCache implements ITableCache {
instanceVersion.incrementAndGet();
}
if (targetTableIsStillDeleting) {
- throw new SemanticException(
- String.format(
- DataNodeSchemaMessages.THE_TABLE_IS_IN_PRE_DELETE_STATE,
- targetDatabase,
- targetTable));
+ throw new SemanticException(new
TableInDeletionException(targetDatabase, targetTable));
}
} finally {
readWriteLock.writeLock().unlock();
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TreeViewTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TreeViewTest.java
index 959bd4b9d6a..68f2b7fc3f6 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TreeViewTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/analyzer/TreeViewTest.java
@@ -24,6 +24,7 @@ import org.apache.iotdb.commons.schema.table.TsTable;
import org.apache.iotdb.db.queryengine.plan.planner.plan.LogicalQueryPlan;
import org.apache.iotdb.db.queryengine.plan.relational.planner.PlanTester;
import
org.apache.iotdb.db.queryengine.plan.relational.planner.assertions.PlanMatchPattern;
+import org.apache.iotdb.db.schemaengine.lease.MetadataLeaseManager;
import org.apache.iotdb.db.schemaengine.table.DataNodeTableCache;
import com.google.common.collect.ImmutableList;
@@ -65,6 +66,8 @@ public class TreeViewTest {
@Before
public void setup() {
+ MetadataLeaseManager.getInstance().updateFenceThresholdMs(Long.MAX_VALUE);
+ MetadataLeaseManager.getInstance().recoveryLeaseForTest(true);
TsTable tsTable = new TsTable(DEVICE_VIEW_TEST_TABLE);
tsTable.addProp(TsTable.TTL_PROPERTY, Long.MAX_VALUE + "");
tsTable.addProp(TreeViewSchema.TREE_PATH_PATTERN, "root.test" + ".**");
@@ -75,6 +78,8 @@ public class TreeViewTest {
@After
public void tearDown() {
DataNodeTableCache.getInstance().invalid(TREE_VIEW_DB);
+ MetadataLeaseManager.getInstance().updateFenceThresholdMs(20_000);
+ MetadataLeaseManager.getInstance().recoveryLeaseForTest(true);
}
@Test
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableHeaderSchemaValidatorTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableHeaderSchemaValidatorTest.java
new file mode 100644
index 00000000000..354d21fa724
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/relational/metadata/fetcher/TableHeaderSchemaValidatorTest.java
@@ -0,0 +1,251 @@
+/*
+ * 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.relational.metadata.fetcher;
+
+import org.apache.iotdb.commons.exception.SemanticException;
+import org.apache.iotdb.commons.exception.table.ColumnInDeletionException;
+import
org.apache.iotdb.commons.queryengine.plan.relational.metadata.ColumnSchema;
+import
org.apache.iotdb.commons.queryengine.plan.relational.metadata.TableSchema;
+import org.apache.iotdb.commons.schema.table.InsertNodeMeasurementInfo;
+import org.apache.iotdb.commons.schema.table.TsTable;
+import org.apache.iotdb.commons.schema.table.column.AttributeColumnSchema;
+import org.apache.iotdb.commons.schema.table.column.FieldColumnSchema;
+import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
+import org.apache.iotdb.db.conf.IoTDBConfig;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import
org.apache.iotdb.db.queryengine.plan.analyze.lock.DataNodeSchemaLockManager;
+import
org.apache.iotdb.db.queryengine.plan.execution.config.executor.ClusterConfigTaskExecutor;
+import org.apache.iotdb.db.schemaengine.table.DataNodeTableCache;
+import org.apache.iotdb.db.schemaengine.table.ITableCache;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.enums.CompressionType;
+import org.apache.tsfile.file.metadata.enums.TSEncoding;
+import org.apache.tsfile.read.common.type.TypeFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class TableHeaderSchemaValidatorTest {
+ private static final String DATABASE = "pre_delete_write_test";
+ private static final String TABLE = "table1";
+ private final ITableCache cache = DataNodeTableCache.getInstance();
+ private final IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
+ private final MPPQueryContext context = new MPPQueryContext(new
QueryId("pre_delete_write_test"));
+ private ClusterConfigTaskExecutor executor;
+ private TableHeaderSchemaValidator validator;
+ private boolean autoCreateSchema;
+ private boolean partialInsert;
+
+ @Before
+ public void setUp() {
+ autoCreateSchema = config.isAutoCreateSchemaEnabled();
+ partialInsert = config.isEnablePartialInsert();
+ executor = Mockito.mock(ClusterConfigTaskExecutor.class);
+ validator = new TableHeaderSchemaValidator(executor);
+ cache.invalid(DATABASE);
+ final TsTable table = new TsTable(TABLE);
+ table.addColumnSchema(
+ new FieldColumnSchema("live", TSDataType.INT32, TSEncoding.RLE,
CompressionType.LZ4));
+ table.addColumnSchema(
+ new FieldColumnSchema("field", TSDataType.INT32, TSEncoding.RLE,
CompressionType.LZ4));
+ table.addColumnSchema(new AttributeColumnSchema("attribute",
TSDataType.STRING));
+ cache.preUpdateTable(DATABASE, table, null);
+ cache.commitUpdateTable(DATABASE, TABLE, null);
+ // A DROP COLUMN invalidates the DataNode cache before removing ConfigNode
metadata.
+ cache.invalid(DATABASE, TABLE, "field");
+ cache.invalid(DATABASE, TABLE, "attribute");
+ Mockito.when(executor.getPreDeletedColumns(DATABASE, TABLE))
+ .thenReturn(new java.util.HashSet<>(Arrays.asList("field",
"attribute")));
+ }
+
+ @After
+ public void tearDown() {
+ DataNodeSchemaLockManager.getInstance().releaseReadLock(context);
+ cache.invalid(DATABASE);
+ config.setAutoCreateSchemaEnabled(autoCreateSchema);
+ config.setEnablePartialInsert(partialInsert);
+ }
+
+ @Test
+ public void testInsertReportsDeletionBeforeUnknownCategory() {
+ for (final boolean autoCreate : new boolean[] {true, false}) {
+ config.setAutoCreateSchemaEnabled(autoCreate);
+ final InsertNodeMeasurementInfo measurements = measurements("field",
null);
+ final SemanticException error =
+ assertThrows(
+ SemanticException.class,
+ () ->
+ validator.validateInsertNodeMeasurements(
+ DATABASE, measurements, context, true, null, null));
+ assertDeletion(error, "field");
+ }
+ }
+
+ @Test
+ public void testInsertRejectsPreDeletedAttribute() {
+ final SemanticException error =
+ assertThrows(
+ SemanticException.class,
+ () ->
+ validator.validateInsertNodeMeasurements(
+ DATABASE,
+ measurements("attribute", TsTableColumnCategory.ATTRIBUTE),
+ context,
+ true,
+ null,
+ null));
+ assertDeletion(error, "attribute");
+ }
+
+ @Test
+ public void testTsFileLoadRejectsPreDeletedFieldAndAttribute() {
+ for (final boolean autoCreate : new boolean[] {true, false}) {
+ config.setAutoCreateSchemaEnabled(autoCreate);
+ for (final String column : Arrays.asList("field", "attribute")) {
+ final TableSchema tableSchema =
+ new TableSchema(
+ TABLE,
+ Collections.singletonList(
+ new ColumnSchema(
+ column,
+ TypeFactory.getType(
+ column.equals("field") ? TSDataType.INT32 :
TSDataType.STRING),
+ false,
+ column.equals("field")
+ ? TsTableColumnCategory.FIELD
+ : TsTableColumnCategory.ATTRIBUTE)));
+ final SemanticException error =
+ assertThrows(
+ SemanticException.class,
+ () ->
+ validator.validateTableHeaderSchema4TsFile(
+ DATABASE, tableSchema, context, true, false, new
AtomicBoolean()));
+ assertDeletion(error, column);
+ }
+ }
+ }
+
+ @Test
+ public void testExistingColumnsDoNotFetchDeletionStatus() throws Exception {
+ validator.validateInsertNodeMeasurements(
+ DATABASE, measurements("live", TsTableColumnCategory.FIELD), context,
true, null, null);
+ validator.validateTableHeaderSchema4TsFile(
+ DATABASE,
+ new TableSchema(
+ TABLE,
+ Collections.singletonList(
+ new ColumnSchema(
+ "live",
+ TypeFactory.getType(TSDataType.INT32),
+ false,
+ TsTableColumnCategory.FIELD))),
+ context,
+ true,
+ false,
+ new AtomicBoolean());
+ Mockito.verify(executor, Mockito.never())
+ .getPreDeletedColumns(Mockito.anyString(), Mockito.anyString());
+ }
+
+ @Test
+ public void testMissingColumnStillReportsUnknownCategory() {
+ final SemanticException error =
+ assertThrows(
+ SemanticException.class,
+ () ->
+ validator.validateInsertNodeMeasurements(
+ DATABASE, measurements("missing", null), context, true,
null, null));
+ assertEquals(TSStatusCode.COLUMN_NOT_EXISTS.getStatusCode(),
error.getErrorCode());
+ }
+
+ @Test
+ public void testMissingColumnsFetchDeletionStatusOnce() {
+ config.setAutoCreateSchemaEnabled(false);
+ config.setEnablePartialInsert(true);
+ final InsertNodeMeasurementInfo measurements =
Mockito.mock(InsertNodeMeasurementInfo.class);
+ Mockito.when(measurements.getTableName()).thenReturn(TABLE);
+ Mockito.when(measurements.getMeasurementCount()).thenReturn(2);
+ Mockito.when(measurements.getColumnCategories())
+ .thenReturn(
+ new TsTableColumnCategory[] {TsTableColumnCategory.FIELD,
TsTableColumnCategory.FIELD});
+ Mockito.when(measurements.getMeasurementName(0)).thenReturn("missing1");
+ Mockito.when(measurements.getMeasurementName(1)).thenReturn("missing2");
+ validator.validateInsertNodeMeasurements(DATABASE, measurements, context,
true, null, null);
+ Mockito.verify(executor).getPreDeletedColumns(DATABASE, TABLE);
+ }
+
+ @Test
+ public void testTsFileMissingColumnsFetchDeletionStatusOnce() throws
Exception {
+ config.setAutoCreateSchemaEnabled(false);
+ config.setEnablePartialInsert(true);
+ final TableSchema tableSchema =
+ new TableSchema(
+ TABLE,
+ Arrays.asList(
+ new ColumnSchema(
+ "missing1",
+ TypeFactory.getType(TSDataType.INT32),
+ false,
+ TsTableColumnCategory.FIELD),
+ new ColumnSchema(
+ "missing2",
+ TypeFactory.getType(TSDataType.INT32),
+ false,
+ TsTableColumnCategory.FIELD)));
+
+ validator.validateTableHeaderSchema4TsFile(
+ DATABASE, tableSchema, context, true, false, new AtomicBoolean());
+
+ Mockito.verify(executor).getPreDeletedColumns(DATABASE, TABLE);
+ }
+
+ private InsertNodeMeasurementInfo measurements(
+ final String name, final TsTableColumnCategory category) {
+ final InsertNodeMeasurementInfo measurements =
Mockito.mock(InsertNodeMeasurementInfo.class);
+ Mockito.when(measurements.getTableName()).thenReturn(TABLE);
+ Mockito.when(measurements.getMeasurementCount()).thenReturn(1);
+ Mockito.when(measurements.getColumnCategories())
+ .thenReturn(new TsTableColumnCategory[] {category});
+ Mockito.when(measurements.getMeasurementName(0)).thenReturn(name);
+ Mockito.when(measurements.getType(0)).thenReturn(TSDataType.INT32);
+ return measurements;
+ }
+
+ private static void assertDeletion(final SemanticException error, final
String column) {
+ assertEquals(TSStatusCode.SEMANTIC_ERROR.getStatusCode(),
error.getErrorCode());
+ assertTrue(error.getCause() instanceof ColumnInDeletionException);
+ assertEquals(
+ new ColumnInDeletionException(DATABASE, TABLE, column).getMessage(),
+ error.getCause().getMessage());
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/table/DataNodeTableCacheTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/table/DataNodeTableCacheTest.java
index fd97da843bd..78af8f3eee0 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/table/DataNodeTableCacheTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/schemaengine/table/DataNodeTableCacheTest.java
@@ -19,7 +19,13 @@
package org.apache.iotdb.db.schemaengine.table;
+import
org.apache.iotdb.commons.exception.MetadataLeaseFencedException.LeaseFencedRetryPolicy;
+import org.apache.iotdb.commons.exception.SemanticException;
+import org.apache.iotdb.commons.exception.table.TableInDeletionException;
+import org.apache.iotdb.commons.schema.table.NonCommittableTsTable;
+import org.apache.iotdb.commons.schema.table.PreDeleteTsTable;
import org.apache.iotdb.commons.schema.table.TsTable;
+import org.apache.iotdb.commons.schema.table.TsTableInternalRPCUtil;
import org.apache.iotdb.commons.schema.table.column.FieldColumnSchema;
import org.apache.tsfile.enums.TSDataType;
@@ -29,12 +35,18 @@ import org.junit.Assert;
import org.junit.Test;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import java.util.concurrent.Semaphore;
public class DataNodeTableCacheTest {
private static final String DATABASE = "interrupted_fetch_database";
private static final String TABLE_CACHE_TEST_DATABASE =
"root.table_cache_test";
+ private static final String TABLE_CACHE_TEST_DATABASE_NAME =
"table_cache_test";
private static final String TABLE_NAME = "table1";
@Test
@@ -85,7 +97,99 @@ public class DataNodeTableCacheTest {
cache.rollbackUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
cache.commitUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
- Assert.assertNull(cache.getTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME,
false));
+ Assert.assertFalse(
+ cache
+ .getTableSnapshot()
+ .getOrDefault(TABLE_CACHE_TEST_DATABASE_NAME,
Collections.emptyMap())
+ .containsKey(TABLE_NAME));
+ } finally {
+ cache.invalid(TABLE_CACHE_TEST_DATABASE);
+ }
+ }
+
+ @Test
+ public void rollbackAlteredTableRestoresOriginalSchema() throws Exception {
+ final ITableCache cache = DataNodeTableCache.getInstance();
+ cache.invalid(TABLE_CACHE_TEST_DATABASE);
+ try {
+ cache.preUpdateTable(TABLE_CACHE_TEST_DATABASE, createTable(TABLE_NAME),
null);
+ cache.commitUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
+
+ final TsTable alteredTable = createTable(TABLE_NAME);
+ ((FieldColumnSchema)
alteredTable.getColumnSchema("s1")).setDataType(TSDataType.DOUBLE);
+ cache.preUpdateTable(TABLE_CACHE_TEST_DATABASE, alteredTable, null);
+
+ // A concurrent fetch may promote the pending table into the regular
cache before rollback.
+ // Keep that path covered because rollback must still restore the
pre-update schema.
+ final Method updateUsingTable =
+ DataNodeTableCache.class.getDeclaredMethod(
+ "updateUsingTable", Map.class, Map.class,
LeaseFencedRetryPolicy.class);
+ updateUsingTable.setAccessible(true);
+ final Map<String, Map<String, TsTable>> fetchedTables = new HashMap<>();
+ fetchedTables.put(
+ TABLE_CACHE_TEST_DATABASE, Collections.singletonMap(TABLE_NAME,
alteredTable));
+ final Map<String, Map<String, Long>> previousVersions = new HashMap<>();
+ previousVersions.put(
+ TABLE_CACHE_TEST_DATABASE_NAME, Collections.singletonMap(TABLE_NAME,
1L));
+ updateUsingTable.invoke(
+ cache, fetchedTables, previousVersions,
LeaseFencedRetryPolicy.RETRY_UNTIL_SUCCESS);
+
+ cache.rollbackUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
+ Assert.assertEquals(
+ TSDataType.INT32,
+ cache
+ .getTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME)
+ .getColumnSchema("s1")
+ .getDataType());
+ } finally {
+ cache.invalid(TABLE_CACHE_TEST_DATABASE);
+ }
+ }
+
+ @Test
+ public void delayedRollbackDoesNotEvictCommittedAlteredSchema() {
+ final ITableCache cache = DataNodeTableCache.getInstance();
+ cache.invalid(TABLE_CACHE_TEST_DATABASE);
+ try {
+ cache.preUpdateTable(TABLE_CACHE_TEST_DATABASE, createTable(TABLE_NAME),
null);
+ cache.commitUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
+
+ final TsTable alteredTable = createTable(TABLE_NAME);
+ ((FieldColumnSchema)
alteredTable.getColumnSchema("s1")).setDataType(TSDataType.DOUBLE);
+ cache.preUpdateTable(TABLE_CACHE_TEST_DATABASE, alteredTable, null);
+ cache.commitUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
+
+ cache.rollbackUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
+ Assert.assertEquals(
+ TSDataType.DOUBLE,
+ cache
+ .getTableSnapshot()
+ .get(TABLE_CACHE_TEST_DATABASE_NAME)
+ .get(TABLE_NAME)
+ .getColumnSchema("s1")
+ .getDataType());
+ } finally {
+ cache.invalid(TABLE_CACHE_TEST_DATABASE);
+ }
+ }
+
+ @Test
+ public void delayedRollbackDoesNotRestoreCommittedDeletedTable() {
+ final ITableCache cache = DataNodeTableCache.getInstance();
+ cache.invalid(TABLE_CACHE_TEST_DATABASE);
+ try {
+ cache.preUpdateTable(TABLE_CACHE_TEST_DATABASE, createTable(TABLE_NAME),
null);
+ cache.commitUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
+
+ cache.preUpdateTable(TABLE_CACHE_TEST_DATABASE, new
PreDeleteTsTable(TABLE_NAME), null);
+ cache.commitUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
+
+ cache.rollbackUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
+ Assert.assertFalse(
+ cache
+ .getTableSnapshot()
+ .getOrDefault(TABLE_CACHE_TEST_DATABASE_NAME,
Collections.emptyMap())
+ .containsKey(TABLE_NAME));
} finally {
cache.invalid(TABLE_CACHE_TEST_DATABASE);
}
@@ -111,12 +215,97 @@ public class DataNodeTableCacheTest {
}
}
+ @Test
+ public void rollbackAfterRestartEvictsUnknownPreviousSchema() {
+ final ITableCache cache = DataNodeTableCache.getInstance();
+ cache.invalid(TABLE_CACHE_TEST_DATABASE);
+ try {
+ final TsTable alteredTable = createTable(TABLE_NAME);
+ ((FieldColumnSchema)
alteredTable.getColumnSchema("s1")).setDataType(TSDataType.DOUBLE);
+ final byte[] initializationBytes =
+ TsTableInternalRPCUtil.serializeTableInitializationInfo(
+ Collections.singletonMap(
+ TABLE_CACHE_TEST_DATABASE,
Collections.singletonList(alteredTable)),
+ Collections.singletonMap(
+ TABLE_CACHE_TEST_DATABASE,
+ Collections.singletonList(new
NonCommittableTsTable(TABLE_NAME))));
+ cache.init(initializationBytes);
+
+ // A restart cannot retain the in-memory pre-update snapshot. Evict the
potentially stale
+ // schema even if the recovered procedure repeats PRE_UPDATE, and fail
closed until the
+ // canonical schema can be fetched from the CN.
+ cache.preUpdateTable(TABLE_CACHE_TEST_DATABASE, alteredTable, null);
+ cache.rollbackUpdateTable(TABLE_CACHE_TEST_DATABASE, TABLE_NAME, null);
+ Assert.assertFalse(
+ cache
+ .getTableSnapshot()
+ .getOrDefault(TABLE_CACHE_TEST_DATABASE_NAME,
Collections.emptyMap())
+ .containsKey(TABLE_NAME));
+ } finally {
+ cache.invalid(TABLE_CACHE_TEST_DATABASE);
+ }
+ }
+
private Semaphore getFetchTableSemaphore(final ITableCache cache) throws
Exception {
final Field field =
DataNodeTableCache.class.getDeclaredField("fetchTableSemaphore");
field.setAccessible(true);
return (Semaphore) field.get(cache);
}
+ @Test
+ public void preDeletedTableRefreshReportsDeletionAndRecovers() throws
Exception {
+ final ITableCache cache = DataNodeTableCache.getInstance();
+ final Method updateDeleteTable =
+ DataNodeTableCache.class.getDeclaredMethod(
+ "updateDeleteTable",
+ Map.class,
+ String.class,
+ String.class,
+ LeaseFencedRetryPolicy.class);
+ updateDeleteTable.setAccessible(true);
+ final String database = "pre_delete_table_test";
+ cache.invalid(database);
+ try {
+ cache.preUpdateTable(database, new PreDeleteTsTable(TABLE_NAME), null);
+ final InvocationTargetException failure =
+ Assert.assertThrows(
+ InvocationTargetException.class,
+ () ->
+ updateDeleteTable.invoke(
+ cache,
+ Collections.singletonMap(
+ database,
+ Collections.singletonMap(TABLE_NAME, new
PreDeleteTsTable(TABLE_NAME))),
+ database,
+ TABLE_NAME,
+ LeaseFencedRetryPolicy.RETRY_UNTIL_SUCCESS));
+ Assert.assertTrue(failure.getCause() instanceof SemanticException);
+ Assert.assertEquals(
+ new TableInDeletionException(database, TABLE_NAME).getMessage(),
+ failure.getCause().getCause().getMessage());
+
+ updateDeleteTable.invoke(
+ cache,
+ Collections.singletonMap(
+ database, Collections.singletonMap(TABLE_NAME,
createTable(TABLE_NAME))),
+ database,
+ TABLE_NAME,
+ LeaseFencedRetryPolicy.RETRY_UNTIL_SUCCESS);
+ Assert.assertNotNull(cache.getTableInWrite(database, TABLE_NAME));
+
+ cache.preUpdateTable(database, new PreDeleteTsTable(TABLE_NAME), null);
+ updateDeleteTable.invoke(
+ cache,
+ Collections.singletonMap(database,
Collections.singletonMap(TABLE_NAME, null)),
+ database,
+ TABLE_NAME,
+ LeaseFencedRetryPolicy.RETRY_UNTIL_SUCCESS);
+ Assert.assertNull(cache.getTableInWrite(database, TABLE_NAME));
+ } finally {
+ cache.invalid(database);
+ }
+ }
+
private TsTable createTable(final String tableName) {
final TsTable table = new TsTable(tableName);
table.addColumnSchema(
diff --git
a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/CommonMessages.java
b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/CommonMessages.java
index a05c8f9915a..806581d0e09 100644
---
a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/CommonMessages.java
+++
b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/CommonMessages.java
@@ -335,4 +335,10 @@ public final class CommonMessages {
public static final String
EXCEPTION_SNAPSHOT_BUFFER_SIZE_MUST_NOT_EXCEED_ARG_BYTES_BUT_WAS_ARG_D1DA6F7E =
"Snapshot buffer size must not exceed %d bytes, but was %d.";
+ public static final String
EXCEPTION_TABLE_ARG_ARG_IS_BEING_DELETED_PLEASE_WAIT_FOR_DELETION_TO_FINISH_OR_RETRY_DROP_TABLE_IF_IT_IS_STUCK_7E22D78F
=
+ "Table '%s.%s' is being deleted. Please wait for deletion to finish, or
retry DROP TABLE if it is stuck.";
+ public static final String
EXCEPTION_COLUMN_ARG_IN_TABLE_ARG_ARG_IS_BEING_DELETED_PLEASE_WAIT_FOR_DELETION_TO_FINISH_OR_RETRY_DROPPING_THE_COLUMN_IF_IT_IS_STUCK_875DAFFE
=
+ "Column '%s' in table '%s.%s' is being deleted. Please wait for deletion
to finish, or retry dropping the column if it is stuck.";
+ public static final String
EXCEPTION_COLUMN_ARG_IN_TABLE_ARG_ARG_IS_BEING_ALTERED_PLEASE_WAIT_FOR_ALTERATION_TO_FINISH_OR_RETRY_ALTERING_THE_COLUMN_IF_IT_IS_STUCK_11155B55
=
+ "Column '%s' in table '%s.%s' is being altered. Please wait for
alteration to finish, or retry altering the column if it is stuck.";
}
diff --git
a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/CommonMessages.java
b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/CommonMessages.java
index c5c6290687e..76ce8769aa1 100644
---
a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/CommonMessages.java
+++
b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/CommonMessages.java
@@ -232,4 +232,10 @@ public final class CommonMessages {
public static final String
EXCEPTION_SNAPSHOT_BUFFER_SIZE_MUST_NOT_EXCEED_ARG_BYTES_BUT_WAS_ARG_D1DA6F7E =
"快照缓冲区大小不得超过 %d 字节,但实际为 %d。";
+ public static final String
EXCEPTION_TABLE_ARG_ARG_IS_BEING_DELETED_PLEASE_WAIT_FOR_DELETION_TO_FINISH_OR_RETRY_DROP_TABLE_IF_IT_IS_STUCK_7E22D78F
=
+ "表 '%s.%s' 正在删除中。请等待删除完成;如果删除一直未完成,请重试 DROP TABLE。";
+ public static final String
EXCEPTION_COLUMN_ARG_IN_TABLE_ARG_ARG_IS_BEING_DELETED_PLEASE_WAIT_FOR_DELETION_TO_FINISH_OR_RETRY_DROPPING_THE_COLUMN_IF_IT_IS_STUCK_875DAFFE
=
+ "列 '%s'(位于表 '%s.%s')正在删除中。请等待删除完成;如果删除一直未完成,请重试删除该列。";
+ public static final String
EXCEPTION_COLUMN_ARG_IN_TABLE_ARG_ARG_IS_BEING_ALTERED_PLEASE_WAIT_FOR_ALTERATION_TO_FINISH_OR_RETRY_ALTERING_THE_COLUMN_IF_IT_IS_STUCK_11155B55
=
+ "列 '%s'(位于表 '%s.%s')正在修改中。请等待修改完成;如果修改一直未完成,请重试修改该列。";
}
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/exception/table/ColumnInAlterException.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/exception/table/ColumnInAlterException.java
new file mode 100644
index 00000000000..20f24abe062
--- /dev/null
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/exception/table/ColumnInAlterException.java
@@ -0,0 +1,40 @@
+/*
+ * 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.commons.exception.table;
+
+import org.apache.iotdb.commons.exception.MetadataException;
+import org.apache.iotdb.commons.i18n.CommonMessages;
+import org.apache.iotdb.commons.utils.PathUtils;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+public class ColumnInAlterException extends MetadataException {
+
+ public ColumnInAlterException(
+ final String database, final String tableName, final String columnName) {
+ super(
+ String.format(
+ CommonMessages
+
.EXCEPTION_COLUMN_ARG_IN_TABLE_ARG_ARG_IS_BEING_ALTERED_PLEASE_WAIT_FOR_ALTERATION_TO_FINISH_OR_RETRY_ALTERING_THE_COLUMN_IF_IT_IS_STUCK_11155B55,
+ columnName,
+ PathUtils.unQualifyDatabaseName(database),
+ tableName),
+ TSStatusCode.SEMANTIC_ERROR.getStatusCode());
+ }
+}
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/exception/table/ColumnInDeletionException.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/exception/table/ColumnInDeletionException.java
new file mode 100644
index 00000000000..b3a6e60be09
--- /dev/null
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/exception/table/ColumnInDeletionException.java
@@ -0,0 +1,40 @@
+/*
+ * 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.commons.exception.table;
+
+import org.apache.iotdb.commons.exception.MetadataException;
+import org.apache.iotdb.commons.i18n.CommonMessages;
+import org.apache.iotdb.commons.utils.PathUtils;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+public class ColumnInDeletionException extends MetadataException {
+
+ public ColumnInDeletionException(
+ final String database, final String tableName, final String columnName) {
+ super(
+ String.format(
+ CommonMessages
+
.EXCEPTION_COLUMN_ARG_IN_TABLE_ARG_ARG_IS_BEING_DELETED_PLEASE_WAIT_FOR_DELETION_TO_FINISH_OR_RETRY_DROPPING_THE_COLUMN_IF_IT_IS_STUCK_875DAFFE,
+ columnName,
+ PathUtils.unQualifyDatabaseName(database),
+ tableName),
+ TSStatusCode.SEMANTIC_ERROR.getStatusCode());
+ }
+}
diff --git
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/exception/table/TableInDeletionException.java
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/exception/table/TableInDeletionException.java
new file mode 100644
index 00000000000..8ec7d18937c
--- /dev/null
+++
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/exception/table/TableInDeletionException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.commons.exception.table;
+
+import org.apache.iotdb.commons.exception.MetadataException;
+import org.apache.iotdb.commons.i18n.CommonMessages;
+import org.apache.iotdb.commons.utils.PathUtils;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+public class TableInDeletionException extends MetadataException {
+
+ public TableInDeletionException(final String database, final String
tableName) {
+ super(
+ String.format(
+ CommonMessages
+
.EXCEPTION_TABLE_ARG_ARG_IS_BEING_DELETED_PLEASE_WAIT_FOR_DELETION_TO_FINISH_OR_RETRY_DROP_TABLE_IF_IT_IS_STUCK_7E22D78F,
+ PathUtils.unQualifyDatabaseName(database),
+ tableName),
+ TSStatusCode.SEMANTIC_ERROR.getStatusCode());
+ }
+}