Skip to content

Commit 52751a2

Browse files
committed
[feature](lance) Add lance_index_entries inspection TVF
Add a read-only six-column TVF (CatalogName, DatabaseName, TableName, IndexName, IndexUuid, DatasetVersion) exposing the physical index entries of one Lance Directory catalog table. The constructor checks the SHOW privilege before any catalog access, the FE master re-checks it from the relayed user identity, and REST catalogs are rejected pre-initialization; rows are produced from the bounded PR1 read path. For apache#66497 PR2 slice 2.
1 parent 6d2a9d9 commit 52751a2

13 files changed

Lines changed: 955 additions & 0 deletions

File tree

be/src/exec/scan/meta_scanner.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ Status MetaScanner::_fetch_metadata(const TMetaScanRange& meta_scan_range) {
265265
case TMetadataType::PARTITION_VALUES:
266266
RETURN_IF_ERROR(_build_partition_values_metadata_request(meta_scan_range, &request));
267267
break;
268+
case TMetadataType::LANCE_INDEX_ENTRIES:
269+
RETURN_IF_ERROR(_build_lance_index_entries_metadata_request(meta_scan_range, &request));
270+
break;
268271
default:
269272
_meta_eos = true;
270273
return Status::OK();
@@ -507,6 +510,28 @@ Status MetaScanner::_build_partition_values_metadata_request(
507510
return Status::OK();
508511
}
509512

513+
Status MetaScanner::_build_lance_index_entries_metadata_request(
514+
const TMetaScanRange& meta_scan_range, TFetchSchemaTableDataRequest* request) {
515+
VLOG_CRITICAL << "MetaScanner::_build_lance_index_entries_metadata_request";
516+
if (!meta_scan_range.__isset.lance_index_params) {
517+
return Status::InternalError(
518+
"Can not find TLanceIndexMetadataParams from meta_scan_range.");
519+
}
520+
521+
// create request
522+
request->__set_cluster_name("");
523+
request->__set_schema_table_name(TSchemaTableName::METADATA_TABLE);
524+
525+
// create TMetadataTableRequestParams
526+
TMetadataTableRequestParams metadata_table_params;
527+
metadata_table_params.__set_metadata_type(TMetadataType::LANCE_INDEX_ENTRIES);
528+
metadata_table_params.__set_lance_index_metadata_params(meta_scan_range.lance_index_params);
529+
metadata_table_params.__set_current_user_ident(_user_identity);
530+
531+
request->__set_metada_table_params(metadata_table_params);
532+
return Status::OK();
533+
}
534+
510535
Status MetaScanner::close(RuntimeState* state) {
511536
VLOG_CRITICAL << "MetaScanner::close";
512537
if (_reader) {

be/src/exec/scan/meta_scanner.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ class MetaScanner : public Scanner {
8686
TFetchSchemaTableDataRequest* request);
8787
Status _build_partition_values_metadata_request(const TMetaScanRange& meta_scan_range,
8888
TFetchSchemaTableDataRequest* request);
89+
Status _build_lance_index_entries_metadata_request(const TMetaScanRange& meta_scan_range,
90+
TFetchSchemaTableDataRequest* request);
8991
bool _meta_eos;
9092
TupleId _tuple_id;
9193
TUserIdentity _user_identity;

fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinTableValuedFunctions.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.apache.doris.nereids.trees.expressions.functions.table.HttpStream;
3030
import org.apache.doris.nereids.trees.expressions.functions.table.HudiMeta;
3131
import org.apache.doris.nereids.trees.expressions.functions.table.Jobs;
32+
import org.apache.doris.nereids.trees.expressions.functions.table.LanceIndexEntries;
3233
import org.apache.doris.nereids.trees.expressions.functions.table.Local;
3334
import org.apache.doris.nereids.trees.expressions.functions.table.MvInfos;
3435
import org.apache.doris.nereids.trees.expressions.functions.table.Numbers;
@@ -67,6 +68,7 @@ public class BuiltinTableValuedFunctions implements FunctionHelper {
6768
tableValued(MvInfos.class, "mv_infos"),
6869
tableValued(Partitions.class, "partitions"),
6970
tableValued(Jobs.class, "jobs"),
71+
tableValued(LanceIndexEntries.class, "lance_index_entries"),
7072
tableValued(Tasks.class, "tasks"),
7173
tableValued(Query.class, "query"),
7274
tableValued(PartitionValues.class, "partition_values"),
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.doris.nereids.trees.expressions.functions.table;
19+
20+
import org.apache.doris.catalog.FunctionSignature;
21+
import org.apache.doris.nereids.exceptions.AnalysisException;
22+
import org.apache.doris.nereids.trees.expressions.Properties;
23+
import org.apache.doris.nereids.types.coercion.AnyDataType;
24+
import org.apache.doris.tablefunction.LanceIndexEntriesTableValuedFunction;
25+
import org.apache.doris.tablefunction.TableValuedFunctionIf;
26+
27+
import java.util.Map;
28+
29+
/** lance_index_entries */
30+
public class LanceIndexEntries extends TableValuedFunction {
31+
public LanceIndexEntries(Properties properties) {
32+
super(LanceIndexEntriesTableValuedFunction.NAME, properties);
33+
}
34+
35+
@Override
36+
public FunctionSignature customSignature() {
37+
return FunctionSignature.of(AnyDataType.INSTANCE_WITHOUT_INDEX, getArgumentsTypes());
38+
}
39+
40+
@Override
41+
protected TableValuedFunctionIf toCatalogFunction() {
42+
try {
43+
Map<String, String> arguments = getTVFProperties().getMap();
44+
return new LanceIndexEntriesTableValuedFunction(arguments);
45+
} catch (Throwable t) {
46+
throw new AnalysisException("Can not build lance_index_entries(): " + t.getMessage(), t);
47+
}
48+
}
49+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.doris.tablefunction;
19+
20+
import org.apache.doris.analysis.TableName;
21+
import org.apache.doris.catalog.Column;
22+
import org.apache.doris.catalog.Env;
23+
import org.apache.doris.catalog.PrimitiveType;
24+
import org.apache.doris.catalog.ScalarType;
25+
import org.apache.doris.catalog.TableIf;
26+
import org.apache.doris.common.ErrorCode;
27+
import org.apache.doris.datasource.CatalogIf;
28+
import org.apache.doris.datasource.lance.LanceExternalCatalog;
29+
import org.apache.doris.datasource.lance.LanceExternalTable;
30+
import org.apache.doris.mysql.privilege.PrivPredicate;
31+
import org.apache.doris.nereids.analyzer.UnboundSlot;
32+
import org.apache.doris.nereids.exceptions.AnalysisException;
33+
import org.apache.doris.nereids.exceptions.ParseException;
34+
import org.apache.doris.nereids.parser.NereidsParser;
35+
import org.apache.doris.nereids.trees.expressions.Expression;
36+
import org.apache.doris.qe.ConnectContext;
37+
import org.apache.doris.thrift.TLanceIndexMetadataParams;
38+
import org.apache.doris.thrift.TMetaScanRange;
39+
import org.apache.doris.thrift.TMetadataType;
40+
41+
import com.google.common.annotations.VisibleForTesting;
42+
import com.google.common.collect.ImmutableList;
43+
import com.google.common.collect.ImmutableMap;
44+
import com.google.common.collect.ImmutableSet;
45+
46+
import java.util.List;
47+
import java.util.Locale;
48+
import java.util.Map;
49+
import java.util.Set;
50+
import java.util.TreeMap;
51+
52+
/**
53+
* Read-only inspection TVF listing the physical index entries of one Lance Directory catalog
54+
* table. Rows are produced on the FE master through the bounded catalog read path; this class
55+
* only validates arguments, authorization and the wire contract.
56+
*/
57+
public class LanceIndexEntriesTableValuedFunction extends MetadataTableValuedFunction {
58+
public static final String NAME = "lance_index_entries";
59+
static final String REST_CATALOG_REJECT_MESSAGE =
60+
"lance_index_entries is not supported for Lance REST catalogs";
61+
private static final String TABLE = "table";
62+
private static final Set<String> PROPERTIES = ImmutableSet.of(TABLE);
63+
private static final String FULLY_QUALIFIED_TABLE_NAME_ERROR =
64+
"'table' must be a fully qualified catalog.database.table name";
65+
private static final ImmutableList<Column> SCHEMA = ImmutableList.of(
66+
new Column("CatalogName", ScalarType.createStringType()),
67+
new Column("DatabaseName", ScalarType.createStringType()),
68+
new Column("TableName", ScalarType.createStringType()),
69+
new Column("IndexName", ScalarType.createStringType()),
70+
new Column("IndexUuid", ScalarType.createStringType()),
71+
new Column("DatasetVersion", PrimitiveType.BIGINT, true));
72+
private static final ImmutableMap<String, Integer> COLUMN_TO_INDEX = buildColumnIndex();
73+
74+
private final TableName sourceTableName;
75+
76+
public LanceIndexEntriesTableValuedFunction(Map<String, String> properties) throws AnalysisException {
77+
sourceTableName = parseTableName(normalizeProperties(properties).get(TABLE));
78+
79+
// This check intentionally precedes catalog lookup/initialization and every provider call.
80+
checkShowPrivilege(ConnectContext.get(), sourceTableName);
81+
resolveLanceTable(sourceTableName);
82+
}
83+
84+
public final String getCatalogName() {
85+
return sourceTableName.getCtl();
86+
}
87+
88+
public final String getDatabaseName() {
89+
return sourceTableName.getDb();
90+
}
91+
92+
public final String getSourceTableName() {
93+
return sourceTableName.getTbl();
94+
}
95+
96+
@Override
97+
public final TMetadataType getMetadataType() {
98+
return TMetadataType.LANCE_INDEX_ENTRIES;
99+
}
100+
101+
@Override
102+
public final TMetaScanRange getMetaScanRange(List<String> requiredFields) {
103+
TLanceIndexMetadataParams params = new TLanceIndexMetadataParams()
104+
.setCatalog(getCatalogName())
105+
.setDatabase(getDatabaseName())
106+
.setTable(getSourceTableName());
107+
return new TMetaScanRange()
108+
.setMetadataType(TMetadataType.LANCE_INDEX_ENTRIES)
109+
.setLanceIndexParams(params);
110+
}
111+
112+
public static Integer getColumnIndexFromColumnName(String columnName) {
113+
return COLUMN_TO_INDEX.get(columnName.toLowerCase(Locale.ROOT));
114+
}
115+
116+
static List<Column> getSchemaForTest() {
117+
return SCHEMA;
118+
}
119+
120+
@Override
121+
public String getTableName() {
122+
return "LanceIndexEntriesTableValuedFunction";
123+
}
124+
125+
@Override
126+
public List<Column> getTableColumns() {
127+
return SCHEMA;
128+
}
129+
130+
@VisibleForTesting
131+
static TableName parseTableName(String value) throws AnalysisException {
132+
Expression expression;
133+
try {
134+
expression = new NereidsParser().parseExpression(value);
135+
} catch (ParseException e) {
136+
throw new AnalysisException(FULLY_QUALIFIED_TABLE_NAME_ERROR, e);
137+
}
138+
if (!(expression instanceof UnboundSlot)) {
139+
throw new AnalysisException(FULLY_QUALIFIED_TABLE_NAME_ERROR);
140+
}
141+
List<String> names = ((UnboundSlot) expression).getNameParts();
142+
if (names.size() != 3) {
143+
throw new AnalysisException(FULLY_QUALIFIED_TABLE_NAME_ERROR);
144+
}
145+
return new TableName(names.get(0), names.get(1), names.get(2));
146+
}
147+
148+
@VisibleForTesting
149+
static Map<String, String> normalizeProperties(Map<String, String> properties)
150+
throws AnalysisException {
151+
Map<String, String> normalized = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
152+
for (Map.Entry<String, String> entry : properties.entrySet()) {
153+
String key = entry.getKey().toLowerCase(Locale.ROOT);
154+
if (!PROPERTIES.contains(key)) {
155+
throw new AnalysisException("'" + entry.getKey()
156+
+ "' is an invalid property for " + NAME);
157+
}
158+
if (normalized.containsKey(key)) {
159+
throw new AnalysisException("Duplicate " + NAME + " property '" + key + "'");
160+
}
161+
normalized.put(key, entry.getValue());
162+
}
163+
String table = normalized.get(TABLE);
164+
if (table == null || table.trim().isEmpty()) {
165+
throw new AnalysisException("Missing required " + NAME + " property '" + TABLE + "'");
166+
}
167+
normalized.put(TABLE, table.trim());
168+
return normalized;
169+
}
170+
171+
static LanceExternalTable resolveLanceTable(TableName tableName) throws AnalysisException {
172+
CatalogIf<?> catalog = Env.getCurrentEnv().getCatalogMgr().getCatalog(tableName.getCtl());
173+
if (!(catalog instanceof LanceExternalCatalog)) {
174+
throw new AnalysisException("Catalog '" + tableName.getCtl() + "' is not a Lance catalog");
175+
}
176+
// REST catalogs are rejected from configuration only, before any database or table
177+
// resolution that could trigger remote namespace requests or catalog initialization.
178+
if (((LanceExternalCatalog) catalog).isRestCatalogConfigured()) {
179+
throw new AnalysisException(REST_CATALOG_REJECT_MESSAGE);
180+
}
181+
TableIf table;
182+
try {
183+
table = catalog.getDbOrAnalysisException(tableName.getDb())
184+
.getTableOrAnalysisException(tableName.getTbl());
185+
} catch (org.apache.doris.common.AnalysisException e) {
186+
throw new AnalysisException(e.getMessage(), e);
187+
}
188+
if (!(table instanceof LanceExternalTable)) {
189+
throw new AnalysisException("Table '" + tableName + "' is not a Lance table");
190+
}
191+
return (LanceExternalTable) table;
192+
}
193+
194+
private static void checkShowPrivilege(ConnectContext context, TableName tableName)
195+
throws AnalysisException {
196+
if (context == null || !Env.getCurrentEnv().getAccessManager()
197+
.checkTblPriv(context, tableName, PrivPredicate.SHOW)) {
198+
String user = context == null ? "unknown" : context.getQualifiedUser();
199+
String remoteIp = context == null ? "unknown" : context.getRemoteIP();
200+
throw new AnalysisException(ErrorCode.ERR_TABLEACCESS_DENIED_ERROR.formatErrorMsg(
201+
"SHOW", user, remoteIp,
202+
tableName.getDb() + ": " + tableName.getTbl()));
203+
}
204+
}
205+
206+
private static ImmutableMap<String, Integer> buildColumnIndex() {
207+
ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder();
208+
for (int i = 0; i < SCHEMA.size(); i++) {
209+
builder.put(SCHEMA.get(i).getName().toLowerCase(Locale.ROOT), i);
210+
}
211+
return builder.build();
212+
}
213+
}

0 commit comments

Comments
 (0)