rust: create_index returns the server-minted job id

BaseTable::create_index and IndexBuilder::execute now return
Option<String> -- the job id the server mints when an index build is
deferred to a background job (pending vector index on a remote table).
Native builds are synchronous and return None; older servers with empty
response bodies parse as None. The pyo3 binding forwards the id to
python; nodejs keeps its unit return.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Wyatt Alt
2026-07-18 20:52:29 -07:00
parent 4d4e3e6d36
commit 08e9ab54a9
6 changed files with 72 additions and 11 deletions
+1 -1
View File
@@ -165,7 +165,7 @@ impl Table {
if let Some(train) = train {
builder = builder.train(train);
}
builder.execute().await.default_error()
builder.execute().await.default_error().map(|_| ())
}
#[napi(catch_unwind)]
+2 -2
View File
@@ -794,8 +794,8 @@ impl Table {
}
future_into_py(self_.py(), async move {
op.execute().await.infer_error()?;
Ok(())
let job_id = op.execute().await.infer_error()?;
Ok(job_id)
})
}
+5 -1
View File
@@ -118,8 +118,12 @@ async fn create_empty_table(db: &Connection) -> Result<LanceDbTable> {
async fn create_index(table: &LanceDbTable) -> Result<()> {
// --8<-- [start:create_index]
table.create_index(&["vector"], Index::Auto).execute().await
table
.create_index(&["vector"], Index::Auto)
.execute()
.await?;
// --8<-- [end:create_index]
Ok(())
}
async fn search(table: &LanceDbTable) -> Result<Vec<RecordBatch>> {
+4 -1
View File
@@ -283,7 +283,10 @@ impl IndexBuilder {
self
}
pub async fn execute(self) -> Result<()> {
/// Returns the server-minted job id when the index build was deferred to
/// a background job (remote tables only); `None` when the build completed
/// synchronously within this call.
pub async fn execute(self) -> Result<Option<String>> {
self.parent.clone().create_index(self).await
}
}
+53 -3
View File
@@ -2109,7 +2109,7 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
Ok(delete_response)
}
async fn create_index(&self, mut index: IndexBuilder) -> Result<()> {
async fn create_index(&self, mut index: IndexBuilder) -> Result<Option<String>> {
self.check_mutable().await?;
let request = self
.client
@@ -2202,14 +2202,28 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
let (request_id, response) = self.send(request, true).await?;
self.check_table_response(&request_id, response).await?;
let response = self.check_table_response(&request_id, response).await?;
// The server returns a job id only when the build was deferred to a
// background job (pending vector index). Older servers return an
// empty body; treat anything unparseable as "no job".
#[derive(serde::Deserialize)]
struct CreateIndexResponse {
job_id: Option<String>,
}
let job_id = response
.text()
.await
.ok()
.and_then(|body| serde_json::from_str::<CreateIndexResponse>(&body).ok())
.and_then(|r| r.job_id);
if let Some(wait_timeout) = index.wait_timeout {
let index_name = index.name.unwrap_or_else(|| format!("{}_idx", column));
self.wait_for_index(&[&index_name], wait_timeout).await?;
}
Ok(())
Ok(job_id)
}
/// Poll until the columns are fully indexed. Will return Error::Timeout if the columns
@@ -4639,6 +4653,42 @@ mod tests {
}
}
#[tokio::test]
async fn test_create_index_returns_deferred_job_id() {
let table =
Table::new_with_handler("my_table", move |request| match request.url().path() {
"/v1/table/my_table/describe/" => {
let schema = Schema::new(vec![Field::new(
"vector",
DataType::FixedSizeList(
Arc::new(Field::new("item", DataType::Float32, true)),
128,
),
false,
)]);
http::Response::builder()
.status(200)
.body(describe_response(&schema))
.unwrap()
}
"/v1/table/my_table/create_index/" => http::Response::builder()
.status(200)
.body(r#"{"job_id": "0a1b2c3d-4e5f-6789-abcd-ef0123456789"}"#.to_string())
.unwrap(),
path => panic!("Unexpected path: {}", path),
});
let job_id = table
.create_index(&["vector"], Index::IvfPq(Default::default()))
.execute()
.await
.unwrap();
assert_eq!(
job_id.as_deref(),
Some("0a1b2c3d-4e5f-6789-abcd-ef0123456789")
);
}
#[tokio::test]
async fn test_create_index_nested_field_paths() {
let schema = nested_index_schema();
+7 -3
View File
@@ -582,7 +582,10 @@ pub trait BaseTable: std::fmt::Display + std::fmt::Debug + Send + Sync {
/// Update rows in the table.
async fn update(&self, update: UpdateBuilder) -> Result<UpdateResult>;
/// Create an index on the provided column(s).
async fn create_index(&self, index: IndexBuilder) -> Result<()>;
///
/// Returns the server-minted job id when the build was deferred to a
/// background job (remote tables only); `None` for synchronous builds.
async fn create_index(&self, index: IndexBuilder) -> Result<Option<String>>;
/// List the indices on the table.
async fn list_indices(&self) -> Result<Vec<IndexConfig>>;
/// Drop an index from the table.
@@ -3031,7 +3034,7 @@ impl BaseTable for NativeTable {
Ok(AddResult { version })
}
async fn create_index(&self, opts: IndexBuilder) -> Result<()> {
async fn create_index(&self, opts: IndexBuilder) -> Result<Option<String>> {
if opts.columns.len() != 1 {
return Err(Error::Schema {
message: "Multi-column (composite) indices are not yet supported".to_string(),
@@ -3054,7 +3057,8 @@ impl BaseTable for NativeTable {
}
builder.await?;
self.dataset.update(dataset);
Ok(())
// Native builds are synchronous -- there is never a background job.
Ok(None)
}
async fn drop_index(&self, index_name: &str) -> Result<()> {