asif-moh commented on PR #3607:
URL: https://github.com/apache/parquet-java/pull/3607#issuecomment-5006760055

   @wgtmac  Thanks for the review. I agree with the overall direction toward 
`ParsedVersion`. Let me share findings from my investigation here,
     
     1. mayHaveCorruptStatistics as public API
   
     Agreed, exposing a PARQUET-251-specific predicate as public API is the 
wrong abstraction. It ties us to maintaining that exact semantic contract. The 
ParsedVersion-based approach keeps this internal.
   
     2. mayHaveCorruptStatistics is not a pure predicate
   
     I looked at this more closely and I believe the logging behavior is 
actually unchanged between master and this PR. The schema gate 
(`schemaHasCorruptStatisticsColumnType`) ensures `mayHaveCorruptStatistics` is 
only called when the schema has `BINARY`/`FIXED_LEN_BYTE_ARRAY` columns, the 
same precondition that would trigger the warning on master. In all 4 cases 
(corrupt/non-corrupt × has-binary/no-binary), the warning fires or doesn't fire 
in identical scenarios. The only difference is microsecond-level timing within 
the same `fromParquetMetadata` call.
   
   That said, I agree the `ParsedVersion` approach is cleaner here regardless 
parsing is side-effect-free, and the warning stays inside 
`shouldIgnoreStatistics` where it's always been.
   
     3. The fix is partial
   
     I investigated this further and the problem is broader than I initially 
realized. `VersionParser.parse(createdBy)` is called from 6 distinct production 
sites, all parsing the same constant string from `FileMetaData.getCreatedBy()`:
   
     | # | Call site | Frequency per file | When |
     |---|-----------|-------------------|------|
     | 1 | `CorruptStatistics.shouldIgnoreStatistics` via 
`buildColumnChunkMetaData` | R × C | Footer decode |
     | 2 | `CorruptStatistics.shouldIgnoreStatistics` via 
`ParquetFileReader.readAllPages` | Pages per column chunk | Page read |
     | 3 | `CorruptStatistics.shouldIgnoreStatistics` via 
`ParquetRewriter.convertStatistics` | Pages per rewritten chunk | Rewrite |
     | 4 | `CorruptStatistics.shouldIgnoreStatistics` via 
`EncryptedColumnChunkMetaData.decryptIfNeeded` | 1 per encrypted column | Lazy 
decrypt |
     | 5 | `CorruptDeltaByteArrays.requiresSequentialReads(String, Encoding)` 
in `ParquetRecordReader` | 1 per reader init (parse only if DELTA_BYTE_ARRAY 
present) | Reader init |
     | 6 | `ColumnReadStoreImpl` constructor via 
`MessageColumnIO.getRecordReader` | R (once per row group read) | Row group 
materialization |
     | 7 | `ColumnReadStoreImpl` constructor via 
`ParquetRewriter.nullifyColumn` | 1 per nullified column | Rewrite with 
nullification |
   
   Notably, `ColumnReadStoreImpl` already parses `createdBy` into a 
`ParsedVersion` and stores it as a field but it does so R times (once per row 
group) because nobody upstream caches the parsed result. The 
`CorruptDeltaByteArrays.requiresSequentialReads(ParsedVersion, Encoding)` 
overload already exists and is used from `ColumnReaderBase` this is exactly the 
pattern you suggested for CorruptStatistics.
   
     4. PARQUET-251 logic leaking into the converter
   
     Agreed. Threading a boolean specific to one bug doesn't scale. The 
ParsedVersion approach keeps the converter generic and it just says "here's the 
writer version" and lets `CorruptStatistics` decide.
   
    Proposed approach:
     
   I'd like to propose addressing the root cause separately: cache a 
`ParsedVersion` in `FileMetaData`. Since `FileMetaData` is constructed once per 
file and already stores the `createdBy` string, it's the natural place to parse 
once and cache:
   
   ```
    // In FileMetaData:
   private final transient ParsedVersion writerVersion;
     
   public FileMetaData(
   .... {
   this.createdBy = createdBy;
   this.writerVersion = parseVersion(createdBy);  // ← just add this
   ...
   }
   
   public ParsedVersion getWriterVersion() {
       return writerVersion;
   }
     
   private static ParsedVersion parseVersion(String createdBy) {
     if (Strings.isNullOrEmpty(createdBy)) return null;
     try {
         return VersionParser.parse(createdBy);
     } catch (RuntimeException | VersionParseException e) {
         return null;
   }
   ```
     
   This is purely additive (new field + getter), doesn't break serialization 
(transient), and enables all 7 call sites to stop re-parsing. 
`ColumnReadStoreImpl` could accept `ParsedVersion` directly instead of 
re-parsing in its constructor, and `CorruptStatistics` would gain a 
`shouldIgnoreStatistics(ParsedVersion, PrimitiveTypeName)` overload following 
the established `CorruptDeltaByteArrays.requiresSequentialReads(ParsedVersion, 
Encoding)` pattern. 
     
   Would you be open to a separate issue/PR for adding `ParsedVersion` caching 
to `FileMetaData`? That would be a small, zero-behavior-change change just 
parsing `createdBy` in the existing constructor body and exposing a  getter. 
The ParsedVersion-based overloads for `CorruptStatistics`, 
`ColumnReadStoreImpl`, etc. can be added incrementally after that, and this PR 
could then rebase onto the foundation cleanly.


-- 
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]

Reply via email to