Skip to content

Commit f94436b

Browse files
steFaizlirui-apache
authored andcommitted
[core] DataEvolution table supports concurrent updates to different columns (#7867)
1 parent 1db1fd2 commit f94436b

8 files changed

Lines changed: 499 additions & 31 deletions

File tree

‎paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.apache.paimon.operation.commit.ManifestEntryChanges;
5050
import org.apache.paimon.operation.commit.RetryCommitResult;
5151
import org.apache.paimon.operation.commit.RetryCommitResult.CommitFailRetryResult;
52+
import org.apache.paimon.operation.commit.RowIdColumnConflictChecker;
5253
import org.apache.paimon.operation.commit.RowTrackingCommitUtils.RowTrackingAssigned;
5354
import org.apache.paimon.operation.commit.StrictModeChecker;
5455
import org.apache.paimon.operation.commit.SuccessCommitResult;
@@ -866,12 +867,22 @@ CommitResult tryCommitOnce(
866867
.filter(entry -> !baseIdentifiers.contains(entry.identifier()))
867868
.collect(Collectors.toList());
868869
}
870+
RowIdColumnConflictChecker rowIdColumnConflictChecker = null;
871+
if (conflictDetection.hasRowIdCheckFromSnapshot()) {
872+
rowIdColumnConflictChecker =
873+
RowIdColumnConflictChecker.fromDataFiles(
874+
schemaManager,
875+
deltaFiles.stream()
876+
.map(ManifestEntry::file)
877+
.collect(Collectors.toList()));
878+
}
869879
Optional<RuntimeException> exception =
870880
conflictDetection.checkConflicts(
871881
latestSnapshot,
872882
baseDataFiles,
873883
SimpleFileEntry.from(deltaFiles),
874884
indexFiles,
885+
rowIdColumnConflictChecker,
875886
commitKind);
876887
if (exception.isPresent()) {
877888
if (allowRollback && rollback != null) {

‎paimon-core/src/main/java/org/apache/paimon/operation/commit/ConflictDetection.java‎

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import org.apache.paimon.types.RowType;
3838
import org.apache.paimon.utils.FileStorePathFactory;
3939
import org.apache.paimon.utils.Pair;
40-
import org.apache.paimon.utils.Range;
4140
import org.apache.paimon.utils.RangeHelper;
4241
import org.apache.paimon.utils.SnapshotManager;
4342

@@ -64,6 +63,7 @@
6463
import static org.apache.paimon.deletionvectors.DeletionVectorsIndexFile.DELETION_VECTORS_INDEX;
6564
import static org.apache.paimon.format.blob.BlobFileFormat.isBlobFile;
6665
import static org.apache.paimon.operation.commit.ManifestEntryChanges.changedPartitions;
66+
import static org.apache.paimon.types.VectorType.isVectorStoreFile;
6767
import static org.apache.paimon.utils.InternalRowPartitionComputer.partToSimpleString;
6868
import static org.apache.paimon.utils.Preconditions.checkState;
6969

@@ -152,6 +152,7 @@ public Optional<RuntimeException> checkConflicts(
152152
List<SimpleFileEntry> baseEntries,
153153
List<SimpleFileEntry> deltaEntries,
154154
List<IndexManifestEntry> deltaIndexEntries,
155+
@Nullable RowIdColumnConflictChecker rowIdColumnConflictChecker,
155156
CommitKind commitKind) {
156157
String baseCommitUser = latestSnapshot.commitUser();
157158
if (deletionVectorsEnabled && bucketMode.equals(BucketMode.BUCKET_UNAWARE)) {
@@ -220,7 +221,8 @@ public Optional<RuntimeException> checkConflicts(
220221
return exception;
221222
}
222223

223-
return checkForRowIdFromSnapshot(latestSnapshot, deltaEntries, deltaIndexEntries);
224+
return checkForRowIdFromSnapshot(
225+
latestSnapshot, deltaEntries, deltaIndexEntries, rowIdColumnConflictChecker);
224226
}
225227

226228
private Optional<RuntimeException> checkBucketKeepSame(
@@ -408,7 +410,7 @@ private Optional<RuntimeException> checkRowIdRangeConflicts(
408410
for (List<SimpleFileEntry> group : merged) {
409411
List<SimpleFileEntry> dataFiles = new ArrayList<>();
410412
for (SimpleFileEntry f : group) {
411-
if (!isBlobFile(f.fileName())) {
413+
if (!dedicatedStorageFile(f.fileName())) {
412414
dataFiles.add(f);
413415
}
414416
}
@@ -426,24 +428,19 @@ private Optional<RuntimeException> checkRowIdRangeConflicts(
426428
private Optional<RuntimeException> checkForRowIdFromSnapshot(
427429
Snapshot latestSnapshot,
428430
List<SimpleFileEntry> deltaEntries,
429-
List<IndexManifestEntry> deltaIndexEntries) {
431+
List<IndexManifestEntry> deltaIndexEntries,
432+
@Nullable RowIdColumnConflictChecker columnChecker) {
430433
if (!dataEvolutionEnabled) {
431434
return Optional.empty();
432435
}
433436
if (rowIdCheckFromSnapshot == null) {
434437
return Optional.empty();
435438
}
439+
if (columnChecker == null || columnChecker.isEmpty()) {
440+
return Optional.empty();
441+
}
436442

437443
List<BinaryRow> changedPartitions = changedPartitions(deltaEntries, deltaIndexEntries);
438-
// collect history row id ranges
439-
List<Range> historyIdRanges = new ArrayList<>();
440-
for (SimpleFileEntry entry : deltaEntries) {
441-
Long firstRowId = entry.firstRowId();
442-
long rowCount = entry.rowCount();
443-
if (firstRowId != null) {
444-
historyIdRanges.add(new Range(firstRowId, firstRowId + rowCount - 1));
445-
}
446-
}
447444

448445
// check history row id ranges
449446
Long checkNextRowId = snapshotManager.snapshot(rowIdCheckFromSnapshot).nextRowId();
@@ -460,23 +457,24 @@ private Optional<RuntimeException> checkForRowIdFromSnapshot(
460457
commitScanner.readIncrementalEntries(snapshot, changedPartitions);
461458
for (ManifestEntry entry : changes) {
462459
DataFileMeta file = entry.file();
463-
Range fileRange = file.nonNullRowIdRange();
464-
if (fileRange.from < checkNextRowId) {
465-
for (Range range : historyIdRanges) {
466-
if (range.hasIntersection(fileRange)) {
467-
return Optional.of(
468-
new RuntimeException(
469-
"For Data Evolution table, multiple 'MERGE INTO' operations have encountered conflicts,"
470-
+ " updating the same file, which can render some updates ineffective."));
471-
}
472-
}
460+
if (file.firstRowId() != null
461+
&& file.nonNullRowIdRange().from < checkNextRowId
462+
&& columnChecker.conflictsWith(file)) {
463+
return Optional.of(
464+
new RuntimeException(
465+
"For Data Evolution table, multiple 'MERGE INTO' operations have encountered conflicts,"
466+
+ " updating the same file, which can render some updates ineffective."));
473467
}
474468
}
475469
}
476470

477471
return Optional.empty();
478472
}
479473

474+
private static boolean dedicatedStorageFile(String fileName) {
475+
return isBlobFile(fileName) || isVectorStoreFile(fileName);
476+
}
477+
480478
static List<SimpleFileEntry> buildBaseEntriesWithDV(
481479
List<SimpleFileEntry> baseEntries, List<IndexManifestEntry> baseIndexEntries) {
482480
if (baseEntries.isEmpty()) {
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.paimon.operation.commit;
20+
21+
import org.apache.paimon.io.DataFileMeta;
22+
import org.apache.paimon.schema.SchemaManager;
23+
import org.apache.paimon.table.SpecialFields;
24+
import org.apache.paimon.types.DataField;
25+
import org.apache.paimon.utils.Range;
26+
import org.apache.paimon.utils.RangeHelper;
27+
28+
import java.util.ArrayList;
29+
import java.util.Collections;
30+
import java.util.Comparator;
31+
import java.util.HashMap;
32+
import java.util.HashSet;
33+
import java.util.List;
34+
import java.util.Map;
35+
import java.util.Set;
36+
import java.util.stream.Collectors;
37+
38+
/**
39+
* Detects row-id range conflicts only when written field ids overlap. The detection process is as
40+
* below:
41+
*
42+
* <ol>
43+
* <li>Merge delta files by row range and calculate updated columns.
44+
* <li>Sort those items by range.
45+
* <li>For each checking files, do binary search to find overlapping ranges. If their updated
46+
* columns also overlap, return conflicting result.
47+
* </ol>
48+
*/
49+
public class RowIdColumnConflictChecker {
50+
51+
private final SchemaManager schemaManager;
52+
private final List<WriteRange> writeRanges;
53+
private final Map<Long, Map<String, Integer>> fieldIdByNameCache = new HashMap<>();
54+
55+
private RowIdColumnConflictChecker(SchemaManager schemaManager, List<DataFileMeta> deltaFiles) {
56+
this.schemaManager = schemaManager;
57+
this.writeRanges = buildWriteRanges(deltaFiles);
58+
}
59+
60+
public static RowIdColumnConflictChecker fromDataFiles(
61+
SchemaManager schemaManager, List<DataFileMeta> deltaFiles) {
62+
return new RowIdColumnConflictChecker(schemaManager, deltaFiles);
63+
}
64+
65+
private List<WriteRange> buildWriteRanges(List<DataFileMeta> deltaFiles) {
66+
List<DataFileMeta> rowIdFiles =
67+
deltaFiles.stream()
68+
.filter(file -> file.firstRowId() != null)
69+
.collect(Collectors.toList());
70+
71+
if (rowIdFiles.isEmpty()) {
72+
return Collections.emptyList();
73+
}
74+
75+
// 1. merge overlapping ranges and calculate [Range, Set<FieldId>] tuples.
76+
RangeHelper<DataFileMeta> rangeHelper = new RangeHelper<>(DataFileMeta::nonNullRowIdRange);
77+
List<WriteRange> writeRanges = new ArrayList<>();
78+
for (List<DataFileMeta> group : rangeHelper.mergeOverlappingRanges(rowIdFiles)) {
79+
Range range = mergeRange(group);
80+
Set<Integer> fieldIds = new HashSet<>();
81+
for (DataFileMeta file : group) {
82+
addWriteFieldIds(fieldIds, file);
83+
}
84+
85+
writeRanges.add(new WriteRange(range, fieldIds));
86+
}
87+
88+
// 2. sort by range for binary search
89+
writeRanges.sort(
90+
Comparator.comparingLong((WriteRange writeRange) -> writeRange.range.from)
91+
.thenComparingLong(writeRange -> writeRange.range.to));
92+
93+
return writeRanges;
94+
}
95+
96+
private void addWriteFieldIds(Set<Integer> fieldIds, DataFileMeta file) {
97+
List<String> writeCols = file.writeCols();
98+
if (writeCols == null) {
99+
fieldIds.addAll(
100+
fieldIdByNameCache
101+
.computeIfAbsent(file.schemaId(), this::fieldIdByName)
102+
.values());
103+
return;
104+
}
105+
106+
for (String writeCol : writeCols) {
107+
Integer fieldId = fieldId(file, writeCol);
108+
if (fieldId != null) {
109+
fieldIds.add(fieldId);
110+
}
111+
}
112+
}
113+
114+
private static Range mergeRange(List<DataFileMeta> files) {
115+
long from = Long.MAX_VALUE;
116+
long to = Long.MIN_VALUE;
117+
for (DataFileMeta file : files) {
118+
Range range = file.nonNullRowIdRange();
119+
from = Math.min(from, range.from);
120+
to = Math.max(to, range.to);
121+
}
122+
return new Range(from, to);
123+
}
124+
125+
boolean isEmpty() {
126+
return writeRanges.isEmpty();
127+
}
128+
129+
/**
130+
* Check whether a committed incremental file entry conflicts with current committing delta
131+
* files. If an existing file has both overlapping row range and overlapping write fields, then
132+
* it conflicts.
133+
*
134+
* @param file committed incremental data file
135+
* @return true if conflict
136+
*/
137+
boolean conflictsWith(DataFileMeta file) {
138+
Long firstRowId = file.firstRowId();
139+
if (firstRowId == null) {
140+
return false;
141+
}
142+
143+
Range range = new Range(firstRowId, firstRowId + file.rowCount() - 1);
144+
int index = firstPossibleRange(range);
145+
while (index < writeRanges.size()) {
146+
WriteRange writeRange = writeRanges.get(index);
147+
if (writeRange.range.from > range.to) {
148+
return false;
149+
}
150+
// overlapping row range and overlapping write fields
151+
if (writeRange.range.hasIntersection(range)
152+
&& containsAnyWriteField(writeRange.fieldIds, file)) {
153+
return true;
154+
}
155+
index++;
156+
}
157+
return false;
158+
}
159+
160+
/**
161+
* Binary search to find the first range whose `to` >= target range's `from`.
162+
*
163+
* @param range querying range
164+
* @return index of the first range
165+
*/
166+
private int firstPossibleRange(Range range) {
167+
int low = 0;
168+
int high = writeRanges.size();
169+
while (low < high) {
170+
int mid = (low + high) >>> 1;
171+
if (writeRanges.get(mid).range.to < range.from) {
172+
low = mid + 1;
173+
} else {
174+
high = mid;
175+
}
176+
}
177+
return low;
178+
}
179+
180+
private boolean containsAnyWriteField(Set<Integer> fieldIds, DataFileMeta file) {
181+
List<String> writeCols = file.writeCols();
182+
// If write cols == null, it's a full-schema write
183+
if (writeCols == null) {
184+
return true;
185+
}
186+
187+
for (String writeCol : writeCols) {
188+
Integer fieldId = fieldId(file, writeCol);
189+
if (fieldId != null && fieldIds.contains(fieldId)) {
190+
return true;
191+
}
192+
}
193+
return false;
194+
}
195+
196+
private Integer fieldId(DataFileMeta file, String writeCol) {
197+
Integer fieldId =
198+
fieldIdByNameCache
199+
.computeIfAbsent(file.schemaId(), this::fieldIdByName)
200+
.get(writeCol);
201+
if (fieldId == null) {
202+
if (SpecialFields.isSystemField(writeCol)) {
203+
return null;
204+
}
205+
throw new RuntimeException(
206+
String.format(
207+
"Cannot find write column '%s' in schema %s.",
208+
writeCol, file.schemaId()));
209+
}
210+
return fieldId;
211+
}
212+
213+
private Map<String, Integer> fieldIdByName(long schemaId) {
214+
Map<String, Integer> fieldIdByName = new HashMap<>();
215+
for (DataField field : schemaManager.schema(schemaId).logicalRowType().getFields()) {
216+
fieldIdByName.put(field.name(), field.id());
217+
}
218+
return fieldIdByName;
219+
}
220+
221+
/** Range and field id Set. */
222+
private static class WriteRange {
223+
224+
private final Range range;
225+
private final Set<Integer> fieldIds;
226+
227+
private WriteRange(Range range, Set<Integer> fieldIds) {
228+
this.range = range;
229+
this.fieldIds = fieldIds;
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)