mmadzia-sfsh opened a new pull request, #67552:
URL: https://github.com/apache/doris/pull/67552

   ### What problem does this PR solve?
   
   Issue Number: close #67544
   
   Related PR: #xxx
   
   Problem Summary:
   
   On an Iceberg catalog whose storage URI the FE rewrites on the way to the 
BE, `UPDATE` / `DELETE`
   committed the position-delete file into the manifest under Doris's 
**internal, normalized** path
   instead of the catalog's own URI. The commit succeeds, and every subsequent 
read of the table then
   fails permanently:
   
   ```
   ERROR 1105 (HY000): errCode = 2, detailMessage =
     Failed to create LocationPath for location: 
s3://<container>/<uuid>/data/delete_pos_<uuid>_125613.zstd.parquet
   ```
   
   The table is not recoverable through SQL — the bad path is durable Iceberg 
metadata and has to be
   rolled back to the previous snapshot out-of-band.
   
   This is the asymmetry between the two write lanes:
   
   | lane | writer | reports to the manifest |
   |---|---|---|
   | data | `VIcebergPartitionWriter` | `original_write_path` ✅ |
   | position delete | `VIcebergDeleteFileWriter` | `_output_path` (normalized) 
❌ |
   | deletion vector (v3) | `VIcebergDeleteSink::_write_deletion_vector_files` 
| the normalized puffin path ❌ |
   
   The data writer keeps the normalized path for I/O and a second, original 
path for the manifest.
   The delete lanes only ever had one path, and used it for both. So the *same* 
`UPDATE` writes a
   correct `abfss://` data file and a broken `s3://` delete file.
   
   Reproduced on ADLS Gen2 
(`abfss://[email protected]/key`, normalized to
   `s3://container/key` by `AzurePropertyUtils.validateAndNormalizeUri()`), but 
it is not
   Azure-specific: it applies to any storage whose location is rewritten for 
the BE. The FE's own
   
`IcebergWritePlanProviderTest.planWriteBuildsDeleteSinkWithTableDerivedFields` 
pins exactly this
   pair for OSS —
   
   ```java
   Assertions.assertEquals("s3://bucket/wh/db1/t1/data", sink.getOutputPath(),
           "delete output path is the normalized data location (legacy 
LocationPath.toStorageLocation)");
   Assertions.assertEquals("oss://bucket/wh/db1/t1/data", 
sink.getTableLocation(),
           "table_location stays the raw data location (legacy 
IcebergUtils.dataLocation)");
   ```
   
   ### The invariant this relies on
   
   `TIcebergDeleteSink.output_path` and `.table_location` are **the same 
directory in two URI forms** —
   both are derived from a single `resolveLocationFields(table, 
schemaContext.getDataLocation())` call,
   in `IcebergWritePlanProvider#buildDeleteSink` (delete) and `#buildMergeSink` 
(update/merge), and
   
`IcebergWritePlanProviderTest.planWriteBuildsDeleteSinkWithTableDerivedFields` 
pins both literal
   values. They are never two different directories, and `table_location` is 
never the table root.
   
   ### The fix
   
   BE-only, no thrift change and no new FE plumbing — as suggested during 
triage.
   
   `TIcebergDeleteSink` **already** carries both forms of the same directory: 
`output_path`
   (normalized, for I/O) and `table_location` (raw, as the catalog reports it). 
`VIcebergDeleteSink`
   now generates one file *name* and joins it to both bases:
   
   - `DeleteFileLocation::write_path` → filesystem I/O and the abort-time 
cleanup list,
   - `DeleteFileLocation::original_path` → `TIcebergCommitData.file_path`, i.e. 
the manifest.
   
   `VIcebergDeleteFileWriter` gained an `original_output_path` alongside 
`_output_path` and reports
   *that* in `commit_data`. The v3 deletion-vector / Puffin lane gets the same 
treatment — it was
   code-identical and would have failed the same way once a v3 table saw a DML 
on rewritten storage.
   
   Both `DELETE` and the delete half of `UPDATE` / `MERGE` go through 
`VIcebergDeleteSink`
   (`VIcebergMergeSink::_build_inner_sinks` synthesizes the inner delete sink 
and propagates both
   `output_path` and `table_location`), so one change covers all three 
statements.
   
   When only one of the two fields is present, it stands in for both. That 
keeps the previous behaviour
   for any location that was never rewritten, and makes the change safe against 
an FE that does not
   send both. Note the fallback cannot do better than "unchanged from today" in 
the rewritten case:
   `abfss://<container>@<account>.dfs.core.windows.net/<key>` is not 
recoverable from
   `s3://<container>/<key>`, because normalization drops the account. (The data 
lane does not fall back
   at all — `viceberg_table_writer.cpp` concatenates an unset 
`original_output_path` as an empty
   string. The delete lane is deliberately more defensive here; happy to match 
the data lane instead if
   you prefer the consistency.)
   
   #### Why not a new `original_output_path` field on `TIcebergDeleteSink`?
   
   For symmetry with the data lane that would be the tidier shape, and I will 
add it if you prefer. I
   chose not to because a thrift field makes the fix new-FE-only and reopens an 
old-FE/new-BE window,
   whereas this BE-only change corrects the manifest against **every** 
already-deployed FE, needs no
   wire-format change, and backports to `branch-4.1` unchanged (its legacy
   `planner/IcebergDeleteSink.java:135-138` sets the same 
raw-data-location/normalized pair).
   
   ### Release note
   
   Fix `UPDATE` / `DELETE` on an Iceberg table whose storage location is 
rewritten for the backend
   (e.g. ADLS `abfss://`, OSS `oss://`) committing position-delete and 
deletion-vector files under
   Doris's internal normalized path, which left the table permanently 
unreadable.
   
   ### Check List (For Author)
   
   - Test
       - [ ] Regression test
       - [x] Unit Test
       - [ ] Manual test (add detailed scripts or steps below)
       - [ ] No need to test or manual test. Explain why:
           - [ ] This is a refactor/code format and no logic has been changed.
           - [ ] Previous test can cover this change.
           - [ ] No code files have been changed.
           - [ ] Other reason
   
   New / updated BE unit tests:
   
   - `be/test/exec/sink/writer/iceberg/viceberg_delete_file_writer_test.cpp` 
(new) — the commit data
     carries the original path, not the normalized one; the two coincide when 
the location was never
     rewritten; the factory forwards both.
   - `be/test/exec/sink/viceberg_delete_sink_test.cpp`
     - `TestDeleteFilePathsKeepCatalogUriForManifest` — position-delete and 
Puffin paths both keep the
       `abfss://` prefix for the manifest and the `s3://` prefix for I/O, with 
an identical file name.
     - `TestDeletionVectorCommitsCatalogUriAndWritesNormalizedPath` — 
end-to-end on the v3 lane: the
       puffin bytes land under the write path while the commit data names the 
catalog URI.
     - `TestSharedPuffinCommitsCatalogUriForEveryBlob` — several blobs sharing 
one puffin: *every*
       commit entry carries the catalog URI, not just the first.
     - `TestDeleteFilePathsFallBackWhenOneFormIsMissing` — one form absent 
(empty, and genuinely
       unset) means both paths agree. A rolling-upgrade guard; it passes 
pre-fix too.
     - The fixture at the top of the file previously used two *different* 
directories for
       `output_path` / `table_location`, which does not model anything the FE 
can send. It now uses a
       normalized/raw pair of one directory.
   
   Not covered, stated plainly: there is no end-to-end test of the **v2 
position-delete** lane
   (`_write_position_delete_files`) equivalent to the v3 one — it needs a 
`RuntimeState` and expression
   harness, and I could not build the BE locally to validate such a test (no 
thirdparty toolchain), so
   I did not want to push an unverified test into your CI. A transposition of 
the two arguments at the
   `VIcebergDeleteFileWriterFactory::create_writer` call site would therefore 
not be caught by a test
   today, only by review. Happy to add it if you would like it in this PR.
   
   No regression test: reproducing this needs a catalog on storage whose URI 
the FE rewrites (ADLS or
   OSS), which the local/S3 regression environments do not exercise — an 
`s3://` location is
   normalized to itself. I have a live ADLS Gen2 + Iceberg REST (Lakekeeper) 
cluster and am happy to
   run a patched build against the reproduction in #67544 and report back.
   
   - Behavior changed:
       - [x] Yes. Position-delete and deletion-vector files are now recorded in 
the Iceberg manifest
         with the catalog's own URI instead of Doris's internal normalized 
path. On storage that is
         not rewritten (HDFS, S3, local) the recorded path is unchanged.
   
   - Does this need documentation?
       - [x] No.
   


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