zy-kkk opened a new pull request, #68453:
URL: https://github.com/apache/doris/pull/68453

   ### What problem does this PR solve?" 起的内容(按根 `AGENTS.md` 模板)。
   示例输出全部是 2026-09-22 在本地 4.1 实例上用 mysql client 跑出来的原样复制,
   只把本机 MinIO / REST 桩地址保留为 127.0.0.1(回归环境里就是这两个服务)。
   
   ---
   
   ### What problem does this PR solve?
   
   Issue Number: close #66491
   
   Related PR: #65730 (baseline Lance read integration, which already carried 
`FOR VERSION AS OF` / `FOR TIME AS OF` on the FE without tests or documentation)
   
   Problem Summary:
   
   Lance catalogs could select a dataset version on the FE, but nothing proved 
it end to end, the error messages leaked SDK internals (`NumberFormatException: 
For input string`, storage paths and Rust source locations), and tables whose 
versions are managed by a REST Namespace (`DescribeTable` returns 
`managed_versioning = true`) were rejected outright with "not supported by the 
current BE reader".
   
   This PR completes the four scope items of the issue:
   
   1. **Time travel** – `FOR VERSION AS OF <n>` and `FOR TIME AS OF 
'<timestamp>'` are covered by regression suites on a filesystem catalog and a 
REST catalog, with a committed three-version fixture (`time_travel.lance`) and 
the existing `multi_frag.lance` (three appends, then a delete) so that reading 
an older version across a deletion file is exercised on the BE.
      The semantics follow the Iceberg and Paimon integrations: the selected 
version is the latest one whose commit time is not later than the timestamp, 
parsed in the session time zone with second or millisecond precision and 
compared with commit times at millisecond precision (the precision a namespace 
reports them in, so storage and namespace resolution agree); a timestamp 
earlier than the first version is an error.
   2. **REST Namespace managed versioning** – the FE now opens such a table 
through the SDK's namespace client 
(`Dataset.open().namespaceClient(..).tableId(..)`), so the latest version comes 
from `ListTableVersions`, an explicit version from `DescribeTableVersion`, and 
`FOR TIME AS OF` from the commit times (`timestamp_millis`) the namespace 
reports. The SDK finalizes a still-staged manifest to its canonical 
`_versions/` path while opening, so the BE keeps opening the dataset by URI and 
version number through lance-c exactly as before; no BE or thrift change is 
needed. `EXPLAIN` shows `lanceManagedVersioning=true` for these tables.
   3. **Selected version fixed for the whole statement** – schema binding, 
fragment planning, predicate pushdown, metadata COUNT and the BE scan all use 
the selected version; two references to the same table in one statement can 
select different versions (self join / union tests).
   4. **Tags and branches** – `tbl@tag(name)` resolves the tag to the branch 
and version it points at, read from the dataset's `_refs/tags/` for both 
storage-versioned and managed tables (Lance's namespace tag APIs expose the 
same refs; the selected version of a managed table is then resolved through the 
namespace); a tag created on a branch selects that branch's version, not the 
same number on `main`. As for Iceberg and Paimon, a non-numeric `FOR VERSION AS 
OF 'v2'` names a tag. `@branch(main)` is `main`; `@tag` / `@branch` take 
exactly one name and reject extra parameters. Inside a branch, versions and 
times select among the branch's own versions, which start at the version it was 
created from. `tbl@branch(name)` opens the branch under `<table>/tree/<name>/`, 
optionally with `FOR VERSION AS OF` / `FOR TIME AS OF` inside the branch; the 
BE receives the branch directory as the dataset URI and needs no change (a 
branch is a shallow clone with its own `_versions/`). For managed table
 s the SDK derives the branch from that directory and sends it in the `branch` 
field of the version APIs. Previously `@tag` / `@branch` were silently ignored 
and the query read the latest version of `main`.
   5. **Errors** – a missing version (never committed, or removed by 
`cleanup_old_versions`) is reported as `Lance version N of db.tbl[ (tag 'x')] 
was not found[ in the namespace]` without the storage path; a missing tag or 
branch, an out-of-range or non-positive version (a signed number is a version, 
not a tag name), a timestamp before the first version and an unparsable 
timestamp have their own messages; a namespace that lists no versions is 
reported as such; a table that is only declared in the namespace fails with 
"declared in the namespace but has no data yet"; for a managed table, a 
namespace `location` that is empty or differs from `table_uri` fails the query 
before anything is read; every other failure keeps the sanitized provider 
message.
   
   Design decisions worth calling out:
   
   - For a managed table the namespace is the source of truth. A version whose 
manifest is still in storage but which the namespace does not record is 
reported as not found, and the table's latest version is the namespace's latest 
even when storage already holds a newer manifest. Reading unrecorded manifests 
would expose staged or rolled-back commits.
   - `timestamp_millis` is optional in the Lance Namespace spec. If a namespace 
lists its versions without it, the only commit times available are the ones in 
the storage manifests, so `FOR TIME AS OF` uses those, restricted to the 
versions the namespace lists so that the selected version is always one the 
namespace records. A manifest records when the writer committed the data, which 
can be earlier than when the namespace registered the version, so the selection 
follows the writer's clock in that case.
   - The SDK opens and checks out a namespace-managed dataset with its own 
native namespace client (the JNI takes the native handle of a REST or Directory 
namespace instead of calling back into the Java object), so those operations 
are not serialized on the FE's namespace lock; only the FE's own namespace 
requests are.
   - `FOR VERSION AS OF` on a dataset whose directory is missing altogether 
reports "version N was not found", because the SDK probes the version's 
manifest path first; the provider's own message is kept in the exception cause, 
handled the same way as every other Lance error.
   - `org.lance:lance-core` is bumped from 11.0.0 to 12.0.0 (its 
`lance-namespace` client from 0.7.7 to 0.11.1). The bump is for the namespace 
client, whose `branch` field on the version requests is what managed-table 
branches need; `Dataset.checkout(Ref)` already existed. The FE code compiled 
unchanged and every Lance unit test class and regression suite passes on it. 
The BE stays on lance-c 0.1.9 (Lance 11.0.0): the FE only reads metadata and 
never writes a dataset, so the newer FE release does not change what the BE can 
read.
   - An expired version (removed by cleanup) is indistinguishable from one that 
never existed, on storage and in a namespace, so both get the same message. "in 
the namespace" is only added when the namespace itself reported the miss.
   - Every selector other than an explicit version on `main` is a checkout from 
one open of the latest `main`, and a managed `FOR TIME AS OF` whose namespace 
reports commit times is resolved before anything is opened. The namespace 
version list is fetched completely, once per read, without a page size: Lance's 
Directory namespace applies a limit without returning a page token, and neither 
the order nor monotonic commit times can be relied on to stop early.
   - Reading a managed table whose manifest is still staged makes the SDK copy 
it to its canonical path, so read credentials must allow that write; an 
access-denied failure on a managed table says so.
   - The BE reads by URI and version number, which assumes the manifest a 
namespace records ends up at its canonical path `_versions/<u64::MAX - 
v>.manifest`. A staged manifest from a normal commit is moved there on read; a 
namespace that keeps finalized manifests elsewhere cannot be detected by the 
FE, and the BE then either fails to find the version or reads a different 
manifest with the same number at the canonical path. The documentation lists 
this as a limitation.
   - Known limitations: two references to the same branch in one statement 
resolve the branch head independently, and the optimizer's row count is that of 
the latest `main` (both shared with the Iceberg integration). 
`vector_search()`, `full_text_search()` and index inspection use the latest 
`main` and reject a version, tag or branch in their `table` argument. Index 
paths open by URI and are only reachable for filesystem catalogs, whose 
Directory namespace never manages versions; an assertion guards that.
   - A Lance branch is a shallow clone whose manifests record the parent's 
location as an absolute URI (`Manifest.base_paths`), so a branch is readable 
only where it was created. The committed `time_travel.lance` branch was 
therefore created against the fixture's final location 
`s3://warehouse/lance/time_travel.lance` (`lance_build_time_travel.py 
--create-branch`) and synced back; the fixture's self-check verifies that 
reference.
   
   Examples (mysql client against a 4.1 FE; `time_travel.lance` has versions 
1..3 committed at 13:06:07.597 / 13:06:09.113 / 13:06:10.621 UTC with rows 1..3 
/ 4..6 / 7..9):
   
   ```sql
   mysql> SET enable_file_scanner_v2 = true;
   mysql> SET time_zone = 'UTC';
   mysql> CREATE CATALOG lance_demo_fs PROPERTIES (
       ->     "type" = "lance",
       ->     "lance.catalog.type" = "filesystem",
       ->     "warehouse" = "s3://warehouse/lance",
       ->     "s3.endpoint" = "http://127.0.0.1:19000";,
       ->     "s3.access_key" = "admin",
       ->     "s3.secret_key" = "password",
       ->     "s3.region" = "us-east-1",
       ->     "use_path_style" = "true"
       -> );
   
   mysql> SELECT * FROM lance_demo_fs.`default`.time_travel FOR VERSION AS OF 1 
ORDER BY row_id;
   +--------+------+
   | row_id | tag  |
   +--------+------+
   |      1 | v1   |
   |      2 | v1   |
   |      3 | v1   |
   +--------+------+
   
   mysql> SELECT count(*), max(row_id) FROM lance_demo_fs.`default`.time_travel 
FOR TIME AS OF '2026-09-19 13:06:10';
   +----------+-------------+
   | count(*) | max(row_id) |
   +----------+-------------+
   |        6 |           6 |
   +----------+-------------+
   
   mysql> SELECT a.row_id, a.tag AS tag_v1, b.tag AS tag_v3
       -> FROM lance_demo_fs.`default`.time_travel FOR VERSION AS OF 1 a
       -> JOIN lance_demo_fs.`default`.time_travel FOR VERSION AS OF 3 b ON 
a.row_id = b.row_id
       -> ORDER BY a.row_id;
   +--------+--------+--------+
   | row_id | tag_v1 | tag_v3 |
   +--------+--------+--------+
   |      1 | v1     | v1     |
   |      2 | v1     | v1     |
   |      3 | v1     | v1     |
   +--------+--------+--------+
   
   mysql> SELECT count(*) FROM lance_demo_fs.`default`.time_travel FOR VERSION 
AS OF 99;
   ERROR 1105 (HY000): errCode = 2, detailMessage = Lance version 99 of 
default.time_travel was not found
   
   mysql> SELECT count(*) FROM lance_demo_fs.`default`.time_travel FOR TIME AS 
OF '2026-09-19 13:06:07';
   ERROR 1105 (HY000): errCode = 2, detailMessage = Failed to load Lance table 
metadata for default.time_travel: IllegalArgumentException: Lance dataset has 
no version at or before '2026-09-19 13:06:07'
   ```
   
   A REST Namespace with managed versioning. `time_travel_managed` records 
versions 1..3, `time_travel_managed_lagging` records only 1..2 while storage 
already holds version 3, and `time_travel_managed_partial` records 1 and 3:
   
   ```sql
   mysql> CREATE CATALOG lance_demo_rest PROPERTIES (
       ->     "type" = "lance",
       ->     "lance.catalog.type" = "rest",
       ->     "lance.rest.uri" = "http://127.0.0.1:19102";,
       ->     "lance.rest.security.type" = "bearer",
       ->     "lance.rest.bearer-token" = "doris-lance-rest-test-token",
       ->     "lance.namespace.root_database" = "default",
       ->     "s3.endpoint" = "http://127.0.0.1:19000";,
       ->     "s3.region" = "us-east-1",
       ->     "use_path_style" = "true"
       -> );
   
   mysql> SELECT count(*), max(row_id) FROM 
lance_demo_rest.`default`.time_travel_managed FOR VERSION AS OF 2;
   +----------+-------------+
   | count(*) | max(row_id) |
   +----------+-------------+
   |        6 |           6 |
   +----------+-------------+
   
   mysql> SELECT count(*), max(row_id) FROM 
lance_demo_rest.`default`.time_travel_managed_lagging;
   +----------+-------------+
   | count(*) | max(row_id) |
   +----------+-------------+
   |        6 |           6 |
   +----------+-------------+
   
   mysql> SELECT count(*) FROM 
lance_demo_rest.`default`.time_travel_managed_lagging FOR VERSION AS OF 3;
   ERROR 1105 (HY000): errCode = 2, detailMessage = Lance version 3 of 
default.time_travel_managed_lagging was not found in the namespace
   
   mysql> SELECT count(*), max(row_id) FROM 
lance_demo_rest.`default`.time_travel_managed_partial FOR TIME AS OF 
'2026-09-19 13:06:10';
   +----------+-------------+
   | count(*) | max(row_id) |
   +----------+-------------+
   |        3 |           3 |
   +----------+-------------+
   
   mysql> EXPLAIN SELECT row_id FROM 
lance_demo_rest.`default`.time_travel_managed_lagging;
   ...
     0:VLANCE_SCAN_NODE(44)
        table: lance_demo_rest.default.time_travel_managed_lagging
        lanceCatalogType=rest
        lanceVersion=2
        lanceManagedVersioning=true
        lanceFragments=2
        lanceFragmentGrouping=FRAGMENT
   ```
   
   Tags and branches (`time_travel.lance` carries tags `v1`..`v3` and a branch 
`dev` forked from version 2 with one extra row; on 
`time_travel_managed_partial` the tag `v2` points at the version the namespace 
no longer records):
   
   ```sql
   mysql> SELECT * FROM lance_demo_fs.`default`.time_travel@tag(v2) ORDER BY 
row_id;
   +--------+------+
   | row_id | tag  |
   +--------+------+
   |      1 | v1   |
   |      2 | v1   |
   |      3 | v1   |
   |      4 | v2   |
   |      5 | v2   |
   |      6 | v2   |
   +--------+------+
   
   mysql> SELECT * FROM lance_demo_fs.`default`.time_travel@branch(dev) ORDER 
BY row_id;
   +--------+------+
   | row_id | tag  |
   +--------+------+
   |      1 | v1   |
   |      2 | v1   |
   |      3 | v1   |
   |      4 | v2   |
   |      5 | v2   |
   |      6 | v2   |
   |    100 | dev  |
   +--------+------+
   
   mysql> SELECT count(*), max(row_id) FROM 
lance_demo_fs.`default`.time_travel@branch(dev) FOR VERSION AS OF 2;
   +----------+-------------+
   | count(*) | max(row_id) |
   +----------+-------------+
   |        6 |           6 |
   +----------+-------------+
   
   mysql> SELECT count(*) FROM 
lance_demo_fs.`default`.time_travel@tag(no_such_tag);
   ERROR 1105 (HY000): errCode = 2, detailMessage = Lance tag 'no_such_tag' of 
default.time_travel was not found
   
   mysql> SELECT count(*) FROM lance_demo_fs.`default`.time_travel@branch(nope);
   ERROR 1105 (HY000): errCode = 2, detailMessage = Lance branch 'nope' of 
default.time_travel was not found
   
   mysql> SELECT count(*), max(row_id) FROM 
lance_demo_rest.`default`.time_travel_managed@branch(dev);
   +----------+-------------+
   | count(*) | max(row_id) |
   +----------+-------------+
   |        7 |         100 |
   +----------+-------------+
   
   mysql> SELECT count(*), max(row_id) FROM 
lance_demo_rest.`default`.time_travel_managed@tag(v1);
   +----------+-------------+
   | count(*) | max(row_id) |
   +----------+-------------+
   |        3 |           3 |
   +----------+-------------+
   
   mysql> SELECT count(*) FROM 
lance_demo_rest.`default`.time_travel_managed_partial@tag(v2);
   ERROR 1105 (HY000): errCode = 2, detailMessage = Lance version 2 of 
default.time_travel_managed_partial (tag 'v2') was not found in the namespace
   
   mysql> EXPLAIN SELECT row_id FROM 
lance_demo_fs.`default`.time_travel@branch(dev);
   ...
     0:VLANCE_SCAN_NODE(44)
        table: lance_demo_fs.default.time_travel
        lanceCatalogType=filesystem
        lanceVersion=3
        lanceManagedVersioning=false
        lanceBranch=dev
        lanceFragments=3
        lanceFragmentGrouping=FRAGMENT
   ```
   
   Test environment changes: the Lance REST fixture (`lance_rest_server.py`) 
gains `ListTableVersions` / `DescribeTableVersion` for tables listed in 
`LANCE_REST_MANAGED_TABLES_JSON`; `time_travel.lance` is a committed 
three-version dataset with tags and a branch, carried over as-is by 
`lance_build_preinstalled_catalog.py` because the suites hard-code its commit 
times (`lance_build_time_travel.py` regenerates it and prints the times to 
update; `--create-branch` forks the branch at the uploaded location). The stub 
additionally serves the `branch` parameter of the version APIs.
   
   ### Release note
   
   Lance catalogs support `FOR VERSION AS OF`, `FOR TIME AS OF`, `@tag(name)` 
and `@branch(name)`, including tables whose versions are managed by a REST 
Namespace (`managed_versioning = true`). Missing versions, tags and branches 
and invalid selectors now report clear errors.
   
   ### Check List (For Author)
   
   - Test: Regression test / Unit Test
       - `external_table_p0/lance/test_lance_time_travel`, 
`test_lance_rest_time_travel`, `test_lance_rest_catalog` (Lance REST fixture + 
MinIO)
       - `LanceManagedVersioningTest` (real SDK dataset behind a REST stub: 
managed / partial / lagging / staged manifest / untimed / growing / server 
error / location checks / declared-only / cleaned-up version / tags / branches 
/ paging / empty version list), plus `test_lance_vector_search` and 
`test_lance_full_text_search` re-run on lance-core 12.0.0, `LanceSnapshotTest`, 
`LanceTableAccessCacheTest`, `LanceRestCatalogTest`, `LanceScanNodeTest`, 
`LanceMetadataLoaderTest`, `LanceCatalogLifecycleTest`
   - Behavior changed: Yes. Tables with `managed_versioning = true` were 
rejected and are now readable; error messages for invalid or missing versions 
changed to the user-facing form above; `@tag` / `@branch` on a Lance table now 
select the tag or branch (including a tag that points into a branch) instead of 
silently reading the latest version of `main`; FE dependency `lance-core` 
11.0.0 → 12.0.0.
   - Does this need documentation: Yes (doris-website PR to follow: feature 
table, REST catalog caution, new "Time Travel" section with the 
managed-versioning limitations)
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to