This is an automated email from the ASF dual-hosted git repository.
luoyuxia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git
The following commit(s) were added to refs/heads/main by this push:
new e2a44039c [server] Support adding aggregation columns (#3459)
e2a44039c is described below
commit e2a44039c303882f58bf9c671501eda1799e1bb1
Author: Liurnly <[email protected]>
AuthorDate: Fri Jul 3 16:23:46 2026 +0800
[server] Support adding aggregation columns (#3459)
---
.../fluss/client/utils/ClientRpcMessageUtils.java | 10 +++
.../fluss/client/admin/FlussAdminITCase.java | 74 ++++++++++++++++++++++
.../org/apache/fluss/metadata/TableChange.java | 55 ++++++++++++----
fluss-rpc/src/main/proto/FlussApi.proto | 2 +
.../fluss/server/coordinator/MetadataManager.java | 2 +
.../fluss/server/coordinator/SchemaUpdate.java | 7 +-
.../fluss/server/utils/ServerRpcMessageUtils.java | 43 ++++++++++---
.../server/utils/TableDescriptorValidation.java | 31 +++++++++
8 files changed, 201 insertions(+), 23 deletions(-)
diff --git
a/fluss-client/src/main/java/org/apache/fluss/client/utils/ClientRpcMessageUtils.java
b/fluss-client/src/main/java/org/apache/fluss/client/utils/ClientRpcMessageUtils.java
index 2df5b91ef..70f940c3c 100644
---
a/fluss-client/src/main/java/org/apache/fluss/client/utils/ClientRpcMessageUtils.java
+++
b/fluss-client/src/main/java/org/apache/fluss/client/utils/ClientRpcMessageUtils.java
@@ -41,6 +41,7 @@ import org.apache.fluss.config.cluster.ConfigEntry;
import org.apache.fluss.fs.FsPath;
import org.apache.fluss.fs.FsPathAndFileName;
import org.apache.fluss.fs.token.ObtainedSecurityToken;
+import org.apache.fluss.metadata.AggFunction;
import org.apache.fluss.metadata.DatabaseChange;
import org.apache.fluss.metadata.DatabaseSummary;
import org.apache.fluss.metadata.PartitionInfo;
@@ -690,6 +691,15 @@ public class ClientRpcMessageUtils {
if (addColumn.getComment() != null) {
pbAddColumn.setComment(addColumn.getComment());
}
+ if (addColumn.getAggFunction().isPresent()) {
+ AggFunction aggFunction = addColumn.getAggFunction().get();
+ pbAddColumn.setAggFunctionType(aggFunction.getType().toString());
+ aggFunction
+ .getParameters()
+ .forEach(
+ (key, value) ->
+
pbAddColumn.addAggFunctionParam().setKey(key).setValue(value));
+ }
return pbAddColumn;
}
diff --git
a/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
b/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
index 68181844e..3d6b48dfc 100644
---
a/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
+++
b/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
@@ -120,6 +120,7 @@ import static
org.apache.fluss.metadata.DataLakeFormat.PAIMON;
import static
org.apache.fluss.record.TestData.DATA1_PARTITIONED_TABLE_DESCRIPTOR;
import static org.apache.fluss.record.TestData.DATA1_SCHEMA;
import static org.apache.fluss.testutils.DataTestUtils.row;
+import static org.apache.fluss.testutils.InternalRowAssert.assertThatRow;
import static org.apache.fluss.testutils.common.CommonTestUtils.waitUntil;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -568,6 +569,73 @@ class FlussAdminITCase extends ClientToServerITCaseBase {
.hasMessageContaining("Column nested_row already exists");
}
+ @Test
+ void testAlterAggregationTableColumnWithAggFunction() throws Exception {
+ TablePath tablePath = TablePath.of("test_db",
"alter_aggregation_table_column");
+ Map<String, String> properties = new HashMap<>();
+ properties.put(ConfigOptions.TABLE_MERGE_ENGINE.key(), "aggregation");
+ TableDescriptor tableDescriptor =
+ TableDescriptor.builder()
+ .schema(
+ Schema.newBuilder()
+ .column("id", DataTypes.INT())
+ .column("value", DataTypes.BIGINT(),
AggFunctions.SUM())
+ .primaryKey("id")
+ .build())
+ .distributedBy(3, "id")
+ .properties(properties)
+ .build();
+ admin.createTable(tablePath, tableDescriptor, false).get();
+
+ admin.alterTable(
+ tablePath,
+ Collections.singletonList(
+ TableChange.addColumn(
+ "new_value",
+ DataTypes.BIGINT(),
+ "new aggregate column",
+ TableChange.ColumnPosition.last(),
+ AggFunctions.SUM())),
+ false)
+ .get();
+
+ SchemaInfo schemaInfo = admin.getTableSchema(tablePath).get();
+
assertThat(schemaInfo.getSchema().getAggFunction("new_value")).hasValue(AggFunctions.SUM());
+
+ try (Table table = conn.getTable(tablePath)) {
+ UpsertWriter upsertWriter = table.newUpsert().createWriter();
+ upsertWriter.upsert(row(1, 10L, 100L));
+ upsertWriter.upsert(row(1, 20L, 200L));
+ upsertWriter.flush();
+
+ assertThatRow(lookupRow(table.newLookup().createLookuper(),
row(1)))
+ .withSchema(schemaInfo.getSchema().getRowType())
+ .isEqualTo(row(1, 30L, 300L));
+ }
+ }
+
+ @Test
+ void testAlterNonAggregationTableColumnWithAggFunction() throws Exception {
+ TablePath tablePath = TablePath.of("test_db",
"alter_non_aggregation_table_column");
+ admin.createTable(tablePath, DEFAULT_TABLE_DESCRIPTOR, false).get();
+
+ assertThatThrownBy(
+ () ->
+ admin.alterTable(
+ tablePath,
+ Collections.singletonList(
+ TableChange.addColumn(
+ "new_value",
+
DataTypes.BIGINT(),
+ "new aggregate
column",
+
TableChange.ColumnPosition.last(),
+
AggFunctions.SUM())),
+ false)
+ .get())
+ .hasMessageContaining(
+ "Aggregation function is only supported for
aggregation merge engine table");
+ }
+
@Test
void testCreateInvalidDatabaseAndTable() throws Exception {
assertThatThrownBy(
@@ -675,6 +743,12 @@ class FlussAdminITCase extends ClientToServerITCaseBase {
TableInfo tableInfoAggregate =
admin.getTableInfo(tablePathAggregate).join();
assertThat(tableInfoAggregate.getTableConfig().getDeleteBehavior())
.hasValue(DeleteBehavior.IGNORE);
+ assertThat(tableInfoAggregate.getSchema().getAggFunction("count"))
+ .hasValue(AggFunctions.SUM());
+ assertThat(tableInfoAggregate.getProperties().toMap())
+ .doesNotContainKey("fields.count.agg");
+ assertThat(tableInfoAggregate.getCustomProperties().toMap())
+ .doesNotContainKey("fields.count.agg");
// Test 2.6: AGGREGATION merge engine with delete behavior explicitly
set to ALLOW - should
// be allowed
diff --git
a/fluss-common/src/main/java/org/apache/fluss/metadata/TableChange.java
b/fluss-common/src/main/java/org/apache/fluss/metadata/TableChange.java
index bb027d690..2890ca82e 100644
--- a/fluss-common/src/main/java/org/apache/fluss/metadata/TableChange.java
+++ b/fluss-common/src/main/java/org/apache/fluss/metadata/TableChange.java
@@ -22,6 +22,7 @@ import org.apache.fluss.types.DataType;
import javax.annotation.Nullable;
import java.util.Objects;
+import java.util.Optional;
/** {@link TableChange} represents the modification of the Fluss Table. */
public interface TableChange {
@@ -42,7 +43,21 @@ public interface TableChange {
DataType dataType,
@Nullable String comment,
ColumnPosition position) {
- return new AddColumn(columnName, dataType, comment, position);
+ return new AddColumn(columnName, dataType, comment, position, null);
+ }
+
+ /**
+ * A table change to add the column with specified position and
aggregation function.
+ *
+ * @return a TableChange represents the modification.
+ */
+ static AddColumn addColumn(
+ String columnName,
+ DataType dataType,
+ @Nullable String comment,
+ ColumnPosition position,
+ @Nullable AggFunction aggFunction) {
+ return new AddColumn(columnName, dataType, comment, position,
aggFunction);
}
/**
@@ -230,15 +245,21 @@ public interface TableChange {
private final String name;
private final DataType dataType;
private final @Nullable String comment;
+ private final @Nullable AggFunction aggFunction;
private final ColumnPosition position;
private AddColumn(
- String name, DataType dataType, @Nullable String comment,
ColumnPosition position) {
+ String name,
+ DataType dataType,
+ @Nullable String comment,
+ ColumnPosition position,
+ @Nullable AggFunction aggFunction) {
this.name = name;
this.dataType = dataType;
this.comment = comment;
this.position = position;
+ this.aggFunction = aggFunction;
}
public String getName() {
@@ -258,20 +279,26 @@ public interface TableChange {
return position;
}
+ public Optional<AggFunction> getAggFunction() {
+ return Optional.ofNullable(aggFunction);
+ }
+
@Override
public String toString() {
- return "AddColumn{"
- + "name='"
- + name
- + '\''
- + ", dataType="
- + dataType
- + ", comment='"
- + comment
- + '\''
- + ", position="
- + position
- + '}';
+ String result =
+ "AddColumn{"
+ + "name='"
+ + name
+ + '\''
+ + ", dataType="
+ + dataType
+ + ", comment='"
+ + comment
+ + '\'';
+ if (aggFunction != null) {
+ result += ", aggFunction=" + aggFunction;
+ }
+ return result + ", position=" + position + '}';
}
}
diff --git a/fluss-rpc/src/main/proto/FlussApi.proto
b/fluss-rpc/src/main/proto/FlussApi.proto
index 41c738777..57e96092d 100644
--- a/fluss-rpc/src/main/proto/FlussApi.proto
+++ b/fluss-rpc/src/main/proto/FlussApi.proto
@@ -1193,6 +1193,8 @@ message PbAddColumn {
required bytes data_type_json = 2;
optional string comment = 3;
required int32 column_position_type = 4; // LAST=0,FIRST=1,AFTER=3
+ optional string agg_function_type = 5;
+ repeated PbKeyValue agg_function_params = 6;
}
message PbDropColumn {
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/MetadataManager.java
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/MetadataManager.java
index 106d4ef66..65b2ee24b 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/MetadataManager.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/MetadataManager.java
@@ -76,6 +76,7 @@ import java.util.Set;
import java.util.concurrent.Callable;
import static
org.apache.fluss.server.utils.TableDescriptorValidation.validateAlterTableProperties;
+import static
org.apache.fluss.server.utils.TableDescriptorValidation.validateAlterTableSchema;
/** A manager for metadata. */
public class MetadataManager {
@@ -440,6 +441,7 @@ public class MetadataManager {
if (!schemaChanges.isEmpty()) {
Schema newSchema =
SchemaUpdate.applySchemaChanges(table.getSchema(),
schemaChanges);
+ validateAlterTableSchema(table, newSchema);
LakeCatalog.Context lakeCatalogContext =
new CoordinatorService.DefaultLakeCatalogContext(
false,
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/SchemaUpdate.java
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/SchemaUpdate.java
index d1ad0c16c..e85219a3b 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/SchemaUpdate.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/SchemaUpdate.java
@@ -82,7 +82,12 @@ public class SchemaUpdate {
}
// Delegate the actual addition to the builder
- builder.column(addColumn.getName(), addColumn.getDataType());
+ if (addColumn.getAggFunction().isPresent()) {
+ builder.column(
+ addColumn.getName(), addColumn.getDataType(),
addColumn.getAggFunction().get());
+ } else {
+ builder.column(addColumn.getName(), addColumn.getDataType());
+ }
// Fixed: Use null check for the String comment
String comment = addColumn.getComment();
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/utils/ServerRpcMessageUtils.java
b/fluss-server/src/main/java/org/apache/fluss/server/utils/ServerRpcMessageUtils.java
index 266abe3bc..1e5e914c0 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/utils/ServerRpcMessageUtils.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/utils/ServerRpcMessageUtils.java
@@ -27,9 +27,13 @@ import org.apache.fluss.config.ConfigOptions;
import org.apache.fluss.config.cluster.AlterConfigOpType;
import org.apache.fluss.config.cluster.ColumnPositionType;
import org.apache.fluss.config.cluster.ConfigEntry;
+import org.apache.fluss.exception.InvalidConfigException;
import org.apache.fluss.fs.FsPath;
import org.apache.fluss.fs.token.ObtainedSecurityToken;
import org.apache.fluss.lake.committer.LakeCommitResult;
+import org.apache.fluss.metadata.AggFunction;
+import org.apache.fluss.metadata.AggFunctionType;
+import org.apache.fluss.metadata.AggFunctions;
import org.apache.fluss.metadata.DatabaseChange;
import org.apache.fluss.metadata.DatabaseSummary;
import org.apache.fluss.metadata.PartitionSpec;
@@ -358,17 +362,40 @@ public class ServerRpcMessageUtils {
return addColumns.stream()
.filter(Objects::nonNull)
.map(
- pbAddColumn ->
- TableChange.addColumn(
- pbAddColumn.getColumnName(),
- JsonSerdeUtils.readValue(
- pbAddColumn.getDataTypeJson(),
- DataTypeJsonSerde.INSTANCE),
- pbAddColumn.hasComment() ?
pbAddColumn.getComment() : null,
-
toColumnPosition(pbAddColumn.getColumnPositionType())))
+ pbAddColumn -> {
+ AggFunction aggFunction =
toAggFunction(pbAddColumn);
+ return TableChange.addColumn(
+ pbAddColumn.getColumnName(),
+ JsonSerdeUtils.readValue(
+ pbAddColumn.getDataTypeJson(),
+ DataTypeJsonSerde.INSTANCE),
+ pbAddColumn.hasComment() ?
pbAddColumn.getComment() : null,
+
toColumnPosition(pbAddColumn.getColumnPositionType()),
+ aggFunction);
+ })
.collect(Collectors.toList());
}
+ private static AggFunction toAggFunction(PbAddColumn pbAddColumn) {
+ if (!pbAddColumn.hasAggFunctionType()) {
+ return null;
+ }
+
+ AggFunctionType type =
AggFunctionType.fromString(pbAddColumn.getAggFunctionType());
+ if (type == null) {
+ throw new InvalidConfigException(
+ String.format(
+ "Unknown aggregation function type: %s",
+ pbAddColumn.getAggFunctionType()));
+ }
+
+ Map<String, String> parameters = new HashMap<>();
+ for (PbKeyValue parameter : pbAddColumn.getAggFunctionParamsList()) {
+ parameters.put(parameter.getKey(), parameter.getValue());
+ }
+ return AggFunctions.of(type, parameters);
+ }
+
public static List<TableChange.SchemaChange>
toDropColumns(List<PbDropColumn> dropColumns) {
return dropColumns.stream()
.filter(Objects::nonNull)
diff --git
a/fluss-server/src/main/java/org/apache/fluss/server/utils/TableDescriptorValidation.java
b/fluss-server/src/main/java/org/apache/fluss/server/utils/TableDescriptorValidation.java
index 949afd8c6..6b60a9409 100644
---
a/fluss-server/src/main/java/org/apache/fluss/server/utils/TableDescriptorValidation.java
+++
b/fluss-server/src/main/java/org/apache/fluss/server/utils/TableDescriptorValidation.java
@@ -17,6 +17,7 @@
package org.apache.fluss.server.utils;
+import org.apache.fluss.annotation.Internal;
import org.apache.fluss.config.ConfigOption;
import org.apache.fluss.config.ConfigOptions;
import org.apache.fluss.config.Configuration;
@@ -129,6 +130,19 @@ public class TableDescriptorValidation {
checkTableLakeFormatMatchesCluster(tableConf, clusterDataLakeFormat);
}
+ /** Validates the schema after altering table columns. */
+ @Internal
+ public static void validateAlterTableSchema(TableInfo table, Schema
newSchema) {
+ if (table.getTableConfig()
+ .getMergeEngineType()
+ .map(MergeEngineType.AGGREGATION::equals)
+ .orElse(false)) {
+ validateAggregationFunctionParameters(newSchema);
+ } else {
+ validateNoAggregationFunctions(newSchema);
+ }
+ }
+
private static void checkTableLakeFormatMatchesCluster(
Configuration tableConf, @Nullable DataLakeFormat
clusterDataLakeFormat) {
if (clusterDataLakeFormat == null) {
@@ -323,6 +337,9 @@ public class TableDescriptorValidation {
private static void checkMergeEngine(
Configuration tableConf, boolean hasPrimaryKey, Schema schema) {
MergeEngineType mergeEngine =
tableConf.get(ConfigOptions.TABLE_MERGE_ENGINE);
+ if (mergeEngine != MergeEngineType.AGGREGATION) {
+ validateNoAggregationFunctions(schema);
+ }
if (mergeEngine != null) {
if (!hasPrimaryKey) {
throw new InvalidConfigException(
@@ -377,6 +394,20 @@ public class TableDescriptorValidation {
}
}
+ /** Validates that the schema doesn't contain any aggregation functions. */
+ private static void validateNoAggregationFunctions(Schema schema) {
+ for (Schema.Column column : schema.getColumns()) {
+ Optional<AggFunction> aggFunction = column.getAggFunction();
+ if (aggFunction.isPresent()) {
+ throw new InvalidConfigException(
+ String.format(
+ "Aggregation function is only supported for
aggregation merge engine table, "
+ + "but column '%s' has aggregation
function '%s'.",
+ column.getName(), aggFunction.get()));
+ }
+ }
+ }
+
/**
* Validates aggregation function parameters in the schema.
*