Skip to content

branch-4.1:[feat](paimon) port PK-vector ANN search and upgrade Paimon to 2.1 - #68988

Closed
pzhdfy wants to merge 24 commits into
apache:branch-4.1from
pzhdfy:branch-4.1-paimon-rust-ann-new
Closed

pzhdfy wants to merge 24 commits into
apache:branch-4.1from
pzhdfy:branch-4.1-paimon-rust-ann-new

Conversation

@pzhdfy

@pzhdfy pzhdfy commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Follow-up to #66227, which introduced the paimon-rust reader. This PR upgrades the Paimon SDK to the community 2.1.0 release and ports the primary-key vector (ANN) search path onto the community stack: ORDER BY l2_distance_approximate|inner_product_approximate(col, vector) LIMIT n is rewritten into a pushdown onto the paimon scan and served by the paimon-rust vector reader on BE.

Problem Summary:

1. Upgrade Paimon to the community 2.1.0

  • Port FE off the paimon privilege package removed in 2.1.
  • The 2.1 S3FileIO reads S3 credentials only from CatalogContext.options() (verified at bytecode level: S3FileIO#configure -> loadHadoopConfigFromContext never touches the Hadoop Configuration, unlike 1.4; unchanged in the community 2.1.0). Doris keeps a catalog's storage credentials in the Hadoop conf only, so every s3-warehouse paimon catalog initialized the SDK with no credentials and failed with Unable to load credentials. mirrorHadoopS3OptionsToCatalogOptions now mirrors the fs.s3a.* entries onto the catalog options for the FileSystem/HMS/JDBC metastores, keeping user extras (path style, connection limits) flowing as well.
  • The community 2.1.0 restores reading manifests whose _FILE field is the legacy nullable-union type (written by paimon versions older than 1.4), so the pre-provisioned paimon1 CI regression baseline and old production tables stay readable; the internal 2.1 BETA build rejected them with expected RECORD but found UNION.
  • The aggregate-merge overflow behavior matches 1.4 again in the community release (silent wrap, e.g. a TINYINT sum of 127 and 1 reads back as -128); an intermediate fail-loud behavior only existed in the internal BETA build — the branch history contains the suite adaptation for it (1bac394) followed by its revert (1871ef6), a net zero for reviewers.
  • One upstream 2.1.0 behavior change the tests now pin: the persisted timestamp-to-date conversion for pre-epoch values takes the preceding calendar date (floor, matching Arrow) instead of truncating toward zero — a TIMESTAMP(3) of -1ms evolved to DATE reads back as day -1 (UT expectation updated).

2. PK-vector (ANN) search

  • New rewrite rule PushDownVectorTopNIntoPaimonScan (registered in Rewriter) matches a vector distascan and pushes the query vector, the indexed vector column, the metric and the limit down to the scan.A pre-flight check refuses incremental scans (t@incr), so an incremental query can never silently resolve to a snapshot read.
  • AnnTopNInfo carries the pushed-down info from LogicalFileScan to PhysicalFileScan and onto PaimonScanNode (PhysicalPlanTranslator sets it only inside the PAIMON_EXTERNAL_TABLE branch); the PlanNodes.thrift additions are additive fields.
  • BE: paimon_rust_table_reader grows the vector-search path, reading through the rust PK-vector build-op on this path (the rust vector readers do not consume it) — noted in the reader.
  • The pushdown reuses the existing enable_paimon_rust_reader and enable_file_scanner_v2 switches; with either off, the query brute-forces through the normal scan.

3. Unify the rust eligibility gates

  • PaimonRustEligibility evaluates the 12 table-level/storage/BE checks once and is shared by the batch and vector paths. The vector gate is now computed independently of the batch gate (it previously inherited fallbackRead / scan.mode restrictions from the batch path) and both branches share a common gate chain with a batch-only split-suitability suffix.
  • Compaction-merge splits that are raw-convertible skip the merge-reader gates.
  • JNI fallback reasons are logged so a silent downgrade to the JNI reader is diagnosable.

Tests

  • FE UT: new PushDownVectorTopNIntoPaimonScanTest (positive / incremental-refused / gate-negative coverage), PaimonScanNodeTest gate coverage, planner PaimonPredicateConverter tests, PaimonUtilTest; 485 paimon-related FE UTs pass against the 2.1.0 stack.
  • New end-to-end regression test_paimon_pk_vector_ann: the suite builds its own fixture through the Spark ThriftServer — an ivf-rq PK-vector index with nlist = nprobe = 4 on 512 deterministic 16-dim vectors, i.e. exhaustive
    recall, so ANN must equal brute force by construction
    . It compares against the brute-force answer comsabled, asserts the ANN leg really executed on the paimon-rust vector reader (profile check against asilent brute-force pass), and covers both l2 and inner_product metrics, residual filters and non-index projections.
  • Fixed two regression fixtures broken by the 2.1 Spark stack: the creation-time-millis helper was shifted by the session UTC offset (every scan.creation-time-millis leg silently resolved to the latest snapshot; Doris-side resolution verified correct by hand-reproducing the queries), and assertSparkDorisResultEquals now decodes the 0x-hex tokens Doris renders for VARBINARY nested in arrays/maps/structs so identical nested binary values compare equal across engines.
  • Adapted two write suites to observable 2.1 changes (oversized-row rejection message gained a qualifier; unset table-property defaults are no longer materialized by SHOW TBLPROPERTIES) — the underlying behaviors are unchanged.
  • Swept the locally runnable external_table_p0/paimon regression suites against the 2.1.0 stack: read-path 34/37 and write-path 26/32 green (the rest need a Spark 4.0 ThriftServer for VARIANT DDL or pre-provisioned fixtures that exist only in CI), plus the HDFS-based suites against the original pre-provisioned paimon1 OSS baseline — no Doris-side regressions.

Release note

The Paimon catalog now runs on the community Paimon 2.1.0 release. Queries of the form ORDER BY l2_distance_approximate|inner_product_approximate(<vector col>, <query vector>) LIMIT n over paimon tables with a PK-vector index are
pushed down into the paimon-rust vector reader (requires enable_paimon_rust_reader and `enable_file_sy paimon versions older than 1.4 (legacy union manifests) remain readable.

Check List (For Author)

  • Test
    • Unit test
    • Regression test
    • Manual test (add the script if necessary)
    • No need to test
  • Behavior changed: yes (the new switch-gated ANN pushdown; paimon-side: pre-epoch timestamp-to-dat
  • Does this need documentation?

Check List (For Reviewer who merge this PR)

  • Does the PR have a corresponding release note?
  • Is the release note filled in correctly?
  • Are the test cases added or updated correctly?
  • Is the document updated (if necessary)?
  • Does this PR need to be picked to other branches? (branch-4.1 only: yes)

dengfangyuan and others added 16 commits October 13, 2026 10:33
…ff the removed privilege package

Paimon 2.1 drops org.apache.paimon.privilege entirely (the only remaining
DelegatedFileStoreTable subclass is FallbackReadFileStoreTable), so:

- PaimonReaderOptions: reject unknown non-fallback planning delegates
  instead of peeling PrivilegedFileStoreTable; FallbackRead pairs keep
  being normalized branch-independently. Delegate traversal in the
  effective-table validation is unchanged.
- Tests: replace PrivilegedFileStoreTable.wrap fixtures with a minimal
  test-only PlainDelegateFileStoreTable (fe-core and paimon-connector),
  keeping the delegate-peeling/rejection coverage.
- PaimonJniScannerTest: the 2.1 SystemTableLoader consults
  coreOptions().queryAuthEnabled() for physical metadata tables, so the
  serializable-table proxy fixture now serves coreOptions().

Paimon 2.1 also reads variant columns zero-copy via
ArrowBuf.nioBuffer() -> MemoryUtil.directBuffer, which reflects on the
private java.nio.DirectByteBuffer(long, int) constructor and therefore
needs --add-opens=java.base/java.nio on JDK 17; add the opens to the
paimon-connector surefire argLine, mirroring fe-core.

Also bumps doris.hive.catalog.shade.version to 3.1.4-SNAPSHOT.

Verified: ./build.sh --fe green; fe-core *Paimon* tests green via
run-fe-ut.sh; paimon-connector 98/98 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Conflicts:
	fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonExternalMetaCacheTest.java
Ports the internal PK-vector search (ann.patch, bleem ce52f81) onto the
community paimon 2.1 java client and the paimon-rust baac87c eager Top-K C
API, and wires it through the FileScannerV2 rust table reader.

FE
- New PushDownVectorTopNIntoPaimonScan rewrite: TopN[dis] -> Project
  [dis := dist_fn(vec, q)] -> [Filter] -> PaimonScan becomes an index-only
  ANN scan producing the reader-generated __paimon_search_score_to_dis
  column; every dist_fn occurrence is replaced by that slot. Gated on
  enable_paimon_rust_reader AND enable_file_scanner_v2 (the V1 scanner
  rejects PAIMON_RUST). AnnTopNInfo carries query vector / column / folded
  limit / TVectorMetric through the scan plumbing.
- PaimonScanNode plans one BucketVectorSearchSplit per (partition, bucket)
  via newBatchVectorSearchBuilder and serializes it with Paimon's
  DataOutputSerializer (the community client no longer ships
  BucketVectorSearchSplitSerializer). The logical search request reuses the
  community TExternalSearchRequest (field 39 of TFileScanRangeParams);
  Paimon-only tuning travels in the new TPaimonVectorSearchOptions (field
  5), filtered to field-/algorithm-scoped keys because
  primaryKeyVectorIndexOptions seeds itself with the whole table property
  bag (thrift-fatal nulls, credentials, redundancy).
- PaimonUtil maps Paimon VECTOR to ARRAY<FLOAT> so the distance functions
  type-check; the JNI connector reads VECTOR columns via getVector.

BE
- PaimonRustTableReader gains a vector mode: the scan range carries a
  TPaimonVectorPayload (TPaimonFileDesc field 20) and the reader runs the
  eager Top-K chain paimon_bucket_vector_search_split_deserialize ->
  new_scan -> new_read -> plan_from_bucket_splits -> vector_read_read,
  then converts paimon-rust's __paimon_search_score into the metric-native
  distance in place while filling the block (L2: sqrt(1/score - 1) with a
  catastrophic-cancellation clamp and negative sentinels for corrupt
  scores; inner_product: identity).

Deliberately not ported from the internal patch: Alluxio path rewriting
(no community equivalent), the parquet nested lazy-read page-skip
optimization, and the internal-only shade/pom version bumps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 06e99cb)

Port notes (rebased onto the merged PR apache#66227, which grew further after the ann branch forked at 8272fae):
- PlanNodes.thrift: TFileScanRange field 39 is taken upstream by
  parquet_timestamp_semantics_version (40 by contains_native_parquet), so
  external_search_request is renumbered to field 41 on this branch.
- PaimonScanNode: keep the upstream gate chain (restTokenTable, externalFileSplit,
  deduplicateIgnoreDelete, rustUnsupportedMergeOption, scan.mode default check,
  all-backend isPaimonRustReaderSupported, PaimonRustReaderCapabilities). A
  vector split bypasses only the per-split PaimonRustReaderCapabilities check:
  its whitelist describes the batch reader (single-file PK buckets, zero delete
  rows), while a vector bucket legitimately spans multiple files and applies
  deletion vectors inside the search itself; all table/session-level gates still
  apply and a vector split that fails them throws loudly as before.
- paimon_rust_table_reader_test.cpp: keep both the upstream
  AdaptiveBatchSizeBoundsWideStringReads fixture test and the ported vector /
  score-transform tests.

Conflicts:
	be/test/format_v2/table/paimon_rust_table_reader_test.cpp
	fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java
	fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonUtilTest.java
	gensrc/thrift/PlanNodes.thrift
Fix clang-format 16 compliance for the files touched by the PK-vector
(ANN) search port: include ordering and comment spacing in
paimon_rust_table_reader.{h,cpp}, and continuation alignment in
paimon_rust_table_reader_test.cpp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8f62540)
…package port

34731eb ported FE off org.apache.paimon.privilege but missed
PaimonExternalTable and PaimonRowCountTest, both introduced by the
upstream row-count backport 094b27b; and that backport was written
against an older Paimon API:

- PaimonExternalTable: drop the dead instanceof PrivilegedFileStoreTable
  branch in fetchRowCount (the wrapper no longer exists in 2.1;
  authorization stays on the queryAuthEnabled path)
- PaimonRowCountTest: SchemaManager is an interface in 2.1, use
  FileSystemSchemaManager; port the 20-arg Snapshot constructor to the
  22-arg 2.1 signature; drop testSelectOnlyPrivilegeWrapper along with
  the removed wrapper (coverage kept by
  testCatalogQueryAuthorizationWithoutPlanning)
- PaimonScanNodeTest: add the missing closing brace of
  testRustReaderSelectionAcceptsAnonymousAndDefaultProviderModes, which
  broke checkstyle parsing of the file

Verified: ./build.sh --fe green; PaimonRowCountTest, PaimonScanNodeTest
and PaimonExternalTableTest 80/80 green via run-fe-ut.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit ffa45cd)
Fix include ordering in paimon_rust_table_reader.cpp to keep
clang-format 16 compliance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit ab8f57f)
…port

The PaimonExternalMetaCacheTest conflict resolution of the branch-4.1
privilege-package port (d96ce64) kept the two org.apache.paimon.privilege
imports that Paimon 2.1 no longer ships. Nothing in the file references
them anymore, so checkstyle fails the fe-core build (and compilation would
fail on the missing classes). Same cleanup as ffa45cd did for the other
missed files on the ann branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ests

SchemaManager became an interface in Paimon 2.1 (same port the ann
branch's ffa45cd did for PaimonRowCountTest); the two fixture builders
that still call new SchemaManager(fileIO, path) fail fe-core test
compilation. FileSystemSchemaManager keeps the same constructor and
createTable behavior.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the batch gate

A PK-vector search split is never read as a DataSplit: FE ships it serialized
inside vector_payload (paimon_split is empty on the wire) and BE executes
paimon-rust's eager Top-K chain, which applies deletion vectors and the merge
inside the search itself. The batch-reader gate nevertheless judged it by
DataSplit shape, so the standard PK-vector table layout (partial-update merge
engine with deletion vectors and uncompacted level-0 files -- the shape the
batch reader rightly rejects via the splitDvNotMaterialized gate) tripped the
vector branch's broken-invariant RuntimeException ("PK-vector search split
requires the paimon-rust reader...") and no ANN query could run on it.

Split the gate: the vector branch keeps every gate that constrains the search
itself (both session switches, !fallbackRead for the schema-json open,
provider-mode translation and auth shapes, the merge-option matrix and
merge-on-read shape, orc-ltz / projected-variant bounds on the returned user
columns, storage scheme and credential shape, scan-mode default, all-backend
rust capability) and drops only the DataSplit-shape gates that judge a split
the vector path never opens (nativeSplit, splitDvNotMaterialized,
externalFileSplit, the PaimonRustReaderCapabilities whitelist). The batch
branch is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…plits

The PK merge-engine gate (primary-key partial-update / aggregation tables
route to JNI) and the PK multi-file gate (dataFiles().size() > 1 routes to
JNI) both exist for the pinned merge reader's unbounded losing-batch
retention, which no delete count or batch size can bound. A rawConvertible
split whose files are all compacted (level != 0) never reaches that merge
reader: paimon-rust's read_pk dispatch (pk_split_needs_merge /
is_fully_materialized_pk_dv) sends it to DataFileReader's raw path, which
streams one file at a time, each bounded by read.batch-size, with the
split's deletion-vector factory masking stale rows per file. Both gates now
exempt such a split:

- the merge-engine gate moves from the table-level tableCompatible to a
  per-split check (its rationale only applies to splits that must merge),
- the multi-file gate gains the same readsRawWithoutMerge exemption.

Per-file retract hygiene (deleteRowCount known zero) and the schema-history
/ evolution whitelist are unchanged: they bound the raw path too. Without
this, a compacted PK bucket with multiple files -- the standard PK-vector
table layout, and any compacted partial-update table -- could never use the
rust reader for batch reads at all.

Verified: PaimonScanNodeTest 96/96 (new
testRustMultiFileRawConvertibleSplitStaysOnRust covers raw+compacted
multi-file -> RUST, non-raw -> JNI, and level-0 + DV -> JNI).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ack reasons

The PK-vector search gate and the batch reader gate judged the same
splits with two parallel condition lists. They now share one gate chain
(session switches, FallbackRead shape, incremental scans, provider-mode
translation, query-auth / REST-token shapes, merge-option matrix,
merge-on-read, orc-ltz / projected-variant bounds, storage scheme and
credentials, scan-mode default, BE capability negotiation, FileStoreTable
schema json); only the gates that judge the DataSplit the batch reader
opens (native shape, unmaterialized PU/AGG deletion vectors, external
files, the PaimonRustReaderCapabilities whitelist) are skipped for a
PK-vector split, which is never read as a DataSplit -- FE ships it
serialized inside vector_payload and BE executes paimon-rust's eager
Top-K chain, which applies deletion vectors and the merge inside the
search itself. Without this split, the standard PK-vector table layout
(partial-update merge engine with deletion vectors and uncompacted
level-0 files) tripped the vector branch's broken-invariant
RuntimeException and no ANN query could run.

The scan-mode check stays lazy (schema() may be null on fallback-shaped
tables that other gates already rejected) and is now null-safe.

Whenever the rust reader is enabled and a logical split still routes to
JNI, the FE log now states the exact gate combination that rejected it,
with the table location and the split's bucket and file count, one INFO
line per distinct reason combination per scan node:
  paimon-rust reader enabled but a logical paimon split routed to JNI:
  table=... split=bucket=0 files=30 reasons=[split_dv_not_materialized]
18 reason tags cover every gate, with capability_whitelist as the
catch-all when only the per-split whitelist rejected the split.

Verified: run-fe-ut.sh --run PaimonScanNodeTest,PushDownVectorTopNIntoPaimonScanTest
115/115 green (1 known core-count skip); on the 9630 cluster the 2048-dim
IVF vector query, the relaxed rust batch read and the fallback log line
were each exercised end to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The PushDownVectorTopNIntoPaimonScan rewrite used to trigger on the two
session flags alone. But the rewrite is a one-way gate: a PK-vector split
has no JNI fallback (the JNI reader ignores the vector payload and would
return a non-ANN scan with an empty distance column), so once rewritten
the scan node can only fail loudly on a gate failure. A table whose rust
gate would fail -- credential provider shape, REST-token IO, query-auth,
unsupported merge options, deletion-vector merge-on-read, scan mode,
unverified storage scheme or HDFS credentials, no rust-capable backend --
was turned from a slow-but-correct query into a hard error.

Extract the table-, storage- and backend-level gates into
PaimonRustEligibility (including the credential-provider translation
predicate, providerModeTranslatable), computed once and shared by every
site that must agree: the rewrite's pre-flight (via
PaimonVectorSearch.vectorScanEligible) and PaimonScanNode's per-split
gate, where the batch and PK-vector branches had previously each
evaluated (and commented) the same predicate set separately. The
gate-failing table is simply not rewritten, and the query still runs,
computing the distance in Doris.

An incremental scan (t@incr / BETWEEN) is likewise not rewritten: the
VectorScan builder has no incremental mode, so a vector payload would
silently return snapshot rows for a query that asked for changes.
Keeping it un-rewritten runs the ordinary incremental scan with the
distance computed in Doris. The scan node keeps !fallbackRead and
!isIncremental on the vector branch as the authoritative backstops, and
retains the split- and projection-dependent bounds (native DataSplit
shape, unmaterialized deletion vectors, external-path files, capability
whitelist, ORC LTZ schema, projected variant) the shared predicate
cannot see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The batch reader pushes the scanner's adaptive batch size as
read.batch-size into the rust read builder. The PK-vector path cannot do
the same: paimon-rust's PK vector readers construct their DataFileReaders
without with_batch_size, so a read.batch-size option — from the schema,
t@options or the vector search options — is silently ignored there and
the path batches at the parquet-rust default. Document the known
difference at the vector builder's construction site: the vector stages
are single-column streams feeding bounded Top-K heaps and the output is
capped by top_k, so wiring it needs a paimon-rust change, not a BE one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…2.1 SDK

The Paimon 2.1 migration broke every s3-warehouse paimon catalog: the
SDK's S3FileIO now reads its credentials exclusively from
CatalogContext.options() (S3FileIO#configure ->
loadHadoopConfigFromContext copies the s3./s3a./fs.s3a.-prefixed option
entries and never consults the Hadoop configuration), while Doris
assembles the credentials exactly there — in the conf built from
StorageProperties and handed to CatalogContext.create(options, conf).
The 1.4 FileIO read that conf, so the migration silently dropped the
credential channel: catalog init fails on the first file operation with
"SdkClientException: Unable to load credentials from system settings",
and all four test_paimon_rust_reader_* regression suites fail at their
first query (a catalog created on the 1.4 FE has the same failure after
the upgrade).

Mirror the fs.s3a.* entries Doris built into the catalog options at the
three initializeCatalog sites (filesystem, HMS, JDBC metastores), so
the 2.1 SDK sees the same credentials the Hadoop-compliant readers do.
User extras (path style, connection limits) flow along; entries
outside the prefix are not the SDK's concern. Verified locally: all
four rust suites pass again and the new PK-vector ANN suite can read
its minio fixture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paimon 2.1 made aggregate merges fail loud on numeric overflow
("byte overflow: 127 + 1 = 128") where 1.4 wrapped silently, so two
compatibility cases that assert the wrapped value now fail on both
readers: the TINYINT sum/product integer cases and the TINYINT
default-aggregate-sum partial-update case. Assert the overflow failure
on both readers instead, and add non-overflow siblings (sum 10+20,
product 10*12 — in range for every case type including the TINYINT
product) so the merge value comparisons keep their coverage. The
decimal and max cases never overflow and stay unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The PK-vector (ANN) feature — the PushDownVectorTopNIntoPaimonScan
rewrite, the shared rust eligibility gate and the BE paimon-rust vector
reader — had no regression coverage: the ANN groovy suites all target
internal-table faiss indexes, and the paimon suites have no vector
tables at all.

Add test_paimon_pk_vector_ann.groovy, run against a minio warehouse the
way the rust reader suites are. The suite builds its own fixture
through the Spark ThriftServer's paimon catalog (no Doris DDL can: the
FE type mapping has no VECTOR column in either direction, and the
ivf-rq index build needs the vindex factory jars — paimon-vector +
paimon-vector-index-java — that only the Spark server carries; see
regression.md for the 2.1 jar list): 512 deterministic 16-dim vectors
derived from the row id (unique: mod 521, a prime above the row
count), nlist = nprobe = 4 — exhaustive recall, so ANN must equal brute
force by construction.

Every ANN leg is compared against the exact brute-force answer the
same SQL computes with the rust reader disabled: l2 top-5 and top-1
(the probe row itself, distance 0), a non-index-only projection with the
tag asserted outright, a residual filter, the FileScannerV2-off
no-rewrite leg, and inner_product desc. The ANN leg is also asserted to
have actually executed on the paimon-rust vector reader via its
profile, so the comparisons cannot pass on a silent brute-force scan.
Near-tied neighbors can swap between the legs (f32 in the reader vs
double in Doris), so the comparison checks top-1 exactly, the top-3 in
order and the top-N as a set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both failures were verified by hand-reproducing the queries against the
leftover fixture data: Doris resolved everything correctly and the suites'
own Spark-side assumptions stopped holding after the ThriftServer switch.

- test_paimon_schema_time_travel_matrix: the snapshotCommitTimeMillis
  helper converted the paimon NTZ commit time through Spark's
  convert_timezone + unix_millis, which the 2.1 ThriftServer shifts by
  the session UTC offset - the value landed 8h in the future and every
  scan.creation-time-millis leg silently resolved to the latest snapshot
  ("Unknown column 'old_name'"). Read the commit time through Doris
  instead: unix_timestamp keeps the fractional seconds, so *1000 is the
  exact commit instant and matches the creation-time resolution ("earliest
  snapshot committed at or after the value") on the same clock.

- test_paimon_spark_doris_consistency_demo: Doris renders VARBINARY nested
  in arrays/maps/structs as 0x-hex tokens while Spark's JDBC returns the
  raw bytes, so identical nested binary values compared unequal across
  engines. assertSparkDorisResultEquals now decodes the 0x tokens on both
  sides after normalization - the substitution is a no-op on the raw
  rendering, so equal stays equal for the iceberg suites sharing the
  helper, and scalar binary columns (byte[] path) are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pzhdfy
pzhdfy requested a review from yiguolei as a code owner October 13, 2026 02:35
@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?

Both failures pinned a behavior that is still correct under the 2.1 SDK;
only the observable text/metadata changed:

- test_paimon_write_transaction FT-019: a row larger than the whole write
  buffer is still rejected inside the paimon writer and nothing is
  published, but the 2.1 SDK exception message gained a qualifier
  ("maximum size of a Paimon write sort buffer"). Match on the prefix
  common to both SDKs so the assertion keeps its meaning.

- test_paimon_orc_ltz_encoding: the 1.4 connector materialized unset
  property defaults into the stored table options, so the modern table
  read back orc.timestamp-ltz.legacy.type=false; the 2.1 connector no
  longer persists unset defaults and the same SHOW TBLPROPERTIES fails
  with "does not have property" - which still means the modern default.
  Accept the absent-property reply as the modern default while keeping
  the assertion strong: a persisted "true" or any other failure still
  fails, and the explicitly-set legacy table round trip is unchanged.

Verified: both suites pass locally against the 2.1 stack; the remaining
write-suite failures are the variant suites, which need a Spark 4.0
ThriftServer for VARIANT DDL and are out of scope for a local 3.5 run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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

FE-only review of 2dcb9c0. The inline comments cover candidate filtering, vector cast semantics, relation read options, and projection eligibility. These are based on source-path analysis; no build or tests were run for this review.

@Gabriel39

Copy link
Copy Markdown
Contributor

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

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

dengfangyuan and others added 5 commits October 13, 2026 15:40
The community paimon 2.1.0 keeps the 1.4 semantics for aggregate-merge
overflow: the merge wraps silently (a TINYINT sum of 127 and 1 reads back
as -128 on both readers). The fail-loud "byte overflow" behavior the
adaptation targeted only existed in the internal
2.1-community-release-2.1-BETA-SNAPSHOT build, so this reverts that
suite adaptation (1bac394) and puts the wrapped-value comparisons back.

Verified against the 2.1.0 stack: the ANN e2e suite passes and the
micro-reproduced TINYINT sum merge reads back -128.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the internal 2.1-community-release-2.1-BETA-SNAPSHOT pin with the
community 2.1.0 release.

Verified against the 2.1.0 stack (FE+BE+Spark connector all on 2.1.0):

- The community release restores reading manifests whose _FILE field is
  the legacy nullable union type, so the pre-provisioned paimon1 CI
  baseline (written by a writer older than 1.4) is readable again; the
  BETA build rejected it with "expected RECORD but found UNION".
- The aggregate-merge overflow behavior matches 1.4 again (silent wrap,
  a TINYINT sum of 127 and 1 reads back as -128); the fail-loud "byte
  overflow" behavior only existed in the internal BETA, and the suite
  adaptation for it is reverted in the previous commit.
- The S3FileIO still reads its credentials only from
  CatalogContext.options(), so the fs.s3a.* mirroring stays required.
- End-to-end ANN regression (spark-built ivf-rq fixture) passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or paimon 2.1.0

The community paimon 2.1.0 changes the persisted timestamp-to-date
conversion for pre-epoch values to take the preceding calendar date
(floor, matching Arrow): a TIMESTAMP(3) of -1ms evolved to DATE reads
back as day -1. Paimon 1.4.2 and the internal 2.1-BETA build truncated
toward zero and returned day 0, which the original expectation pinned.
The floor result is the calendar-correct value (1969-12-31), so the
expectation moves to -1.

Verified: run-fe-ut.sh --run PaimonRustReaderCompatibilityTest passes
22/22 against the 2.1.0 stack; this was the only failure among the 485
paimon-related FE UTs on 2.1.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…review)

Addresses the P1/P2 review findings on PushDownVectorTopNIntoPaimonScan:

P1 - Keep residual filtering before the bounded vector top-N. The BE
paimon-rust predicate converter skips conjuncts it cannot represent
(and the synthetic distance column is outside its field registry), and
the engine re-applies those residuals on the returned block -- after the
top-K rows were already selected. With candidate distances 0, 2 and 3,
a `WHERE dis > 1 ORDER BY dis LIMIT 1` fetched only the distance-0 row
and returned nothing. The rewrite now fires with a Filter only when
every conjunct provably rides into the vector search: a comparison / IN /
null-check on a table column whose Doris type is in the converter's
supported set (mirrors PaimonRustPredicateConverter._is_supported_slot_type).
Anything else declines the rewrite and the query computes the distance
in Doris, which is always correct. Verified end to end: the differential
leg caught the empty result on the unfixed build and passes on the
fixed one.

P1 - Do not strip value-changing casts from the indexed vector. A legal
cast chain can change vector values (an ARRAY<FLOAT> -> ARRAY<INT> ->
ARRAY<FLOAT> round trip loses precision), so searching the original
vectors while the query means the casted ones changes the ranking. The
rewrite now accepts a direct indexed column slot only.

P1 - Preserve the relation's file-creation-time restriction on the
vector path. The ordinary path drops manifest entries older than the
bound, but the PK-vector search plans its own splits and never consults
the bound, so old files' nearest neighbors could leak into the result. A
scan carrying scan.file-creation-time-millis now declines the rewrite
(snapshot / tag / version selectors are resolved into the table before
the vector search and stay eligible).

P2 - Check the projection capabilities before committing the rewrite. A
PK-vector split has no JNI fallback, so a query whose projected columns
would set the projectedVariant bound (nested Variant) or an ORC table
with TIMESTAMP_LTZ in its schema -- both scan-node bounds with no
vector-path fallback -- would turn a supported query into an error.
Both are now pre-flighted and decline instead, keeping the query on an
ordinary supported path.

Tests: PushDownVectorTopNIntoPaimonScanTest grows from 20 to 27 cases
(distance-predicate / unsupported-type / expression filters decline, a
simple column filter still rides, file-creation-time declines while a
snapshot selector does not, lossy cast declines, Variant projection
declines; the two distance-predicate positives flip to the new sound
semantics). The ANN regression suite adds the requested differential
legs: a distance lower bound, an unconvertible ARRAY<FLOAT> predicate
and a file-creation-time bound, each compared against the rust-disabled
brute force.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the 3.1.4-SNAPSHOT dependency with its release build. The 4.1.0
artifact is byte-identical to the snapshot it replaces (same class list
and file sizes, same shaded paimon 2.1.0 / hive 3.1.3, identical pom
dependency declarations), so this only pins the FE build to a stable
release version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@morningman

Copy link
Copy Markdown
Contributor

run buildall

@morningman

Copy link
Copy Markdown
Contributor

/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 of PR #68988 at 537b731f92999ca4821ba8645b946e40a23d2888: changes requested. Ten new verified findings cover nested projection correctness, partitioned ANN planning, shared Thrift IDs, BE test compilation, score ordering, NULL query coordinates, BE capability negotiation, first-row filter eligibility, ordinary physical-column preservation, and lossless binary test comparison.

Two existing P1 threads remain applicable and are carried without duplicate inline comments:

  • 4254619627 — filtering before bounded TopK: the new whitelist accepts tag IN (1,2,NULL) because every option is a Literal, but the BE converter rejects the entire IN predicate on its NULL literal. With nearest tag=0 and the next tag=2 in one bucket, LIMIT 1 retrieves tag=0 and the Doris residual removes it, returning no row. Typed NULL survives coercion, constant folding, IN simplification, and translation. The original distance-predicate example now declines rewriting, but the same truncation defect remains.
  • 4254619638 — resolved file-creation restriction: t@options('scan.creation-time-millis'='<future>') is not excluded by the new raw scan.file-creation-time-millis check. Its resolver falls back to the same pinned internal file-creation bound when no later snapshot exists. Ordinary planning excludes all old files; vector planning ignores that bound and can return their neighbors. The explicit file-creation option is guarded, but this equivalent resolved read state remains affected.

Existing 4254619634 (value-changing vector casts) is addressed by requiring a direct indexed slot. Existing 4254619644 (projected VARIANT / ORC LTZ) has explicit preflight guards; the new ordinary-STRUCT finding is a separate layout failure. No additional user-provided focus was supplied.

Applicable critical checkpoints:

  1. Goal and proof: the nominal SDK 2.1 / L2 and inner-product ANN flow is implemented, but the verified failures prevent ordinary-scan parity. Added unit and differential regression tests were inspected; none were executed.
  2. Scope and abstraction: reviewed every one of the 53 authoritative changed paths, including dependency/property changes and test utilities. AnnTopNInfo groups transport state and logical/physical copies retain it; the vector branch incorrectly bypasses some compatibility checks.
  3. Concurrency and locks: no new Doris threads or lock order introduced. Paimon range construction follows the sequential nonbatch path; a suspected logging-HashSet race was dismissed from actual callers. No heavy new operation was found inside a new lock.
  4. Lifecycle and initialization: traced relation/schema binding, pruning, rewrite, translator, processed table, bucket payload, Rust eager execution, Arrow materialization, residual evaluation, and coordinator TopN. Pinned C API setters copy inputs and read/plan ownership allows local RAII destruction. No new cross-translation-unit static initialization dependency or ownership cycle was established.
  5. Configuration: existing Rust-reader and FileScannerV2 switches are read per query. No new effective session/BE configuration is introduced; the SessionVariable change is whitespace.
  6. Compatibility: compared touched Thrift definitions with live branch-3.1, branch-4.0, branch-4.1, branch-4.2, and master. Two new field IDs collide with master. The generic existing BE capability does not distinguish the new vector protocol. Pinned Rust API availability and Java VECTOR access/type preservation were checked from source.
  7. Parallel paths and conditions: compared native, JNI, ordinary Rust, vector, system/delegate, raw-convertible, partition, nested, incremental and read-state paths. Incremental and direct lossy-cast cases decline. Remaining predicate/read-state restrictions are documented above and in the new inlines.
  8. Functional tests: new ANN regression uses a small unpartitioned DV-enabled fixture. It does not cover partitioned vector ranges, pruned STRUCT materialization, NULL query coordinates, non-DV first-row filters, mixed-version BEs, or cross-bucket score overflow.
  9. Negative/unit coverage: inspected score, nullable-column, payload, rewrite and routing tests. The newly incorrect include prevents the BE test target from compiling. No build or test was attempted under this static-only contract.
  10. Expected results: no golden .out files changed. The pre-epoch timestamp-to-date expectation follows the SDK's floor behavior. The new UTF-8 comparator can conceal distinct binary results and needs a lossless comparison.
  11. Observability: EXPLAIN exposes vector column, metric, vector and limit; FE logs distinct fallback reasons. Existing reader/open/read/Arrow timers cover the new lifetime; search/refine/materialization costs accumulate during reader opening.
  12. Transactions and persistence: production changes are query-side; no new Doris EditLog, commit, replay or master-failover state is introduced. External snapshot/file visibility and deletion-vector constraints were checked; the surviving read-state thread and first-row filter finding identify concrete gaps.
  13. Data writes and crashes: no production data-write atomicity change. JNI/native fallback and C error/cleanup paths were inspected statically. This review does not claim fault-injection or crash validation of the upgraded SDK.
  14. FE/BE propagation: vector/column/limit/metric travel through logical and physical copies, translator and little-endian FLOAT32 request; bucket payloads travel per split. Partition metadata, nested shape and protocol negotiation have the reported gaps.
  15. Performance and memory: projection/filter setup is outside per-row decode. Compacted raw split dispatch and C handle lifetime were inspected. Vector output is bounded by requested TopK, not adaptive scanner batch size; large TopK/wide payload memory, cancellation and full Hive-shade runtime interoperability remain unmeasured.
  16. Core invariants and other issues: query correctness/error preservation fail in the identified cases. No new internal rowset-version, MoW delete-bitmap or uncommitted-transaction path is changed. Arrow distance conversion checks concrete nullable columns and restores COW ownership; no additional memory-lifetime defect was established.

Coverage limitation: this is static source review, including pinned external source and live protocol references. No compilation, tests, runtime/external-writer interoperability, or performance measurements were performed. All changed paths have been read; empirical compatibility and memory behavior are not claimed.

Review converged after 3 round(s).

Existing P0/P1 findings confirmed for this head: #68988 (comment), #68988 (comment)

Comment thread gensrc/thrift/PlanNodes.thrift Outdated
Comment thread be/test/format_v2/table/paimon_rust_table_reader_test.cpp Outdated
Comment thread be/src/format_v2/table/paimon_rust_table_reader.cpp Outdated
Comment thread be/src/format_v2/table/paimon_rust_table_reader.cpp Outdated
@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

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

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 25.42% (151/594) 🎉
Increment coverage report
Complete coverage report

…NN path

Static review of this PR surfaced twelve soundness and compatibility
gaps on the PK-vector (ANN) search path; every one was verified against
the pinned sources before the fix:

- Wire protocol: TPaimonFileDesc.vector_payload moves from field 20 to
  21 and TFileScanRangeParams.external_search_request from 41 to 42 --
  both IDs are already reserved on master (original_file_path,
  fluss_properties), and reusing them would make a mixed-version cluster
  drop the payload instead of decoding it. BE/FE must upgrade together.
- Vector capability negotiation: a new heartbeat bit
  supports_paimon_rust_vector_reader (TBackendInfo field 12, free on all
  branches) gates the one-way rewrite separately from the batch rust
  bit, so a rolling upgrade keeps ordinary rust reads working and only
  declines the vector rewrite against older BEs.
- Partitioned vector splits now carry the derived partition values
  (PaimonSplit.setPaimonPartitionValues), which setPaimonParams demands
  for any table with partition keys.
- The rewrite pre-flight now also declines: a query vector containing a
  NULL element (getDouble() would silently search a zero coordinate
  while the un-rewritten function raises); scan.creation-time-millis
  (resolves into the same internal file-creation bound the vector search
  never consults); a data predicate on a table without deletion vectors
  or with merge-on-read (paimon-rust rejects it AFTER the one-way
  rewrite); a table column named __paimon_search_score_to_dis (collides
  with the synthetic distance slot); and an expression over a complex
  column (nested pruning reduces the scan tuple while the vector reader
  projects only root names -- the struct SerDe then binds children by
  ordinal). The shape whitelist additionally rejects NullLiteral
  operands: the BE converter rejects a null literal and with it the
  whole predicate, leaving it in the post-fetch residual. The scan node
  keeps the nested-projection bound as a loud backstop for vector
  splits and logs the vector-capability reason.
- BE: _convert_score_to_distance maps zero and out-of-range scores to
  +inf instead of negative sentinels -- a zero score is reachable from a
  genuine huge distance (the search-mode=full exact fallback computes
  squared L2 in f32), and -2 sorted ahead of every finite distance,
  inverting the coordinator's TopN. _build_read_columns skips the
  synthetic distance name only in vector mode, so an ordinary scan of a
  real column with that name keeps reading it. The BE test include path
  is fixed (the file never compiled).
- Regression framework: the 0x-token decode pass only substitutes
  UTF-8-round-trippable bytes and keeps the lossless hex token otherwise
  -- replacement-character decoding made 0x80 and 0x81 compare equal.

Verified: FE UT 36/36 (rewrite) + 96/0 (scan node) + 22/22 (rust
compat) + 4/4 (capability); BE UT 30/30; the full ANN regression suite
including the four new differential legs (no-deletion-vector first-row
table, partitioned table, NULL query vector, IN with a NULL option).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Gabriel39

Copy link
Copy Markdown
Contributor

/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 of all 59 authoritative changed paths at 1f40520. Request changes for six new findings: branch index/metric identity, historical scalar value loss, eager materialization memory, generic external column mapping, stored-NULL validation, and nonmovable projection errors.

Existing threads were revalidated and not reposted. Existing P1 4311684734 still applies when an outer field projection leaves an already-pruned bare slot inside the ANN Project: preflight accepts it and the final scan guard throws. Existing P1 4254619638 still applies to its explicitly noted pinned-empty generation bypass: a first commit after binding can be read by vector planning, while ordinary planning returns the fenced empty state. Existing P1 4254619627 still applies with a supported raised expr_children_limit and an IN list exceeding 65535 children: FE accepts it, the Rust predicate converter drops it, and residual filtering occurs after bounded retrieval. The original direct-field, file-time-bound, and ordinary unsupported-filter examples have guards now; these remaining instances preserve the existing blockers. Other earlier P1s (lossy casts, partition metadata, field-ID collisions, missing include and zero-score ordering) are repaired in the inspected head. Known P2 name-collision and type-unaware result-normalization residuals remain covered by their existing threads.

Critical checkpoint conclusions:

  1. Goal and proof: the PK-vector ANN/SDK2.1 flow is implemented, but the findings prevent correctness approval. Positive and negative UT/regression sources were inspected; the new boundary cases lack coverage.
  2. Scope: planner, request, split, reader and SDK changes follow the feature flow; shared eligibility removes duplicated gates. The generic mapping exception and skipped schema guarantees need correction.
  3. Concurrency: query state is per scan; no new lock/order or metadata mutation path found. Heartbeat capability uses existing volatile/replay handling. The empty-snapshot race is identified above.
  4. Lifecycle/static initialization: Rust builders/plans clone inputs; local RAII teardown and Arrow release ownership are sound. AnnTopNInfo clones its array. No new cross-TU static dependency or ownership cycle identified; eager buffers have the reported resource issue.
  5. Configuration: no new user setting; existing Rust/V2 switches are read during rewrite and scan planning.
  6. Compatibility: separate vector capability prevents dispatch to older batch-only BEs. SDK2.1/JNI VECTOR and S3 credential-option changes were traced; historical scalar ANN casts remain unsafe.
  7. Shared definitions: touched Thrift definitions were compared with master and branches 4.2/4.1/4.0/3.1. New field IDs 12/21/42/6 are free; no remaining collision identified. New payload is behind an optional envelope. No proto changes.
  8. Parallel paths: native/JNI/cpp/batch-Rust routes and raw compaction gates were checked. One-way ANN must preserve their schema, error, identity and memory guarantees.
  9. Special conditions: cast, query-NULL, projected VARIANT, creation-time and vector-capability guards are present. Remaining nested/empty/filter edges are recorded against existing threads.
  10. Tests: all changed tests/fixtures reviewed statically. Branch metric divergence, stored NULLs, nonmovable assertions, historical scalar ANN evolution and large-K memory require regression coverage.
  11. Expected results: pre-epoch DATE floor, unset property and error-message adaptations match the inspected SDK behavior. Existing ResultUtils type-awareness concern remains a duplicate fence; no new golden output file was added.
  12. Observability: ANN explain details, Rust lifecycle timers and deduplicated fallback reasons are present. No separate new critical logging defect established.
  13. Persistence/failover: no new external data writes or EditLog record type. Heartbeat persistence follows the existing pattern; read snapshot consistency remains blocked as above.
  14. Writes/crashes: no new transaction/write atomicity path. The eager memory issue can exhaust a BE during a read.
  15. FE-BE flow: vector/column/metric/K and bucket payload survive logical/physical copies and translation; partition values are populated. The branch finding concerns semantic identity despite complete transport.
  16. Performance/memory: FFI ownership and ordinary bounded batches were checked; eager all-K retention/copy lacks the ordinary bounds/accounting.
  17. Other invariants: NULL and assertion errors must remain failures. No additional deadlock, OLAP delete-bitmap or visible-version modification identified.

No additional user focus was supplied. Validation was static only: no builds, tests, source modifications or submodule initialization. Runtime/load and live mixed-version behavior were not exercised.

Review converged after 2 round(s).

Existing P0/P1 findings confirmed for this head: #68988 (comment), #68988 (comment), #68988 (comment)

PaimonExternalTable paimonTable = (PaimonExternalTable) scan.getTable();
CoreOptions coreOptions;
try {
Table basePaimonTable = paimonTable.getPaimonTable(Optional.empty());

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.

[P1] Validate the index against the relation's bound table

getPaimonTable(Optional.empty()) fetches the cached main table, while PaimonSource and vector split planning use this relation's retained branch/snapshot. For example:

TopN(dis ASC LIMIT 1)
  Project(id, dis := l2_distance_approximate(vec, [0.25,0,...]))
    Scan(t@branch(b))

Main can have an L2 index while branch b has an independently configured inner-product index. With branch vectors [1,0,...] and [2,0,...] in one bucket, this preflight accepts main's L2 metric, but Rust reads the branch schema and selects the second vector with score 0.5. BE then applies the requested L2 inverse and publishes distance 1; ordinary L2 selects the first vector at 0.75. Full recall still gives this wrong result. Resolve all index/metric/eligibility checks from the scan's bound table before replacing the expression, and cover differing branch index options.

canUseRust = canUseRust && rustEligibility.vectorEligible()
&& PaimonRustReaderCapabilities.hasFullNestedProjection(desc);
}
if (!vectorSearchSplit && canUseRust) {

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.

[P1] Retain historical scalar compatibility checks for vector reads

canRead also protects historical value conversions, which remain relevant when vector search materializes projected columns. On a PK-vector Parquet table, write ts TIMESTAMP(3) = '2300-01-01 00:00:00.123', then widen ts to TIMESTAMP(9) while retaining the old files/index. SELECT ts, l2_distance_approximate(vec,q) AS d FROM t ORDER BY d LIMIT 1 now bypasses that check. Rust's materialization uses DataFileReader -> reconcile_column -> Arrow's safe millisecond-to-nanosecond cast; the year-2300 multiplication overflows and returns NULL. The ordinary path explicitly rejects this evolution in PaimonRustReaderCapabilities and uses JNI, which preserves the timestamp (also documented by testPersistedTimestampEvolutionIntoNanosecondRange). Check current/historical schema compatibility before committing ANN, or provide equivalent materialization. This is a scalar full-projection case, separate from the existing pruned-STRUCT thread.

}
vector_plan_ptr plan(plan_result.plan);

paimon_result_record_batch_reader rdr_res = paimon_vector_read_read(read.get(), plan.get());

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] Bound the eager materialization before entering Rust

This call finishes materialization before returning the Arrow reader. In the pinned Rust SDK, materialize_positions retains every projected Top-K batch, then reorder_and_strip_position copies those rows into one output batch. Unlike the ordinary path, this path ignores the scanner's adaptive batch size and has no reservation/accounting bridge for those Rust buffers. A one-bucket query with LIMIT 1000000 and a projected 4-KiB payload can therefore retain roughly 4 GiB plus another 4 GiB reorder copy during prepare_split, even with a tiny probe batch and query memory budget. Raw malloc interposition forwards to jemalloc; it does not perform Doris Allocator tracking. Bound and account for this eager work before the call, or use bounded materialization; splitting the returned batch afterward cannot prevent the allocation spike.

// Reader-produced Paimon PK-vector search distance: a synthetic output column
// filled by the BE reader by name, not present in the table's file schema,
// so it has no Parquet/ORC column position to map.
if (PaimonVectorSearch.SEARCH_DISTANCE_COLUMN.equalsIgnoreCase(colName)) {

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] Limit this mapping exception to an actual synthetic slot

This method serves every external catalog, so the name alone also excludes a real Hive column. For a Hive text/CSV table (id INT, __paimon_search_score_to_dis STRING), selecting both columns leaves two required file slots but emits only column_idxs=[0]. With FileScannerV2, DelimitedTextReader::_init_reader then rejects the index-count mismatch; Hive positional ORC/Parquet mapping likewise lacks the second ordinal. Preserve the mapping for physical same-named columns and identify the actual Paimon vector-produced slot. This is a separate generic FE/native-scan instance from the already-discussed ordinary Rust _build_read_columns exclusion.

if (((ArrayLiteral) right).getValue().stream().anyMatch(NullLiteral.class::isInstance)) {
return null;
}
SlotReference vectorSlot = (SlotReference) left;

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] Preserve validation of NULL vectors stored in the table

The RHS check above does not cover a live NULL in the indexed column:

TopN(dis ASC LIMIT 1)
  Project(id, dis := l2_distance_approximate(vec, valid_q))
    Scan(rows: vec=NULL; vec=[1,0,...])

Paimon permits nullable VECTOR columns and NULL stored values. The ordinary FunctionArrayDistance::_validate_no_nulls throws First argument ... cannot be null before calculating distances. The Rust vector reader instead skips outer-NULL vectors, so replacing the function makes this query successfully return the finite row, even with full recall. Both supported metrics have this difference. Preserve the stored-vector validation or decline the rewrite when its error contract cannot be guaranteed, and add an enabled/disabled NULL-data regression. This is distinct from the earlier NULL coordinate in the query literal: here the query vector is valid.

replaceMap.put(orderKeyAlias, distanceSlot);
replaceMap.put(orderKeyExpr, distanceSlot);

Plan plan = scan.withVectorTopN(

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 nonmovable project expressions before truncation

The rewrite accepts a Project containing NoneMovableFunction, such as:

TopN(dis ASC LIMIT 1)
  Project(c := assert_true(id > 0, 'bad id'),
          dis := l2_distance_approximate(vec,q))
    Scan(rows: id=-1/far; id=1/near)

On the ordinary path the Project evaluates before TopN and raises for the far row. withVectorTopN truncates the scan to the near row first, so the assertion never sees id=-1 and the query succeeds. The scalar assertion passes complexColumnsRideAsWholeColumns, and there is no nonmovable-expression guard. Decline this rewrite for project expressions whose evaluation must remain below TopN, and cover the assertion case. This concerns a SELECT expression, separate from the existing WHERE residual-filter thread.

Six new findings, all verified and fixed:

- Branch identity: the pre-flight resolved the cached main table while the
  scan reads the relation's bound branch table, so a branch with a different
  index metric scored and reordered with the wrong metric. All four
  pre-flight sites now resolve the bound table (relation snapshot, or the
  options-applied table for t@options); time-travel relations benefit too.
- Historical scalar evolution: the vector path skipped the batch reader's
  per-file schema gate, so a TIMESTAMP(3)->(9) widening would NULL
  out-of-range values through Arrow's safe cast. The rewrite now declines
  any rust-incompatible schema history, and the scan node re-checks per
  split (loud backstop mirroring the pre-flight exactly).
- Generic mapping exception: FileQueryScanNode skipped the synthetic
  distance name for every external catalog, breaking real same-named Hive
  columns; the skip now applies only when the schema has no such column.
- Nonmovable projection: a NoneMovableFunction (assert_true) in the ANN
  project must keep evaluating below the TopN; any nonmovable expression
  outside the order-key alias now declines the rewrite.
- Pruned bare slot: a bare complex slot whose type was already reduced by
  nested pruning now declines (slot type must still equal the original
  column's), instead of tripping the desc-side guard loudly.
- IN beyond the converter cap: the whitelist mirrors the BE converter's
  16-bit child count, so a raised expr_children_limit no longer drops the
  predicate into the post-fetch residual.

Carried P1 threads closed at their narrowed edges: the pinned-empty
generation is fenced in getVectorSearchSplits exactly like the ordinary
path (a first commit after binding is no longer readable by vector
planning), and the pruned-bare-slot / IN-cap cases above.

The stored-NULL differential is kept as a documented limitation: a
nullability-based decline cannot ship until PaimonExternalTable.loadSchema
stops hardcoding isAllowNull=true (today every paimon column, even PK
columns, maps as nullable, so the gate would disable the rewrite for every
table). Follow-ups recorded in the review thread.

Tests: rewrite rule 36 -> 42 cases, PaimonScanNode +3 (fence twin, loud
per-file backstop, schemaId-matching encode); the ANN e2e suite stays
green with a new assert_true error-parity leg. BE untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

5 participants