Skip to content

[fix](be) Reject incompatible Arrow arrays without crashing Stream Load - #69872

Merged
morningman merged 1 commit into
apache:masterfrom
morningman:fix/arrow-stream-load-type-validation
Oct 31, 2026
Merged

morningman merged 1 commit into
apache:masterfrom
morningman:fix/arrow-stream-load-type-validation

Conversation

@morningman

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #69858

Related PR: None

Problem Summary:

The shared Arrow BOOLEAN and DECIMAL SerDes dereference unchecked dynamic_cast results. Passing an Int32Array to the BOOLEAN reader or a StringArray to the DECIMAL64 reader can therefore terminate the BE instead of returning a load error. MAP and STRUCT have the same unchecked input-cast pattern.

Reject incompatible arrays before accessing buffers, decimal metadata, or values, independently of enable_arrow_input_validation. Reject mismatched STRUCT field counts before indexing children. Add the column name and source/destination types to V1/V2 Arrow load errors.

The current V2 Stream Load reader already binds supported input types with safe Arrow casts. Preserve those conversions, including valid integer-to-BOOLEAN and string-to-DECIMAL inputs, and test the shared SerDes directly for incompatible arrays.

Release note

Reject incompatible Arrow arrays with an error instead of dereferencing a null pointer, and include column and type details in Arrow Stream Load conversion errors.

Check List (For Author)

  • Test

    • Regression test: the new test_arrow_stream_load_type_validation matrix and existing test_arrow_stream_load_uppercase_column suite passed with enable_arrow_input_validation=true and false, restarting the isolated BE between configurations.
    • Unit Test: 65 ASAN tests passed across DataTypeSerDeArrowValidationTest, the scalar/collection Arrow round-trip controls, and LoadArrowPipeTest, including eight new named tests.
    • Manual test
    • No need to test or manual test.

    Local validation used macOS aarch64 and a cluster built from this checkout. The new HTTP matrix runs 114 loads across BOOLEAN, DECIMAL32/64/128/256, ARRAY, MAP, and STRUCT with nullable/required targets. It covers valid conversions, invalid values/container types, nested NULLs, decimal precision/scale failures, empty/multiple batches, and failure after a preceding valid IPC stream. All 63 failed-load visibility checks returned zero rows, and subsequent correct loads succeeded on the same BE with HTTP retries disabled. The 36 IPC fixtures have a reproducible PyArrow generator; the regression suite does not require Python. The .out file was generated by the regression runner and then verified in comparison mode.

    FE build, BE ASAN build, clang-format v16, build hygiene, and clang-tidy checks passed.

  • Behavior changed:

    • No.
    • Yes. Incompatible SerDe array types and STRUCT field counts return InvalidArgument. Supported V2 safe casts are preserved.
  • Does this need documentation?

    • No. This fixes invalid-input handling and error context without adding syntax or configuration.
    • Yes.

Check List (For Reviewer who merge this PR)

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

### What problem does this PR solve?

Issue Number: close apache#69858

Related PR: None

Problem Summary: Arrow BOOLEAN and DECIMAL readers dereference unchecked dynamic_cast results when an input array does not match the destination SerDe, such as Int32Array into BOOLEAN or StringArray into DECIMAL64. MAP and STRUCT have the same unchecked input-cast pattern. Reject incompatible arrays before accessing buffers, type metadata or values, regardless of enable_arrow_input_validation, and reject mismatched STRUCT field counts.

Keep the current V2 Stream Load reader's supported safe conversions, including the issue's valid integer-to-BOOLEAN and string-to-DECIMAL inputs. Add column and source/destination type context to Arrow load errors. Add direct SerDe and V2 pipe unit tests plus a reproducible HTTP regression matrix covering nullable and required scalar/nested columns, malformed conversions, decimal bounds, empty/multiple batches, rollback visibility and subsequent loading on the same BE.

### Release note

Reject incompatible Arrow arrays with a load error instead of dereferencing a null pointer, and include column and type details in Arrow Stream Load conversion errors.

### Check List (For Author)

- Test: ASAN unit tests and regression tests
    - 65 BE unit tests passed, including the Arrow validation, SerDe round-trip and V2 Arrow pipe suites.
    - The new 114-load HTTP regression matrix and existing Arrow uppercase/expression regression passed with enable_arrow_input_validation both true and false; results were generated by run-regression-test.sh.
    - clang-format v16, build hygiene and clang-tidy checks passed.
- Behavior changed: Yes. Unsupported SerDe array types and mismatched STRUCT field counts return InvalidArgument; supported V2 safe casts are preserved.
- Does this need documentation: No. This fixes invalid-input handling and error context without adding syntax or configuration.
@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.

No verified actionable defects were found in PR 69872.

The review covered all 47 changed paths: the four Arrow SerDe guards, legacy and FileScannerV2 Arrow readers, direct and load-reader tests, the regression matrix, generator/output, and all checked-in IPC fixtures. The new guards reject incompatible BOOLEAN, DECIMAL, MAP, and STRUCT arrays before unsafe access; STRUCT arity is checked; V2 recursive binding and null-container masking preserve supported conversions; and the legacy error wrapper preserves the underlying status code while adding column/source/target context.

The main risk scan and two review rounds rechecked nested/null/empty arrays, offset and child materialization, recursive LIST/MAP/STRUCT binding, timestamp/decimal/variant storage exceptions, stream boundaries, EOF/cancellation/close, rollback and recovery, and fixture/output alignment. The proposed MAP offset and partial-mutation concerns were dismissed because supported MapType uses Int32 offsets and production readers discard failed blocks/splits; the only column-retaining helper is explicitly test-only.

No existing inline P0/P1 findings or review threads apply to this head. The user provided no additional focus. Required BE review checkpoints were inspected; no source edits, builds, tests, benchmarks, or submodule work were performed under the static-only contract. Convergence was reached after round 2 with no new valuable findings.

Review converged after 2 round(s).

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17614	5145	5275	5145
q2	2123	301	254	254
q3	10271	1360	820	820
q4	4698	460	337	337
q5	7435	797	539	539
q6	182	187	162	162
q7	726	792	600	600
q8	9260	1295	1067	1067
q9	5496	4202	4197	4197
q10	6824	1284	1023	1023
q11	423	242	223	223
q12	650	399	289	289
q13	18106	3582	3018	3018
q14	276	270	249	249
q15	q16	713	699	670	670
q17	1199	748	579	579
q18	6998	6147	6121	6121
q19	1093	955	589	589
q20	376	327	230	230
q21	4976	2119	2313	2119
q22	342	295	258	258
Total cold run time: 99781 ms
Total hot run time: 28489 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5562	5611	5561	5561
q2	672	557	545	545
q3	5239	5317	4951	4951
q4	1979	2131	1536	1536
q5	5265	5187	5552	5187
q6	251	195	148	148
q7	2368	2121	1881	1881
q8	3029	2649	2652	2649
q9	8173	8015	7994	7994
q10	3976	3621	3307	3307
q11	626	455	428	428
q12	676	683	517	517
q13	3538	3870	3215	3215
q14	307	300	298	298
q15	q16	708	702	664	664
q17	8107	6999	6866	6866
q18	13025	12201	12829	12201
q19	896	784	773	773
q20	2134	2132	1900	1900
q21	5569	4535	4674	4535
q22	533	473	472	472
Total cold run time: 72633 ms
Total hot run time: 65628 ms

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 33.33% (4/12) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 67.23% (32557/48425)
Line Coverage 52.11% (344967/661945)
Region Coverage 47.54% (280370/589759)
Branch Coverage 49.25% (132475/268997)

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152011 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 165bfbc10a2dfb2485e7b91214da5ba9a9e8b46a, data reload: false

query5	4335	591	441	441
query6	426	206	207	206
query7	4802	439	228	228
query8	311	162	150	150
query9	8786	3225	3240	3225
query10	466	320	257	257
query11	6031	3861	3568	3568
query12	149	93	88	88
query13	1265	452	322	322
query14	6130	3919	3649	3649
query14_1	3406	3419	3434	3419
query15	203	202	185	185
query16	935	448	417	417
query17	876	667	538	538
query18	2424	410	300	300
query19	191	160	135	135
query20	82	83	79	79
query21	203	124	105	105
query22	13018	13013	12807	12807
query23	13428	12806	12520	12520
query23_1	12453	12524	12541	12524
query24	7373	838	473	473
query24_1	480	474	494	474
query25	504	391	338	338
query26	1263	253	133	133
query27	2770	434	271	271
query28	4619	2082	2108	2082
query29	1691	575	430	430
query30	304	220	186	186
query31	1019	917	832	832
query32	140	93	92	92
query33	536	308	237	237
query34	1176	878	526	526
query35	745	749	660	660
query36	827	804	743	743
query37	131	101	85	85
query38	1811	1765	1679	1679
query39	779	788	746	746
query39_1	745	734	759	734
query40	220	125	97	97
query41	67	82	63	63
query42	90	87	85	85
query43	346	368	315	315
query44	1371	710	725	710
query45	182	172	165	165
query46	913	977	575	575
query47	2908	2876	2930	2876
query48	313	334	238	238
query49	589	408	304	304
query50	689	282	219	219
query51	10476	10578	10574	10574
query52	82	87	71	71
query53	193	213	158	158
query54	245	218	197	197
query55	80	72	66	66
query56	243	240	220	220
query57	1632	1634	1589	1589
query58	301	272	271	271
query59	2135	2135	1988	1988
query60	302	257	262	257
query61	164	164	166	164
query62	391	334	290	290
query63	193	158	164	158
query64	2868	1066	884	884
query65	3312	3255	3247	3247
query66	1786	424	324	324
query67	20265	20050	20104	20050
query68	2962	1020	623	623
query69	382	288	244	244
query70	831	747	769	747
query71	302	239	214	214
query72	2723	2560	2256	2256
query73	529	544	304	304
query74	4601	4508	4334	4334
query75	2314	2274	1910	1910
query76	2319	1043	606	606
query77	365	363	272	272
query78	9754	9679	9148	9148
query79	2119	858	554	554
query80	1332	414	355	355
query81	589	319	270	270
query82	557	137	106	106
query83	305	197	183	183
query84	309	118	101	101
query85	851	416	379	379
query86	401	241	229	229
query87	2001	1940	1799	1799
query88	3894	2833	2810	2810
query89	325	283	245	245
query90	1864	165	167	165
query91	162	157	129	129
query92	94	87	92	87
query93	1149	1020	583	583
query94	646	305	275	275
query95	594	326	298	298
query96	703	490	247	247
query97	2444	2433	2325	2325
query98	167	154	149	149
query99	768	763	658	658
Total cold run time: 237340 ms
Total hot run time: 152011 ms

@hello-stephen

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

query1	0.01	0.01	0.01
query2	0.09	0.04	0.04
query3	0.26	0.10	0.09
query4	1.61	0.10	0.10
query5	0.24	0.24	0.24
query6	1.18	0.68	0.68
query7	0.04	0.01	0.00
query8	0.06	0.04	0.04
query9	0.40	0.36	0.35
query10	0.59	0.56	0.58
query11	0.20	0.13	0.14
query12	0.19	0.15	0.16
query13	0.49	0.49	0.49
query14	1.03	1.01	1.01
query15	0.62	0.60	0.63
query16	0.34	0.35	0.36
query17	1.14	1.13	1.14
query18	0.21	0.19	0.20
query19	2.09	1.96	1.99
query20	0.02	0.02	0.01
query21	15.48	0.19	0.13
query22	4.90	0.06	0.05
query23	16.28	0.31	0.12
query24	3.16	0.49	0.31
query25	0.08	0.05	0.06
query26	1.02	0.21	0.15
query27	0.05	0.04	0.04
query28	3.20	0.75	0.37
query29	12.48	4.07	3.20
query30	0.28	0.13	0.10
query31	2.77	0.57	0.33
query32	3.23	0.59	0.52
query33	3.24	3.18	3.16
query34	15.38	3.97	3.40
query35	3.34	3.35	3.32
query36	0.57	0.44	0.41
query37	0.10	0.06	0.06
query38	0.06	0.03	0.03
query39	0.04	0.03	0.03
query40	0.17	0.14	0.16
query41	0.09	0.03	0.03
query42	0.03	0.02	0.03
query43	0.04	0.04	0.03
Total cold run time: 96.8 s
Total hot run time: 24.13 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 33.33% (4/12) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.58% (36406/46927)
Line Coverage 62.83% (411505/654912)
Region Coverage 59.32% (348570/587620)
Branch Coverage 60.21% (161557/268339)

@raghav-reglobe

Copy link
Copy Markdown
Contributor

Verified in our environment — the fix behaves as described.

Setup. aarch64 (Graviton), Ubuntu 22.04, a scratch single-node FE + BE, enable_arrow_input_validation = true (the default). Our BE is built from apache/doris master as of 2026-10-20 with local patches that do not touch the Arrow reader or the serdes; this PR's commit was cherry-picked onto it (the two format_v2 files were dropped — our base predates format_v2; every other hunk applied as-is). Reproducer = the one from #69858 (int32 → BOOLEAN, utf8 → DECIMAL(10,2)) plus two more families for the MAP/STRUCT guards: an int32 array into a MAP<STRING,INT> column and into a STRUCT<a:INT> column. format: arrow Stream Load; the matching-typed controls (bool / decimal128(10,2) / map / struct) load first.

Before (unpatched BE). The four controls load. The first mismatch — an Int32Array into the BOOLEAN column — kills the BE: curl: (52) Empty reply from server, BE exit 139, the same stack as the issue (check_arrow_validity_bitmap ← DataTypeNumberSerDe<TYPE_BOOLEAN>::read_column_from_arrow ← DataTypeNullableSerDe::read_column_from_arrow ← ArrowStreamReader::_do_get_next_block).

After (this PR). All four mismatches come back as a failed load, the BE stays up (0 restarts), and the controls load again afterwards:

Arrow column 'b' (int32 -> Nullable(BOOL)): Expected Arrow BooleanArray, got int32.
Arrow column 'v' (string -> Nullable(Decimal(10, 2))): Expected Arrow Decimal128Array for DECIMAL64, got string.
Arrow column 'm' (int32 -> Nullable(Map(Nullable(String), Nullable(INT)))): Expected Arrow MapArray, got int32.
Arrow column 's' (int32 -> Nullable(Struct(a:Nullable(INT)))): Expected Arrow StructArray, got int32.

Each returns HTTP 200 with "Status": "Fail" and the message above as Message ([INVALID_ARGUMENT] prefix) within a few seconds. One cosmetic observation, not from this PR: the message carries a trailing cur path: with an empty path on a Stream Load (the file-scanner suffix) — harmless.

Thanks for the quick fix. We will carry it on our build until it lands in 4.2.x.

@morningman
morningman merged commit 339e087 into apache:master Oct 31, 2026
37 of 38 checks passed
github-actions Bot pushed a commit that referenced this pull request Oct 31, 2026
…ad (#69872)

### What problem does this PR solve?

Issue Number: close #69858

Related PR: None

Problem Summary:

The shared Arrow BOOLEAN and DECIMAL SerDes dereference unchecked
`dynamic_cast` results. Passing an `Int32Array` to the BOOLEAN reader or
a `StringArray` to the DECIMAL64 reader can therefore terminate the BE
instead of returning a load error. MAP and STRUCT have the same
unchecked input-cast pattern.

Reject incompatible arrays before accessing buffers, decimal metadata,
or values, independently of `enable_arrow_input_validation`. Reject
mismatched STRUCT field counts before indexing children. Add the column
name and source/destination types to V1/V2 Arrow load errors.

The current V2 Stream Load reader already binds supported input types
with safe Arrow casts. Preserve those conversions, including valid
integer-to-BOOLEAN and string-to-DECIMAL inputs, and test the shared
SerDes directly for incompatible arrays.
yiguolei pushed a commit that referenced this pull request Nov 2, 2026
…ng Stream Load #69872 (#69905)

Cherry-picked from #69872

Co-authored-by: Mingyu Chen (Rayner) <[email protected]>
yiguolei pushed a commit that referenced this pull request Nov 6, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: #65730 #66608 #66805 #66841 #66999 #67005 #67011 #67110
#67268 #67289 #67325 #67650 #67921 #68028 #68335 #68375 #68615 #68690
#68813 #68815 #68707 #68649

Problem Summary:

**In short.** This ports the Lance BE read path of branch-4.1 /
branch-4.2 to master, in its branch-4.2 form as of `cabcf45c1af`, plus
the BE side of three 4.x changes merged after that snapshot: the search
filter `prefilter` flag and profile layout (#68813), distance bounds for
`vector_search` (#68815), and the split-level index rule of #68707 that
#68815 relies on. It gathers the BE parts of the PRs listed above.
Master links `liblance_c` but has no Lance reader, so every
`FORMAT_LANCE` scan fails today. With this PR the BE reads Lance
datasets through FileScannerV2: plain scans, vector search and full-text
search, predicate and runtime-filter pushdown, the schema fetch of a
local Lance TVF, and the second phase of TopN lazy materialization. The
PR is BE only. The FE connector plugin and the regression suites come in
separate PRs, so until then nothing on master plans a Lance scan, and
the BE unit tests carry the proof.

Original authors: @zhangstar333 @Gabriel39 @wangzhaobo957-cloud @Jay-ju
@FANNG1

**Background.** Lance shipped in 4.1.4: the Lance catalog, `local()` /
`s3()` with `format=lance`, and the `vector_search` / `full_text_search`
TVFs. On master the FE side is being rebuilt as a connector plugin
(`fe-connector-lance`), and the BE needs the reader from 4.x. The thrift
ids are the branch-4.1 / 4.2 ones, including `TExternalSearchRequest.5`
(`prefilter`) and `TVectorSearchParams.6/7` (distance bounds) added
after the snapshot, so a BE built from master also reads the Lance scan
ranges and search requests that a 4.x FE sends during a rolling upgrade.
The one exception is `TVectorSearchOptions` 6, branch-4.2's
`approx_mode` (#69852, merged into branch-4.2 after the snapshot and not
ported): it is reserved in a comment, and a master BE ignores it, so
during a rolling upgrade a 4.2 FE that asks for `fast` or `accurate`
gets Lance's default (`normal`).

**The problem, and what it cost.**

| | master today | with this PR |
|---|---|---|
| A `FORMAT_LANCE` scan | FileScannerV2 has no Lance reader, so the scan
fails with "FileScannerV2 does not support file format" | read by
`LanceTableReader` in FileScannerV2 |
| `fetch_table_schema` for a local Lance TVF | not handled | returns the
Arrow schema mapped to Doris types; Null leaves keep `NULL_TYPE` |
| The row location of a Lance row in TopN lazy materialization |
`GlobalRowLoacationV2` only has a 32-bit row id, but a Lance row id is a
64-bit id within the snapshot | a version 1 layout carries the 64-bit
row id; phase two reads the rows with the dataset's `take_rows` |

**How this PR fixes it.**

- Reader: `be/src/format_v2/lance/*` and
`be/src/format_v2/table/lance_reader.*`, as on branch-4.2 except one
local renamed for master's `-Wshadow`. The unit tests construct
`RuntimeState(TQueryOptions(), globals)`, because master removed the
globals-only constructor.
- FileScannerV2: the reader dispatch and `_to_file_format` handle
`FORMAT_LANCE` / `"lance"`; `FileFormat::LANCE` is added to its two
exhaustive switches. Since #69723 every file scan uses FileScannerV2, so
nothing else routes Lance scans (on branch-4.2 a Lance query fails with
NotSupported when `enable_file_scanner_v2` is off).
- Row locations: `ROW_VERSION {FILE_LOCAL_ROW_ID = 0,
LANCE_DATASET_ROW_ID = 1}` and a union layout for
`GlobalRowLoacationV2`. Version 0 keeps its byte layout, which static
asserts pin. `PRequestBlockDesc.row_id` becomes `uint64` (a proto2
varint, so the wire format is compatible). The materialization operator
decodes row locations by version, rejects an unknown version or a wrong
size, and stamps `row_location_version`. `RowIdStorageReader` validates
the counts and the version, range-checks internal row ids against
`uint32`, and groups Lance rows by plan node, snapshot version and
dataset URI, so each dataset is read with one `take_rows`. Master's
segment batching, scan-slot dedup and fetch concurrency stay as they
are.
- Thrift (`PlanNodes.thrift`): the external search request body
(`TVectorElementType`, `TVectorMetric`, `TSearchVector`,
`TVectorSearchParams` 1-7, `TFtsCoverageMode`, `TFtsQueryType`,
`TFtsMatchOperator`, `TFullTextSearchParams`, `TSearchFilterFormat`,
`TSearchFilter`, `TVectorSearchOptions`, `TExternalSearchQuery`),
`TExternalSearchRequest` 2-5 and `TLanceFileDesc` 4-6, all with the
branch-4.1 / 4.2 ids. The enums spell out their 4.x values. No id is
reused. On master, #70075 reserved `TExternalSearchRequest` 2-5 in
comments and added 6 (`paimon_options`); this PR replaces those comments
with the fields and leaves 6 as it is. `TVectorSearchOptions` 6 is
reserved in a comment for branch-4.2's `approx_mode` (#69852).
- After the snapshot (one commit each):
- #68813: `prefilter` decides whether the TVF search filter runs before
(true) or after (unset / false) candidate selection, for vector and
full-text search. The profile groups the Lance counters under
`LanceTiming`, `LanceIO`, `LanceIndex`, `LanceVectorIndex`,
`LanceScalarIndex`, `LanceExecutionPlan`, `LanceScan`, `LanceSearchPlan`
and `LanceDataCache`, uses the 4.x counter names, and registers every
scan metric before the statistics callback is installed, so the
callback, which Lance invokes from its execution streams, never adds
counters. #68813's FE tweak that hides `TUnit.NONE` labels in
`AggCounter` belongs to the FE port.
- #68707 (part): a vector split searches exactly the index segments FE
planned for it, and a split without segments is searched flat whatever
`use_index` says; a split with segments under `use_index = false` is
rejected. 4.x FEs plan segments only when `use_index` allows them.
- #68815: `distance_lower_bound` (inclusive) and `distance_upper_bound`
(exclusive) are validated (finite FLOAT values, lower < upper at FLOAT
precision, single-vector only) and installed with
`lance_scanner_set_distance_range()`, which master's pinned lance-c
provides.
- Review fixes:
- A nested array goes to the Doris SerDes without a copy only when it
and every descendant start at offset 0 and reference exactly their child
ranges. Lance's legacy (v1) decoder can return a struct or list that is
tight at its own level over a Boolean child with a bit offset (a batch
that starts mid-byte in the bitmap), and the SerDes reject that under
Arrow input validation.
- A zero-field `STRUCT<>`, which Lance decodes, is reported as an
unsupported column at any nesting level, like the other unsupported
Lance types. It used to be typed as a STRUCT that `ColumnStruct` cannot
hold (its row count comes from its first child).
- A Lance Decimal256 field with precision <= 38 is narrowed to
Decimal128 by a safe Arrow cast before decoding. Doris types it
DECIMAL32/64/128I by precision, and those SerDes read only Decimal128
arrays; the BE used to crash on the first value, also for nested leaves.
Only values a reader can see are cast: before a nested leaf is narrowed,
every child slot that no visible row references (under a NULL list,
fixed-size-list, map or struct row, or outside a sliced parent) is
marked NULL, so a hidden value does not fail the cast and a visible
value outside the declared precision still does; map keys, which cannot
be NULL, stay checked.
- FLOAT / DOUBLE `>=` runtime filters stay residual-only. Lance compares
floats by totalOrder, so a pushed `x >= c` dropped negative-NaN rows
that Doris keeps.
- Runtime filters that mention INT64_MIN stay residual-only. The pinned
Lance SQL parser reads `-9223372036854775808` as the negation of a
magnitude that it parses as i64 first; the overflow turns the literal
into a Float64, which the planner cannot coerce to Int64 or Decimal, so
a pushed IN, `>=` or `<=` filter on an INT64 or scale-zero decimal
column failed planning and the query failed. Every other i64 is still
pushed.
- The shared Arrow DATE32 decoding
(`DataTypeDateV2SerDe::read_column_from_arrow`) rejects days outside
0000-01-01..9999-12-31 instead of returning a non-null `0000-00-00`,
skips null slots and adds the epoch in 64 bits: the DATE32 range part of
branch-4.1 / 4.2 #68649. It also changes ADBC, Arrow stream load and
paimon-rust reads.
- Child values hidden by a NULL list, fixed-size-list, map or struct row
are not decoded: the array, map and struct SerDes give the element SerDe
a validity bitmap that also clears those slots
(`mask_arrow_children_hidden_by_null_parents()`). Arrow leaves them
unspecified, and Lance's v2.1 miniblock encoding keeps them, so a hidden
10000-01-01 under a NULL fixed-size list used to fail the DATE32 range
check above. Visible values are unchanged; this applies to ADBC, Arrow
stream load and paimon-rust as well. The masks come from ExecEnv's
tracked Arrow pool. A nested Lance array that is compacted before
decoding is masked the same way, at every level, before its
`ValidateFull()`, so a hidden invalid UTF-8 string, out-of-precision
decimal, Date64 or time no longer fails the batch while a visible one
still does. Since #69697 the same masks keep hidden DATETIMEV2 /
TIMESTAMPTZ values out of its range checks. A mask costs at most one
pass per batch: a NULL row that owns no child slots, or only NULL ones,
allocates nothing, and Arrow stream load masks each retained IPC batch
once (`mask_arrow_descendants_not_visible()`) before it hands row ranges
to the SerDes, so its per-block reads allocate nothing. The one
per-block mask left is for map keys under a NULL map row that still owns
entries, since a MapArray cannot hold NULL keys; Arrow's map builder
writes NULL map rows without entries.
- Core: `to_protobuf` keeps `NULL_TYPE` for a NULL literal type, so a
Null leaf in ARRAY / MAP / STRUCT no longer comes back as BOOLEAN. The
nullable serde reads an Arrow Null array as NULLs without touching
buffers that the array does not have.
- Configs, caches and metrics, with the 4.x names and defaults:

  | Config | Default |
  |---|---|
  | `lance_index_cache_size_bytes` | 10 GiB |
  | `lance_metadata_cache_size_bytes` | 1 GiB |
  | `enable_lance_data_cache` | true |
  | `lance_data_cache_path` | `${DORIS_HOME}/lance_data_cache` |
  | `lance_data_cache_disk_capacity_bytes` | 100 GiB |
  | `lance_data_cache_read_block_size_bytes` | 1 MiB |
  | `lance_io_buffer_size_bytes` (mutable) | 2 GiB |
  | `lance_batch_readahead` (mutable) | 5 |
  | `lance_fragment_readahead` (mutable) | 5 |

The BE-wide Lance session (index cache, metadata cache and the optional
Foyer disk cache) is created by the first Lance scan, so a BE that never
reads Lance creates no cache directory. The first six configs take
effect after a restart. A negative cache size (and, while the data cache
is enabled, a negative disk capacity or read block size) fails the first
Lance scan with a message that names the setting, as does any data-cache
value lance-c rejects; the rest of the BE keeps serving. Ten
`lance_session_*` metrics report the two in-memory caches. With the
defaults, the disk tier keeps 51,200 sparse 2 MiB block files open from
the first Lance scan on (100 GiB in blocks of twice the read block
size), the same as on branch-4.1 / 4.2. Size
`lance_data_cache_disk_capacity_bytes` and
`lance_data_cache_read_block_size_bytes` against the BE's open-file
limit, or set `enable_lance_data_cache = false`.

The index and metadata caches live in lance-c memory: they are not
charged to a MemTracker or registered with CacheManager, so memory GC,
the cache capacity adjustment under memory pressure and
`/api/clear_cache/all` do not shrink them, and `enable_lance_data_cache
= false` does not turn them off. lance-c has no call to clear, evict or
resize them, so size them with the two configs. The disk tier keys
blocks by Lance's store prefix (`scheme$bucket` for S3, GCS and OSS,
without the endpoint) and the object path
(`thirdparty/patches/lance-c-foyer.patch`). Several endpoints that serve
the same bucket name and data-file path with different bytes (data files
registered under chosen names or replaced in place; Lance's writers use
random file names) share entries, also across a BE restart; such a
deployment should set `enable_lance_data_cache = false`. Both are as on
branch-4.1 / 4.2 and tracked as follow-ups.

A Lance scan's native working set, that is the I/O buffered up to
`lance_io_buffer_size_bytes` per scanner, the decode readahead and the
operators of a search plan such as the top-k of a flat vector search, is
allocated inside lance-c and is charged to neither the query nor its
workload group. It counts toward the BE process memory only, so memory
GC, which ranks queries by tracked memory, does not attribute it to the
Lance query. lance-c accepts no memory budget, so on BEs with little
memory headroom size `lance_io_buffer_size_bytes`,
`lance_batch_readahead` and `lance_fragment_readahead` for the number of
concurrent Lance scanners. As on branch-4.1 / 4.2; tracked as a
follow-up.
- Build: macOS links `-framework IOKit`. This is the first time objects
of `liblance_c.a` are pulled into `doris_be`, so the binary and the link
time grow. With this PR `util/rle_encoding.h` reaches 231 TUs and
`util/threadpool.h` 589, the same as master `315c2e00331` alone (limits
275 and 660 since #69717), and `gen_cpp/internal_service.pb.h` 562
(master alone 559, limit 660); the reach budgets are unchanged.

Not included: the Lance index metadata scanner (`LANCE_INDEX_ENTRIES`);
the FE side, including the rolling-upgrade guard that keeps Lance lazy
materialization off while an old BE could decode a version 1 row
location as version 0; the regression suites; the rest of #68707
(searching historical versions, tags and branches, rejecting fragment
ids missing from the snapshot, naming the snapshot in errors and the
profile). The `InitReaderAvgTime` / `GetBlockAvgTime` profile fix that
#67005 carried goes in a separate PR.

**Results.**

| | master | with this PR |
|---|---|---|
| A `FORMAT_LANCE` query | fails before reading anything | read by
`LanceTableReader` in FileScannerV2 |
| A Lance row in the second phase of TopN lazy materialization | cannot
be represented | a version 1 row location, read back with `take_rows` |
| A row location of an unknown version or size in phase two | decoded as
version 0 | rejected with an error |
| An Arrow DATE32 day outside 0000-01-01..9999-12-31 (ADBC, Arrow stream
load, paimon-rust, Lance) | read as a non-null `0000-00-00` | the read
fails with `InvalidArgument` |

BE unit tests (ASAN, macOS arm64, against master's pinned lance-c), on
the head rebased onto master `378404eeb12`: 1029 cases in 54 suites run,
1027 pass and 2 Parquet microbenchmarks skip by design. They cover the
Lance suites (107: `LanceTableReader*` 83, `LanceNestedNullTest` 10,
`LanceRuntimeFilterHelperTest` 10, `LanceSessionManagerTest` 4); the
row-location protocol (`MaterializationSharedStateTest`,
`RowIdStorageReaderTest`, and the new `RowIdFetchRequestValidationTest`:
count mismatch, unknown version, a Lance version on an internal mapping,
an internal row id of 2^32); `FileScannerV2Test` (28); the ORC and
Parquet reader suites (`NewOrcReaderTest`, `NewParquetReaderTest`,
`ParquetScanTest` and the other `Parquet*` suites), whose row ids now go
through `file_local`; and the Arrow / protobuf / DATE serde suites with
the new `ArrowParentNullsTest`, plus the Iceberg and Parquet reader
suites that decode Arrow dates. Reverting any of the review fixes, the
#68813 flag, the #68707 split rule or the #68815 bounds makes its new
tests fail. The sixth review round adds 13 cases (`ArrowParentNullsTest`
10, `LanceTableReaderTypeTest` 3) and extends one; with them the Arrow
SerDe, Lance, ADBC and `IcebergV2ReaderTest` suites run 317 cases: 310
pass and the 7 ADBC SQLite cases skip without a local SQLite driver. The
ninth review round adds
`LanceRuntimeFilterHelperTest.KeepsInt64MinFiltersResidualOnly` and
`LanceTableReaderFilterTest.KeepsInt64MinRuntimeFiltersOutOfLanceScanner`:
both pass with the 106 Lance cases (`Lance*`) on the branch rebased onto
`339e087b11f`, and restoring `parse_int64()` in either check makes
exactly these two fail. On the head rebased onto master `315c2e00331`,
with the per-block mask fix (`42cf4e3e5d1`), 328 cases in 24 suites
pass: the Arrow SerDe and validation suites, `ArrowParentNullsTest`
(15), `LoadArrowPipeTest` (47), the DATETIMEV2 / TIMESTAMPTZ SerDe
suites, `ArrowColumnToDorisColumnTest`, `ArrowBlockConvertorTest`,
`AdbcReaderTest`, `RemoteDorisV2ReaderTest`, `PaimonRustTableReaderTest`
and the Lance suites. Allocating at the first NULL row of each call
again fails the new `ArrowParentNullsTest` case and the four new
`LoadArrowPipeTest` block cases, dropping the per-batch mask fails the
LIST and STRUCT block cases, and returning the child unmasked fails the
new timestamp case, four `ArrowParentNullsTest` cases and
`LanceTableReaderTypeTest.IgnoresDate32PayloadHiddenByNullParents`.

### Release note

Add nine `lance_*` BE configs and `lance_session_*` BE metrics. They
take effect once the FE can plan Lance scans. Reading an Arrow DATE32
value outside 0000-01-01..9999-12-31 through ADBC, Arrow stream load or
paimon-rust now fails instead of returning 0000-00-00.

### Check List (For Author)

- Test <!-- At least one of them must be included. -->
    - [ ] Regression test
    - [x] Unit Test
- `run-be-ut.sh --run --filter=...` with the suites listed under
Results: 420 of 420 pass on macOS arm64 (ASAN) on the content before the
rebases, and the Lance and Arrow SerDe suites pass again with the
nested-offset, zero-field struct, hidden-child, hidden-Decimal256 and
compaction-mask fixes (the ADBC SQLite cases skip without a local SQLite
driver). Rebased onto master `43f9ceb48d7`: #69668 conflicted in the
scanner selection and in `fetch_table_schema`, which now hands every
file format to `format::fetch_file_schema()`, so the Lance branch runs
ahead of that call; #69646 added a TopN row-group test that reads the
version 0 row location fields, which now go through `file_local` (the
earlier rebase onto `5caaeb3e3d4` resolved #69614 and #69601 in the
scanner selection, the `FileFormat` enum, its test and the reach budget
table). The 95 TUs that this PR changes or that master changed since
`5caaeb3e3d4` were syntax-checked against the rebased tree with the UT
build flags, and CI runs the suites again. Rebased again onto master
`8d88f1cc6f7`: the only conflict was the budget table, where #65858
moved the `util/rle_encoding.h` baseline to 828, and the 163 TUs that
this PR changes or that master changed since `43f9ceb48d7` pass the same
syntax check. Rebased again onto master `339e087b11f`: the only conflict
was the budget table again, where #67292 moved the `util/rle_encoding.h`
baseline to 834; #69872 added Arrow type checks ahead of the NULL-parent
masking in the map and struct readers and merged cleanly. Of the 114
sources that this PR changes or that master changed since `8d88f1cc6f7`,
111 pass the same syntax check. The other three are master's: two new
#67292 files under `storage/segment` fail the same way on master alone,
since they compile only inside their unity batch, and this PR adds no
source to that target; the #69818 benchmark is not in the UT build and
includes only `util/simd` headers that this PR does not touch. Reverting
any of the review fixes, the #68813 flag, the #68707 split rule or the
#68815 bounds makes its new tests fail. Rebased again onto master
`378404eeb12`, which removed the legacy file scanner (#69723): the
conflicts were in the scanner-selection code and tests that #69723
deleted, so this PR's Lance lines there went with them (including its
`LanceQueriesAlwaysUseScannerV2` test), in the profile keys of
`materialization_opertor.cpp`, and in the row fetch of
`rowid_fetcher.cpp`, whose non-Lance path is now only
`FileScannerV2::read_by_rows`; the deleted V1 ORC / Parquet row id tests
took this PR's `file_local` rename with them, and the
`util/rle_encoding.h` budget commit is dropped because master is no
longer near that limit. Of the 100 sources that this PR changes or that
master changed since `339e087b11f`, 99 pass the same syntax check; the
other, `format_v2/orc/orc_reader.cpp`, fails the same way on master
alone. On the rebased head the suites listed under Results run 1029
cases: 1027 pass and 2 microbenchmarks skip. The test-utility fix that
`DataTypeDateTimeV2SerDeTest` needed on macOS (#69695) is now on master.
Rebased again onto master `315c2e00331`: the only conflict was
`TExternalSearchRequest` in `PlanNodes.thrift`, where #70075 had
reserved fields 2-5 in comments; the two commits that add the fields
replace those comments, and field 6 stays #70075's `paimon_options`.
#69717 cut transitive includes from shared headers: of the 160 sources
that this PR changes or that master changed since `378404eeb12`, 156
pass the same syntax check; the two `jemalloc_hook` tests have no
compile command on macOS, `runtime/memory/jemalloc_hook.cpp` uses glibc
aliases that macOS cannot build (#69819), and
`format_v2/table_reader.cpp` alone fails the same way on master alone
while its unity batch compiles. The full `doris_be_test` builds and
links on the rebased head; `ArrowParentNulls*` and `Lance*` (121 cases)
pass before the mask fix, and the 328 cases above with it.
- `build-support/check-build-hygiene.sh` (on this branch rebased onto
master `315c2e00331`), clang-format 16 and
`build-support/run-clang-tidy.sh` on the changed files: all pass; on the
changed lines clang-tidy (llvm 22) reports only
`readability-inconsistent-ifelse-braces` on the `DORIS_CHECK` lines of
the masking functions, the macro's expansion that master's existing
`DORIS_CHECK`s report too.
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:
- Behavior changed:
    - [ ] No.
- [x] Yes. `PRequestBlockDesc.row_id` is `uint64`; phase two of TopN
lazy materialization rejects row locations of an unknown version or
size; `FORMAT_LANCE` scans run on FileScannerV2; an Arrow DATE32 day
outside the Doris DATE range fails the read instead of becoming
`0000-00-00`.
- Does this need documentation?
    - [x] No. Lance is documented with the FE connector.
- [ ] Yes. <!-- Add document PR link here. eg:
apache/doris-website#1214 -->

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->

---------

Co-authored-by: zhangstar333 <[email protected]>
Co-authored-by: Gabriel <[email protected]>
Co-authored-by: zhaobo wang <[email protected]>
Co-authored-by: jay <[email protected]>
Co-authored-by: FANNG <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: zy-kkk <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

5 participants