Skip to content

[improvement](lance) Add ANN prefilter optimization and profiles (branch-4.1) - #68615

Merged
Gabriel39 merged 9 commits into
apache:branch-4.1from
Gabriel39:dev/lance-ann-segment-prefilter-4.1
Sep 30, 2026
Merged

Gabriel39 merged 9 commits into
apache:branch-4.1from
Gabriel39:dev/lance-ann-segment-prefilter-4.1

Conversation

@Gabriel39

@Gabriel39 Gabriel39 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Unfiltered ANN searches scoped to complete index segments can materialize redundant row-ID allowlists, and the existing scan profile leaves the remaining search work unexplained. Update the upstream dependency and expose ANN stage timings on branch-4.1.

This includes the dependency integration from #68613: adopt the merged lance-format/lance#9599 and lance-format/lance#9602 through lance-format/lance-c#89, retain the merged lance-format/lance#9537 PQ scoring fix, remove superseded local lance-c patches, and retain only the Foyer cache integration.

Add BE profile counters for index opening, partition loading/preparation, prefilter readiness, CPU queue wait, partition search, query lookup-table preparation, fused distance/TopK work, and result materialization. Export operator baselines for ANN, sort/merge, take, and vector-distance work. Extend the existing indexed multivector regression to check that stage timers reach the Doris profile, alongside existing result and prefilter assertions.

Timings accumulate across concurrent work and overlap parent stages. Distance/TopK includes candidate filtering in fused paths; these counters must not be summed to reconstruct wall time. docs/lance-ann-profile.md documents the boundaries, supported paths, and relationship to the existing scanner and prefilter timers. The BE changes add observability; the prefilter optimization itself remains in the upstream dependency.

Pin lance-c 9bd730add2ac70316c1d642b8459011e2dd92022, using the merged Lance #9602 revision 68c12dfd7efe02f90ad2d7f3a239a7eb64884e57. This includes parallel-path partition-preparation timing. The upstream callback regression checks timer presence and nanosecond units while accepting valid zero durations.

The Foyer wrapper caches immutable ObjectMeta and Attributes independently of HTTP response extensions, avoiding repeated HEAD requests on warm range reads. It does not replay transport extensions from cached metadata. The HTTP regression checks HEAD/GET counts, returned bytes/ranges/metadata, fresh dataset scopes, and explicit HEAD bypass.

Generate the retained Foyer patch from source commit 24c7ca4bcb9422c113b0d3e07e4efe1173b0bc9f relative to the pinned lance-c baseline. The header records both revisions and the regeneration command. The source is submitted as zhangstar333/lance-c#3 against the branch behind lance-format/lance-c#73, on top of merged source-alignment PR #2. Doris consumes the exact reviewed source revision while that supplementary PR awaits merge.

Scope metadata, size, and block keys to a random namespace for each live underlying object-store instance. The wrapping API omits a complete stable backend identity; identical bucket/path names alone cannot distinguish S3-compatible endpoints. Weak identity records preserve sharing for the same live store without retaining it, while new stores and process restarts start cold. A fresh Dataset open that creates a new store consequently needs to warm its own cache. This deliberately reduces reuse across instances to prevent returning another origin's data; it does not claim unchanged cache-hit rates or production latency.

Avoid inserting an unchanged size entry into the WriteOnInsertion hybrid cache: look up both tiers first and populate only absent or invalid size records. Regression tests cover real HTTP endpoints sharing bucket/path/ETag, batched data and NotFound isolation, replaced origins after disk recovery, weak-reference lifetime, and actual disk-write bytes. Five warm range reads wrote 40 KB before the fix and zero bytes after it in the regression.

Fingerprint the patch in the downloader so existing source trees refresh after updates, including legacy empty markers. Identical patches reuse cached sources. Check platform definitions under nounset with simulated Darwin x86_64/arm64, and make the optional ADBC source guard safe when unset on master. Downloader lifecycle and platform handling remain downstream in Doris.

Validation: 461 regular Rust tests passed on the final source; Rust formatting and diff checks passed. GNU patch and git apply checks passed, and all 87 tracked source files match the recorded source commit. Downloader tests passed on master, branch-4.1, and the downstream hotfix branch for fresh extraction, idempotence, re-extraction, generic markers, legacy/mismatched Foyer markers, and patch failure. Shell syntax and simulated macOS initialization passed. All three opt-in native consumer tests passed: C calls, C++ calls, and static OSS HTTP transport. Full Doris builds and BE integration execution remain in PR CI.

### What problem does this PR solve?

Related PR: lance-format/lance#9599, lance-format/lance-c#89

Problem Summary: Complete index-segment vector scans without predicates
materialize an unnecessary row-ID allowlist. Pin upstream lance-c with the
Lance development-branch optimization and metric-based regressions. Remove
superseded local patches, retaining only the Foyer cache integration with
object-store compatibility and bounded response-metadata reuse.

### Release note

Avoid redundant prefilter row-ID construction for unfiltered vector queries
covering complete visible index segments.

### Check List (For Author)

- Test: Lance prefilter and segment-contract tests; lance-c and Foyer unit/C API
  suites; Rust format and Clippy; C/C++ executable tests; dependency downloader
  patch lifecycle, checksum, and fallback regressions.
- Behavior changed: Yes, eligible vector scans skip redundant prefilter work;
  predicates, partial coverage, deletions, and flat fallback retain semantics.
- Does this need documentation: No; no SQL or configuration interface change.
@Gabriel39
Gabriel39 requested a review from yiguolei as a code owner September 29, 2026 05:50
@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?

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Complete static review of apache/doris PR #68615 at head 29f3df9 (base 4750b1c). Opinion: no new actionable finding and no existing P0/P1 inline finding. All changed paths and the three recorded risk areas have a conclusion.

Critical checkpoints:

  • Goal and evidence: the PR pins lance-c/Lance revisions that contain the complete-segment prefilter guard and loader metrics, removes the superseded patch chain, and retains the Foyer cache patch. Exact pinned source includes targeted ANN/FTS and C API regression cases; the downloader regression is added here. These tests were inspected, not executed.
  • Scope: the integration changes are concentrated in the third-party pin, patch lifecycle, retained cache patch, and one downloader regression. No FE/BE source or protocol interface is changed.
  • Concurrency: shared cache state uses Foyer synchronization; dataset counters use relaxed atomics for counters; wrapper registry map operations hold a mutex and release it before dropping intermediate wrappers. No new lock-order or thread-entry issue was substantiated.
  • Lifecycle and initialization: one shared session creates dataset cache scopes; restore attaches a fresh scope; writer-created handles begin without one. The new SHA-named source directory and Foyer marker govern patch lifecycle. No cross-translation-unit static initializer was introduced.
  • Configuration: no new Doris runtime setting is added. The retained C cache options validate directory and capacities; dynamic configuration behavior is unchanged.
  • Compatibility: the pinned C interface supplies Doris's Lance calls and the removed patch APIs; static OSS transport remains installed. The exact upstream source was checked because its history diverges from the prior Lance pin. Runtime and data-format compatibility were not independently exercised under this review contract.
  • Parallel paths and conditions: whole/ranged get_opts and batched get_ranges, conditional/versioned bypass, direct data-file selection, ANN/FTS/scalar segment selection, deletion masks, and unindexed fallback were traced. The prefilter shortcut requires complete selected fragment coverage and no actual predicate.
  • Tests and expected results: upstream tests cover partial/complete coverage, predicates, stable/physical IDs, deletion and fallback; retained cache tests cover reads, EOF, restore, and disk reuse; the new downloader script covers fresh, repeated, re-extracted, generic-marker, and patch-rejection states. No .out result changes. No build or test was run, as instructed.
  • Errors and observability: failed patch application aborts before marking success or building; origin read failures propagate; cache lookup warnings and existing scan/cache metrics provide diagnostics. No material new observability gap was substantiated.
  • Persistence, transactions, and writes: Doris transaction, EditLog, visible-version, and FE/BE variable paths are unchanged. Foyer's persistent cache remains restricted to immutable data-file reads; no new Doris write path was introduced.
  • Performance and memory: the intended guard skips redundant row-ID prefilter loading; bounded metadata reuse avoids repeated HEAD requests. The additional metadata budget is one eighth of configured cache memory. No new material performance or memory issue was substantiated.
  • Other concerns: a possible S3 endpoint cache-key collision predates this PR in the merge-base Foyer patch and older Lance provider, so it was not duplicated as a new finding.

Review completion: one convergence round with two normal full-review passes and two separate focused risk passes; all returned NO_NEW_VALUABLE_FINDINGS. The main final sweep found no unresolved candidate. Live base/head and inline comments were refreshed before submission and matched the review bundle; there were no existing inline comments. User focus: none was supplied.

### What problem does this PR solve?

Related PR: lance-format/lance#9599, lance-format/lance#9537, lance-format/lance-c#89

Problem Summary: Pin the reviewed Lance prefilter documentation and expanded partial-coverage regression through upstream lance-c. Include the C API regression verifying identical 4-bit PQ candidates and distances with and without an all-row prefilter. The dependency already contains the final merged exact FastScan scoring fix. Refresh the immutable archive checksum; the retained Foyer patch is unchanged.

### Release note

None

### Check List (For Author)

- Test: 138 Lance prefilter tests, 62 PQ tests, and 443 lance-c Rust tests passed. Fresh/idempotent/re-extracted/invalid-patch downloader regressions passed on both Doris branches; shell syntax and diff checks passed.
- Behavior changed: No; this update carries upstream documentation and regression coverage.
- Does this need documentation: No
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

COMPLETE static review of PR #68615 at 4a86e23553670f3a630037bb85ecc020d42c0fb3 against base 4750b1c988fd6acca403f2211c6882941f74d902. I reviewed all 12 changed paths and the affected upstream/Doris read paths. I found no new actionable issue and confirmed no existing P0/P1 inline finding.

Goal, scope, and tests. The change replaces the local Lance-C search/scanner patch chain with an immutable upstream pin and retains the Foyer data-file cache as one rebased patch. The pinned archive's MD5 and source directory match vars.sh. The pinned C header retains the public APIs added by the removed patches; the pinned Lance revision includes the full-snapshot/FTS and segment-prefilter fixes. Upstream regression sources cover full and partial segment coverage, predicates, deletes, FTS metrics, multi-vector/scalar paths, and PQ scoring. The new shell script covers fresh, repeated, re-extracted, cached-marker, and rejected-patch scenarios, but it is not invoked by the current third-party CI workflow. This review did not run builds or tests.

Correctness and parallel paths. The upstream ANN shortcut checks the actual selected segment coverage and empty predicate before omitting the redundant row-ID prefilter; partial or unknown coverage and predicates keep the filtering path. The downstream dataset mask still handles deleted rows, unavailable fragments, and overlay blocks. I checked ordinary, vector, FTS, scalar-segment, multi-vector, and static OSS transport paths against the exact pinned source and Doris's Lance reader. Existing visible-version selection and read paths are unchanged. The Foyer wrapper remains limited to immutable direct data/*.lance children; conditional/versioned requests, writes, and other files use the origin store.

Focus, compatibility, and lifecycle. The new download gate is limited to the SHA-named Lance-C source directory and an independent Foyer marker. The archive check and patch exit status fail before success is marked. All 15 Foyer patch file hunks match the pinned source at fuzz 0; the patched Cargo manifest and lock parse and their dependency versions resolve statically. The rebased object_store wrapper, paginated listing, and GetResult extensions match the pinned APIs. The metadata cache is bounded to one eighth of the configured memory capacity. Existing weak-reference wrapper registration, dataset/session ownership, restore scopes, and short mutex use showed no new leak, deadlock, or unsafe static initialization. No new Doris config, FE/BE protocol field, persistent metadata, transaction, or write-format change is introduced; the existing cache setting remains restart-scoped.

Error handling, performance, and observability. Invalid cache options and patch failures return errors. The new metadata path reuses immutable-file HEAD results when safe and falls back to origin lookup on a miss; the existing per-dataset cache counters and warnings remain available. Search metrics for prefilter loads and rows remain in the pinned upstream revision. The conditions around the optimization and marker are documented in source. No distinct correctness, compatibility, performance, or coverage defect was substantiated after the complete changed-file and unresolved-point sweep.

User focus: no additional focus was provided. Review status: complete, static only; no new inline comments. The Foyer dependency rebase, upstream patch replacement, and cache-marker hypotheses were investigated and dismissed with exact-source evidence.

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@Gabriel39 Gabriel39 changed the title [improvement](lance) Adopt upstream segment prefilter optimization (branch-4.1) [improvement](lance) Add ANN prefilter optimization and profiles (branch-4.1) Sep 29, 2026

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

Review conclusion

Complete static review of PR #68615 at a7ff3f86f1aa270bd5a4fffc58cd12a3627f2cd4: no new actionable findings. All 15 changed paths were covered. Three normal full-review and two separate risk-focused reviewers returned NO_NEW_VALUABLE_FINDINGS in the first round; I checked their conclusions against the reader, pinned dependencies, cache patch, and final changed-file list. No existing inline thread or independently confirmed P0/P1 comment was present. The user supplied no additional focus point.

Critical checkpoints

  • Goal, scope, and proof: The PR replaces the local lance-c search patch chain with pinned upstream lance-c/Lance revisions, retains a rebased Foyer cache patch, avoids the redundant row-ID allowlist for eligible unfiltered ANN scans, and exposes ANN stage timers in the Doris profile. The pinned archive checksum matches thirdparty/vars.sh; the removed C entry points and enum values remain available. Source traces support the requested behavior, and focused upstream and BE regression cases are present. This review did not run them.
  • Focus and parallel paths: The Doris edit adds profile mappings, a focused indexed multivector assertion, and timing documentation. The larger third-party diff removes superseded patches and rebases the one retained cache integration. I traced normal/scalar, single-vector, multivector, and prepared FTS paths, including partial index segments, appended unindexed fragments, filters, deletes, and overlay masks. When the full-segment no-predicate guard does not hold, Lance retains the filtered row-ID path; deletion and overlay masks still apply when it does. The scope guard and fallback are documented in the pinned code.
  • Concurrency, lifecycle, and memory: Lance executes partition work concurrently, so its stage durations can overlap and must not be summed as query wall time. Doris registers the existing statistics callback before execution and closes the scanner while its callback context is alive; the final summary is collected on stream release. The Foyer wrapper keeps per-dataset atomic byte counters, a shared cache, and a short mutex-protected weak-wrapper registry; the registry lock is released before I/O. Reader close snapshots cache counters before dataset close. No new Doris thread, lock-order chain, cross-translation-unit static initializer, or BE-owned allocation path was introduced.
  • Errors, visibility, persistence, and compatibility: The search configuration checks C API return codes. The scoped ANN path retains snapshot visibility through the selected-segment guard and downstream deletion, missing-fragment, and overlay masks. Foyer caches only immutable direct data/*.lance reads; conditional, versioned, and extension-bearing requests bypass metadata admission, and missing cached blocks are fetched from the origin. The patch applied to the clean pinned source with zero fuzz in a read-only dry-run. No Doris EditLog, transaction, table-write, storage-format, or FE-to-BE variable change is in this PR. The upstream dependency moves from Lance v11 to v13 beta; its public C symbols and explicit legacy enum values were checked statically, but runtime mixed-version behavior was not exercised.
  • Configuration, performance, and observability: No new Doris configuration item or dynamic-change expectation is introduced; the existing shared Lance session remains process scoped. The upstream guard removes redundant row-ID materialization only for eligible ANN scopes. The rebased cache adds bounded in-memory response metadata to avoid repeat HEADs for cached range reads. The nine new index_*_time keys match the pinned Lance definitions and travel as nanoseconds through lance-c into the Doris profile; the operator baseline names also match. Counters cover coarse partition work, and the documentation explains nesting and paths that legitimately report zero. No latency or memory benchmark was run.
  • Tests and remaining validation: The changed BE test reads the indexed multivector fixture to completion and checks nonzero stage counters along with existing result and prefilter assertions. The new third-party shell case covers fresh/repeated extraction, marker reuse, and rejected patch application; upstream tests cover partial scopes and search variants. No expected-output file changed. Repository source, builds, and runtime tests were left untouched under the review instructions, so test outcomes and production latency remain unverified by this review; author-reported validation was not treated as independent evidence.

All initial suspicious mechanisms have evidence-backed conclusions in the shared review ledger. The final sweep found no unresolved candidate and no accepted inline issue; comments and existing_blocking_comment_ids are empty.

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Complete static review of PR #68615 at 8c7f614. Two review rounds converged. One new P2 issue is reported inline; no existing P0/P1 inline finding was present in the review context.

Critical checkpoints:

  • Goal and tests: The PR consolidates local lance-c search patches into a pinned upstream revision and exposes ANN stage timings. The pinned archive hash, expected C APIs, FTS/scalar/vector/multi-vector and prefilter paths, and OSS transport were checked. The new BE test verifies timings after a full EOF read, but misses the reported early-stop path. The shell test covers patch-source setup rather than runtime search.
  • Scope: The dependency switch, retained Foyer rebase, profile mapping, test, and documentation form a focused change. All 15 changed paths were swept.
  • Concurrency: Lance stage timers accumulate across asynchronous partition tasks; profile counters receive nanosecond totals through the C callback. Foyer shared cache wrappers, atomic statistics, and mutex-protected wrapper tracking were reviewed; no distinct new race, lock-order, or heavy-lock defect was substantiated.
  • Lifecycle: Dataset/session and wrapper ownership, re-extraction markers, scanner close, cancellation, and EOF were traced. The EOF-only callback leaves new ANN timings unpublished when a pushed SQL LIMIT retires a full block before another poll; see the inline finding. No other lifecycle defect was substantiated.
  • Configuration and compatibility: No dynamic Doris configuration, FE-BE variable, network protocol, or Doris storage-format change is added. The pinned lance-c manifest and build script both require Rust 1.91, and its C API plus the retained Foyer patch cover Doris callers. The Foyer patch applies cleanly to the exact pinned archive in a read-only dry run.
  • Parallel paths and conditions: Indexed and unindexed vector/FTS/scalar paths, prefilter and fallbacks, conditional/versioned cache bypass, and immutable data-file range reads were checked. Removed patch behavior was cross-checked against the pinned source; no separate new correctness issue was confirmed.
  • Coverage and results: The added BE fixture is IVF_FLAT and exercises the asserted timing names on EOF. Its early-stop profile gap needs a regression case. No builds or runtime tests were run under the review-only instructions; author and source tests were inspected statically, not treated as execution evidence.
  • Observability, performance, and memory: Timing names and units match the pinned source, and the documentation correctly warns that overlapping stages cannot be summed. The reported missing partial-scan profile is the observability defect. Cache key, metadata budget, batch range behavior, allocation and coarse timer overhead showed no other substantiated new regression.
  • Persistence, writes, and core invariants: This diff changes external reads and a cache patch, not Doris transaction, EditLog, MoW delete bitmap, or write/publish paths. The FE still requires a fixed positive Lance version. No new data-visibility, error-propagation, or memory-ownership defect was substantiated.
  • User focus: No additional focus was supplied. Existing inline threads in the review context were empty; no issue was duplicated.

This is a complete static review with one accepted inline finding.

Comment thread be/src/format_v2/table/lance_reader.cpp Resolved
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Review conclusion

Complete static review of apache/doris PR #68615 at head 1bdd1ea against base 4750b1c: no new actionable finding. All 15 changed paths and the three recorded risk areas have a conclusion. The current [P2] inline comment 4132705390 about ANN timings missing when a scan stops before EOF still applies; it is already reported and is not duplicated here. No existing P0/P1 inline finding remains, so existing_blocking_comment_ids is empty. One round converged: two normal full-review subagents and one separate upstream-migration risk subagent all returned NO_NEW_VALUABLE_FINDINGS. The main final sweep found no unresolved candidate.

Critical checkpoints

  • Goal, scope, and proof: The PR pins an upstream lance-c/Lance revision that retains the complete-segment prefilter optimization and removed local patch APIs, keeps the rebased Foyer cache integration, and maps ANN stage timings into Doris profiles. The immutable archive digest matches vars.sh. The C API surface, upstream prefilter guard, metrics producer names, and BE consumer names were checked against the exact pin. The code changes are focused on dependency migration, profile registration, one BE regression, and documentation. Source tests exist, but this review did not execute them.
  • Concurrency and lifecycle: Lance partition work is concurrent; the emitted stage durations are cumulative and can overlap. Doris registers the existing statistics callback before scanning, and the reader closes its scanner while its callback context remains alive. The callback publishes only after a derived stream reaches EOF, which explains the already reported P2 on early stop. The Foyer cache is shared by sessions, with dataset-scoped atomic counters and a short mutex around weak wrapper registration; wrapper destruction occurs outside that lock. Open and restore attach scopes, while writer-created handles start without a data cache. No new Doris thread, lock-order chain, cross-translation-unit initializer, or unreleased ownership cycle was substantiated.
  • Configuration and compatibility: No new Doris runtime configuration or FE-to-BE variable is added. The rebased cache options validate directory and capacities; dynamic configuration behavior is unchanged. The pinned C interface retains Doris's FTS, scalar-segment, multivector, scanner, and cache symbols; the static OSS transport initializer remains reachable. This review inspected API and source compatibility, without independently exercising a rolling upgrade or runtime storage-format compatibility.
  • Parallel paths and conditions: The upstream shortcut requires complete selected fragment coverage and no actual predicate. Partial coverage, filters, deletes, and unindexed tails retain their filtering or fallback paths. Normal, vector, multivector, FTS, scalar-segment, and OSS paths were traced. Foyer handles immutable direct data/*.lance whole-object and range reads; conditional and versioned requests bypass cached data, and missing blocks fall back to origin reads. Error status and patch failure paths propagate rather than marking success.
  • Tests and results: The changed BE fixture verifies indexed multivector rows and six nonzero stage timers after consuming to EOF; it does not cover the existing early-stop issue. The new downloader script covers fresh and repeated extraction, existing markers, and rejected patches. Pinned upstream tests cover full and partial segment scopes, predicates, deletes, scoring, and metrics. No expected-output file changed. The task prohibits builds and runtime tests, so test results and production latency were not independently validated; author-reported results were not treated as proof.
  • Observability, performance, and memory: Nine new index time names and operator baseline names match pinned producers and nanosecond profile units. The documentation identifies nested and overlapping durations and legitimate zero counters. The prefilter guard avoids redundant row-ID materialization in eligible searches. The rebased Foyer metadata cache is bounded to one eighth of configured data-cache memory and avoids repeat HEAD requests on usable immutable-file entries. No other material performance, allocation, or observability regression was substantiated.
  • Persistence, transactions, writes, and core invariants: This diff changes external reads and cache integration, not Doris EditLog, transaction publication, visible-version selection, MoW delete bitmaps, FE/BE request fields, or table write formats. Cache errors retain origin-read/error behavior. No new data-visibility, write-atomicity, or memory-tracking violation was substantiated.

User focus: no additional focus was supplied. Review status: complete, static only; no new inline comment.

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Complete static review of PR #68615 at eb51a98 (base 4750b1c). One convergence round completed: the main risk scan, both full-coverage reviews, and the separate risk-focused review returned NO_NEW_VALUABLE_FINDINGS. All 15 changed files and both initial risk items received a final sweep. No new inline findings or confirmed existing P0/P1 issues.

The existing P2 early-stop ANN timing thread still applies to this head. The pinned lance-c callback publishes its execution summary only after a stream is polled to EOF, so closing after a full first block can leave the new Doris timers at zero. This is already reported and is not duplicated here.

Critical checkpoints:

  • Goal, scope, and tests: the pinned lance-c/Lance revisions supply the complete-segment ANN prefilter path and the new stage timings; Doris maps the emitted keys into its scan profile. The BE multivector test checks populated timers on an EOF-drained indexed scan, and the new third-party script covers archive extraction and Foyer patch markers. The full-block early-stop test remains the known P2 gap. The changes are focused on the dependency replacement, retained cache patch, profile mapping, test, and documentation.
  • Concurrency and lifecycle: Lance may search partitions concurrently; its timing summaries are cumulative and overlap parent stages. Doris profile updates use atomic counters, and the Foyer wrapper registry holds its mutex only for short identity lookups. The scanner is retired between splits and before its reader context is destroyed. The only identified retirement-time observability failure is the existing P2 thread; no separate lock-order, deadlock, static-initialization, or ownership issue was substantiated.
  • Configuration and compatibility: no new Doris configuration, FE-BE variable, storage format, transaction log, or write protocol was added. The pinned archive checksum matches the configured value; the source directory is revision-specific. The pinned C API plus the retained Foyer patch retain the Doris-used scanner, FTS, scalar-segment, multivector, session, and cache entry points. The retained patch follows the pinned ObjectStore wrapper interface. Static inspection found no distinct rolling-upgrade or parallel-path regression.
  • Correctness, conditional checks, and parallel paths: the selected segment's complete-fragment coverage gates the prefilter shortcut, while deletion and visibility handling remain in Lance's prefilter path. Vector, FTS, ordinary/scalar, cached/uncached, and multi-split paths were checked against the changed integration. No data-visibility, delete, or error-propagation defect was substantiated. Doris data writes and persistence are outside the changed scan path.
  • Performance and observability: the named index-stage metrics match the pinned producer names and nanosecond units; ANN, sort/merge, take, and vector-distance operator baselines map to the execution-summary keys. Timers may overlap and are documented as cumulative. The upstream optimization avoids a redundant row-ID allowlist only when complete segment coverage proves it safe. No additional hot-path allocation or I/O regression was substantiated. The known early-stop profile gap is noted above.
  • Validation and focus: this was static inspection only, as required by the review instructions; no Doris build, test, source edit, or runtime benchmark was performed. I independently checked the live base/head, the pinned archive MD5, source/API contracts, every changed path, and current inline comments. The user supplied no additional review focus.

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (47/47) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 74.88% (31833/42513)
Line Coverage 59.20% (355878/601192)
Region Coverage 55.81% (296369/531067)
Branch Coverage 56.70% (134164/236603)

@Gabriel39
Gabriel39 marked this pull request as draft September 30, 2026 04:19
### What problem does this PR solve?

Problem Summary: HTTP response extensions prevented immutable object metadata
from entering the Foyer metadata cache. Every cached single-range read could
therefore issue another origin HEAD. Cache ObjectMeta and Attributes regardless
of transport extensions, without caching or replaying the extensions themselves.

Fingerprint the retained patch so previously patched third-party source trees
are refreshed when the patch changes. Preserve reuse for identical patches.

### Release note

Avoid redundant metadata requests for cached Lance data-file reads.

### Check List (For Author)

- Test: Real HTTP regression and Foyer unit tests; third-party downloader lifecycle checks; Rust formatting, shell syntax, and diff checks.
- Behavior changed: Yes, cached immutable metadata avoids repeated origin HEAD requests.
- Does this need documentation: No
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Static review complete for head 065e490a0116b3d3c78978cb3c805d0a3a56f6ce after two convergence rounds, two normal coverage passes per round, a separate risk-focused pass per round, and a final sweep of all 15 changed paths. I found one new P2 test-contract issue, posted inline. The previously reported P2 at comment 4132705390 still applies: Lance publishes these timings only when a scan reaches EOF, so early LIMIT or cancellation can leave the new profile timers zero. I did not repost it. No existing P0/P1 inline finding was confirmed. There were no additional user focus points.

Critical checkpoints:

  • Goal and proof: the PR pins upstream Lance search/prefilter fixes, retains the Foyer data cache, and exposes ANN stage timings. The pinned archive checksum, Doris-reachable C symbols, metric names, and nanosecond units matched the selected dependency. The added BE test exercises indexed multivector output and profile counters, but its new positive-duration assertion is stronger than the producer contract. The shell test covers extraction and marker states. This review did not execute tests or a Doris build, so runtime integration and performance remain unverified here.
  • Scope and paths: the changes are focused on the Lance dependency, retained cache patch, profile wiring, test, and documentation. Scalar, FTS, multivector, ordinary scan, and row-ID fetch interactions were checked. The removed local patch chain's Doris-reachable API surface is present in the pinned source or retained Foyer patch. No new FE/BE transmitted variable, storage format, or Doris configuration item is introduced.
  • Concurrency and lifecycle: ANN partition timers accumulate concurrent work and overlap parent stages; the profile documentation states that correctly. The scanner callback, stream EOF/close, dataset/session lifetime, and Foyer wrapper map and metadata cache were traced. No new lock-order or static-initialization issue was substantiated. The known EOF-only callback lifecycle defect remains the existing P2 thread.
  • Guards, compatibility, and failure behavior: the Foyer metadata cache applies to unconditioned, unversioned immutable data-file reads and keys by store prefix and path; invalid ranges delegate to the origin. Patch fingerprints refresh changed cached source trees, and the archive name and MD5 match the pin. No additional correctness or compatibility failure was substantiated in the reviewed branches.
  • Tests, observability, and performance: one valid zero-duration stage can fail the new BE test; the inline comment gives the correction. The new counters otherwise map to emitted metrics, with nested timing semantics documented. Metadata caching aims to avoid repeated HEAD requests; no independent benchmark or test run was performed. No Doris transaction, EditLog, managed data-write, or persistence path is changed. No other actionable issue remained after the final sweep.

"LanceIndexDistanceTopKTime", "LanceIndexResultMaterializeTime"}) {
auto* counter = profile.get_counter(name);
ASSERT_NE(nullptr, counter) << name;
EXPECT_GT(counter->value(), 0) << name;

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] Check ANN timer emission without requiring every duration to be positive. The pinned lance-c timing test explicitly allows zero nanoseconds for short or uncontended stages; this four-result indexed scan requires six independent timers to be > 0. A valid zero CPU queue wait or fast stage therefore fails the BE test even when the metrics and results are correct. Assert presence and unit/kind, or use a workload with a guaranteed measurable stage for any positive-value check.

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

### What problem does this PR solve?

Related PR: apache#68613

Problem Summary: Identical bucket/path names on distinct storage endpoints could
reuse cached metadata or bytes. Warm range reads also repeatedly enqueued size
records to disk. Regenerate the Foyer patch from its reviewed source fix and
add macOS nounset coverage to the patch harness.

### Release note

Cache entries are isolated by live object-store instance. New instances and
process restarts start cold; same-instance hot reads avoid redundant size writes.

### Check List (For Author)

- Test: 461 Rust tests; generated source equality; GNU patch and git apply;
  downloader lifecycle, shell syntax, and simulated macOS platform checks.
- Behavior changed: Yes, cache isolation and warm-read disk write behavior.
- Does this need documentation: Yes, included in the upstream README patch.
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Review recovery stopped: No final review submission was declared
Workflow run: https://github.com/apache/doris/actions/runs/36679613845

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

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

Complete static review of #68615 at head 4c1f03e50960bb9297b0cfb251cf01d9968f6e32. I found one new P2 performance regression in the Foyer cache namespace. The two existing P2 inline threads (ANN timings lost on early stop, and strict-positive timer assertions) still apply and are not repeated. I found no existing P0/P1 findings to carry forward. Two complete review rounds and the final 15-file sweep found no other substantiated new issue.

Critical checkpoints:

  • Goal and proof: The PR replaces the old Lance C patch stack with a pinned source and Foyer patch, and adds ANN profile counters and documentation. The pinned source retains the removed C APIs and the new timer names/units match its Lance dependency. The default cache does not achieve warm reuse across ordinary fresh dataset opens (inline finding). The new repeated-open test expects remote reads on its second handle, so it does not prove that reuse.
  • Scope: The pin, patch replacement, download handling, BE profile, tests, and documentation form one dependency update. All 15 changed paths and their relevant call chains were reviewed; no separate scope defect was substantiated.
  • Concurrency: Scanner metric callbacks and shared-session cache attachment were traced. The new origin map serializes namespace insertion with a mutex; the reviewed paths showed no separate race, lock-order, or blocking-I/O-under-lock issue.
  • Lifecycle: Lance's registry retains weak outer object-store references, while wrapper attachment replaces the dataset and Refs stores. Fresh Doris reader opens therefore get new cache namespaces even while an earlier wrapped handle lives (inline finding). Same-live-handle and derived-handle reads can still reuse entries. No separate reference cycle or release failure was found.
  • Configuration: No new FE/BE configuration item or dynamic-setting path is introduced. The existing data-cache setting is enabled by default with a 100 GB disk tier, making the missed cross-open reuse material.
  • Compatibility: The pinned archive checksum, patch preimages, static library, C API symbols, and Lance dependency revision were checked against the removed patch chain and Doris callers. No new FE/BE protocol or Doris storage-format change was found. The local persisted cache-key behavior is covered by the inline finding; no other rolling-upgrade issue was substantiated.
  • Parallel paths and conditions: Normal, multi-vector, and prepared-FTS scanners share the metric registration path. Cache direct/range, metadata/size, derived wrapper, and paginated-list paths were reviewed; conditional/versioned metadata requests bypass the cache as intended. The patch checksum marker handles normal fresh, reused, stale, and failed patch states. No separate condition or parallel-path issue was substantiated.
  • Tests and results: The new BE assertions, Rust cache tests, shell harness, and documentation examples were inspected. The reopened-handle cache test reinforces the inline finding; the strict-positive BE timer assertion is already reported in an existing thread. No builds or tests were run, as required by this review task, so runtime behavior is not independently verified.
  • Observability: New ANN timer names, nanosecond units, and additive profile mappings agree with the pinned dependency. The missing early-stop publication is already reported in an existing thread. No additional logging or metric issue was substantiated.
  • Transactions, persistence, and writes: This PR changes no Doris transaction, EditLog, data-write, or FE/BE variable-transfer path. Foyer disk-cache persistence is affected by the fresh namespace finding; the third-party source refresh writes its success marker only after patching succeeds.
  • Performance and remaining issues: The cross-open Foyer miss is the sole accepted new finding. A short-block assembly concern was present in the renamed predecessor patch and is outside the changed hunks; the independent risk reviews found no further reachable new defect.

User review focus: none was supplied. This review is complete for the pinned head, based on static inspection only.

+ let reader = DataCacheReader {
+ cache: self.cache.clone(),
+ store_prefix: store_prefix.to_owned(),
+ store_prefix: format!("{}\0{store_prefix}", origin_namespace(&original)),

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] Keep Foyer keys reusable across dataset opens. origin_namespace(&original) adds a random UUID for each object-store instance, but Dataset::with_object_store_wrappers replaces the registry's store and the shared registry retains only a weak reference. The next Doris reader therefore gets a new origin/UUID and misses every data and size block admitted by the previous query, including persisted disk entries; the added repeated-open test even expects remote reads on its second handle. With the data cache enabled by default, ordinary warm queries cannot benefit from its 100 GB disk tier. Keep a verified backend identity or shared origin stable across opens, and test a close/reopen warm scan.

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 100.00% (47/47) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 61.14% (26725/43710)
Line Coverage 45.80% (277601/606137)
Region Coverage 41.46% (219764/530029)
Branch Coverage 42.96% (101787/236950)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (47/47) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 74.92% (31851/42515)
Line Coverage 59.27% (356357/601282)
Region Coverage 55.88% (296842/531176)
Branch Coverage 56.80% (134421/236669)

@Gabriel39
Gabriel39 marked this pull request as ready for review September 30, 2026 14:15
@Gabriel39
Gabriel39 merged commit 78aa9c9 into apache:branch-4.1 Sep 30, 2026
53 checks passed
yiguolei pushed a commit that referenced this pull request Oct 8, 2026
…nch-4.1) (#68615)

Unfiltered ANN searches scoped to complete index segments can
materialize redundant row-ID allowlists, and the existing scan profile
leaves the remaining search work unexplained. Update the upstream
dependency and expose ANN stage timings on `branch-4.1`.

This includes the dependency integration from #68613: adopt the merged
lance-format/lance#9599 and lance-format/lance#9602 through
lance-format/lance-c#89, retain the merged lance-format/lance#9537 PQ
scoring fix, remove superseded local lance-c patches, and retain only
the Foyer cache integration.

Add BE profile counters for index opening, partition
loading/preparation, prefilter readiness, CPU queue wait, partition
search, query lookup-table preparation, fused distance/TopK work, and
result materialization. Export operator baselines for ANN, sort/merge,
take, and vector-distance work. Extend the existing indexed multivector
regression to check that stage timers reach the Doris profile, alongside
existing result and prefilter assertions.

Timings accumulate across concurrent work and overlap parent stages.
Distance/TopK includes candidate filtering in fused paths; these
counters must not be summed to reconstruct wall time.
`docs/lance-ann-profile.md` documents the boundaries, supported paths,
and relationship to the existing scanner and prefilter timers. The BE
changes add observability; the prefilter optimization itself remains in
the upstream dependency.

Pin lance-c `9bd730add2ac70316c1d642b8459011e2dd92022`, using the merged
Lance #9602 revision `68c12dfd7efe02f90ad2d7f3a239a7eb64884e57`. This
includes parallel-path partition-preparation timing. The upstream
callback regression checks timer presence and nanosecond units while
accepting valid zero durations.

The Foyer wrapper caches immutable ObjectMeta and Attributes
independently of HTTP response extensions, avoiding repeated HEAD
requests on warm range reads. It does not replay transport extensions
from cached metadata. The HTTP regression checks HEAD/GET counts,
returned bytes/ranges/metadata, fresh dataset scopes, and explicit HEAD
bypass.

Generate the retained Foyer patch from source commit
`24c7ca4bcb9422c113b0d3e07e4efe1173b0bc9f` relative to the pinned
lance-c baseline. The header records both revisions and the regeneration
command. The source is submitted as
zhangstar333/lance-c#3 against the branch behind
lance-format/lance-c#73, on top of merged source-alignment PR #2. Doris
consumes the exact reviewed source revision while that supplementary PR
awaits merge.

Scope metadata, size, and block keys to a random namespace for each live
underlying object-store instance. The wrapping API omits a complete
stable backend identity; identical bucket/path names alone cannot
distinguish S3-compatible endpoints. Weak identity records preserve
sharing for the same live store without retaining it, while new stores
and process restarts start cold. A fresh Dataset open that creates a new
store consequently needs to warm its own cache. This deliberately
reduces reuse across instances to prevent returning another origin's
data; it does not claim unchanged cache-hit rates or production latency.

Avoid inserting an unchanged size entry into the WriteOnInsertion hybrid
cache: look up both tiers first and populate only absent or invalid size
records. Regression tests cover real HTTP endpoints sharing
bucket/path/ETag, batched data and NotFound isolation, replaced origins
after disk recovery, weak-reference lifetime, and actual disk-write
bytes. Five warm range reads wrote 40 KB before the fix and zero bytes
after it in the regression.

Fingerprint the patch in the downloader so existing source trees refresh
after updates, including legacy empty markers. Identical patches reuse
cached sources. Check platform definitions under nounset with simulated
Darwin x86_64/arm64, and make the optional ADBC source guard safe when
unset on master. Downloader lifecycle and platform handling remain
downstream in Doris.

Validation: 461 regular Rust tests passed on the final source; Rust
formatting and diff checks passed. GNU patch and git apply checks
passed, and all 87 tracked source files match the recorded source
commit. Downloader tests passed on master, branch-4.1, and the
downstream hotfix branch for fresh extraction, idempotence,
re-extraction, generic markers, legacy/mismatched Foyer markers, and
patch failure. Shell syntax and simulated macOS initialization passed.
All three opt-in native consumer tests passed: C calls, C++ calls, and
static OSS HTTP transport. Full Doris builds and BE integration
execution remain in PR CI.
Gabriel39 pushed a commit that referenced this pull request Oct 9, 2026
…ector_search and full_text_search (#68707)

### What problem does this PR solve?

Issue Number: close #68625

Related PR: #68453 (time travel, tags, branches and managed versioning
for Lance table scans)

Problem Summary:

`vector_search()` and `full_text_search()` always searched the latest
version of `main`, and their `table` argument rejected `FOR VERSION AS
OF`, `@tag` and `@branch`. A search could not be reproduced against a
released tag, compared across versions, or run on an experimental
branch.

Both TVFs now take four optional properties, with the semantics of the
table syntax from #68453:

| Property | Value | Table syntax | Combines with |
|-|-|-|-|
| `version` | positive integer | `FOR VERSION AS OF N` | `branch` |
| `timestamp` | `yyyy-MM-dd HH:mm:ss[.SSS]`, session time zone | `FOR
TIME AS OF` | `branch` |
| `tag` | tag name | `@tag(name)` | none |
| `branch` | branch name, `main` is the main chain | `@branch(name)` |
`version` or `timestamp` |

```sql
SELECT id, _distance FROM vector_search(
    "table" = "lance_catalog.db.documents", "column" = "embedding",
    "query_vector" = "[0.1, 0.2, 0.3]", "version" = "3")
ORDER BY _distance;

SELECT id, _score FROM full_text_search(
    "table" = "lance_catalog.db.documents", "column" = "content", "query" = "database",
    "branch" = "dev", "timestamp" = "2026-09-20 12:00:00")
ORDER BY _score DESC;
```

How it works:

- **FE.** The TVF parses the properties into a `LanceRefSelector` and
loads its metadata through the same
`LanceCatalogClient.readTableSnapshot` as table scans, so
storage-versioned and namespace-managed tables follow the #68453 rules
unchanged. Schema, search column and field id, vector type and
dimension, fragments and index segments all come from the selected
snapshot, and planning never reopens the dataset. The splits carry the
branch directory URI and the version, so no thrift change is needed.
- **Each execution resolves again.** A bound TVF is never cached,
prepared statements re-analyze on every `EXECUTE`, and TVF statements
skip the SQL cache, so the latest version, a future time, a branch head
and a moved tag are resolved per execution and stay fixed within its
plan. `CREATE TABLE AS SELECT` plans its query twice (schema, then
rows), so there a moving selector can resolve twice, as it already can
for a table scan with an explicit selector; pinning `version` gives an
exact copy. This is documented.
- **No fallback.** A snapshot that cannot be selected, or whose index
files are missing, is an error; nothing falls back to the latest
version.
- **EXPLAIN** of a search node now shows `lanceCatalogType`,
`lanceManagedVersioning` and `lanceBranch` next to `lanceVersion`, as
table scans do. Planner errors that name "dataset version N" also name
the branch, since branch versions overlap main's.

Decisions worth calling out:

- `version` takes only a number. `FOR VERSION AS OF 'x'` names a tag
because SQL has one clause for both; the TVF has a `tag` property, and
Lance allows tag names made of digits, which `FOR VERSION AS OF` cannot
reach.
- An empty selector value is an error rather than "not set", so an empty
client-side variable cannot silently search the latest version of
`main`.
- The selector properties must be constants. Accepting `?` would leave
PREPARE without a snapshot to take the result schema from.
`full_text_search` now rejects `?` in any property: until now a
placeholder reached it as the literal text "?", so `'query'=?` searched
for "?".
- Each search relation resolves its own snapshot, as each explicit
selector already does for table scans. `vector_search(t) JOIN t` can
read two versions if a commit lands in between; pinning both with
`version` / `FOR VERSION AS OF` avoids that. This is documented.
- A managed table's branch is opened by a URI Doris joins
(`<table>/tree/<branch>`), which Lance does not validate on that path,
so Doris now rejects names that could leave the table's `tree/`
directory or change that URI: empty segments, `..`, `\`, and ASCII
characters other than letters, digits, `.`, `-`, `_` and `/`, so
`../other` and a percent-encoded dot segment are rejected before the
branch is opened. Outside ASCII only whitespace and control characters
are rejected, so no name Lance accepts is rejected, whichever Unicode
version the JDK and Lance's Rust use. Other tables check a branch out
through Lance, which validates the name itself.

BE changes:

- A search split or a two-phase row fetch without a positive version is
rejected instead of opening the latest version.
- Errors from opening the dataset, reading batches, preparing the FTS
query context and the two-phase row fetch name the snapshot: "... at
Lance dataset version N at <uri>", with the query string and any
password removed from the URI. Missing index files surface at execution
because Lance loads indexes lazily.
- Fragment ids from the FE must exist in the opened snapshot. lance-c
silently skips unknown fragment ids, which would drop rows without an
error.
- The profile records `LanceDatasetVersion` and `LanceDatasetUri`, the
snapshot an execution actually read. EXPLAIN cannot show this for moving
selectors because it resolves again.
- **Behavior change for every vector search:** a split without index
segments now always runs flat (`use_index=false`). The FE plans such
splits as flat search, and EXPLAIN and the profile count them as flat,
but Lance used to search them with the first index on the column in
manifest order, which need not be the index the FE selected (for example
while an index is rebuilt under a new name). Indexes the FE cannot plan
with (no fragment coverage metadata, no recorded metric, or a legacy
index whose details Lance cannot infer from its files, which the FE
skips; `lanceVectorIndexStatus=UNKNOWN_COVERAGE` / `METRIC_MISMATCH` /
`NO_MATCH`) are therefore no longer used implicitly, and such searches
become exact flat searches. Results can change only where the unplanned
index was approximate; latency can grow on such datasets. Legacy indexes
that lack details but whose details Lance infers, such as those written
by Lance 0.10 to 0.24, are planned and searched as before
(`test_lance_vector_search_legacy_index`).
- A vector split that carries index segments while the request says
`use_index=false` is rejected. The FE plans segments only when
`use_index` allows them, so it never sends such a split; the BE now
takes `use_index` from the segments and would otherwise have to override
either the plan or the request.

Known limitations (documented):

- A branch of a branch, or a branch of a shallow clone, can fail to find
the index files it inherited, because Lance writers before v13 record
them under the intermediate branch (lance-format/lance#9176). The search
fails with an error.
- After a branch is deleted and recreated under the same name, or a
dataset is replaced at the same URI, version numbers repeat. Since
#68615 the BE's Lance keys the index list and the row-id index by the
manifest e_tag (lance-format/lance#8904, #8930), but the BE's
process-wide Lance session still keys two entries by version alone,
until eviction or restart. On datasets with stable row ids, the live-row
mask that vector and full-text search prefilter with can come from the
old data, so rows deleted in the new data can reappear or live rows can
be missing (lance-format/lance#9079, open). For a manifest without an
e_tag (V1 naming, or a store that returns no ETag), the index list can
come from the old data, on the BE and in the FE's Lance session until
the catalog is refreshed. Neither is introduced by this PR.
- `SHOW INDEX` and `lance_index_entries()` keep showing the latest
version of `main`.

### Release note

`vector_search()` and `full_text_search()` accept `version`,
`timestamp`, `tag` and `branch` to search a historical version, the
version at a time, a tag or a branch of a Lance table, including REST
Namespace managed tables. Vector search no longer uses an index the
planner did not select, and `full_text_search()` rejects
prepared-statement placeholders.

### Check List (For Author)

- Test: Regression test / Unit Test
- Regression: `external_table_p0/lance/test_lance_search_snapshot`
(filesystem catalog) and `test_lance_rest_search_snapshot` (REST,
storage-versioned and managed) on a new fixture `search_snapshot.lance`.
It has seven versions (appends, a vector index, an FTS index, an append
no index covers, a delete, a vector index rebuild), a tag, and a branch
`dev` whose versions and fragment ids overlap main's with different
rows, plus a tag at a non-latest branch version.
`search_snapshot_pruned.lance` lost a version to cleanup and the index
files of a tagged version. `search_snapshot_evolved.lance` adds a column
and renames the vector column after its index is built, then appends
data the index does not cover. The suites check rows, distances,
two-phase reads (on a branch, a tag, the columns of a version before and
after a rename, and a managed table through REST), EXPLAIN, full-text
search across a delete, `use_index=false` with a selector, every
selector error, and a managed branch named with a Unicode 16 letter
(`unicode_branch.lance`, built by pylance 12.0.0) through a table scan,
a search and a two-phase read. `test_lance_rest_catalog.out` and
`test_lance_rest_time_travel.out` list the new REST tables.
`test_lance_vector_search_legacy_index` searches a vector index written
by pylance 0.18.2 without index details and checks that it is planned
(`USED`, one segment) and returns the flat search's rows. All 30 suites
in `external_table_p0/lance` pass locally.
- FE unit tests: `LanceSearchSelectorTest` (property rules, time zones,
selector forwarding), `LancePreparedSearchTest` (both TVFs pass their
selector to the table; each analysis resolves it exactly once, and a
moved tag is resolved again on the next `EXECUTE`; search statements
bypass the SQL cache; placeholders rejected), `LanceScanNodeTest`
(EXPLAIN, errors naming the branch), `LanceCatalogClientTest` (branch
names, including letters newer than JDK 17's Unicode tables), and, with
real datasets and locally only like the other JNI tests,
`LanceSearchSnapshotTest` (historical index coverage, branches, tags,
schema evolution, cleanup, a snapshot planned before later commits) and
`LanceManagedVersioningTest` (managed versions, tags, branches,
unfinalized heads through the search entry points).
- BE unit tests in `lance_reader_test.cpp`: missing fragment ids on
normal, vector and FTS splits, unpinned versions, error and profile
contents, a split without index segments searching flat although an
index covers it, and a split with index segments under `use_index=false`
being rejected. The indexed variants of the existing multi-vector tests
now pass the index segments the FE would plan.
- Behavior changed: Yes. New TVF properties; vector splits without index
segments always search flat; `full_text_search` rejects `?`; managed
branch names are validated; the error for a time in removed history now
reads "Lance cannot select the version at or before '...'" instead of
"Lance cannot resolve FOR TIME AS OF '...'", because the TVF reaches it
too; BE errors name the dataset version and URI.
- Does this need documentation: Yes (apache/doris-website#4185)
yiguolei pushed a commit that referenced this pull request Oct 10, 2026
…ector_search and full_text_search (#68707)

### What problem does this PR solve?

Issue Number: close #68625

Related PR: #68453 (time travel, tags, branches and managed versioning
for Lance table scans)

Problem Summary:

`vector_search()` and `full_text_search()` always searched the latest
version of `main`, and their `table` argument rejected `FOR VERSION AS
OF`, `@tag` and `@branch`. A search could not be reproduced against a
released tag, compared across versions, or run on an experimental
branch.

Both TVFs now take four optional properties, with the semantics of the
table syntax from #68453:

| Property | Value | Table syntax | Combines with |
|-|-|-|-|
| `version` | positive integer | `FOR VERSION AS OF N` | `branch` |
| `timestamp` | `yyyy-MM-dd HH:mm:ss[.SSS]`, session time zone | `FOR
TIME AS OF` | `branch` |
| `tag` | tag name | `@tag(name)` | none |
| `branch` | branch name, `main` is the main chain | `@branch(name)` |
`version` or `timestamp` |

```sql
SELECT id, _distance FROM vector_search(
    "table" = "lance_catalog.db.documents", "column" = "embedding",
    "query_vector" = "[0.1, 0.2, 0.3]", "version" = "3")
ORDER BY _distance;

SELECT id, _score FROM full_text_search(
    "table" = "lance_catalog.db.documents", "column" = "content", "query" = "database",
    "branch" = "dev", "timestamp" = "2026-09-20 12:00:00")
ORDER BY _score DESC;
```

How it works:

- **FE.** The TVF parses the properties into a `LanceRefSelector` and
loads its metadata through the same
`LanceCatalogClient.readTableSnapshot` as table scans, so
storage-versioned and namespace-managed tables follow the #68453 rules
unchanged. Schema, search column and field id, vector type and
dimension, fragments and index segments all come from the selected
snapshot, and planning never reopens the dataset. The splits carry the
branch directory URI and the version, so no thrift change is needed.
- **Each execution resolves again.** A bound TVF is never cached,
prepared statements re-analyze on every `EXECUTE`, and TVF statements
skip the SQL cache, so the latest version, a future time, a branch head
and a moved tag are resolved per execution and stay fixed within its
plan. `CREATE TABLE AS SELECT` plans its query twice (schema, then
rows), so there a moving selector can resolve twice, as it already can
for a table scan with an explicit selector; pinning `version` gives an
exact copy. This is documented.
- **No fallback.** A snapshot that cannot be selected, or whose index
files are missing, is an error; nothing falls back to the latest
version.
- **EXPLAIN** of a search node now shows `lanceCatalogType`,
`lanceManagedVersioning` and `lanceBranch` next to `lanceVersion`, as
table scans do. Planner errors that name "dataset version N" also name
the branch, since branch versions overlap main's.

Decisions worth calling out:

- `version` takes only a number. `FOR VERSION AS OF 'x'` names a tag
because SQL has one clause for both; the TVF has a `tag` property, and
Lance allows tag names made of digits, which `FOR VERSION AS OF` cannot
reach.
- An empty selector value is an error rather than "not set", so an empty
client-side variable cannot silently search the latest version of
`main`.
- The selector properties must be constants. Accepting `?` would leave
PREPARE without a snapshot to take the result schema from.
`full_text_search` now rejects `?` in any property: until now a
placeholder reached it as the literal text "?", so `'query'=?` searched
for "?".
- Each search relation resolves its own snapshot, as each explicit
selector already does for table scans. `vector_search(t) JOIN t` can
read two versions if a commit lands in between; pinning both with
`version` / `FOR VERSION AS OF` avoids that. This is documented.
- A managed table's branch is opened by a URI Doris joins
(`<table>/tree/<branch>`), which Lance does not validate on that path,
so Doris now rejects names that could leave the table's `tree/`
directory or change that URI: empty segments, `..`, `\`, and ASCII
characters other than letters, digits, `.`, `-`, `_` and `/`, so
`../other` and a percent-encoded dot segment are rejected before the
branch is opened. Outside ASCII only whitespace and control characters
are rejected, so no name Lance accepts is rejected, whichever Unicode
version the JDK and Lance's Rust use. Other tables check a branch out
through Lance, which validates the name itself.

BE changes:

- A search split or a two-phase row fetch without a positive version is
rejected instead of opening the latest version.
- Errors from opening the dataset, reading batches, preparing the FTS
query context and the two-phase row fetch name the snapshot: "... at
Lance dataset version N at <uri>", with the query string and any
password removed from the URI. Missing index files surface at execution
because Lance loads indexes lazily.
- Fragment ids from the FE must exist in the opened snapshot. lance-c
silently skips unknown fragment ids, which would drop rows without an
error.
- The profile records `LanceDatasetVersion` and `LanceDatasetUri`, the
snapshot an execution actually read. EXPLAIN cannot show this for moving
selectors because it resolves again.
- **Behavior change for every vector search:** a split without index
segments now always runs flat (`use_index=false`). The FE plans such
splits as flat search, and EXPLAIN and the profile count them as flat,
but Lance used to search them with the first index on the column in
manifest order, which need not be the index the FE selected (for example
while an index is rebuilt under a new name). Indexes the FE cannot plan
with (no fragment coverage metadata, no recorded metric, or a legacy
index whose details Lance cannot infer from its files, which the FE
skips; `lanceVectorIndexStatus=UNKNOWN_COVERAGE` / `METRIC_MISMATCH` /
`NO_MATCH`) are therefore no longer used implicitly, and such searches
become exact flat searches. Results can change only where the unplanned
index was approximate; latency can grow on such datasets. Legacy indexes
that lack details but whose details Lance infers, such as those written
by Lance 0.10 to 0.24, are planned and searched as before
(`test_lance_vector_search_legacy_index`).
- A vector split that carries index segments while the request says
`use_index=false` is rejected. The FE plans segments only when
`use_index` allows them, so it never sends such a split; the BE now
takes `use_index` from the segments and would otherwise have to override
either the plan or the request.

Known limitations (documented):

- A branch of a branch, or a branch of a shallow clone, can fail to find
the index files it inherited, because Lance writers before v13 record
them under the intermediate branch (lance-format/lance#9176). The search
fails with an error.
- After a branch is deleted and recreated under the same name, or a
dataset is replaced at the same URI, version numbers repeat. Since
#68615 the BE's Lance keys the index list and the row-id index by the
manifest e_tag (lance-format/lance#8904, #8930), but the BE's
process-wide Lance session still keys two entries by version alone,
until eviction or restart. On datasets with stable row ids, the live-row
mask that vector and full-text search prefilter with can come from the
old data, so rows deleted in the new data can reappear or live rows can
be missing (lance-format/lance#9079, open). For a manifest without an
e_tag (V1 naming, or a store that returns no ETag), the index list can
come from the old data, on the BE and in the FE's Lance session until
the catalog is refreshed. Neither is introduced by this PR.
- `SHOW INDEX` and `lance_index_entries()` keep showing the latest
version of `main`.

### Release note

`vector_search()` and `full_text_search()` accept `version`,
`timestamp`, `tag` and `branch` to search a historical version, the
version at a time, a tag or a branch of a Lance table, including REST
Namespace managed tables. Vector search no longer uses an index the
planner did not select, and `full_text_search()` rejects
prepared-statement placeholders.

### Check List (For Author)

- Test: Regression test / Unit Test
- Regression: `external_table_p0/lance/test_lance_search_snapshot`
(filesystem catalog) and `test_lance_rest_search_snapshot` (REST,
storage-versioned and managed) on a new fixture `search_snapshot.lance`.
It has seven versions (appends, a vector index, an FTS index, an append
no index covers, a delete, a vector index rebuild), a tag, and a branch
`dev` whose versions and fragment ids overlap main's with different
rows, plus a tag at a non-latest branch version.
`search_snapshot_pruned.lance` lost a version to cleanup and the index
files of a tagged version. `search_snapshot_evolved.lance` adds a column
and renames the vector column after its index is built, then appends
data the index does not cover. The suites check rows, distances,
two-phase reads (on a branch, a tag, the columns of a version before and
after a rename, and a managed table through REST), EXPLAIN, full-text
search across a delete, `use_index=false` with a selector, every
selector error, and a managed branch named with a Unicode 16 letter
(`unicode_branch.lance`, built by pylance 12.0.0) through a table scan,
a search and a two-phase read. `test_lance_rest_catalog.out` and
`test_lance_rest_time_travel.out` list the new REST tables.
`test_lance_vector_search_legacy_index` searches a vector index written
by pylance 0.18.2 without index details and checks that it is planned
(`USED`, one segment) and returns the flat search's rows. All 30 suites
in `external_table_p0/lance` pass locally.
- FE unit tests: `LanceSearchSelectorTest` (property rules, time zones,
selector forwarding), `LancePreparedSearchTest` (both TVFs pass their
selector to the table; each analysis resolves it exactly once, and a
moved tag is resolved again on the next `EXECUTE`; search statements
bypass the SQL cache; placeholders rejected), `LanceScanNodeTest`
(EXPLAIN, errors naming the branch), `LanceCatalogClientTest` (branch
names, including letters newer than JDK 17's Unicode tables), and, with
real datasets and locally only like the other JNI tests,
`LanceSearchSnapshotTest` (historical index coverage, branches, tags,
schema evolution, cleanup, a snapshot planned before later commits) and
`LanceManagedVersioningTest` (managed versions, tags, branches,
unfinalized heads through the search entry points).
- BE unit tests in `lance_reader_test.cpp`: missing fragment ids on
normal, vector and FTS splits, unpinned versions, error and profile
contents, a split without index segments searching flat although an
index covers it, and a split with index segments under `use_index=false`
being rejected. The indexed variants of the existing multi-vector tests
now pass the index segments the FE would plan.
- Behavior changed: Yes. New TVF properties; vector splits without index
segments always search flat; `full_text_search` rejects `?`; managed
branch names are validated; the error for a time in removed history now
reads "Lance cannot select the version at or before '...'" instead of
"Lance cannot resolve FOR TIME AS OF '...'", because the TVF reaches it
too; BE errors name the dataset version and URI.
- Does this need documentation: Yes (apache/doris-website#4185)
morningman added a commit to morningman/doris that referenced this pull request Oct 26, 2026
### What problem does this PR solve?

Issue Number: None

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

Problem Summary:

Port the Lance BE read path of branch-4.1/4.2 (as of branch-4.2
cabcf45) to master. Master links liblance_c but has no Lance reader:
file_scan_operator keeps FORMAT_LANCE off FileScannerV2 and V1 has no
Lance path, so every FORMAT_LANCE scan fails today. This change is BE
only. The FE connector plugin and the regression suites follow
separately, so nothing on master plans a Lance scan yet and the BE unit
tests carry the proof.

- Reader: be/src/format_v2/lance/* and format_v2/table/lance_reader.*
  (plain scans, vector and full-text search, Substrait and runtime
  filter pushdown, scalar index segments, COUNT(*) from metadata, LIMIT,
  profiles), copied from branch-4.2 except one local renamed for
  -Wshadow.
- Hooks: FileFormat::LANCE and its exhaustive switches, FileScannerV2
  dispatch, and fetch_table_schema for FORMAT_LANCE. Lance queries use
  FileScannerV2 whatever enable_file_scanner_v2 says, as ADBC and fluss
  do: there is no V1 Lance reader. The second phase of TopN lazy
  materialization reads Lance rows through dataset take_rows.
- Row locations: GlobalRowLoacationV2 gets a version 1 layout with a
  64-bit Lance row id (the version 0 layout is unchanged),
  PRequestBlockDesc.row_id becomes uint64 (varint, wire compatible), and
  the materialization operator and RowIdStorageReader decode and
  validate by version.
- Thrift: the external search request body, TLanceFileDesc 4-6 and
  TExternalSearchRequest 2-4 with the branch-4.1/4.2 ids.
- Core: PTypeDesc keeps NULL_TYPE for a NULL literal type, and the
  nullable serde reads an Arrow Null array as NULLs.
- Nine lance_* BE configs, a BE-wide Lance session with an optional
  Foyer data cache and 10 lance_session_* metrics; macOS links IOKit.
- Tests: the Lance BE unit tests and their fixtures, including the
  docker fixture all_types.lance that one of them reads. Under master's
  -ffile-prefix-map the session test locates its fixture from the
  repository root, and the reader tests shrink the data cache to 64 MiB
  under the UT temp dir: the default 100 GiB disk tier is 51200 block
  files that stay open for the rest of the test process.

Not included: the MaterializationOperator::push timer fix (apache#69579), the
Lance index metadata scanner, the FE side, regression suites, and the
4.x changes after cabcf45 (apache#68707, apache#68813, apache#68815).

### Release note

Add nine lance_* BE configs and lance_session_* BE metrics. They take
effect once the FE can plan Lance scans.

### Check List (For Author)

- Test: Unit Test
    - BE UT: the Lance reader, nested Null, runtime filter and session
      suites, plus the row-location protocol and FileScannerV2 cases
- Behavior changed: Yes. PRequestBlockDesc.row_id is uint64, and the
  second phase of TopN lazy materialization rejects row locations of an
  unknown version or size.
- Does this need documentation: No (documented with the FE connector)

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]>
morningman added a commit to morningman/doris that referenced this pull request Oct 27, 2026
### What problem does this PR solve?

Issue Number: None

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

Problem Summary:

Port the Lance BE read path of branch-4.1/4.2 (as of branch-4.2
cabcf45) to master. Master links liblance_c but has no Lance reader:
file_scan_operator keeps FORMAT_LANCE off FileScannerV2 and V1 has no
Lance path, so every FORMAT_LANCE scan fails today. This change is BE
only. The FE connector plugin and the regression suites follow
separately, so nothing on master plans a Lance scan yet and the BE unit
tests carry the proof.

- Reader: be/src/format_v2/lance/* and format_v2/table/lance_reader.*
  (plain scans, vector and full-text search, Substrait and runtime
  filter pushdown, scalar index segments, COUNT(*) from metadata, LIMIT,
  profiles), copied from branch-4.2 except one local renamed for
  -Wshadow.
- Hooks: FileFormat::LANCE and its exhaustive switches, FileScannerV2
  dispatch, and fetch_table_schema for FORMAT_LANCE. Lance queries use
  FileScannerV2 whatever enable_file_scanner_v2 says, as ADBC and fluss
  do: there is no V1 Lance reader. The second phase of TopN lazy
  materialization reads Lance rows through dataset take_rows.
- Row locations: GlobalRowLoacationV2 gets a version 1 layout with a
  64-bit Lance row id (the version 0 layout is unchanged),
  PRequestBlockDesc.row_id becomes uint64 (varint, wire compatible), and
  the materialization operator and RowIdStorageReader decode and
  validate by version.
- Thrift: the external search request body, TLanceFileDesc 4-6 and
  TExternalSearchRequest 2-4 with the branch-4.1/4.2 ids.
- Core: PTypeDesc keeps NULL_TYPE for a NULL literal type, and the
  nullable serde reads an Arrow Null array as NULLs.
- Nine lance_* BE configs, a BE-wide Lance session with an optional
  Foyer data cache and 10 lance_session_* metrics; macOS links IOKit.
- Tests: the Lance BE unit tests and their fixtures, including the
  docker fixture all_types.lance that one of them reads. Under master's
  -ffile-prefix-map the session test locates its fixture from the
  repository root, and the reader tests shrink the data cache to 64 MiB
  under the UT temp dir: the default 100 GiB disk tier is 51200 block
  files that stay open for the rest of the test process.

Not included: the MaterializationOperator::push timer fix (apache#69579), the
Lance index metadata scanner, the FE side, regression suites, and the
4.x changes after cabcf45 (apache#68707, apache#68813, apache#68815).

### Release note

Add nine lance_* BE configs and lance_session_* BE metrics. They take
effect once the FE can plan Lance scans.

### Check List (For Author)

- Test: Unit Test
    - BE UT: the Lance reader, nested Null, runtime filter and session
      suites, plus the row-location protocol and FileScannerV2 cases
- Behavior changed: Yes. PRequestBlockDesc.row_id is uint64, and the
  second phase of TopN lazy materialization rejects row locations of an
  unknown version or size.
- Does this need documentation: No (documented with the FE connector)

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]>
morningman added a commit to morningman/doris that referenced this pull request Oct 28, 2026
### What problem does this PR solve?

Issue Number: None

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

Problem Summary:

Port the Lance BE read path of branch-4.1/4.2 (as of branch-4.2
cabcf45) to master. Master links liblance_c but has no Lance reader:
file_scan_operator keeps FORMAT_LANCE off FileScannerV2 and V1 has no
Lance path, so every FORMAT_LANCE scan fails today. This change is BE
only. The FE connector plugin and the regression suites follow
separately, so nothing on master plans a Lance scan yet and the BE unit
tests carry the proof.

- Reader: be/src/format_v2/lance/* and format_v2/table/lance_reader.*
  (plain scans, vector and full-text search, Substrait and runtime
  filter pushdown, scalar index segments, COUNT(*) from metadata, LIMIT,
  profiles), copied from branch-4.2 except one local renamed for
  -Wshadow.
- Hooks: FileFormat::LANCE and its exhaustive switches, FileScannerV2
  dispatch, and fetch_table_schema for FORMAT_LANCE. Lance queries use
  FileScannerV2 whatever enable_file_scanner_v2 says, as ADBC and fluss
  do: there is no V1 Lance reader. The second phase of TopN lazy
  materialization reads Lance rows through dataset take_rows.
- Row locations: GlobalRowLoacationV2 gets a version 1 layout with a
  64-bit Lance row id (the version 0 layout is unchanged),
  PRequestBlockDesc.row_id becomes uint64 (varint, wire compatible), and
  the materialization operator and RowIdStorageReader decode and
  validate by version.
- Thrift: the external search request body, TLanceFileDesc 4-6 and
  TExternalSearchRequest 2-4 with the branch-4.1/4.2 ids.
- Core: PTypeDesc keeps NULL_TYPE for a NULL literal type, and the
  nullable serde reads an Arrow Null array as NULLs.
- Nine lance_* BE configs, a BE-wide Lance session with an optional
  Foyer data cache and 10 lance_session_* metrics; macOS links IOKit.
- Tests: the Lance BE unit tests and their fixtures, including the
  docker fixture all_types.lance that one of them reads. Under master's
  -ffile-prefix-map the session test locates its fixture from the
  repository root, and the reader tests shrink the data cache to 64 MiB
  under the UT temp dir: the default 100 GiB disk tier is 51200 block
  files that stay open for the rest of the test process.

Not included: the MaterializationOperator::push timer fix (apache#69579), the
Lance index metadata scanner, the FE side, regression suites, and the
4.x changes after cabcf45 (apache#68707, apache#68813, apache#68815).

### Release note

Add nine lance_* BE configs and lance_session_* BE metrics. They take
effect once the FE can plan Lance scans.

### Check List (For Author)

- Test: Unit Test
    - BE UT: the Lance reader, nested Null, runtime filter and session
      suites, plus the row-location protocol and FileScannerV2 cases
- Behavior changed: Yes. PRequestBlockDesc.row_id is uint64, and the
  second phase of TopN lazy materialization rejects row locations of an
  unknown version or size.
- Does this need documentation: No (documented with the FE connector)

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]>
morningman added a commit to morningman/doris that referenced this pull request Oct 29, 2026
### What problem does this PR solve?

Issue Number: None

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

Problem Summary:

Port the Lance BE read path of branch-4.1/4.2 (as of branch-4.2
cabcf45) to master. Master links liblance_c but has no Lance reader:
file_scan_operator keeps FORMAT_LANCE off FileScannerV2 and V1 has no
Lance path, so every FORMAT_LANCE scan fails today. This change is BE
only. The FE connector plugin and the regression suites follow
separately, so nothing on master plans a Lance scan yet and the BE unit
tests carry the proof.

- Reader: be/src/format_v2/lance/* and format_v2/table/lance_reader.*
  (plain scans, vector and full-text search, Substrait and runtime
  filter pushdown, scalar index segments, COUNT(*) from metadata, LIMIT,
  profiles), copied from branch-4.2 except one local renamed for
  -Wshadow.
- Hooks: FileFormat::LANCE and its exhaustive switches, FileScannerV2
  dispatch, and fetch_table_schema for FORMAT_LANCE. Lance queries use
  FileScannerV2 whatever enable_file_scanner_v2 says, as ADBC and fluss
  do: there is no V1 Lance reader. The second phase of TopN lazy
  materialization reads Lance rows through dataset take_rows.
- Row locations: GlobalRowLoacationV2 gets a version 1 layout with a
  64-bit Lance row id (the version 0 layout is unchanged),
  PRequestBlockDesc.row_id becomes uint64 (varint, wire compatible), and
  the materialization operator and RowIdStorageReader decode and
  validate by version.
- Thrift: the external search request body, TLanceFileDesc 4-6 and
  TExternalSearchRequest 2-4 with the branch-4.1/4.2 ids.
- Core: PTypeDesc keeps NULL_TYPE for a NULL literal type, and the
  nullable serde reads an Arrow Null array as NULLs.
- Nine lance_* BE configs, a BE-wide Lance session with an optional
  Foyer data cache and 10 lance_session_* metrics; macOS links IOKit.
- Tests: the Lance BE unit tests and their fixtures, including the
  docker fixture all_types.lance that one of them reads. Under master's
  -ffile-prefix-map the session test locates its fixture from the
  repository root, and the reader tests shrink the data cache to 64 MiB
  under the UT temp dir: the default 100 GiB disk tier is 51200 block
  files that stay open for the rest of the test process.

Not included: the MaterializationOperator::push timer fix (apache#69579), the
Lance index metadata scanner, the FE side, regression suites, and the
4.x changes after cabcf45 (apache#68707, apache#68813, apache#68815).

### Release note

Add nine lance_* BE configs and lance_session_* BE metrics. They take
effect once the FE can plan Lance scans.

### Check List (For Author)

- Test: Unit Test
    - BE UT: the Lance reader, nested Null, runtime filter and session
      suites, plus the row-location protocol and FileScannerV2 cases
- Behavior changed: Yes. PRequestBlockDesc.row_id is uint64, and the
  second phase of TopN lazy materialization rejects row locations of an
  unknown version or size.
- Does this need documentation: No (documented with the FE connector)

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]>
morningman added a commit to morningman/doris that referenced this pull request Oct 31, 2026
### What problem does this PR solve?

Issue Number: None

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

Problem Summary:

Port the Lance BE read path of branch-4.1/4.2 (as of branch-4.2
cabcf45) to master. Master links liblance_c but has no Lance reader:
file_scan_operator keeps FORMAT_LANCE off FileScannerV2 and V1 has no
Lance path, so every FORMAT_LANCE scan fails today. This change is BE
only. The FE connector plugin and the regression suites follow
separately, so nothing on master plans a Lance scan yet and the BE unit
tests carry the proof.

- Reader: be/src/format_v2/lance/* and format_v2/table/lance_reader.*
  (plain scans, vector and full-text search, Substrait and runtime
  filter pushdown, scalar index segments, COUNT(*) from metadata, LIMIT,
  profiles), copied from branch-4.2 except one local renamed for
  -Wshadow.
- Hooks: FileFormat::LANCE and its exhaustive switches, FileScannerV2
  dispatch, and fetch_table_schema for FORMAT_LANCE. Lance queries use
  FileScannerV2 whatever enable_file_scanner_v2 says, as ADBC and fluss
  do: there is no V1 Lance reader. The second phase of TopN lazy
  materialization reads Lance rows through dataset take_rows.
- Row locations: GlobalRowLoacationV2 gets a version 1 layout with a
  64-bit Lance row id (the version 0 layout is unchanged),
  PRequestBlockDesc.row_id becomes uint64 (varint, wire compatible), and
  the materialization operator and RowIdStorageReader decode and
  validate by version.
- Thrift: the external search request body, TLanceFileDesc 4-6 and
  TExternalSearchRequest 2-4 with the branch-4.1/4.2 ids.
- Core: PTypeDesc keeps NULL_TYPE for a NULL literal type, and the
  nullable serde reads an Arrow Null array as NULLs.
- Nine lance_* BE configs, a BE-wide Lance session with an optional
  Foyer data cache and 10 lance_session_* metrics; macOS links IOKit.
- Tests: the Lance BE unit tests and their fixtures, including the
  docker fixture all_types.lance that one of them reads. Under master's
  -ffile-prefix-map the session test locates its fixture from the
  repository root, and the reader tests shrink the data cache to 64 MiB
  under the UT temp dir: the default 100 GiB disk tier is 51200 block
  files that stay open for the rest of the test process.

Not included: the MaterializationOperator::push timer fix (apache#69579), the
Lance index metadata scanner, the FE side, regression suites, and the
4.x changes after cabcf45 (apache#68707, apache#68813, apache#68815).

### Release note

Add nine lance_* BE configs and lance_session_* BE metrics. They take
effect once the FE can plan Lance scans.

### Check List (For Author)

- Test: Unit Test
    - BE UT: the Lance reader, nested Null, runtime filter and session
      suites, plus the row-location protocol and FileScannerV2 cases
- Behavior changed: Yes. PRequestBlockDesc.row_id is uint64, and the
  second phase of TopN lazy materialization rejects row locations of an
  unknown version or size.
- Does this need documentation: No (documented with the FE connector)

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]>
morningman added a commit to morningman/doris that referenced this pull request Nov 2, 2026
### What problem does this PR solve?

Issue Number: None

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

Problem Summary:

Port the Lance BE read path of branch-4.1/4.2 (as of branch-4.2
cabcf45) to master. Master links liblance_c but has no Lance reader:
file_scan_operator keeps FORMAT_LANCE off FileScannerV2 and V1 has no
Lance path, so every FORMAT_LANCE scan fails today. This change is BE
only. The FE connector plugin and the regression suites follow
separately, so nothing on master plans a Lance scan yet and the BE unit
tests carry the proof.

- Reader: be/src/format_v2/lance/* and format_v2/table/lance_reader.*
  (plain scans, vector and full-text search, Substrait and runtime
  filter pushdown, scalar index segments, COUNT(*) from metadata, LIMIT,
  profiles), copied from branch-4.2 except one local renamed for
  -Wshadow.
- Hooks: FileFormat::LANCE and its exhaustive switches, FileScannerV2
  dispatch, and fetch_table_schema for FORMAT_LANCE. Lance queries use
  FileScannerV2 whatever enable_file_scanner_v2 says, as ADBC and fluss
  do: there is no V1 Lance reader. The second phase of TopN lazy
  materialization reads Lance rows through dataset take_rows.
- Row locations: GlobalRowLoacationV2 gets a version 1 layout with a
  64-bit Lance row id (the version 0 layout is unchanged),
  PRequestBlockDesc.row_id becomes uint64 (varint, wire compatible), and
  the materialization operator and RowIdStorageReader decode and
  validate by version.
- Thrift: the external search request body, TLanceFileDesc 4-6 and
  TExternalSearchRequest 2-4 with the branch-4.1/4.2 ids.
- Core: PTypeDesc keeps NULL_TYPE for a NULL literal type, and the
  nullable serde reads an Arrow Null array as NULLs.
- Nine lance_* BE configs, a BE-wide Lance session with an optional
  Foyer data cache and 10 lance_session_* metrics; macOS links IOKit.
- Tests: the Lance BE unit tests and their fixtures, including the
  docker fixture all_types.lance that one of them reads. Under master's
  -ffile-prefix-map the session test locates its fixture from the
  repository root, and the reader tests shrink the data cache to 64 MiB
  under the UT temp dir: the default 100 GiB disk tier is 51200 block
  files that stay open for the rest of the test process.

Not included: the MaterializationOperator::push timer fix (apache#69579), the
Lance index metadata scanner, the FE side, regression suites, and the
4.x changes after cabcf45 (apache#68707, apache#68813, apache#68815).

### Release note

Add nine lance_* BE configs and lance_session_* BE metrics. They take
effect once the FE can plan Lance scans.

### Check List (For Author)

- Test: Unit Test
    - BE UT: the Lance reader, nested Null, runtime filter and session
      suites, plus the row-location protocol and FileScannerV2 cases
- Behavior changed: Yes. PRequestBlockDesc.row_id is uint64, and the
  second phase of TopN lazy materialization rejects row locations of an
  unknown version or size.
- Does this need documentation: No (documented with the FE connector)

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]>
morningman added a commit to morningman/doris that referenced this pull request Nov 5, 2026
### What problem does this PR solve?

Issue Number: None

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

Problem Summary:

Port the Lance BE read path of branch-4.1/4.2 (as of branch-4.2
cabcf45) to master. Master links liblance_c but has no Lance reader:
file_scan_operator keeps FORMAT_LANCE off FileScannerV2 and V1 has no
Lance path, so every FORMAT_LANCE scan fails today. This change is BE
only. The FE connector plugin and the regression suites follow
separately, so nothing on master plans a Lance scan yet and the BE unit
tests carry the proof.

- Reader: be/src/format_v2/lance/* and format_v2/table/lance_reader.*
  (plain scans, vector and full-text search, Substrait and runtime
  filter pushdown, scalar index segments, COUNT(*) from metadata, LIMIT,
  profiles), copied from branch-4.2 except one local renamed for
  -Wshadow.
- Hooks: FileFormat::LANCE and its exhaustive switches, FileScannerV2
  dispatch, and fetch_table_schema for FORMAT_LANCE. Lance queries use
  FileScannerV2 whatever enable_file_scanner_v2 says, as ADBC and fluss
  do: there is no V1 Lance reader. The second phase of TopN lazy
  materialization reads Lance rows through dataset take_rows.
- Row locations: GlobalRowLoacationV2 gets a version 1 layout with a
  64-bit Lance row id (the version 0 layout is unchanged),
  PRequestBlockDesc.row_id becomes uint64 (varint, wire compatible), and
  the materialization operator and RowIdStorageReader decode and
  validate by version.
- Thrift: the external search request body, TLanceFileDesc 4-6 and
  TExternalSearchRequest 2-4 with the branch-4.1/4.2 ids, in place of the
  comments that reserve them on master (apache#70075); field 6 stays master's
  paimon_options.
  TVectorSearchOptions 6 is branch-4.2's approx_mode (apache#69852), reserved
  in a comment.
- Core: PTypeDesc keeps NULL_TYPE for a NULL literal type, and the
  nullable serde reads an Arrow Null array as NULLs.
- Nine lance_* BE configs, a BE-wide Lance session with an optional
  Foyer data cache and 10 lance_session_* metrics; macOS links IOKit.
- Tests: the Lance BE unit tests and their fixtures, including the
  docker fixture all_types.lance that one of them reads. Under master's
  -ffile-prefix-map the session test locates its fixture from the
  repository root, and the reader tests shrink the data cache to 64 MiB
  under the UT temp dir: the default 100 GiB disk tier is 51200 block
  files that stay open for the rest of the test process.

Not included: the MaterializationOperator::push timer fix (apache#69579), the
Lance index metadata scanner, the FE side, regression suites, and the
4.x changes after cabcf45 (apache#68707, apache#68813, apache#68815).

### Release note

Add nine lance_* BE configs and lance_session_* BE metrics. They take
effect once the FE can plan Lance scans.

### Check List (For Author)

- Test: Unit Test
    - BE UT: the Lance reader, nested Null, runtime filter and session
      suites, plus the row-location protocol and FileScannerV2 cases
- Behavior changed: Yes. PRequestBlockDesc.row_id is uint64, and the
  second phase of TopN lazy materialization rejects row locations of an
  unknown version or size.
- Does this need documentation: No (documented with the FE connector)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants