github-actions[bot] commented on code in PR #66112:
URL: https://github.com/apache/doris/pull/66112#discussion_r3662394796
##########
be/src/exec/sink/viceberg_merge_sink.cpp:
##########
@@ -120,17 +122,22 @@ Status VIcebergMergeSink::write(RuntimeState* state,
Block& block) {
if (delete_op) {
delete_filter[i] = 1;
has_delete = true;
- ++_delete_row_count;
++delete_rows;
}
if (insert_op) {
insert_filter[i] = 1;
has_insert = true;
- ++_insert_row_count;
++insert_rows;
}
}
+ // The physical sink hashes matched rows by row_id, so identical targets
reach the same sink.
Review Comment:
[P1] Keep MERGE cardinality routing independent of strict-consistency DML
This set is local to one `IcebergMergeSinkLocalState`, but
`RequestPropertyDeriver.visitPhysicalIcebergMergeSink()` requests
`PhysicalProperties.ANY` when `enable_strict_consistency_dml=false`. MERGE
builds the source on the left and target on the right, so a broadcast-target
join can process two source rows matching the same target in different fragment
instances; each reaches a different set and both validations succeed. That
session switch controls replica-consistency distribution, but it must not
disable the SQL cardinality rule. Please require the row-id/MERGE distribution
unconditionally (or use a query-global validator) and add a multi-instance
broadcast case with the switch off.
##########
be/src/exec/sink/viceberg_merge_sink.h:
##########
@@ -73,6 +75,7 @@ class VIcebergMergeSink final : public AsyncResultWriter {
int _operation_idx = -1;
int _row_id_idx = -1;
std::vector<int> _data_column_indices;
+ std::unordered_set<std::string> _matched_row_ids;
Review Comment:
[P1] Avoid retaining one full row-id string per matched row
Every valid matched target now keeps a separately allocated
file-path-plus-position string and hash node until the writer is destroyed. A
large MERGE therefore adds linear query-lifetime memory on top of the delete
sink's existing file-keyed position bitmaps; this state cannot be revoked or
spilled in low-memory mode, and the writer's `MemoryUsage` profile only
accounts queued/free blocks. The task memory hook does charge the query, but
otherwise-valid high-cardinality MERGEs can still hit the limit solely because
of this duplicate representation. Please use a compact exact structure (for
example interned file paths plus position bitmaps, ideally reusing the delete
collection), expose/reserve its retained bytes, and cover a high-cardinality
low-memory case.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateTableCommand.java:
##########
@@ -107,9 +107,11 @@ public void run(ConnectContext ctx, StmtExecutor executor)
throws Exception {
throw new AnalysisException(e.getMessage(), e.getCause());
}
- query =
UnboundTableSinkCreator.createUnboundTableSink(createTableInfo.getTableNameParts(),
- ImmutableList.of(), ImmutableList.of(), ImmutableList.of(),
query);
try {
+ // Once CTAS metadata is visible, every setup or execution failure
must pass through
+ // the same rollback boundary so an unusable destination table is
never left behind.
+ query =
UnboundTableSinkCreator.createUnboundTableSink(createTableInfo.getTableNameParts(),
Review Comment:
[P1] Roll back only metadata created by this CTAS invocation
`createTable(...) == false` is not an ownership proof under concurrent `IF
NOT EXISTS`. Internally, a loser can get `result.second == true` from the
atomic registration while `createOlapTable()` still returns its never-updated
`false`; Paimon and MaxCompute ignore-if-exists paths likewise return `false`
even when another creator won. If sink setup or execution then fails inside
this expanded catch, `handleFallbackFailedCtas()` force-drops whichever table
currently resolves by name, so it can delete the concurrent winner. Please
propagate an unambiguous atomic created/existed result plus the exact table
identity/version, and drop only if that same object is still installed
(validating unsupported sinks before publishing metadata would also close the
immediate setup case). Add a deterministic losing-race test; the new mock test
assumes the disputed `false == owned` invariant.
##########
be/src/exec/sink/viceberg_merge_sink.cpp:
##########
@@ -167,6 +174,80 @@ Status VIcebergMergeSink::write(RuntimeState* state,
Block& block) {
return Status::OK();
}
+Status VIcebergMergeSink::_validate_matched_row_ids(const Block& block,
+ const uint8_t*
delete_filter) {
+ const auto& row_id = block.get_by_position(_row_id_idx);
+ const IColumn* row_id_data = row_id.column.get();
+ const IDataType* row_id_type = row_id.type.get();
+ const auto* nullable_row_id =
check_and_get_column<ColumnNullable>(row_id_data);
+ if (nullable_row_id != nullptr) {
+ row_id_data = nullable_row_id->get_nested_column_ptr().get();
+ }
+ if (const auto* nullable_type =
check_and_get_data_type<DataTypeNullable>(row_id_type)) {
+ row_id_type = nullable_type->get_nested_type().get();
+ }
+
+ const auto* struct_column =
check_and_get_column<ColumnStruct>(row_id_data);
+ const auto* struct_type =
check_and_get_data_type<DataTypeStruct>(row_id_type);
+ if (struct_column == nullptr || struct_type == nullptr) {
+ return Status::InternalError("Iceberg merge row_id column is not a
struct");
+ }
+
+ int file_path_idx = -1;
+ int row_position_idx = -1;
+ const auto& field_names = struct_type->get_element_names();
+ for (size_t i = 0; i < field_names.size(); ++i) {
+ std::string field_name = doris::to_lower(field_names[i]);
+ if (field_name == "file_path") {
+ file_path_idx = static_cast<int>(i);
+ } else if (field_name == "row_position") {
+ row_position_idx = static_cast<int>(i);
+ }
+ }
+ if (file_path_idx < 0 || row_position_idx < 0) {
+ return Status::InternalError(
+ "Iceberg merge row_id must contain file_path and row_position
fields");
+ }
+
+ const auto& file_path_column =
struct_column->get_column_ptr(file_path_idx);
+ const auto& row_position_column =
struct_column->get_column_ptr(row_position_idx);
+ const auto* nullable_file_path =
check_and_get_column<ColumnNullable>(file_path_column.get());
+ const auto* nullable_row_position =
+ check_and_get_column<ColumnNullable>(row_position_column.get());
+ const auto* file_paths =
+
check_and_get_column<ColumnString>(remove_nullable(file_path_column).get());
+ const auto* row_positions =
check_and_get_column<ColumnVector<TYPE_BIGINT>>(
+ remove_nullable(row_position_column).get());
+ if (file_paths == nullptr || row_positions == nullptr) {
+ return Status::InternalError("Iceberg merge row_id fields have
incorrect types");
+ }
+
+ for (size_t i = 0; i < block.rows(); ++i) {
+ if (delete_filter[i] == 0) {
+ continue;
+ }
+ if ((nullable_row_id != nullptr && nullable_row_id->is_null_at(i)) ||
+ (nullable_file_path != nullptr &&
nullable_file_path->is_null_at(i)) ||
+ (nullable_row_position != nullptr &&
nullable_row_position->is_null_at(i))) {
+ return Status::InternalError("Iceberg merge matched row_id cannot
be null");
+ }
+
+ int64_t row_position = row_positions->get_element(i);
+ if (row_position < 0) {
+ return Status::InternalError("Invalid row_position {} in Iceberg
merge row_id",
+ row_position);
+ }
+ std::string exact_row_id = file_paths->get_data_at(i).to_string();
+ exact_row_id.append(reinterpret_cast<const char*>(&row_position),
sizeof(row_position));
+ if (!_matched_row_ids.emplace(std::move(exact_row_id)).second) {
+ return Status::InvalidArgument(
Review Comment:
[P2] Activate the existing statement-level regression for this fix
`test_iceberg_write_merge_duplicate_source_negative.groovy` already
reproduces this exact bug and verifies that snapshots, files, and table
contents remain unchanged, but it still sits behind
`enableIcebergKnownBugTest`. It also expects the exception to contain `"more
than one"`, whereas this new message contains only `"multiple source rows..."`;
`TestAction` uses a case-sensitive `contains`, so enabling the suite still
fails. Please update either the diagnostic or oracle, remove the known-bug gate
for this suite, and run it. The added unit test proves only duplicate detection
inside one writer, not statement-level atomicity or distribution.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]