Skip to content

[fix](paimon) Fail scans of binary partitions with legacy partition names - #69571

Merged
morningman merged 3 commits into
apache:masterfrom
morningman:b2m-t14h-paimon-legacy-binary-partition-read
Oct 26, 2026
Merged

morningman merged 3 commits into
apache:masterfrom
morningman:b2m-t14h-paimon-legacy-binary-partition-read

Conversation

@morningman

@morningman morningman commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Related PR: #69567

Problem Summary:

In short. A query of a Paimon table with a BINARY / VARBINARY partition column now fails with an error naming 'partition.legacy-name' when it would read a non-NULL binary partition whose files Paimon cannot locate, instead of silently returning only the rows of the other partitions. This is the read-side counterpart of #69567, which rejects writes to such tables.

Background. Paimon looks for the files of a partition under the directory its partition values name, computed when the split is planned; a writer puts the files under the name it computes when it writes. With partition.legacy-name = true, the default, the name of a value is its toString(), which for a binary value is the Java array object ([B@1b6d3586) rather than its bytes, so every resolution differs. With partition.legacy-name = false it is the text of the bytes (p=AB for X'4142'). The option can be changed on an existing table, which changes how existing partitions are looked up but does not move their files. The BE skips a file of an external table that no longer exists (ignore_not_found_file_in_external_table, default true), which is meant for files removed between planning and reading.

The problem, and what it cost. Measured on master with Paimon 2.1.0, on a file:// Paimon catalog of a local FE + BE, with tables created and written through the Paimon API:

Table Paimon API read Doris before this PR
legacy names (default), rows in X'4142', X'EF', NULL fails: FileNotFoundException (p=%5BB@...) SELECT * returns only the NULL row; COUNT(*), answered from metadata, returns 3
legacy names when X'4142' was written, then 'partition.legacy-name' = 'false' and rows in X'4344', NULL fails: looks for p=AB, the file is under p=%5BB@... returns 2 of 3 rows, the X'4142' row is missing
new names when X'4142' and NULL were written, then 'partition.legacy-name' = 'true' fails returns 1 of 2 rows
legacy names, rows only in the NULL partition reads reads correctly

So the query succeeds with a partial result and nothing tells the user. Following the obvious fix, setting 'partition.legacy-name' = 'false', does not make the old partitions readable either; it only moves the loss to the files written before the change.

How this PR fixes it. PaimonScanPlanProvider.planScanInternal checks every data split that will read files (after the splits whose COUNT(*) is answered from metadata) whose binary partition value is not NULL:

  • if the split is looked up under legacy names (the options of its table, or of its branch for a fallback read), planning fails;
  • otherwise, if one of its data files was written under a schema whose options use legacy names (the file's schemaId, looked up once per schema and branch, inside the catalog authentication) and is also missing at its planned path (bucketPath/fileName, the path the native reader is given), planning fails and names that file. The schema alone does not prove how a file was named: a writer's dynamic options (spark.paimon.*, Flink hints, Table.copy) keep the schema ID, so a file written with a dynamic 'partition.legacy-name' = 'false' on a table that keeps the default refers to a schema with legacy names while it sits under the bytes of its value, and it is read once the table uses those names;
  • and, on a table with 'index-file-in-data-file-dir' = 'true', every deletion vector the split carries must exist, whether or not its data file has an external path, or planning fails and names the deletion vector file and its data file. Such a table keeps its deletion vectors with its data files: one written while no external data path is configured records only its file name and is looked up under the directory of its partition, which a legacy name never finds, while its data file keeps its external path, and the BE skips the whole data file when it cannot open the deletion vector. The option is immutable, so every writer used the reading table's value. Deletion vectors under the table's index/ directory do not depend on partition names and are not checked.

A NULL partition is named after partition.default-name under both namings, and a data file with an external path is read from the path its metadata records, so neither is rejected; the deletion vectors of a file with an external path are still checked. The system tables read through JNI ($audit_log, $row_tracking) report the options of the main branch, while a fallback split is planned and read under its own branch, so the check resolves the branch that produced each split from the pinned source of the wrapper, as it does for data tables. $binlog exposes its partition columns as arrays and is not checked; Paimon's own reader fails on a file it cannot find. A scan.file-creation-time-millis read of a fallback table plans the main branch through its SnapshotReader, and its plain splits are judged under the main branch. The error names the table, the partition and the option. The existence checks run inside the catalog authentication, once per path and scan: a table that keeps its index files under index/ and whose files were written under its current naming makes no extra filesystem calls.

Not covered. A file written with a dynamic 'partition.legacy-name' = 'true' (or by a Paimon writer too old to know the option) while its schema says 'false' sits under a legacy name but is looked up under its bytes, and the BE still skips it, as on master and branch-4.2. Catching it would take an existence call for every file of every non-NULL binary partition on every scan.

Results. On the same local cluster, after this PR:

Query Doris after this PR
SELECT * of the legacy table (also with force_jni_scanner = true) fails: Cannot read Paimon table d.api_legacy: the table uses legacy partition names ('partition.legacy-name' = 'true'), under which Paimon names partition p = 0xEF after a Java array object rather than its bytes, so the files of the partition are never found. Paimon cannot read the partition either; ...
SELECT COUNT(*) of the legacy table 3, answered from metadata as before
SELECT * ... WHERE p IS NULL of the legacy table the NULL row; Paimon prunes the other partitions
SELECT * of the table switched to new names fails: ... data file data-b3abc825-...-0.parquet of partition p = 0x4142 was written while the table used legacy partition names ..., the file the Paimon API read reports as not found
SELECT * of the table switched back to legacy names fails: ... the table uses legacy partition names ...
SELECT * of the legacy table with only NULL partitions, and of tables with new names all rows, the same as the Paimon API read

PaimonScanPlanProviderTest adds eight tests: a scan under legacy names fails while the NULL partition and COUNT(*) still plan; a file written under legacy names fails after the option changes to false, and a file written under the new names fails after it changes back to true; a file at an external path still plans and points at an existing file; a file at an external path whose deletion vector was written after the external paths were removed (persisted the way PaimonSparkWriter.persistDeletionVectors does it for a DELETE) fails and names the deletion vector, while a deletion vector written with the external paths in place still plans and points at an existing file; a file written through table.copy({"partition.legacy-name": "false"}) on a table that keeps the default fails as a legacy lookup, then plans once the option is altered to false; a fallback table whose main branch holds only a NULL partition and whose fallback branch holds a non-NULL binary partition plans the data table and $audit_log when the fallback branch names partitions by bytes, and the planned $audit_log splits read both rows back through Paimon, while both fail at planning with the legacy-name error when only the main branch names them by bytes; and a scan.file-creation-time-millis read of a fallback table with a non-NULL binary partition plans the main branch's file. nativeBinaryPartitionsKeepAllPathValues, added by #68786 for the rendering of binary partition values, now creates its table with 'partition.legacy-name' = 'false': under legacy names the files it planned did not exist. The new tests fail when the lookup check, the check of the files' schemas, the exemption of external paths or of NULL values is removed, when the check moves before the COUNT(*) splits, when the deletion vectors are not checked, when every deletion vector fails without its existence being checked, when a file whose schema uses legacy names fails without its existence being checked, when a system table read through JNI is checked under the wrapper's options, when every split is judged under the main branch, or when a split that does not come from the fallback scan is cast to a fallback split (the file-creation-time read then failed with a ClassCastException). The 853 tests of fe-connector-paimon run without failures (one is skipped, as before).

No regression test is added: Doris cannot create a binary column, so the tables would have to come from the Spark container of the Paimon docker environment, which was not run locally.

Release note

Fix: a query of a Paimon table with a binary partition column fails with an error naming 'partition.legacy-name' when it would read a non-NULL binary partition whose files Paimon cannot locate under legacy partition names, instead of silently returning only the rows of the other partitions.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
      Local FE + BE with CREATE CATALOG ... PROPERTIES ('type' = 'paimon', 'warehouse' = 'file:///...'); tables created and written with the Paimon Catalog API (Schema.newBuilder().column("p", DataTypes.BYTES()).partitionKeys("p"), the option changed with SchemaChange.setOption); the queries above, compared with a read through the Paimon API. The cluster results above were measured before the follow-up commits for the deletion vectors, the dynamic naming option and fallback branches, which are covered by unit tests only; none of the tables above has a deletion vector, a fallback branch or a file written with a dynamic option.
    • 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
  • Behavior changed:

    • No.
    • Yes.
      A query that reads a non-NULL binary partition of a Paimon table whose files are named or looked up under legacy partition names fails instead of returning a partial result.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

…ames

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69567

Problem Summary: Paimon finds the files of a partition under the directory
its partition values name. With partition.legacy-name = true, the default,
Paimon names a partition after the string of each value, which for a
non-NULL BINARY / VARBINARY value is the Java array object ([B@1b6d3586)
rather than its bytes, so every resolution of the name differs. The paths
the connector planned for such a partition never existed: Paimon's own
read fails with FileNotFoundException, while the BE skips missing files of
an external table (ignore_not_found_file_in_external_table, default true),
so a Doris query silently returned only the rows of the NULL partition
(SELECT * of a table with rows in X'4142', X'' and NULL returned 1 row,
while COUNT(*), answered from metadata, returned 3). The same happens to
files written under a legacy name after the table switches to
'partition.legacy-name' = 'false', and to files written under the new
names after it switches back.

Scan planning now fails for a split whose binary partition value is not
NULL when the split is looked up under legacy partition names, or when one
of its data files was written under a schema whose options use legacy
names, naming the table, the partition and the option. A split of the NULL
partition, a file at an external path (found by the path its metadata
records) and a COUNT(*) answered from metadata still plan as before.

### Release note

Fix: a query of a Paimon table with a binary partition column whose
partitions use legacy partition names ('partition.legacy-name' = 'true',
the default) fails with an error naming the option, instead of silently
returning only the rows of the NULL partition.

### Check List (For Author)

- Test: Unit Test, Manual test
    - PaimonScanPlanProviderTest: legacy names fail planning while the NULL partition and COUNT(*) do not; a file written under legacy names fails after the option changes, and a file written under the new names fails after it changes back; a file at an external path still plans
    - Manual: local file:// Paimon catalog with tables created by the Paimon API
- Behavior changed: Yes. A query that reads a non-NULL binary partition of a Paimon table with legacy partition names fails instead of returning a partial result.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot 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.

Two verified P2 issues: historical schema options can incorrectly reject readable binary-partition files, and the external-data exemption leaves the targeted silent row loss unfixed when a required deletion vector has a local legacy binary path. Both findings include concrete supported write sequences. Metadata COUNT and NULL-partition exceptions remain appropriate.

Critical checkpoint conclusions:

  • Goal and proof: ordinary unreadable legacy binary data paths are rejected, but valid dynamic-option writes can also be rejected and external data with a local deletion vector can still silently lose rows. Tests omit both cases.
  • Scope and clarity: changes are localized to the provider and its unit tests; existing branch resolution and authentication helpers are reused.
  • Concurrency and locks: new arrays, suppliers and caches are local to a scan call; no new threads, shared mutable state, locks or lock-order changes.
  • Lifecycle and initialization: caches end with scan planning; no new closeable resource, ownership cycle or static initialization dependency.
  • Configuration: no new Doris configuration or dynamic-setting contract.
  • Compatibility: no SPI, wire protocol or storage format change; the newly rejected readable-file case is the behavior regression reported inline.
  • Thrift/protobuf IDs: neither directory changes.
  • Parallel paths: native, Rust and JNI DataSplit routes share the guard; COUNT bypass is appropriate. Fallback branch selection, read-optimized wrappers and deferred JNI system-table paths were traced; the external-data/local-DV combination is the separate incomplete fix reported inline.
  • Special conditions: the NULL exception matches SDK naming; an external data path proves nothing about its deletion-vector path, and persisted schema options do not prove writer options.
  • Test coverage: inspected tests cover Parquet/ORC partition values, legacy failures, both persisted naming transitions, metadata COUNT, NULL keys and external file existence. Dynamic writer options and partial DELETE after external-root removal with local DV indexes are missing. No builds, unit tests or end-to-end tests were run under the static-only contract.
  • Test results: no regression result files changed; new unit-test expectations match their constructed cases by inspection.
  • Observability: the new exception includes table, partition and relevant file context; no new hot-path logging or metric requirement identified.
  • Transactions and persistence: no Doris transaction/EditLog changes. External schema history is read only. The findings concern writer-option provenance and an existing deletion-vector-only commit path omitted by the new guard.
  • Data writes and crash atomicity: no write path, commit protocol or resource acquisition is modified.
  • FE/BE variable propagation: no transmitted fields or new variables require propagation across send paths.
  • Performance: binary positions are computed once per scan; historical naming options are cached per branch identity and schema ID. Added work is linear in inspected files, with no separate demonstrated performance regression.
  • Other issues and focus: no existing inline threads or additional user focus points were supplied. All changed hunks and both initial risk areas were reviewed; no other substantiated finding or unresolved static coverage gap remains.

Validation is static source analysis of the pinned PR head and Paimon 2.1.0 dependency sources; no runtime reproduction is claimed.

Review converged after 3 round(s).

? null : legacyOrcSplitTable((FileStoreTable) schemaTable.get(), split);
boolean legacyLookup = CoreOptions.fromMap((splitTable == null ? table : splitTable).options())
.legacyPartitionName();
for (DataFileMeta file : split.dataFiles()) {

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] Validate deletion-vector paths before exempting external data

An absolute externalPath() only makes the data file independent of partition naming. Create a binary-partitioned append table with bucket=-1, deletion-vectors.enabled=true, index-file-in-data-file-dir=true, default legacy names and external data paths. Insert two rows in one file, unset data-file.external-paths and its strategy, then delete only one row through Spark. Paimon's persistDeletionVectors commits only index changes using the current local path factory: the data keeps its recorded external path, but the new DV has a relative name under an identity-based binary directory. This continue exempts the data without checking its DV. The native reader's missing-DV NOT_FOUND reaches FileScanner, which skips the whole data range under the default ignore_not_found_file_in_external_table=true, silently losing the surviving row. This leaves the PR's targeted failure unfixed for that supported layout. Include required deletion files in the naming validation and cover this external-data/local-DV transition.

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.

Confirmed, and fixed in 65a1682d334. With index-file-in-data-file-dir = true, a deletion vector records an absolute path only if an external data path is configured when it is written (DeletionFileWriter.result). Otherwise Paimon resolves it under the partition's directory each time a split is planned (IndexInDataFileDirPathFactory.toPath). Under legacy names that directory is the identity string of a fresh array. So once the external paths are removed, a deletion vector written by a DELETE is never found, while its data file keeps its absolute path. Both native readers then get NOT_FOUND when they open the deletion vector (V1 _init_deletion_vector, V2 TableReader::_parse_delete_predicates), and FileScanner / FileScannerV2 skip the whole data range, surviving rows included. Paimon's own reader, JNI and Rust fail instead.

For a split with a non-NULL binary partition value on a table that keeps index files in data directories, every deletion vector the split carries must now exist when the split is planned, whether or not its data file has an external path. A missing one fails planning, naming the deletion vector file, its data file, the partition and the option. The option is immutable, so the reading table's value is the one every writer used. Deletion vectors under the table's index/ directory do not depend on partition names and are not checked. The existence check runs inside the catalog authentication, once per deletion vector file and scan. PaimonScanPlanProviderTest#externalBinaryPartitionFileWithLocalDeletionVectorFailsScanPlanning replays your sequence through the Paimon API: two rows written to one file with external paths, the paths removed, then one row deleted by persisting a deletion vector the way PaimonSparkWriter.persistDeletionVectors does it. Without the new check that split plans. #externalBinaryPartitionDeletionVectorAtExternalPathStaysReadable keeps the external paths, so the deletion vector records its absolute path and the split still plans.

master before this PR and branch-4.2 drop these rows too, since neither checks binary partitions at all. A BE-side change that makes a missing Paimon deletion vector an error instead of a skipped range, which would also cover deletion vectors lost in other ways, is tracked separately.

}
if (legacyLookup || (splitTable != null && legacyPartitionNameSchemas
.computeIfAbsent(splitTable, ignored -> new HashMap<>())
.computeIfAbsent(file.schemaId(), id -> withSchemaHistoryAuthentication(() -> CoreOptions

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] Do not infer writer naming options from the file schema ID

Paimon's FileStoreTable.copy(...) can override partition.legacy-name without persisting a new schema; TableSchema.copy retains the ID, which the writer stores in DataFileMeta. For example, create a binary-partitioned append table with the default true, write via table.copy(Collections.singletonMap("partition.legacy-name", "false")), then ALTER the persisted option to false. Those files have valid bytes-based paths and are readable with the current options, but still reference the original schema whose persisted option is true, so this branch now rejects every scan that reads them. The inverse dynamic override also shows that this schema boolean does not prove how a file was named. Check actual path/write provenance before declaring historical files unreadable, and add a test that writes with a dynamic naming option; the added ALTER-only tests do not cover it.

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.

Confirmed. FileStoreTable.copy keeps the schema ID (TableSchema.copy), so a file written with a dynamic partition.legacy-name = false (SET spark.paimon.partition.legacy-name = false, a Flink hint, or table.copy) refers to a schema whose persisted option is still true. Nothing in the metadata records the directory a file was written to: DataFileMeta has only the file name and an optional external path, and dynamic options are not persisted. The schema's option is therefore only a hint. Failing on it alone rejected files that are readable once the table is altered to false, and master reads them.

Fixed in 65a1682d334. The file's schema still selects which files to look at, but planning now fails only if such a file is also missing at its planned path (bucketPath/fileName, the path the native reader is given). In a table whose option was switched after legacy writes, the file is missing and the error stays. In your sequence the file is there and the scan plans. This costs one existence call per flagged file, and none for tables whose files were written under the current naming. PaimonScanPlanProviderTest#binaryPartitionFilesWrittenWithDynamicNamesStayReadable writes through table.copy({"partition.legacy-name": "false"}) on a table that keeps the default: planning fails as a legacy lookup until the option is altered to false, and then plans the file under the bytes of its value. Without the existence check it fails with the error you describe.

The inverse case is not covered: a file written with a dynamic partition.legacy-name = true while the schema says false is still looked up under its bytes, and the BE skips it. Catching that would take an existence call for every file of every binary partition on every scan. master and branch-4.2 behave the same.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28329 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit a77603c0a0efba7aaed96099afd2692d8b696a55, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17594	5169	5076	5076
q2	2141	313	254	254
q3	10250	1364	813	813
q4	4678	457	344	344
q5	7440	794	524	524
q6	182	188	153	153
q7	733	790	602	602
q8	9253	1293	1042	1042
q9	5412	4238	4247	4238
q10	6832	1303	1018	1018
q11	436	249	234	234
q12	637	394	275	275
q13	18114	3575	2970	2970
q14	276	278	248	248
q15	q16	713	711	658	658
q17	1195	745	603	603
q18	7024	6141	6108	6108
q19	1119	946	601	601
q20	383	334	224	224
q21	4878	2094	2285	2094
q22	343	295	250	250
Total cold run time: 99633 ms
Total hot run time: 28329 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5637	5632	5612	5612
q2	667	556	544	544
q3	5208	5377	4893	4893
q4	2010	2141	1541	1541
q5	5289	5177	5510	5177
q6	258	195	156	156
q7	2433	2044	1876	1876
q8	3056	2777	2683	2683
q9	8077	7918	7880	7880
q10	3747	3667	3250	3250
q11	608	448	540	448
q12	707	727	561	561
q13	3483	3941	3274	3274
q14	306	312	281	281
q15	q16	713	751	665	665
q17	8371	7436	6836	6836
q18	13003	12157	12828	12157
q19	902	773	764	764
q20	2147	2150	1890	1890
q21	5797	4699	4868	4699
q22	522	490	421	421
Total cold run time: 72941 ms
Total hot run time: 65608 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150321 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit a77603c0a0efba7aaed96099afd2692d8b696a55, data reload: false

query5	4313	589	438	438
query6	432	211	188	188
query7	4801	453	225	225
query8	322	167	146	146
query9	8774	3046	3079	3046
query10	461	308	243	243
query11	5982	3832	3567	3567
query12	160	89	87	87
query13	1247	439	319	319
query14	6107	3904	3615	3615
query14_1	3427	3363	3356	3356
query15	202	196	177	177
query16	947	431	415	415
query17	844	657	539	539
query18	2417	434	316	316
query19	199	171	139	139
query20	87	85	83	83
query21	207	128	105	105
query22	12920	13060	12884	12884
query23	13320	12826	12349	12349
query23_1	12426	12436	12424	12424
query24	7386	800	448	448
query24_1	471	468	461	461
query25	520	394	334	334
query26	1265	252	132	132
query27	2817	440	265	265
query28	5133	1936	1972	1936
query29	1541	584	431	431
query30	299	225	185	185
query31	1020	930	812	812
query32	150	85	91	85
query33	524	295	227	227
query34	1116	870	526	526
query35	726	763	646	646
query36	820	839	739	739
query37	116	98	76	76
query38	1757	1768	1666	1666
query39	790	802	764	764
query39_1	737	732	743	732
query40	222	118	93	93
query41	67	67	66	66
query42	84	90	81	81
query43	357	364	315	315
query44	1318	689	712	689
query45	181	182	164	164
query46	852	977	569	569
query47	2917	2760	2812	2760
query48	300	317	219	219
query49	561	402	286	286
query50	689	275	220	220
query51	10649	10700	10556	10556
query52	80	78	67	67
query53	183	230	149	149
query54	244	226	188	188
query55	79	77	69	69
query56	212	205	206	205
query57	1613	1616	1561	1561
query58	279	260	254	254
query59	2167	2141	2010	2010
query60	287	239	217	217
query61	143	137	139	137
query62	375	334	300	300
query63	185	160	154	154
query64	2760	919	772	772
query65	3319	3277	3248	3248
query66	1825	409	289	289
query67	20135	20040	19900	19900
query68	2958	993	615	615
query69	376	280	240	240
query70	828	801	790	790
query71	296	223	208	208
query72	2655	2583	2269	2269
query73	507	522	290	290
query74	4619	4490	4346	4346
query75	2307	2291	1915	1915
query76	2312	1006	573	573
query77	337	372	261	261
query78	9681	9736	9154	9154
query79	881	840	527	527
query80	457	398	326	326
query81	512	335	275	275
query82	201	128	103	103
query83	191	188	180	180
query84	291	130	97	97
query85	761	412	361	361
query86	288	240	225	225
query87	1958	1993	1804	1804
query88	3535	2623	2611	2611
query89	315	277	237	237
query90	2103	162	164	162
query91	158	152	122	122
query92	98	86	84	84
query93	974	978	561	561
query94	431	299	270	270
query95	574	351	358	351
query96	651	485	227	227
query97	2428	2426	2310	2310
query98	163	149	147	147
query99	758	759	654	654
Total cold run time: 233298 ms
Total hot run time: 150321 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.31 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit a77603c0a0efba7aaed96099afd2692d8b696a55, data reload: false

query1	0.01	0.01	0.01
query2	0.09	0.06	0.05
query3	0.26	0.09	0.09
query4	1.62	0.11	0.10
query5	0.23	0.24	0.25
query6	1.17	0.69	0.68
query7	0.04	0.01	0.01
query8	0.06	0.05	0.04
query9	0.40	0.34	0.34
query10	0.58	0.58	0.62
query11	0.20	0.14	0.15
query12	0.20	0.15	0.15
query13	0.49	0.48	0.49
query14	1.05	1.01	1.02
query15	0.65	0.60	0.62
query16	0.35	0.35	0.35
query17	1.14	1.16	1.13
query18	0.21	0.19	0.19
query19	2.12	2.02	2.06
query20	0.02	0.01	0.02
query21	15.48	0.20	0.13
query22	4.78	0.06	0.05
query23	16.28	0.33	0.13
query24	3.07	0.65	0.36
query25	0.14	0.05	0.05
query26	1.29	0.21	0.16
query27	0.05	0.05	0.05
query28	3.44	0.74	0.37
query29	12.46	4.06	3.23
query30	0.29	0.13	0.10
query31	2.78	0.55	0.32
query32	3.22	0.60	0.50
query33	3.14	3.18	3.15
query34	15.45	3.97	3.36
query35	3.32	3.37	3.32
query36	0.55	0.43	0.46
query37	0.09	0.07	0.07
query38	0.05	0.03	0.04
query39	0.04	0.03	0.03
query40	0.17	0.15	0.15
query41	0.09	0.03	0.03
query42	0.03	0.03	0.03
query43	0.04	0.04	0.04
Total cold run time: 97.14 s
Total hot run time: 24.31 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage `` 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 100% (0/0) 🎉
Increment coverage report
Complete coverage report

…ary partitions

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69571

Problem Summary: Follow-up to the scan-planning check of binary partitions
in this PR, for two cases found in review.

With 'index-file-in-data-file-dir' = 'true', a deletion vector records an
absolute path only if an external data path is configured when it is
written; otherwise Paimon looks it up under the directory of its
partition when the split is planned, which under legacy partition names is
named after a fresh Java array object. The check exempted every data file
with an external path without looking at its deletion vector, so once the
external data paths were removed from such a table, a deletion vector
written by a DELETE was never found while its data file kept its absolute
path: the native readers get NOT_FOUND when they open the deletion vector
and the BE skips the whole data file, surviving rows included. For a split
with a non-NULL binary partition value on such a table, every deletion
vector the split carries must now exist, whether or not its data file has
an external path; a missing one fails planning, naming the deletion vector
file, the data file, the partition and the option. Deletion vectors under
the table's index/ directory do not depend on partition names and are not
checked.

A writer's dynamic options (spark.paimon.*, Flink hints, Table.copy) keep
the schema ID, so a file written with a dynamic 'partition.legacy-name' =
'false' on a table that keeps the default refers to a schema with legacy
names while it sits under the bytes of its value. The check rejected such
files after the table was altered to 'false', although Paimon and master
read them. A file whose schema uses legacy names now fails planning only
if it is also missing at its planned path (bucket path / file name).

Both existence checks run inside the catalog authentication and are
cached per path for the scan. A table that keeps its index files under
index/ and whose files were written under its current naming makes no
extra filesystem calls.

### Release note

None (follow-up inside the PR)

### Check List (For Author)

- Test: Unit Test
    - PaimonScanPlanProviderTest: a data file at an external path whose deletion vector was written after the external paths were removed fails planning; a deletion vector written while the external paths are configured still plans; a file written through table.copy({"partition.legacy-name": "false"}) on a table that keeps the default fails as a legacy lookup, and plans after the option is altered to false
    - fe-connector-paimon module tests: 851 run, 0 failures, 0 errors, 1 skipped (pre-existing)
- Behavior changed: Yes. A scan of a non-NULL binary partition fails when a deletion vector it carries is missing, and a file whose schema uses legacy names fails it only when the file is missing at its planned path.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@morningman

Copy link
Copy Markdown
Contributor Author

Addressed the first review round in 65a1682d334. Per finding:

  • 4366822772 (P2, deletion vectors of data files at external paths): fixed. On a table with index-file-in-data-file-dir = true, every deletion vector of a split with a non-NULL binary partition value must exist, whether or not its data file has an external path; a missing one fails planning and names the deletion vector file. A BE-side change that makes a missing Paimon deletion vector an error instead of a skipped range is tracked separately.
  • 4366822776 (P2, the schema ID does not prove how a file was named): fixed. A file whose schema uses legacy names fails planning only if it is also missing at its planned path, so a file written with a dynamic partition.legacy-name = false stays readable once the table uses those names. The inverse, a dynamic true over a false schema, is not covered, as on master and branch-4.2.

Three new tests in PaimonScanPlanProviderTest; the 851 tests of fe-connector-paimon run without failures (1 skipped, as before). The PR description is updated.

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot 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.

Reviewed the complete supplied aggregate diff at 65a1682d33409db5bcb2146bfbb4ac7ac66dbccf: both changed files, their calling/reader paths, and the existing review context. One new verified P2 is inline: the binary guard rejects readable fallback partitions of an audit-log system table by consulting the main branch's naming option.

Existing findings were independently revalidated: 4366822772 is fixed because required deletion vectors are checked even for external data; 4366822776 is fixed because a historical schema hint now requires an actually missing reader path before rejecting a file. Neither is reposted. No existing P0/P1 was present. No additional user focus was supplied.

Flow checked: resolved/pinned table -> SDK pruning and split planning -> metadata COUNT(*) bypass -> binary guard -> native / JNI / Rust routing -> data and deletion-vector read paths.

Required checkpoint Conclusion
Goal and evidence The targeted legacy-name layouts now fail during planning; the inline finding identifies a readable-layout regression. Six added SDK unit fixtures cover the intended fixes statically.
Scope and reuse Changes stay in Paimon planning/tests and reuse authentication and branch helpers; the native schema-dictionary helper is too narrow for selecting system-table options.
Concurrency and locks New maps/supplier belong to one synchronous plan invocation. No new worker, shared cache, lock order, or deadlock mechanism. Existing statement-plan reuse remains unchanged.
Lifecycle and initialization Plan-local heap state expires with planning; no static initialization or native-resource ownership added.
Configuration No Doris setting added. Existing naming and index-layout options are read from table generations; branch selection needs the inline correction.
Compatibility and rolling upgrades No public SPI, function symbol, storage format, or serialized protocol changes. Rejection of unreadable layouts is intentional.
Thrift/protobuf IDs No gensrc changes; branch-ID comparison is inapplicable.
Parallel paths Checked native Parquet/ORC, JNI, Rust, count pushdown, external files, NULL keys, ordinary fallback and system wrappers. Normal data/$ro branch selection is preserved; audit-log fallback is the finding. Non-DataSplit chain/format paths remain JNI.
Special conditions Non-NULL detection, count exemption, external data/DV independence, writer-schema hints and actual paths have concrete rationale. The guard must also use the producing branch for wrappers without a native dictionary.
Test coverage All six new tests, their real committed-file/DV helpers and the adjusted binary Parquet/ORC test were inspected. Missing wrapper-plus-fallback binary coverage corresponds to the inline defect.
Expected results Added assertions match the intended planned paths/errors and metadata counts on inspection. No result files changed. Tests were not executed.
Observability and failures Errors identify table, binary partition, relevant file and option; IO errors retain path/cause. Schema/exists IO uses the authenticated boundary. No swallowed failure or separate critical metric omission found.
Transactions, persistence and failover Production is read-only; SDK snapshot selection, EditLog and commit/failover protocols are unchanged.
Writes, atomicity and crash cleanup No production write or transaction state added. Errors discard planning results; no new crash-leak mechanism found.
FE/BE variables None added; checked data/DV paths are the existing paths passed to readers.
Performance and memory CPU/heap grow with selected metadata; schema facts memoize per branch/schema and existence checks per path per scan. Schema IO is once per branch/schema; existence IO is limited to flagged historical data and relevant DVs. No additional demonstrated redundant operation.
Other core invariants No change to committed visibility, version limits, DV offsets/cardinality or attachment to native subranges. BE allocator/MoW bitmap concerns are inapplicable to this Java planning change. No other verified new issue.

Validation limits: static review only, as required; no builds, tests, clusters, runtime reproductions or source edits. The new tests do not execute a BE/remote-storage reader matrix or inject authentication/IO failures. The already-discussed inverse dynamic override (writer legacy=true while its persisted schema says false), unrelated missing-file layouts, and files disappearing after planning remain pre-existing limits of this targeted guard and are not reported again.

Review converged in three rounds: both complete-scope reviewers and the separate path-risk reviewer returned NO_NEW_VALUABLE_FINDINGS on the unchanged final payload. All risk focuses and candidates have an explicit disposition; no unresolved review candidate remains. The verified P2 still needs correction; convergence describes review completion, not a bug-free verdict.

Review converged after 3 round(s).

int binaryField = nonNullField.getAsInt();
FileStoreTable splitTable = schemaTable.get() == null
? null : legacyOrcSplitTable((FileStoreTable) schemaTable.get(), split);
Table optionsTable = splitTable == null ? table : splitTable;

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] Resolve fallback options for audit-log binary partitions

For $audit_log, resolveSchemaDictTable returns null, so this selects the system wrapper's options. Paimon's audit wrapper exposes the main branch's options while still planning and reading the fallback branch. For example, write only a NULL binary partition on main with the default partition.legacy-name=true; create a backup branch, set its persisted naming option to false, and write a non-NULL binary partition there. After setting main's scan.fallback-branch=backup, SELECT id FROM t$audit_log now throws the legacy-name error for the backup split, even though its byte-named file exists and the JNI reader selects backup correctly. Resolve the pinned system source and the split's producing branch independently of native-schema-dictionary availability, and add this wrapper/fallback case to the tests. This uses ordinary persisted branch options, with no dynamic writer override or schema-ID provenance ambiguity.

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.

Confirmed, and fixed in cf79a1ac228. resolveSchemaDictTable has no answer for $audit_log, since an AuditLogTable is a DataTable rather than a FileStoreTable. The check then fell back to the wrapper's own options, and AuditLogTable.options() returns those of the fallback pair, which are the main branch's. The splits still come from FallbackReadScan, which plans a partition that is missing on main from the fallback branch and reads it with that branch's read. I replayed your history with the Paimon 2.1.0 API. Main keeps the default partition.legacy-name = true and holds only a NULL partition, and backup has false and a row in key = 0x4142. $audit_log reports true, while the fallback split's file is at key=AB/bucket-0/…, exists, and Paimon's own read returns the row. The check rejected that scan although master reads it through JNI.

For the system tables read through JNI, the check now resolves the table that produced the splits from the wrapper's pinned source, the same source the timestamp-history checks already use (resolveTimestampHistoryTable). It then picks each split's branch as it does for data tables, so a fallback split of $audit_log or $row_tracking is judged by the naming and schemas of its own branch. In the reverse history (main false, backup true) the scan now fails at planning with the legacy-name error. Before, it planned and Paimon's reader failed at execution with a FileNotFoundException. $binlog is not affected: it exposes its partition columns as arrays, so it has no binary partition column to check. fallbackBinaryPartitionsUseTheNamingOfTheirBranch covers both directions for the data table and $audit_log, and reads the planned $audit_log splits back through the wrapper.

While there: a scan.file-creation-time-millis read plans a fallback table through the pair's SnapshotReader, which returns plain DataSplits of the wrapped branch. legacyOrcSplitTable cast every split to FallbackSplit, so such a scan of a table with a non-NULL binary partition failed with a ClassCastException. A split that is not a FallbackSplit now maps to the wrapped branch. The same cast on the native timestamp path predates this PR and is fixed with it. fileCreationTimeScanOfFallbackTableChecksBinaryPartitionsOfTheMainBranch plans such a read.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28816 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 65a1682d33409db5bcb2146bfbb4ac7ac66dbccf, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17613	5220	5103	5103
q2	2183	318	260	260
q3	10204	1370	833	833
q4	4683	464	338	338
q5	7449	809	567	567
q6	190	187	159	159
q7	770	808	602	602
q8	9262	1383	1052	1052
q9	5480	4245	4171	4171
q10	6815	1293	1029	1029
q11	424	249	230	230
q12	636	396	286	286
q13	18110	3795	3023	3023
q14	272	284	252	252
q15	q16	732	721	668	668
q17	1204	749	582	582
q18	7038	6174	6108	6108
q19	1116	950	616	616
q20	393	335	224	224
q21	4973	2472	2415	2415
q22	398	364	298	298
Total cold run time: 99945 ms
Total hot run time: 28816 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6025	5976	6133	5976
q2	747	602	605	602
q3	5641	5758	5326	5326
q4	2037	2171	1594	1594
q5	5640	5539	5755	5539
q6	238	187	149	149
q7	2246	1995	1793	1793
q8	3108	2702	2653	2653
q9	7955	7985	7796	7796
q10	3630	3594	3162	3162
q11	585	418	399	399
q12	670	687	511	511
q13	3269	3655	3080	3080
q14	291	290	278	278
q15	q16	675	698	623	623
q17	7807	7055	6903	6903
q18	12988	12212	12928	12212
q19	897	783	771	771
q20	2162	2168	1944	1944
q21	5709	4630	4712	4630
q22	507	473	416	416
Total cold run time: 72827 ms
Total hot run time: 66357 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151019 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 65a1682d33409db5bcb2146bfbb4ac7ac66dbccf, data reload: false

query5	4308	584	452	452
query6	428	209	185	185
query7	4799	447	231	231
query8	329	184	150	150
query9	8755	3109	3125	3109
query10	489	306	264	264
query11	6098	3880	3589	3589
query12	153	91	86	86
query13	1246	438	331	331
query14	6143	3943	3690	3690
query14_1	3452	3420	3423	3420
query15	202	197	180	180
query16	956	442	404	404
query17	857	656	546	546
query18	2419	419	312	312
query19	203	167	137	137
query20	86	88	86	86
query21	207	129	109	109
query22	13027	13047	12900	12900
query23	13512	12873	12462	12462
query23_1	12663	12633	12599	12599
query24	7427	820	468	468
query24_1	479	472	472	472
query25	545	425	359	359
query26	1255	260	139	139
query27	2748	443	276	276
query28	4607	1961	1933	1933
query29	1560	603	499	499
query30	301	219	185	185
query31	1027	922	807	807
query32	141	109	95	95
query33	526	320	238	238
query34	881	866	495	495
query35	743	754	633	633
query36	809	823	728	728
query37	125	101	86	86
query38	1785	1751	1678	1678
query39	795	805	755	755
query39_1	732	725	751	725
query40	218	117	101	101
query41	77	72	68	68
query42	92	83	82	82
query43	353	368	306	306
query44	1323	673	686	673
query45	196	195	175	175
query46	868	971	557	557
query47	2630	2949	2789	2789
query48	287	301	214	214
query49	582	395	291	291
query50	712	275	212	212
query51	10546	10261	10458	10261
query52	81	86	70	70
query53	185	209	151	151
query54	230	191	174	174
query55	72	68	64	64
query56	248	231	226	226
query57	1641	1628	1598	1598
query58	291	268	281	268
query59	2083	2133	1960	1960
query60	281	240	232	232
query61	151	147	150	147
query62	399	339	282	282
query63	189	158	162	158
query64	2728	964	768	768
query65	3326	3291	3275	3275
query66	1814	422	307	307
query67	20003	20194	20011	20011
query68	3059	1014	605	605
query69	386	298	256	256
query70	862	765	776	765
query71	299	237	213	213
query72	2687	2556	2262	2262
query73	512	529	302	302
query74	4596	4520	4329	4329
query75	2304	2295	1917	1917
query76	2312	1012	606	606
query77	353	386	265	265
query78	9710	9674	9167	9167
query79	999	843	508	508
query80	1258	421	352	352
query81	584	314	268	268
query82	603	135	112	112
query83	310	206	183	183
query84	313	124	96	96
query85	861	439	368	368
query86	403	235	232	232
query87	2011	1939	1805	1805
query88	3660	2660	2649	2649
query89	336	284	240	240
query90	1901	178	173	173
query91	165	156	134	134
query92	99	86	93	86
query93	1025	998	574	574
query94	634	275	261	261
query95	576	380	309	309
query96	660	492	233	233
query97	2523	2416	2288	2288
query98	163	156	154	154
query99	761	762	651	651
Total cold run time: 235157 ms
Total hot run time: 151019 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.52 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 65a1682d33409db5bcb2146bfbb4ac7ac66dbccf, data reload: false

query1	0.01	0.00	0.00
query2	0.10	0.05	0.06
query3	0.27	0.09	0.09
query4	1.61	0.10	0.10
query5	0.25	0.24	0.25
query6	1.16	0.69	0.68
query7	0.04	0.00	0.00
query8	0.05	0.04	0.03
query9	0.40	0.35	0.34
query10	0.62	0.60	0.58
query11	0.22	0.14	0.14
query12	0.19	0.15	0.15
query13	0.50	0.50	0.49
query14	1.04	1.00	1.02
query15	0.64	0.61	0.61
query16	0.35	0.36	0.35
query17	1.15	1.20	1.14
query18	0.19	0.19	0.17
query19	2.01	1.95	1.97
query20	0.02	0.01	0.01
query21	15.49	0.20	0.13
query22	4.88	0.06	0.06
query23	16.28	0.32	0.13
query24	2.78	0.59	0.60
query25	0.11	0.06	0.04
query26	1.70	0.22	0.15
query27	0.04	0.05	0.04
query28	3.36	0.71	0.36
query29	12.49	4.13	3.28
query30	0.29	0.13	0.10
query31	2.77	0.56	0.32
query32	3.23	0.60	0.51
query33	3.16	3.17	3.17
query34	15.35	4.03	3.41
query35	3.36	3.32	3.34
query36	0.57	0.48	0.43
query37	0.09	0.06	0.07
query38	0.06	0.04	0.04
query39	0.03	0.03	0.03
query40	0.18	0.14	0.15
query41	0.09	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.03	0.04
Total cold run time: 97.21 s
Total hot run time: 24.52 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage `` 🎉
Increment coverage report
Complete coverage report

…tions of their branch

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69571

Problem Summary: Follow-up to the scan-planning check of binary partitions
in this PR, for two cases found in review.

The check takes the naming option and the file schemas from the branch
that produced each split, but only when the table has a schema
dictionary. For $audit_log and $row_tracking, which are read through JNI,
there is none, so the check used the wrapper's own options, and an
AuditLogTable reports the options of the fallback pair, which are the
main branch's. With scan.fallback-branch set, a partition missing on the
main branch is still planned and read under the fallback branch's
options. A table whose main branch keeps the default
'partition.legacy-name' = 'true' and holds only a NULL partition, while
its fallback branch uses 'false' and holds a non-NULL binary partition,
was therefore rejected for $audit_log although Paimon reads the
fallback rows; in the reverse layout the scan planned and Paimon's reader
failed with FileNotFoundException at execution. For the wrappers read
through JNI the check now resolves the table that produced the splits
from the wrapper's pinned source, as the timestamp-history checks already
do, and picks each split's branch from it as it does for data tables.
$binlog exposes its partition columns as arrays and is not checked.

A scan.file-creation-time-millis read plans a fallback table through the
pair's SnapshotReader, which reads the wrapped branch and returns plain
DataSplits. Picking the branch cast every split to FallbackSplit, so such
a scan of a table with a non-NULL binary partition failed with a
ClassCastException (the same cast on the native timestamp path predates
this PR). A split that is not a FallbackSplit now maps to the wrapped
branch.

### Release note

None (follow-up inside the PR)

### Check List (For Author)

- Test: Unit Test
    - PaimonScanPlanProviderTest: a fallback pair whose main branch holds only a NULL partition and whose fallback branch holds a non-NULL binary partition plans the data table and $audit_log, and the $audit_log splits read back both rows through Paimon, when the fallback branch names partitions by bytes; both fail at planning with the legacy-name error when only the main branch does; a scan.file-creation-time-millis read of a fallback pair with a non-NULL binary partition plans the main branch's file
    - fe-connector-paimon module tests: 853 run, 0 failures, 0 errors, 1 skipped (pre-existing)
- Behavior changed: Yes. $audit_log and $row_tracking of a fallback table check binary partitions under the options of the branch that produced each split, and a file-creation-time read of a fallback table with a binary partition no longer fails with a ClassCastException.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@morningman

Copy link
Copy Markdown
Contributor Author

Addressed the second review round in cf79a1ac228. Per finding:

  • 4367401232 (P2, fallback options for $audit_log binary partitions): fixed. For the system tables read through JNI, the check now resolves the branch that produced each split from the wrapper's pinned source and uses that branch's naming and schemas, as it already did for data tables.
  • Also: a split that does not come from the fallback scan (a scan.file-creation-time-millis read of a fallback table) no longer fails with a ClassCastException when its branch is picked.

Two new tests in PaimonScanPlanProviderTest; the 853 tests of fe-connector-paimon run without failures (1 skipped, as before). The PR description is updated.

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot 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.

Reviewed the complete authoritative diff at cf79a1a. No new substantiated finding. Two complete-review agents and a separate branch-provenance risk reviewer returned NO_NEW_VALUABLE_FINDINGS; the main review independently checked their evidence and reconciled every risk.

The reported triggers in existing P2 threads 4366822772 (external data/local deletion vector), 4366822776 (readable dynamic byte-named files), and 4367401232 (audit-log fallback options) are addressed at this head. No duplicate inline comments are proposed, and no existing P0/P1 finding was confirmed. The inverse dynamic-option limitation discussed in 4366822776 remains explicitly outside the fix.

Individual critical-checkpoint conclusions:

  1. Goal and correctness: the guard rejects known unreadable non-NULL binary partition files during planning, before native missing-file skipping can hide rows. NULL-only partitions, readable external/dynamic byte-named files and metadata-only COUNT retain their intended paths. The added real-file unit fixtures support these conclusions by inspection; they were not executed here.
  2. Scope and reuse: changes are confined to the scan provider and its tests. Existing branch resolution, pinned-source handling and authenticated execution are reused; no unrelated change was identified.
  3. Concurrency and locking: new maps and memoized suppliers are local to one planning invocation. No new shared mutable state, thread, lock, lock-order edge or heavy operation under a new lock is introduced.
  4. Lifecycle and initialization: the new caches end with scan planning; no production closeable resource, ownership cycle, worker lifecycle or static initialization dependency is added.
  5. Configuration: no Doris configuration item is added. Checks use effective scan/branch options; the existing deletion-index layout option is immutable, while naming-option transitions are handled explicitly.
  6. Compatibility: no public SPI, function symbol, storage format or serialized scan representation changes. Earlier failure for unreadable physical layouts is the intended behavior change.
  7. Thrift/protobuf IDs: no gensrc definitions change, so the cross-release field/enum-ID audit is inapplicable.
  8. Parallel paths: validation precedes native/JNI/Rust data-reader selection and follows the file-less merged-count short-circuit. Audit-log, row-tracking and read-optimized sources retain the relevant branch provenance. Reversed fallback order matches SDK dispatch; plain SnapshotReader splits originate from the wrapped branch. Nested chain results use non-DataSplit JNI dispatch, and ordinary data-chain reads are already rejected upstream.
  9. Conditional checks: NULL and external-data exemptions follow actual path semantics. Historical schema naming is only a hint and is combined with absence at the planned path; external data still receives required deletion-vector validation. Comments explain these distinctions, and no new speculative defensive branch was identified.
  10. Test coverage: eight added tests exercise legacy lookup, NULL/count, both persisted naming transitions, dynamic byte naming, external data, local/external deletion vectors, fallback/audit behavior and file-creation-time scans. The existing binary ORC/Parquet fixture now uses byte naming. Dedicated binary fixtures are not added for every wrapper, reversed priority, BINARY(n), multiple binary keys, explicit Rust or session-forced JNI; those routes were checked statically. No new end-to-end suite is included.
  11. Test results: no regression .out files change. The expected errors, paths, counts and sorted audit rows are consistent with the inspected setup and SDK semantics. Builds, tests, checkstyle and cluster behavior were not run or independently verified in this static-only invocation.
  12. Observability and errors: failures include database/table, binary partition, relevant file and naming/layout option; filesystem exceptions preserve path and cause. No new background operation requiring metrics or additional INFO logging is introduced.
  13. Transactions and persistence: production changes only read scan metadata/files. No EditLog, commit, persistence, visibility-version or master-failover protocol changes.
  14. Data writes and atomicity: no production write path changes; added SDK writes/commits are test fixtures. The guard does not change baseline races with files removed after planning.
  15. FE-BE variables: no new transmitted variable or sending path; both native scanner implementations and existing JNI/Rust payload forms consume the same range shapes.
  16. Performance and memory: binary-field discovery is once per scan; schema naming is cached by table identity/schema ID and existence by path. Work is linear in relevant files, not native subranges; metadata count and NULL-only splits avoid new file I/O. No new shared unbounded cache or native memory ownership is introduced.
  17. Other issues and focus: the final missed-issue rechecks found no additional substantiated regression. No additional user focus was supplied. All changed hunks and initial risks have evidence-backed dispositions.

Remaining limits: this is a static review, with no runtime or author-test-result verification. As already disclosed in the PR and existing thread, a dynamic legacy-name=true writer (or equivalent old writer) under a persisted false schema can still evade the historical-schema hint. Binlog exposes ARRAY partition fields and retains existing SDK/JNI errors rather than this scalar binary guard. These known boundaries are not new inline findings. There is no unresolved candidate or unreviewed changed code within the agreed static review scope.

Review converged after 1 round(s).

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28143 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit cf79a1ac2286e679617b819016acf7c06a3f59fa, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17639	5079	5014	5014
q2	2104	325	251	251
q3	10251	1351	792	792
q4	4680	457	332	332
q5	7430	794	518	518
q6	188	186	153	153
q7	737	786	592	592
q8	9259	1300	1090	1090
q9	5469	4174	4148	4148
q10	6859	1289	1018	1018
q11	446	254	238	238
q12	646	394	272	272
q13	18100	3555	2978	2978
q14	275	272	242	242
q15	q16	706	708	656	656
q17	1176	745	591	591
q18	7089	6168	6085	6085
q19	1102	969	595	595
q20	376	331	233	233
q21	4638	2098	2256	2098
q22	350	296	247	247
Total cold run time: 99520 ms
Total hot run time: 28143 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5541	5565	5584	5565
q2	660	593	517	517
q3	5219	5417	4940	4940
q4	1982	2124	1540	1540
q5	5230	5149	5403	5149
q6	256	199	158	158
q7	2364	2040	1817	1817
q8	3053	2646	2645	2645
q9	7937	7942	7905	7905
q10	3748	3631	3197	3197
q11	604	433	400	400
q12	853	710	507	507
q13	3561	3783	3193	3193
q14	323	316	316	316
q15	q16	705	710	657	657
q17	8329	7384	6825	6825
q18	13015	12205	12926	12205
q19	900	771	781	771
q20	2128	2114	1940	1940
q21	5608	4551	4840	4551
q22	496	455	399	399
Total cold run time: 72512 ms
Total hot run time: 65197 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 149955 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit cf79a1ac2286e679617b819016acf7c06a3f59fa, data reload: false

query5	4318	585	467	467
query6	426	213	189	189
query7	4798	443	223	223
query8	313	169	152	152
query9	8774	3046	3077	3046
query10	492	311	254	254
query11	5939	3819	3569	3569
query12	141	89	87	87
query13	1249	435	319	319
query14	6141	3849	3600	3600
query14_1	3370	3393	3356	3356
query15	204	196	183	183
query16	945	423	395	395
query17	845	653	540	540
query18	2422	433	312	312
query19	194	172	137	137
query20	87	85	83	83
query21	214	125	103	103
query22	12984	12986	12694	12694
query23	13338	12786	12463	12463
query23_1	12462	12666	12695	12666
query24	7373	844	460	460
query24_1	475	476	462	462
query25	542	424	363	363
query26	1258	254	140	140
query27	2774	437	269	269
query28	4618	1920	1924	1920
query29	1578	599	437	437
query30	295	220	186	186
query31	1009	920	807	807
query32	148	113	95	95
query33	532	305	248	248
query34	905	837	485	485
query35	728	782	642	642
query36	818	802	723	723
query37	120	98	85	85
query38	1780	1710	1653	1653
query39	788	780	746	746
query39_1	727	742	741	741
query40	228	118	97	97
query41	89	75	71	71
query42	86	83	83	83
query43	349	368	309	309
query44	1297	672	682	672
query45	175	183	163	163
query46	823	960	605	605
query47	2523	3021	2747	2747
query48	295	305	221	221
query49	572	394	285	285
query50	677	282	216	216
query51	10463	10392	10311	10311
query52	84	85	70	70
query53	177	201	150	150
query54	233	183	191	183
query55	69	64	66	64
query56	218	223	198	198
query57	1644	1591	1555	1555
query58	282	256	252	252
query59	2152	2211	1992	1992
query60	275	235	215	215
query61	145	146	137	137
query62	383	329	287	287
query63	190	152	150	150
query64	2776	943	748	748
query65	3304	3263	3231	3231
query66	1783	432	303	303
query67	20185	19964	19774	19774
query68	2959	984	608	608
query69	370	274	237	237
query70	848	740	758	740
query71	272	229	215	215
query72	2701	2541	2262	2262
query73	501	515	294	294
query74	4568	4478	4297	4297
query75	2312	2315	1913	1913
query76	2315	1033	570	570
query77	338	365	262	262
query78	9847	9751	9184	9184
query79	969	838	524	524
query80	827	411	344	344
query81	577	312	270	270
query82	575	133	110	110
query83	305	193	183	183
query84	314	121	104	104
query85	903	430	359	359
query86	353	229	222	222
query87	1991	1953	1816	1816
query88	3609	2656	2607	2607
query89	333	279	243	243
query90	1790	169	161	161
query91	158	150	126	126
query92	97	86	85	85
query93	976	970	574	574
query94	531	300	269	269
query95	570	378	303	303
query96	664	489	229	229
query97	2453	2432	2318	2318
query98	153	159	154	154
query99	770	764	657	657
Total cold run time: 233360 ms
Total hot run time: 149955 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.16 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit cf79a1ac2286e679617b819016acf7c06a3f59fa, data reload: false

query1	0.00	0.00	0.00
query2	0.09	0.05	0.04
query3	0.26	0.09	0.08
query4	1.61	0.10	0.10
query5	0.23	0.24	0.25
query6	1.17	0.70	0.66
query7	0.03	0.01	0.00
query8	0.05	0.04	0.04
query9	0.40	0.34	0.36
query10	0.58	0.58	0.61
query11	0.20	0.16	0.14
query12	0.18	0.14	0.15
query13	0.49	0.49	0.48
query14	1.02	1.00	1.02
query15	0.62	0.61	0.63
query16	0.35	0.35	0.35
query17	1.16	1.15	1.10
query18	0.19	0.19	0.18
query19	2.12	1.94	2.01
query20	0.01	0.02	0.01
query21	15.49	0.19	0.14
query22	4.88	0.05	0.06
query23	16.29	0.31	0.12
query24	3.08	0.50	0.41
query25	0.11	0.07	0.04
query26	1.05	0.21	0.16
query27	0.04	0.04	0.05
query28	3.66	0.76	0.35
query29	12.46	4.08	3.22
query30	0.28	0.12	0.11
query31	2.77	0.55	0.32
query32	3.22	0.60	0.49
query33	3.15	3.22	3.16
query34	15.37	3.98	3.40
query35	3.35	3.34	3.36
query36	0.54	0.43	0.42
query37	0.09	0.06	0.06
query38	0.05	0.04	0.04
query39	0.03	0.04	0.04
query40	0.17	0.14	0.14
query41	0.09	0.04	0.03
query42	0.04	0.02	0.03
query43	0.04	0.04	0.03
Total cold run time: 97.01 s
Total hot run time: 24.16 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 0.00% (0/39) 🎉
Increment coverage report
Complete coverage report

morningman added a commit that referenced this pull request Oct 26, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: #69571, #68142

Problem Summary:

**In short.** When a Paimon split's deletion vector file is missing, the
native readers report `NOT_FOUND`, and both file scanners take that for
a missing data file and skip the whole range. The query succeeds and
silently drops every surviving row of the data file. This PR reports a
missing deletion vector as an error, so the query fails instead. A
missing data file is still skipped as before.

**Background.** A Paimon split for a table with deletion vectors names a
data file and the deletion vector that marks its deleted rows. The
native readers open the deletion vector before they read a row: V1 in
`PaimonParquetReader` / `PaimonOrcReader::_init_deletion_vector`, V2 in
`TableReader::_parse_delete_predicates`, both through
`DeletionVectorReader`. `FileScanner` and `FileScannerV2` skip a range
whose reader reports `NOT_FOUND` while
`ignore_not_found_file_in_external_table` is on (the default). That
setting exists for file listings that are cached by the FE and may name
a file that has since been removed. #68142 already exempted Iceberg
ranges, whose files all belong to a committed snapshot.

**The problem, and what it cost.** A deletion vector is never a stale
listing: it belongs to the snapshot that planned its data file. It can
still be missing, for example when Paimon expires the snapshot that
wrote it while the data file stays live under a newer one, or when it is
looked up under a partition name that is never found (#69571). The data
file is still there, but the scanners skip the whole range, so its
surviving rows are dropped together with the deleted ones. The BE logs
`Skip missing file` with the path of the data file, which exists.
Paimon's own reader, the JNI reader and the Rust reader fail on the same
split instead.

**How this PR fixes it.** `DeletionVectorReader`, which opens every
deletion vector for V1 Paimon, V1 Iceberg and the V2 table reader, now
reports a missing file as `INTERNAL_ERROR` with the path of the deletion
vector, both when it opens the file and when it reads it. The scanners
only skip `NOT_FOUND`, so the query fails. A missing data file still
reports `NOT_FOUND` and is skipped as before, with or without a deletion
vector. For Iceberg, which already failed on any missing file (#68142),
only the error code of a missing deletion vector changes.

One boundary: if the data file is gone as well, the result depends on
which file the reader reaches first. FileScannerV2 parses the deletion
vector while it prepares the split, before it opens the data file, so
such a split now fails; in the local run below V1 Parquet failed too,
while V1 ORC, which opens the data file first, still skipped it.

**Results.** A local cluster with a `file://` Paimon catalog, two append
tables (Parquet and ORC, `deletion-vectors.enabled = true`), five rows
each, the first row deleted through a deletion vector the way a Spark
`DELETE` persists it:

| Files on storage | Reader | Before | After |
|---|---|---|---|
| data file and deletion vector | V1 / V2, Parquet / ORC | `2,3,4,5` |
`2,3,4,5` |
| data file only (deletion vector removed) | V1 / V2, Parquet / ORC |
empty result, query succeeds; `Skip missing file: path=<data file>` in
be.INFO | `[INTERNAL_ERROR]Deletion vector file is missing: path
.../index/index-...-0, failed to get file size ...: No such file or
directory` |
| deletion vector only (data file removed) | V1 / V2, Parquet / ORC |
empty result | empty result (unchanged) |
| neither | V2, V1 Parquet | empty result | the deletion vector error
above |
| neither | V1 ORC | empty result | empty result (unchanged) |
@morningman
morningman merged commit ea34d1c into apache:master Oct 26, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants