fix(python): implement remote namespace and table existence checks (#4725)

Remote Python connections inherited the unsupported-operation stubs for
`namespace_exists` and `table_exists`, so both raised
`NotImplementedError` even when namespace and table operations worked.

Add the missing remote overrides, async methods, and PyO3 bindings using
the existing namespace describe and table open paths. Only an explicit
not-found result returns `False`; permission, transport, and server
errors propagate. This preserves authentication, dynamic headers,
retries, and nested namespace handling, and rechecks cached tables
before reporting that they exist.

The async namespace helper also recognizes native `NamespaceNotFound`
errors so missing local namespaces return `False`. The table and
namespace guides document Python remote support and distinguish it from
the unsupported `exists` routes used by direct REST namespace clients.

Validation:
- All 40 focused regression cases pass, covering successful and missing
lookups, local and remote namespaces, dynamic headers, permission/server
errors, invalid table identifiers, and cached tables that disappear.
- The original inherited stubs reproduce the reported
`NotImplementedError`; the local namespace regression reproduced the
native missing-namespace `RuntimeError` before its fix.
- Full remote connection suite: 255 passed. Local namespace tests: 14
passed.
- `cargo clippy --quiet -p lancedb-python --features remote --lib`,
`cargo fmt --all`, Python Ruff lint, and Python formatting checks pass.
- Root `ruff check .` reports 48 existing errors in documentation
examples under `docs/web-tests`; the Python subtree passes and those
unrelated files are unchanged.

Fixes #4722

<!-- lance-gatekeeper-fix:v1 agent=4d369a0681756fdb5a8b35853d7f5607
generation=1 -->

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
This commit is contained in:
lancedb-gatefixer[bot]
2026-10-28 16:08:43 -07:00
committed by GitHub
parent fc5170cbed
commit 741cb9ad4e
8 changed files with 257 additions and 11 deletions
+47
View File
@@ -711,6 +711,53 @@ impl Connection {
})
}
pub fn namespace_exists(
self_: PyRef<'_, Self>,
namespace_id: Vec<String>,
) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.get_inner()?.clone();
future_into_py(self_.py(), async move {
let request = lance_namespace::models::DescribeNamespaceRequest {
id: Some(namespace_id),
..Default::default()
};
match inner.describe_namespace(request).await {
Ok(_) => Ok(true),
Err(lancedb::Error::Http {
status_code: Some(status),
..
}) if status.as_u16() == 404 => Ok(false),
Err(lancedb::Error::Lance {
source: lance_core::Error::Namespace { ref source, .. },
}) if matches!(
source.downcast_ref::<lance_namespace::NamespaceError>(),
Some(lance_namespace::NamespaceError::NamespaceNotFound { .. })
) =>
{
Ok(false)
}
Err(err) => Err(err).infer_error(),
}
})
}
pub fn table_exists(
self_: PyRef<'_, Self>,
mut table_id: Vec<String>,
) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.get_inner()?.clone();
let name = table_id
.pop()
.ok_or_else(|| PyValueError::new_err("table_id must include a table name"))?;
future_into_py(self_.py(), async move {
match inner.open_table(name).namespace(table_id).execute().await {
Ok(_) => Ok(true),
Err(lancedb::Error::TableNotFound { .. }) => Ok(false),
Err(err) => Err(err).infer_error(),
}
})
}
#[pyo3(signature = (namespace_path=None, page_token=None, limit=None))]
pub fn list_tables(
&self,