Skip to content

[rust][client][server] Require only omitted columns to be nullable in partial update - #3885

Draft
gstamatakis95 wants to merge 1 commit into
apache:mainfrom
gstamatakis95:fix-3849
Draft

[rust][client][server] Require only omitted columns to be nullable in partial update#3885
gstamatakis95 wants to merge 1 commit into
apache:mainfrom
gstamatakis95:fix-3849

Conversation

@gstamatakis95

@gstamatakis95 gstamatakis95 commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Partial update validation required every non-primary-key column of a primary key table to be nullable, including columns explicitly listed in the target columns. A listed column is supplied by the writer on every request, so the requirement rejected valid schemas. The loop's own comment described the intended behaviour ("check the columns not in targetColumns"), but the loop never consulted the target column set.

The check is duplicated across three layers that validate independently, and all three carried the defect:

Layer Location
Rust client fluss-rust/.../client/table/upsert.rs, UpsertWriterFactory::sanity_check
Java client fluss-client/.../writer/UpsertWriterImpl.java, sanityCheck
Server fluss-server/.../kv/partialupdate/PartialUpdater.java, sanityCheck

The reported symptom cannot be resolved in the Rust client alone. A client-only fix permits writer construction and defers the same rejection to the server at first write.

Resulting validation rules

Column Requirement Rationale
Omitted from target columns Must be nullable updateRow writes null into it when the row does not yet exist
Listed in target columns May be NOT NULL The writer supplies a value on every request
Auto increment Must be nullable Always omitted, and assigned its value only after the merge
Non-primary-key target column, under partial delete Must be nullable deleteRow sets it to null unless the whole row is removed

Divergence from the fix proposed in the issue

The issue proposes retaining the auto-increment exemption (!target_column_set[i] && !pk_column_set[i] && !auto_increment_column_set[i]). This PR removes it.

Removing the exemption is the correct direction because of ordering on the write path. PartialUpdater.updateRow null-fills omitted columns before AutoIncrementUpdater assigns a value (KvTablet.processUpsert to applyInsert to updateAutoIncrementColumns). Relaxing the server instead would allow a NOT NULL auto increment column to reach BinaryWriter.createNotNullValueWriter holding null, producing an NPE surfaced to the client as UnknownServerException.

Restored invariant

The previous rule made "a partial update row cannot carry null in a non-nullable slot" true by construction. Relaxing it removes that guarantee, and the decode path is not defensive: InternalRow.createFieldGetter and CompactedRowReader omit the isNullAt branch for non-nullable types, and CompactedRow is a sequential variable-length encoding, so a null bit in a non-nullable slot desynchronises every subsequent field rather than yielding null. Two guards restore the invariant:

  • updateRow rejects a null value supplied for a NOT NULL target column, primary key columns included. The check runs before any field getter, because the first getter deserialises the whole row with the non-null-checking readers and would fail first, with an error dependent on the bytes that follow the record in the batch. isNullAt reads only the null-bit header, so the check itself never deserialises.
  • deleteRow rejects partial delete when a non-primary-key target column is NOT NULL. The guard is placed after the isFieldsNull short circuit, so whole-row removal, which nulls nothing, remains legal.

The two guards use separate column sets, since the delete guard must exempt the primary key while the update guard must not.

The delete guard is server-side only. Legality depends on the stored row, so a client-side equivalent would necessarily be stricter and would render a server-supported operation unreachable.

Simplification

The primary key bitset is removed from all three checks. The containment check that runs first proves every primary key column is a target column, so !targetColumns.get(i) already excludes them. The bitset remains in the delete computation, which must exclude primary key columns. The auto increment bitset stays in all three checks, only to select the dedicated error message below.

Error message

Changed from:

Partial Update requires all columns except primary key to be nullable, but column %s is NOT NULL.

to:

Partial Update requires all columns omitted from the target columns to be nullable, but omitted column %s is NOT NULL.

A NOT NULL auto increment column gets a dedicated message, because the user cannot follow the generic advice of listing the column, targeting an auto increment column is itself rejected:

Partial Update requires the auto increment column %s to be nullable, since it is always omitted from the target columns and assigned by the server.

A repository-wide search confirmed no other code, test, golden file, documentation, or language binding referenced the previous wording.

Compatibility

No wire format, storage format, or public API signature changes. The change relaxes validation, so previously accepted writes remain accepted. No corruption is possible because no format changed, but the behaviour warrants a release note.

Known limitation

AggregationContext.sanityCheckTargetColumns retains the stricter rule, because PartialAggregateRowMerger returns the new row unchanged on first write instead of null-filling omitted columns. Consequently, partial update on an aggregation merge-engine table with a NOT NULL column now fails on the server at first write rather than at writer creation. The failure is a clean InvalidTargetColumnException. The behaviour is documented in the new docs section. Tracking the relaxation separately.

Documentation

website/docs/table-design/table-types/pk-table.md gains a nullability requirements section covering the relaxed rule for omitted columns, the stricter rule for partial delete, and the stricter rule kept by the aggregation merge engine.

Fixes #3849

Test plan

  • New PartialUpdaterTest, 6 methods and 10 cases, constructor checks and both runtime guards parameterized over every KvFormat, covering the relaxed rule, primary key containment, auto increment nullability, and a server-typed malformed row that proves the update guard fires before deserialization
  • New Rust unit tests not_null_column_in_target_columns_is_accepted and omitted_not_null_auto_increment_column_is_rejected. The omitted-rejection case requested in the issue is already covered by the existing sanity_check test, whose expected message is updated rather than duplicated
  • New integration tests FlussTableITCase#testPartialPutWithNotNullTargetColumn, which asserts InvalidTargetColumnException propagates over RPC, and KvTabletTest#testPartialDeleteWithNotNullTargetColumn
  • mvn test -Dtest='org.apache.fluss.server.kv.**' -pl fluss-server passes, 320 tests
  • mvn spotless:check checkstyle:check validate passes on affected modules
  • cargo fmt --check, cargo clippy -D warnings and cargo deny check licenses clean
  • Guards confirmed load-bearing by reverting individual conditions. Dropping !partialUpdateCols.get(i) fails PartialUpdaterTest. Dropping !target_column_set[i] fails not_null_column_in_target_columns_is_accepted

🤖 AI-assisted changes - reviewed by human developer

…al update

Partial update required every non-primary-key column to be nullable, even
columns explicitly listed in the target columns. A listed column is always
supplied by the writer, so the requirement rejected valid usage.

Restrict the requirement to omitted columns in the Rust client, the Java
client and the server. Fixing only the clients is not enough, the server
runs its own copy of the check.

Auto increment columns keep the requirement. They are always omitted and
only receive their value after the merge, so updateRow writes null into
them first.

Partial delete keeps the requirement on non-primary-key target columns,
since it sets them to null. The check sits after the whole-row-removal
short-circuit and only exists on the server, because legality depends on
the stored row.
@gstamatakis95 gstamatakis95 changed the title [WIP] [rust][client][server] Require only omitted columns to be nullable in partial update [rust][client][server] Require only omitted columns to be nullable in partial update Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[rust] Partial update wrongly rejects NOT NULL columns that are explicitly listed in target columns

1 participant