fix(rust): list indices when an FTS index's files are missing (#4330)

Fixes #4250.

`list_indices()` reads each FTS index's files to fill `index_details`.
When those files were missing, the error failed the whole call, so the
caller could not list any index, including the damaged one it needs to
rebuild. Scalar indexes were not affected because their listing reads
only the manifest.

Now a failed read logs a warning and leaves that index's `index_details`
as `None`. The other fields still come from the manifest, and
`tokenize()` already returns a clear error when an FTS index has no
details.

The new test `test_list_indices_with_missing_fts_index_files` creates an
FTS index and a BTree index, deletes the FTS index directory, and
reopens the table. It then checks that both indexes are still listed.
Without the change, it fails with the same `Not found` error as the
issue.
This commit is contained in:
Bilal Atique
2026-09-29 10:11:34 -05:00
committed by GitHub
parent 629f39d4a5
commit 166c114710
2 changed files with 58 additions and 1 deletions
+14 -1
View File
@@ -3791,7 +3791,20 @@ impl BaseTable for NativeTable {
let Some(segment) = segments.first() else {
continue;
};
let params = load_segment_params(&dataset, segment).await?;
// The listing itself only needs the manifest. Missing index files must
// not hide every other index, or callers cannot find the one to repair.
let params = match load_segment_params(&dataset, segment).await {
Ok(params) => params,
Err(err) => {
log::warn!(
"Failed to read full text search configuration for index '{}': {}",
index.name,
err
);
index.index_details = None;
continue;
}
};
let details = serde_json::to_string(&params).map_err(|source| Error::Other {
message: format!(
"Failed to serialize full text search configuration for index '{}'",
+44
View File
@@ -2078,6 +2078,50 @@ mod tests {
assert_eq!(index.columns, vec!["tags".to_string()]);
}
#[tokio::test]
async fn test_list_indices_with_missing_fts_index_files() {
let tmp_dir = tempdir().unwrap();
let uri = tmp_dir.path().to_str().unwrap();
let conn = connect(uri).execute().await.unwrap();
let batch = record_batch!(
("id", Int32, [1, 2, 3]),
("text", Utf8, ["alpha", "beta", "gamma"])
)
.unwrap();
let table = conn.create_table("t", batch).execute().await.unwrap();
table
.create_index(&["text"], Index::FTS(FtsIndexBuilder::default()))
.execute()
.await
.unwrap();
table
.create_index(&["id"], Index::BTree(BTreeIndexBuilder::default()))
.execute()
.await
.unwrap();
let fts_uuid = table
.list_indices()
.await
.unwrap()
.into_iter()
.find(|index| index.name == "text_idx")
.and_then(|index| index.index_uuid)
.unwrap();
std::fs::remove_dir_all(tmp_dir.path().join("t.lance/_indices").join(fts_uuid)).unwrap();
// A new connection, so the index cache cannot serve the deleted files.
let conn = connect(uri).execute().await.unwrap();
let table = conn.open_table("t").execute().await.unwrap();
let mut indices = table.list_indices().await.unwrap();
indices.sort_by(|a, b| a.name.cmp(&b.name));
let names = indices.iter().map(|i| i.name.as_str()).collect::<Vec<_>>();
assert_eq!(names, vec!["id_idx", "text_idx"]);
assert_eq!(indices[1].index_type, crate::index::IndexType::FTS);
assert_eq!(indices[1].index_details, None);
assert!(indices[0].index_details.is_some());
}
#[tokio::test]
async fn test_create_inverted_index() {
let conn = connect("memory://").execute().await.unwrap();