Skip to content

Commit 18c93e8

Browse files
committed
[core] Require write-only mode for snapshot-sequence-ordering
Snapshot ordering relies on snapshot id to determine record order, but inline compaction happens before snapshot creation — files have not been stamped with the correct snapshot id yet. Compaction would propagate incorrect values to KV records. Enforce write-only=true at schema validation time so that writers use NoopCompactManager. Dedicated compact jobs override write-only at runtime via table.copy(), which passes dynamicOptionKeys to skip this specific check.
1 parent 1b503a7 commit 18c93e8

7 files changed

Lines changed: 173 additions & 45 deletions

File tree

docs/layouts/shortcodes/generated/core_configuration.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1305,7 +1305,7 @@
13051305
<td><h5>sequence.snapshot-ordering</h5></td>
13061306
<td style="word-wrap: break-word;">false</td>
13071307
<td>Boolean</td>
1308-
<td>When enabled, merge uses the commit snapshot id as the primary tiebreaker for primary-key conflicts: records from later snapshots always win, with the existing sequence number used as a secondary tiebreaker. Designed for multi-writer scenarios on the same primary-key table where wall-clock sequence numbers cannot be globally ordered. Mutually exclusive with sequence.field. Requires a primary-key table.</td>
1308+
<td>When enabled, merge uses the commit snapshot id as the primary tiebreaker for primary-key conflicts: records from later snapshots always win, with the existing sequence number used as a secondary tiebreaker. Designed for multi-writer scenarios on the same primary-key table where wall-clock sequence numbers cannot be globally ordered. Mutually exclusive with sequence.field. Requires a primary-key table with write-only=true. Inline compaction is not allowed because snapshot ids are assigned only after commit. To compact such tables, run a dedicated compaction job/action with write-only=false.</td>
13091309
</tr>
13101310
<tr>
13111311
<td><h5>sink.process-time-zone</h5></td>

paimon-api/src/main/java/org/apache/paimon/CoreOptions.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,11 @@ public InlineElement getDescription() {
977977
+ "as a secondary tiebreaker. Designed for multi-writer "
978978
+ "scenarios on the same primary-key table where wall-clock "
979979
+ "sequence numbers cannot be globally ordered. Mutually "
980-
+ "exclusive with sequence.field. Requires a primary-key table.");
980+
+ "exclusive with sequence.field. Requires a primary-key table "
981+
+ "with write-only=true. Inline compaction is not allowed "
982+
+ "because snapshot ids are assigned only after commit. To "
983+
+ "compact such tables, run a dedicated compaction job/action "
984+
+ "with write-only=false.");
981985

982986
@Immutable
983987
public static final ConfigOption<Boolean> AGGREGATION_REMOVE_RECORD_ON_DELETE =

paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreWrite.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public abstract class AbstractFileStoreWrite<T> implements FileStoreWrite<T> {
9595
private boolean closeCompactExecutorWhenLeaving = true;
9696
private boolean ignorePreviousFiles = false;
9797
private boolean ignoreNumBucketCheck = false;
98+
private final boolean compactOnly;
9899

99100
protected CompactionMetrics compactionMetrics = null;
100101
protected final String tableName;
@@ -123,6 +124,7 @@ protected AbstractFileStoreWrite(
123124
this.tableName = tableName;
124125
this.writerNumberMax = options.writeMaxWritersToSpill();
125126
this.legacyPartitionName = options.legacyPartitionName();
127+
this.compactOnly = options.snapshotSequenceOrdering() && !options.writeOnly();
126128
this.partitionTimestampValidator =
127129
PartitionTimestampValidator.create(options, partitionType);
128130
}
@@ -167,6 +169,15 @@ public void withCompactExecutor(ExecutorService compactExecutor) {
167169

168170
@Override
169171
public void write(BinaryRow partition, int bucket, T data) throws Exception {
172+
if (compactOnly) {
173+
throw new IllegalStateException(
174+
CoreOptions.SEQUENCE_SNAPSHOT_ORDERING.key()
175+
+ " is enabled with "
176+
+ CoreOptions.WRITE_ONLY.key()
177+
+ " = false. This writer only allows compaction, not data writes. "
178+
+ "Inline compaction is not supported because snapshot ids are "
179+
+ "assigned only after commit.");
180+
}
170181
WriterContainer<T> container = getWriterWrapper(partition, bucket);
171182
container.writer.write(data);
172183
if (container.dynamicBucketMaintainer != null) {

paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ public class SchemaValidation {
104104
* @param schema the schema to be validated
105105
*/
106106
public static void validateTableSchema(TableSchema schema) {
107+
validateTableSchema(schema, Collections.emptySet());
108+
}
109+
110+
public static void validateTableSchema(TableSchema schema, Set<String> dynamicOptionKeys) {
107111
CoreOptions options = new CoreOptions(schema.options());
108112

109113
validateOnlyContainPrimitiveType(schema.fields(), schema.primaryKeys(), "primary key");
@@ -255,7 +259,7 @@ public static void validateTableSchema(TableSchema schema) {
255259
}
256260

257261
if (options.snapshotSequenceOrdering()) {
258-
validateSnapshotSequenceOrdering(schema, options);
262+
validateSnapshotSequenceOrdering(schema, options, dynamicOptionKeys);
259263
}
260264

261265
// vector field names must point to vector type
@@ -582,7 +586,8 @@ private static void validateFileIndex(TableSchema schema) {
582586
}
583587
}
584588

585-
private static void validateSnapshotSequenceOrdering(TableSchema schema, CoreOptions options) {
589+
private static void validateSnapshotSequenceOrdering(
590+
TableSchema schema, CoreOptions options, Set<String> dynamicOptionKeys) {
586591
checkArgument(
587592
!schema.primaryKeys().isEmpty(),
588593
"%s = true requires a primary-key table; append-only tables cannot use "
@@ -593,6 +598,18 @@ private static void validateSnapshotSequenceOrdering(TableSchema schema, CoreOpt
593598
"%s = true is mutually exclusive with %s; the snapshot id is the sole tiebreaker.",
594599
CoreOptions.SEQUENCE_SNAPSHOT_ORDERING.key(),
595600
CoreOptions.SEQUENCE_FIELD.key());
601+
// Skip writeOnly check when write-only is dynamically overridden (e.g. by dedicated
602+
// compact jobs that override write-only=false at runtime).
603+
if (!dynamicOptionKeys.contains(CoreOptions.WRITE_ONLY.key())) {
604+
checkArgument(
605+
options.writeOnly(),
606+
"%s = true requires %s = true. Snapshot ordering relies on snapshot id to "
607+
+ "determine record order, but inline compaction happens before "
608+
+ "snapshot creation — files have not been stamped with the correct "
609+
+ "snapshot id yet. Use dedicated compaction job instead.",
610+
CoreOptions.SEQUENCE_SNAPSHOT_ORDERING.key(),
611+
CoreOptions.WRITE_ONLY.key());
612+
}
596613
}
597614

598615
private static void validateForDeletionVectors(CoreOptions options) {

paimon-core/src/main/java/org/apache/paimon/table/AbstractFileStoreTable.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ protected FileStoreTable copyInternal(
373373
}
374374

375375
// validate schema with new options
376-
SchemaValidation.validateTableSchema(newTableSchema);
376+
SchemaValidation.validateTableSchema(newTableSchema, dynamicOptions.keySet());
377377

378378
return copy(newTableSchema);
379379
}

paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,13 +449,23 @@ private void validateTableSchemaWithMapField(Map<String, String> options) {
449449
public void testSnapshotSequenceOrderingHappyPath() {
450450
Map<String, String> options = new HashMap<>();
451451
options.put(CoreOptions.SEQUENCE_SNAPSHOT_ORDERING.key(), "true");
452+
options.put(CoreOptions.WRITE_ONLY.key(), "true");
452453
assertThatNoException().isThrownBy(() -> validateTableSchemaExec(options));
453454
}
454455

456+
@Test
457+
public void testSnapshotSequenceOrderingRejectsNonWriteOnly() {
458+
Map<String, String> options = new HashMap<>();
459+
options.put(CoreOptions.SEQUENCE_SNAPSHOT_ORDERING.key(), "true");
460+
assertThatThrownBy(() -> validateTableSchemaExec(options))
461+
.hasMessageContaining(CoreOptions.WRITE_ONLY.key());
462+
}
463+
455464
@Test
456465
public void testSnapshotSequenceOrderingRejectsSequenceField() {
457466
Map<String, String> options = new HashMap<>();
458467
options.put(CoreOptions.SEQUENCE_SNAPSHOT_ORDERING.key(), "true");
468+
options.put(CoreOptions.WRITE_ONLY.key(), "true");
459469
options.put(CoreOptions.SEQUENCE_FIELD.key(), "f2");
460470
assertThatThrownBy(() -> validateTableSchemaExec(options))
461471
.hasMessageContaining("sequence.field");

0 commit comments

Comments
 (0)