FANNG1 opened a new issue, #66772:
URL: https://github.com/apache/doris/issues/66772

   ### Search before asking
   
   - [X] I had searched in the 
[issues](https://github.com/apache/doris/issues?q=is%3Aissue) and found no 
similar issues.
   
   ### Version
   
   `branch-4.1` @ `0e53b31f586`. The FE Lance catalog currently exists only on 
`branch-4.1` — `master` has no 
`fe/fe-core/src/main/java/org/apache/doris/datasource/lance/`. 
`LanceStorageOptions` came in with e3289c1a5df (#65730); the REST properties 
were reshaped by 8375559ce88 (#66581).
   
   Server side: **Apache Gravitino 1.3.0**, native `lance-rest` auxiliary 
service (`gravitino.auxService.names = lance-rest`, 
`gravitino.lance-rest.httpPort = 9101`, `namespace-backend = gravitino`), base 
path `/lance`.
   
   Namespace library versions differ across the wire but are not the cause of 
either problem below: Gravitino bundles `lance-namespace-core` 0.4.5, while 
Doris FE pulls `lance-namespace-apache-client` 0.7.7 transitively from 
`org.lance:lance-core:9.1.0-beta.3`.
   
   ### What's Wrong?
   
   I ran the Lance REST catalog against a real Lance Namespace server 
(Gravitino) rather than the Python stub in 
`docker/thirdparties/docker-compose/iceberg/scripts/lance_rest_server.py`, and 
hit two independent problems.
   
   The protocol itself lines up — endpoint paths, the `$` delimiter, 
pagination, `managed_versioning`, and the `table_uri`/`location` fallback all 
match, and once both problems are worked around **every read path passes** (see 
*Anything else?*). These are integration defects, not a protocol mismatch.
   
   ---
   
   #### Problem 1 — namespace-vended storage credentials are silently dropped 
(blocking)
   
   Credential vending is the main reason to prefer a REST catalog over a 
filesystem one, and Doris explicitly asks for it — 
`LanceExternalCatalog.describeTable` sends `.vendCredentials(true)` for REST 
catalogs.
   
   Gravitino answers with **unprefixed** object-store option names (it stores 
them as `lance.storage.<key>` and strips that prefix in 
`LancePropertiesUtils.resolveLanceStorageOptions`):
   
   ```json
   "storage_options": {
     "access_key_id":     "minioadmin",
     "secret_access_key": "minioadmin",
     "endpoint":          "http://127.0.0.1:9000";,
     "region":            "us-east-1",
     "allow_http":        "true"
   }
   ```
   
   But `LanceStorageOptions.forBackend` only ever looks up five `aws_`-prefixed 
names:
   
   ```java
   // 
fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceStorageOptions.java
   S3_KEYS.put("AWS_ACCESS_KEY", "aws_access_key_id");
   S3_KEYS.put("AWS_SECRET_KEY", "aws_secret_access_key");
   S3_KEYS.put("AWS_TOKEN",      "aws_session_token");
   S3_KEYS.put("AWS_ENDPOINT",   "aws_endpoint");
   S3_KEYS.put("AWS_REGION",     "aws_region");
   ...
   S3_KEYS.forEach((dorisKey, lanceKey) -> putIfNotEmpty(result, dorisKey,
           lanceStorageOptions.get(lanceKey)));
   ```
   
   Nothing matches, so **every vended credential is discarded before it reaches 
the BE**.
   
   The resulting failure is split across FE and BE, which makes it hard to 
read. The FE merges the vended map in raw and hands it to the Lance Java SDK, 
whose object-store layer accepts the unprefixed spelling — so all metadata 
operations succeed. Only the BE, which goes through `forBackend`, ends up with 
nothing:
   
   ```
   DESC   lance_novend.doris_probe.rest_probe;   -- OK, 5 columns
   SELECT count(*) FROM lance_novend.doris_probe.rest_probe;
   
   ERROR 1105 (HY000): errCode = 2, detailMessage = (127.0.0.1)[INTERNAL_ERROR]
   open Lance dataset failed: LanceError(IO): Generic N/A error: ...
   Failed to get AWS credentials: CredentialsNotLoaded(CredentialsNotLoaded {
     source: Some("no providers in chain provided credentials") }),
   .../lance-io/src/object_store/providers/aws.rs:401:21
   ```
   
   **Positive control isolating the key spelling.** I created a second 
Gravitino catalog whose properties spell the same values 
`lance.storage.aws_access_key_id`, `lance.storage.aws_secret_access_key`, 
`lance.storage.aws_endpoint`, `lance.storage.aws_region`, pointed it at the 
*same physical dataset*, and read it through the *same* Doris configuration 
(still no static credentials). The scan returned all 1024 rows. Key spelling 
was the only variable.
   
   Both spellings are valid for the Rust `object_store` config that ultimately 
consumes them, and the FE's success is direct evidence that the unprefixed form 
works — only Doris's own BE-side converter insists on one of the two.
   
   ---
   
   #### Problem 2 — the parent namespace is published as a database that cannot 
be listed
   
   `LanceExternalCatalog.listDatabaseNames` unconditionally publishes the 
configured root database:
   
   ```java
   // The configured root database represents the empty relative Lance 
namespace.
   LinkedHashSet<String> databases = new LinkedHashSet<>();
   databases.add(rootDatabase);
   ```
   
   so `default` always appears in `SHOW DATABASES`. It maps to the parent 
namespace itself, which is one level deep — but Gravitino's `ListTables` 
requires an identifier of exactly two levels (catalog + schema), so listing it 
fails:
   
   ```
   SHOW TABLES FROM lance_gvt.`default`;
   ERROR 1105 (HY000): errCode = 2, detailMessage = Invalid input: Expected 
2-level namespace but got: 1
   ```
   
   The same collision makes `"test_connection" = "true"` unusable, because the 
probe lists tables directly under the parent:
   
   ```java
   testNamespace.listTables(new ListTablesRequest().id(parent).limit(1));
   testNamespace.listNamespaces(new 
ListNamespacesRequest().id(parent).limit(1));
   ```
   
   With `test_connection = true`, `CREATE CATALOG` fails outright with the same 
message. Leaving it at its default of `false` is the workaround.
   
   This is not a data-correctness problem — Doris loads databases lazily, so 
`default` is just an entry that must be routed around — but it does mean `SHOW 
DATABASES` advertises something unusable, and it silently rules out the 
connectivity check.
   
   ### What You Expected?
   
   1. Vended `storage_options` should reach the BE regardless of which accepted 
alias the namespace server uses for a key. A server that vends `access_key_id` 
should work exactly like one that vends `aws_access_key_id` — as it already 
does on the FE side.
   2. A database returned by `SHOW DATABASES` should be listable, and 
`test_connection = true` should not require the parent namespace itself to be 
table-listable.
   
   ### How to Reproduce?
   
   **1. Gravitino 1.3.0 with the Lance REST service**, and a 
`lakehouse-generic` catalog carrying unprefixed storage options:
   
   ```properties
   # conf/gravitino.conf
   gravitino.auxService.names            = lance-rest
   gravitino.lance-rest.classpath        = lance-rest-server/libs
   gravitino.lance-rest.httpPort         = 9101
   gravitino.lance-rest.namespace-backend = gravitino
   gravitino.lance-rest.gravitino-uri    = http://127.0.0.1:8090
   gravitino.lance-rest.gravitino-metalake = test
   ```
   
   ```bash
   curl -X POST http://127.0.0.1:8090/api/metalakes/test/catalogs \
     -H 'Accept: application/vnd.gravitino.v1+json' -H 'Content-Type: 
application/json' \
     -d 
'{"name":"lance_catalog","type":"relational","provider":"lakehouse-generic","properties":{
          "location":"s3://contacts/raw/lance",
          "lance.storage.access_key_id":"minioadmin",
          "lance.storage.secret_access_key":"minioadmin",
          "lance.storage.endpoint":"http://127.0.0.1:9000";,
          "lance.storage.region":"us-east-1",
          "lance.storage.allow_http":"true"}}'
   
   curl -X POST 
'http://127.0.0.1:9101/lance/v1/namespace/lance_catalog%24doris_probe/create?delimiter=%24'
 \
     -H 'Content-Type: application/json' \
     -d '{"id":["lance_catalog","doris_probe"],"mode":"CREATE","properties":{}}'
   ```
   
   **2. Write a dataset and register it** (pylance 7.0.0):
   
   ```python
   import lance, numpy as np, pyarrow as pa
   URI = "s3://contacts/raw/lance/doris_probe/rest_probe/"
   SO = {"access_key_id": "minioadmin", "secret_access_key": "minioadmin",
         "endpoint": "http://127.0.0.1:9000";, "region": "us-east-1",
         "allow_http": "true", "virtual_hosted_style_request": "false"}
   rid = np.arange(1, 1025, dtype=np.int64)
   emb = (rid[:, None] - 1).astype(np.float32) + np.arange(16, 
dtype=np.float32)[None, :]
   t = pa.table({
       "row_id": pa.array(rid, type=pa.int64()),
       "int32_value": pa.array((rid % 100).astype(np.int32), type=pa.int32()),
       "embedding": pa.FixedSizeListArray.from_arrays(
           pa.array(emb.reshape(-1), type=pa.float32()), 16)})
   lance.write_dataset(t, URI, mode="overwrite", storage_options=SO, 
max_rows_per_file=512)
   lance.dataset(URI, storage_options=SO).create_index(
       "embedding", index_type="IVF_FLAT", metric="l2", num_partitions=4)
   ```
   
   ```bash
   curl -X POST 
'http://127.0.0.1:9101/lance/v1/table/lance_catalog%24doris_probe%24rest_probe/register?delimiter=%24'
 \
     -H 'Content-Type: application/json' \
     -d '{"id":["lance_catalog","doris_probe","rest_probe"],
          "location":"s3://contacts/raw/lance/doris_probe/rest_probe/"}'
   ```
   
   **3. Problem 1** — a catalog with no static credentials, relying entirely on 
vending:
   
   ```sql
   CREATE CATALOG lance_novend PROPERTIES (
       "type"                   = "lance",
       "lance.catalog.type"     = "rest",
       "lance.rest.uri"         = "http://127.0.0.1:9101/lance";,
       "lance.namespace.parent" = "lance_catalog"
   );
   
   DESC lance_novend.doris_probe.rest_probe;                    -- OK
   SELECT count(*) FROM lance_novend.doris_probe.rest_probe;    -- 
CredentialsNotLoaded
   ```
   
   **4. Problem 2** — against the same catalog:
   
   ```sql
   SHOW DATABASES FROM lance_novend;              -- lists `default`
   SHOW TABLES FROM lance_novend.`default`;       -- Expected 2-level namespace 
but got: 1
   ```
   
   and adding `"test_connection" = "true"` to the properties above makes 
`CREATE CATALOG` itself fail.
   
   ### Anything Else?
   
   **Everything else works.** With both problems worked around — static 
`s3.access_key`/`s3.secret_key`, and `test_connection` left at its default — 
the REST catalog reads Gravitino-managed Lance tables correctly. Verified 
against a deterministic fixture (1024 rows in 2 fragments, 16-dim float32 
`embedding` where row *n* is `[n-1, n, …, n+14]`, so the exact squared L2 
distance between rows *n* and *r* is `16·(n−r)²`, plus an IVF_FLAT index over 4 
partitions), cross-checked row-for-row against pylance:
   
   | Layer | Result |
   |---|---|
   | `CREATE CATALOG`, database/table discovery | ok |
   | Type mapping (int64→bigint, string→text, int32→int, double→double, 
`fixed_size_list<float,16>`→`array<float>`) | ok |
   | Full scan — 1024 rows, `sum(row_id) = 524800` | ok |
   | Substrait predicate pushdown (`lancePushdownPredicate`), non-convertible 
expression left as BE residual | ok |
   | Projection pruning | ok |
   | IVF_FLAT vector search — full probe reproduces flat search row-for-row on 
the exact `16·d²` ladder | ok |
   | Global Top-K across fragments (row 1024 lives in the second fragment) | ok 
|
   | `FOR VERSION AS OF` — after appending 10 rows: latest 1034, v2 1024, v1 
1024 | ok |
   
   Working configuration, for reference:
   
   ```sql
   CREATE CATALOG lance_gvt PROPERTIES (
       "type"                   = "lance",
       "lance.catalog.type"     = "rest",
       "lance.rest.uri"         = "http://127.0.0.1:9101/lance";,  -- 
Gravitino's /lance base path
       "lance.namespace.parent" = "lance_catalog",                -- Doris 
database == Gravitino schema
       "s3.endpoint"            = "http://127.0.0.1:9000";,
       "s3.access_key"          = "minioadmin",
       "s3.secret_key"          = "minioadmin",
       "s3.region"              = "us-east-1",
       "use_path_style"         = "true"
   );
   ```
   
   Two notes for whoever picks this up:
   
   1. `lance.rest.uri` has to carry the server's base path (`/lance` here). 
`LanceRestMetastoreProperties.validateRestUri` already permits a path 
component, and the client appends `/v1/…` to it, so this works — worth a line 
in the docs, since the in-repo stub is mounted at the root and never exercises 
it.
   2. **Test coverage has a hole shaped exactly like Problem 1.** 
`test_lance_rest_catalog.groovy` does cover credential vending, but the stub it 
talks to vends `aws_`-prefixed keys (`lance_rest_server.py` returns 
`aws_access_key_id`, `aws_secret_access_key`, `aws_region`), so the unprefixed 
alias path — which is what a real server emits — is never exercised. Teaching 
the stub to vend the unprefixed spelling in one case would pin this.
   
   ### Are you willing to submit PR?
   
   - [X] Yes I am willing to submit a PR!
   
   ### Code of Conduct
   
   - [X] I agree to follow this project's [Code of 
Conduct](https://www.apache.org/foundation/policies/conduct)
   


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