Skip to content

[python] BLOB descriptor parsing and read-path compatibility. - #9148

Open
Stephen0421 wants to merge 1 commit into
apache:masterfrom
Stephen0421:pypaimon-blob-pr1-foundation
Open

[python] BLOB descriptor parsing and read-path compatibility.#9148
Stephen0421 wants to merge 1 commit into
apache:masterfrom
Stephen0421:pypaimon-blob-pr1-foundation

Conversation

@Stephen0421

Copy link
Copy Markdown
Contributor

Summary

First PR in the stacked series for #9099. Shared descriptor-byte parsing and read/write foundations for managed BLOB v1/v2 compatibility — no PK-specific logic.

  • Parsing API (blob.py): from_bytes (v2 magic heuristic) vs from_descriptor_bytes (v1 strict + v2 deserialize with optional trailing padding)
  • Batch read: BlobInlineConvertReader uses from_descriptor_bytes for descriptor fields
  • Row read: descriptor_field_indices when blob-as-descriptor=trueOffsetRow.get_blob() uses from_descriptor_bytes
  • Config: legacy blob.stored-descriptor-fields; blank blob-descriptor-field treated as unset
  • Write: blob_format_writer rejects truncated copies (EOFError) when descriptor length is known
  • URI lifecycle: UriReaderFactory owned FileIO tracking; clear_cache() without LRU double-close; FileIO close() wiring

Behavior changes (intentional)

  • Descriptor columns: malformed bytes → ValueError (was silent BlobData passthrough)
  • blob-descriptor-field="" + legacy set → now falls back to blob.stored-descriptor-fields
  • from_bytes on arbitrary inline payload → unchanged (BlobData; v2-only heuristic)

Test plan

  • BlobTest
  • UriReaderFactoryTest
  • CI green

Follow-ups

  • PR2: managed BLOB lifecycle + staged commit
  • PR3: PK managed BLOB write
  • PR4: PK managed BLOB read/view
  • PR5: dynamic bucket HASH callback

Related: #9099

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline comments focus on behavioral regressions and consistency with the Java implementation.

data = blob_value.to_data()
crc32 = self._write_with_crc(data, crc32)
else:
expected_length = self._expected_blob_length(blob_value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please restrict the exact-length path to an exact BlobRef (for example, type(blob_value) is BlobRef). This currently trusts to_descriptor().length for every Blob subtype, so a custom subtype whose new_input_stream() exposes more bytes than its descriptor is silently truncated. I reproduced a descriptor length of 3 with a stream containing abcdef; this branch writes only abc, while the base branch writes all 6 bytes. The Java writer deliberately checks blob.getClass() == BlobRef.class and reads other implementations to EOF.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. Agreed — the exact-length copy path should be restricted to plain BlobRef, matching Java's blob.getClass() == BlobRef.class check.

Updated to use type(blob_value) is BlobRef before calling _copy_exactly. Other Blob subtypes (e.g. resolved BlobView) now read to EOF as before. Removed the test that expected truncation on resolved BlobView, since that behavior was incorrect.

value = self.options.get(CoreOptions.BLOB_DESCRIPTOR_FIELD, None)
if isinstance(value, str):
value = value.strip()
if not value:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please choose the legacy fallback based on key presence rather than value truthiness. Java Options.applyWithOption consults fallback keys only when the canonical key is absent. With blob-descriptor-field="" and blob.stored-descriptor-fields set, Java resolves an empty descriptor-field set, but this code revives the legacy fields. That can make Python interpret ordinary BLOB bytes as descriptors or choose a different write layout. An explicitly present blank canonical value should win.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The legacy fallback should only apply when the canonical key is absent, not when it is explicitly set to a blank value.

Updated blob_descriptor_fields() to consult blob.stored-descriptor-fields only when blob-descriptor-field is None, matching Java Options.applyWithOption / parseCommaSeparatedSet semantics. With blob-descriptor-field="" and legacy set, Python now resolves an empty descriptor-field set, same as Java.

return None
uri_length = struct.unpack('<I', raw[offset:offset + 4])[0]
total = offset + 4 + uri_length + 16
if total != len(raw):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from_descriptor_bytes is used when the schema/storage context already says that the value is a descriptor, but this exact-length check rejects v1 bytes with trailing padding. Java BlobDescriptor.deserialize accepts trailing bytes for every supported version, so a padded legacy v1 descriptor can be read by Java but is rejected by Python (and the new test currently codifies that mismatch). Please deserialize v1 with the same Java semantics here, or coordinate a strict contract change on both implementations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — from_descriptor_bytes should follow Java BlobDescriptor.deserialize semantics and accept trailing padding for both v1 and v2.

Switched to always calling BlobDescriptor.deserialize() in from_descriptor_bytes, and updated the test to verify padded v1 descriptors are accepted. The heuristic from_bytes entry point still uses v2 magic only (is_blob_descriptor) so inline v1-shaped payload bytes are not misclassified.

Also aligned from_bytes(allow_blob_data=False) with Java Blob.fromBytes: when allow_blob_data=False, any bytes are deserialized as a descriptor (including v1), not only v2 magic-prefixed ones.

@Stephen0421
Stephen0421 force-pushed the pypaimon-blob-pr1-foundation branch from f20aa56 to 0c0865d Compare August 11, 2026 01:58

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One remaining Java-compatibility issue in the legacy v1 BLOB view read path.


if not CoreOptions.blob_as_descriptor(table.options):
return set()
return descriptor_field_indices(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] When blob-as-descriptor=true, BlobInlineConvertReader resolves configured blob-view-field values to descriptor.serialize() before rows are exposed. If the upstream value is a legacy v1 descriptor, Python reserializes it as v1 without the magic header (unlike Java, whose BlobDescriptor.serialize() always writes CURRENT_VERSION). Because this helper marks only blob-descriptor-field, OffsetRow.get_blob() takes the heuristic path for the resolved view column and returns BlobData containing the descriptor bytes instead of a reference to the payload. Please either mark resolved view fields as descriptor-backed here or normalize reserialized descriptors to v2, and add a v1 blob-view regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. Fixed in the latest push:

  • When blob-as-descriptor=true, resolved blob-view-field columns are now included in descriptor routing, so OffsetRow.get_blob() returns BlobRef instead of BlobData.
  • BlobDescriptor.serialize() now always writes v2 + magic (aligned with Java), so v1 descriptors re-serialized by BlobInlineConvertReader are unambiguous.

Added test_offset_row_get_blob_v1_resolved_blob_view_field plus routing/serialize coverage.

Introduce explicit descriptor-byte parsing for managed BLOB v1/v2 reads,
legacy blob.stored-descriptor-fields fallback, write-path truncation
checks, UriReaderFactory lifecycle handling, and row-level descriptor
field routing for blob-as-descriptor tables.
@Stephen0421
Stephen0421 force-pushed the pypaimon-blob-pr1-foundation branch from 0c0865d to a7975e5 Compare August 11, 2026 11:44
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.

2 participants