From 79e9dffd0613e4c8e70c6ef22f0cb79e4cfc6d48 Mon Sep 17 00:00:00 2001 From: Wyatt Alt Date: Wed, 12 Aug 2026 19:11:21 -0700 Subject: [PATCH] fix: make computed columns explicitly local-only Both operations were advertised on remote tables and neither could work. A declaration reaches the wire as AllNulls, which RemoteTable::add_columns does not accept, and refresh_column fell through to the BaseTable default; the TypeScript wrapper reached the same surface. Callers got errors that named neither the feature nor the reason. The remote protocol has no representation for a stored expression, and what one should look like is not settled -- the server persists a declaration under a different vocabulary. So this states the boundary rather than guessing at a wire format: an explicit AllNulls arm, a default that names computed columns, and NotImplementedError raised in Python before the round trip. --- python/python/lancedb/remote/table.py | 8 ++++-- rust/lancedb/src/remote/table.rs | 38 +++++++++++++++++++++++++++ rust/lancedb/src/table.rs | 22 ++++++++++++++-- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/python/python/lancedb/remote/table.py b/python/python/lancedb/remote/table.py index 82da3779f..5bd446775 100644 --- a/python/python/lancedb/remote/table.py +++ b/python/python/lancedb/remote/table.py @@ -964,10 +964,14 @@ class RemoteTable(Table): *, computed: Dict[str, str] | None = None, ) -> AddColumnsResult: - return LOOP.run(self._table.add_columns(transforms, computed=computed)) + if computed: + raise NotImplementedError( + "computed columns are supported only on local tables" + ) + return LOOP.run(self._table.add_columns(transforms)) def refresh_column(self, column: str): - return LOOP.run(self._table.refresh_column(column)) + raise NotImplementedError("computed columns are supported only on local tables") def alter_columns( self, *alterations: Iterable[Dict[str, str]] diff --git a/rust/lancedb/src/remote/table.rs b/rust/lancedb/src/remote/table.rs index 0d843dd54..e48a9b571 100644 --- a/rust/lancedb/src/remote/table.rs +++ b/rust/lancedb/src/remote/table.rs @@ -2706,6 +2706,13 @@ impl BaseTable for RemoteTable { Ok(result) } + // A declaration reaches here as AllNulls, which the remote protocol + // has no representation for. + NewColumnTransform::AllNulls(_) => { + return Err(Error::NotSupported { + message: "computed columns are supported only on local tables".into(), + }); + } _ => { return Err(Error::NotSupported { message: "Only SQL expressions are supported for adding columns".into(), @@ -6455,6 +6462,37 @@ mod tests { assert_eq!(result.version, if old_server { 0 } else { 43 }); } + /// Computed columns are local-only. Both halves say so here rather than + /// reaching the wire and failing somewhere less legible. + #[tokio::test] + async fn test_computed_columns_are_refused() { + let table = Table::new_with_handler("my_table", |request| -> http::Response { + panic!("unexpected request: {}", request.url().path()) + }); + + let declared = Arc::new(Schema::new(vec![Field::new( + "doubled", + DataType::Int32, + true, + )])); + let err = table + .add_columns() + .transform(NewColumnTransform::AllNulls(declared)) + .execute() + .await + .unwrap_err(); + assert!( + matches!(&err, Error::NotSupported { message } if message.contains("local tables")), + "{err:?}" + ); + + let err = table.refresh_column("doubled").await.unwrap_err(); + assert!( + matches!(&err, Error::NotSupported { message } if message.contains("local tables")), + "{err:?}" + ); + } + #[tokio::test] async fn test_prewarm_index() { let table = Table::new_with_handler("my_table", |request| { diff --git a/rust/lancedb/src/table.rs b/rust/lancedb/src/table.rs index bb85df8a8..2b842aa74 100644 --- a/rust/lancedb/src/table.rs +++ b/rust/lancedb/src/table.rs @@ -793,7 +793,7 @@ pub trait BaseTable: std::fmt::Display + std::fmt::Debug + Send + Sync { /// The default returns `NotSupported`; Lance-backed tables override it. async fn refresh_column(&self, _column: &str) -> Result { Err(Error::NotSupported { - message: "refresh_column is not supported on this table type".into(), + message: "computed columns are supported only on local tables".into(), }) } /// Alter columns in the table. @@ -1688,7 +1688,25 @@ impl Table { AddColumnsBuilder::new(self.inner.clone()) } - /// Compute and store values for a computed column's unfilled rows. + /// Fill the fragments of a computed column that hold no values yet. + /// + /// Declared with + /// [`AddColumnsBuilder::computed`](add_columns::AddColumnsBuilder::computed), + /// a column starts empty and gets its values here. Fragments appended + /// since the last refresh are filled by the next one; fragments already + /// filled are left as they are, so the call is idempotent and does not + /// observe a mutated input. + /// + /// Local tables only. + /// + /// ``` + /// # use lancedb::Table; + /// # async fn refresh(table: &Table) -> Result<(), Box> { + /// let result = table.refresh_column("doubled").await?; + /// println!("filled {} rows at version {}", result.rows_filled, result.version); + /// # Ok(()) + /// # } + /// ``` pub async fn refresh_column(&self, column: impl AsRef) -> Result { self.inner.refresh_column(column.as_ref()).await }