mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 14:16:54 +00:00
feat: add UTM-tagged docs links to SDK warnings and enterprise-only errors (#4797)
Add a central documentation link registry (`lancedb::docs::DocLink`) in the Rust core that builds `docs.lancedb.com` URLs with UTM parameters (`utm_source=lancedb_sdk`, `utm_medium=warning|error`, `utm_campaign=<id>`, `utm_content=<sdk>-<version>`) placed before any `#anchor` fragment and without user-identifying data. - Initialize `utm_content` per language binding (`python`, `nodejs`, `cli`, defaulting to `rust`) and expose the link builder to Python so `DBConnection` and `Job` stubs reuse the core definitions. - Append published documentation links to Enterprise-only `NotSupported` / `NotImplementedError` errors across database, job, catalog, and table operations. - Emit rate-limited maintenance warnings (once per table per process when `warn` logging is enabled) when opening a native table with at least 100 small fragments (`< 100k` physical rows each) or at least 1,000,000 unindexed rows on an optimizable user index, reading only manifest metadata without opening data or `_indices/` files, and document the thresholds in the compaction and reindexing guides. - Verify published/in-repo `docs/web/` targets and `#anchor` fragments in unit tests and include extracted SDK links in the scheduled `docs-link-check` workflow. Linear: OSS-2772
This commit is contained in:
@@ -27,8 +27,11 @@ from lance_namespace import (
|
||||
from .remote import ClientConfig
|
||||
from .types import BaseTokenizerType
|
||||
|
||||
__version__: str
|
||||
_experimental_functions_enabled: bool
|
||||
|
||||
def _doc_url(campaign: str) -> str: ...
|
||||
def _append_doc_url(message: str, campaign: str) -> str: ...
|
||||
def _parse_field_path(path: str) -> List[str]: ...
|
||||
|
||||
IvfHnswPq: type[HnswPq] = HnswPq
|
||||
|
||||
+126
-30
@@ -57,7 +57,7 @@ from lance_namespace import (
|
||||
from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError
|
||||
|
||||
from . import __version__
|
||||
from ._lancedb import connect as lancedb_connect # type: ignore
|
||||
from ._lancedb import _append_doc_url, connect as lancedb_connect # type: ignore
|
||||
from .functions import FunctionVersion, UdfDefinition
|
||||
from .graph import (
|
||||
AsyncPropertyGraph,
|
||||
@@ -718,7 +718,10 @@ class DBConnection(EnforceOverrides):
|
||||
MaterializedView
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"materialized views are not supported on this connection type"
|
||||
_append_doc_url(
|
||||
"materialized views are not supported on this connection type",
|
||||
"materialized_views",
|
||||
)
|
||||
)
|
||||
|
||||
def create_materialized_view_async(
|
||||
@@ -743,7 +746,10 @@ class DBConnection(EnforceOverrides):
|
||||
Wait for the job before opening or querying the view.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"materialized views are not supported on this connection type"
|
||||
_append_doc_url(
|
||||
"materialized views are not supported on this connection type",
|
||||
"materialized_views",
|
||||
)
|
||||
)
|
||||
|
||||
def open_materialized_view(self, name: str) -> MaterializedView:
|
||||
@@ -753,7 +759,10 @@ class DBConnection(EnforceOverrides):
|
||||
view.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"materialized views are not supported on this connection type"
|
||||
_append_doc_url(
|
||||
"materialized views are not supported on this connection type",
|
||||
"materialized_views",
|
||||
)
|
||||
)
|
||||
|
||||
def list_materialized_views(
|
||||
@@ -768,7 +777,10 @@ class DBConnection(EnforceOverrides):
|
||||
iteration. See [pagination state][lancedb.listing.AsyncListing].
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"materialized views are not supported on this connection type"
|
||||
_append_doc_url(
|
||||
"materialized views are not supported on this connection type",
|
||||
"materialized_views",
|
||||
)
|
||||
)
|
||||
|
||||
def drop_materialized_view(
|
||||
@@ -781,7 +793,10 @@ class DBConnection(EnforceOverrides):
|
||||
Physical cleanup continues in the background.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"materialized views are not supported on this connection type"
|
||||
_append_doc_url(
|
||||
"materialized views are not supported on this connection type",
|
||||
"materialized_views",
|
||||
)
|
||||
)
|
||||
|
||||
def drop_materialized_view_async(
|
||||
@@ -795,7 +810,10 @@ class DBConnection(EnforceOverrides):
|
||||
catalog removal; physical cleanup continues in the background.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"materialized views are not supported on this connection type"
|
||||
_append_doc_url(
|
||||
"materialized views are not supported on this connection type",
|
||||
"materialized_views",
|
||||
)
|
||||
)
|
||||
|
||||
def drop_table(self, name: str, namespace_path: Optional[List[str]] = None):
|
||||
@@ -994,7 +1012,11 @@ class DBConnection(EnforceOverrides):
|
||||
LanceDB Embedded connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Function catalog operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"Function catalog operations are not supported "
|
||||
"for this connection type",
|
||||
"functions",
|
||||
)
|
||||
)
|
||||
|
||||
def get_function(
|
||||
@@ -1006,7 +1028,11 @@ class DBConnection(EnforceOverrides):
|
||||
) -> FunctionVersion:
|
||||
"""Open one exact immutable Function version from the remote catalog."""
|
||||
raise NotImplementedError(
|
||||
"Function catalog operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"Function catalog operations are not supported "
|
||||
"for this connection type",
|
||||
"functions",
|
||||
)
|
||||
)
|
||||
|
||||
def list_functions(
|
||||
@@ -1037,7 +1063,11 @@ class DBConnection(EnforceOverrides):
|
||||
iteration. See [pagination state][lancedb.listing.AsyncListing].
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Function catalog operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"Function catalog operations are not supported "
|
||||
"for this connection type",
|
||||
"functions",
|
||||
)
|
||||
)
|
||||
|
||||
def drop_function(
|
||||
@@ -1058,7 +1088,11 @@ class DBConnection(EnforceOverrides):
|
||||
revision. Local connections raise NotImplementedError.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Function catalog operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"Function catalog operations are not supported "
|
||||
"for this connection type",
|
||||
"functions",
|
||||
)
|
||||
)
|
||||
|
||||
def drop_function_async(
|
||||
@@ -1078,7 +1112,11 @@ class DBConnection(EnforceOverrides):
|
||||
connections raise NotImplementedError.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Function catalog operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"Function catalog operations are not supported "
|
||||
"for this connection type",
|
||||
"functions",
|
||||
)
|
||||
)
|
||||
|
||||
def create_secret(
|
||||
@@ -1092,7 +1130,10 @@ class DBConnection(EnforceOverrides):
|
||||
connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Secret operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"Secret operations are not supported for this connection type",
|
||||
"secrets",
|
||||
)
|
||||
)
|
||||
|
||||
def alter_secret(
|
||||
@@ -1106,7 +1147,10 @@ class DBConnection(EnforceOverrides):
|
||||
before it. LanceDB Embedded connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Secret operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"Secret operations are not supported for this connection type",
|
||||
"secrets",
|
||||
)
|
||||
)
|
||||
|
||||
def list_secrets(
|
||||
@@ -1127,7 +1171,10 @@ class DBConnection(EnforceOverrides):
|
||||
iteration. See [pagination state][lancedb.listing.AsyncListing].
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Secret operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"Secret operations are not supported for this connection type",
|
||||
"secrets",
|
||||
)
|
||||
)
|
||||
|
||||
def drop_secret(
|
||||
@@ -1141,7 +1188,10 @@ class DBConnection(EnforceOverrides):
|
||||
LanceDB Embedded connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Secret operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"Secret operations are not supported for this connection type",
|
||||
"secrets",
|
||||
)
|
||||
)
|
||||
|
||||
def describe_secret(
|
||||
@@ -1153,7 +1203,10 @@ class DBConnection(EnforceOverrides):
|
||||
connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Secret operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"Secret operations are not supported for this connection type",
|
||||
"secrets",
|
||||
)
|
||||
)
|
||||
|
||||
def create_view(
|
||||
@@ -1178,7 +1231,10 @@ class DBConnection(EnforceOverrides):
|
||||
name: string
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"View operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"View operations are not supported for this connection type",
|
||||
"views",
|
||||
)
|
||||
)
|
||||
|
||||
def describe_view(
|
||||
@@ -1192,7 +1248,10 @@ class DBConnection(EnforceOverrides):
|
||||
``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"View operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"View operations are not supported for this connection type",
|
||||
"views",
|
||||
)
|
||||
)
|
||||
|
||||
def drop_view(
|
||||
@@ -1206,7 +1265,10 @@ class DBConnection(EnforceOverrides):
|
||||
LanceDB Embedded connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"View operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"View operations are not supported for this connection type",
|
||||
"views",
|
||||
)
|
||||
)
|
||||
|
||||
def drop_view_async(
|
||||
@@ -1219,7 +1281,10 @@ class DBConnection(EnforceOverrides):
|
||||
in the background. Local connections raise ``NotImplementedError``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"View operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"View operations are not supported for this connection type",
|
||||
"views",
|
||||
)
|
||||
)
|
||||
|
||||
def list_views(
|
||||
@@ -1240,7 +1305,10 @@ class DBConnection(EnforceOverrides):
|
||||
iteration. See [pagination state][lancedb.listing.AsyncListing].
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"View operations are not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"View operations are not supported for this connection type",
|
||||
"views",
|
||||
)
|
||||
)
|
||||
|
||||
def create_property_graph(
|
||||
@@ -1347,7 +1415,12 @@ class DBConnection(EnforceOverrides):
|
||||
Raises `JobNotFoundError` when the server has no such job, the way
|
||||
`open_table` does for a missing table.
|
||||
"""
|
||||
raise NotImplementedError("open_job is not supported for this connection type")
|
||||
raise NotImplementedError(
|
||||
_append_doc_url(
|
||||
"open_job is not supported for this connection type",
|
||||
"jobs",
|
||||
)
|
||||
)
|
||||
|
||||
def list_gpu_classes(self) -> List[GpuClass]:
|
||||
"""List the operator-configured Function GPU classes.
|
||||
@@ -1361,7 +1434,10 @@ class DBConnection(EnforceOverrides):
|
||||
``GET /v1/gpu/classes``.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"GPU class discovery is not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"GPU class discovery is not supported for this connection type",
|
||||
"gpu_classes",
|
||||
)
|
||||
)
|
||||
|
||||
def list_jobs(
|
||||
@@ -1373,7 +1449,12 @@ class DBConnection(EnforceOverrides):
|
||||
``page_token`` resumes from a saved token. Requests and errors occur during
|
||||
iteration. See [pagination state][lancedb.listing.AsyncListing].
|
||||
"""
|
||||
raise NotImplementedError("list_jobs is not supported for this connection type")
|
||||
raise NotImplementedError(
|
||||
_append_doc_url(
|
||||
"list_jobs is not supported for this connection type",
|
||||
"jobs",
|
||||
)
|
||||
)
|
||||
|
||||
def cancel_job(self, job_id: str) -> bool:
|
||||
"""Request cancellation of a server-side job by id.
|
||||
@@ -1383,7 +1464,10 @@ class DBConnection(EnforceOverrides):
|
||||
success.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"cancel_job is not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"cancel_job is not supported for this connection type",
|
||||
"jobs",
|
||||
)
|
||||
)
|
||||
|
||||
def pause_job(self, job_id: str) -> str:
|
||||
@@ -1393,7 +1477,12 @@ class DBConnection(EnforceOverrides):
|
||||
"pausing", "already_paused", or "committing" -- a job finalizing its
|
||||
results cannot be parked; retry shortly.
|
||||
"""
|
||||
raise NotImplementedError("pause_job is not supported for this connection type")
|
||||
raise NotImplementedError(
|
||||
_append_doc_url(
|
||||
"pause_job is not supported for this connection type",
|
||||
"jobs",
|
||||
)
|
||||
)
|
||||
|
||||
def resume_job(self, job_id: str) -> str:
|
||||
"""Resume a paused server-side job by id.
|
||||
@@ -1403,7 +1492,10 @@ class DBConnection(EnforceOverrides):
|
||||
confirmed yet; retry shortly -- or "not_paused".
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"resume_job is not supported for this connection type"
|
||||
_append_doc_url(
|
||||
"resume_job is not supported for this connection type",
|
||||
"jobs",
|
||||
)
|
||||
)
|
||||
|
||||
def execute_query(
|
||||
@@ -1459,11 +1551,15 @@ class DBConnection(EnforceOverrides):
|
||||
|
||||
LanceDB Embedded connections do not support SQL.
|
||||
"""
|
||||
raise NotImplementedError("SQL is not supported for this connection type")
|
||||
raise NotImplementedError(
|
||||
_append_doc_url("SQL is not supported for this connection type", "sql")
|
||||
)
|
||||
|
||||
def describe_query(self, query_id: Union[UUID, str]) -> QueryDescription:
|
||||
"""Describe a submitted SQL query by its connection-scoped id."""
|
||||
raise NotImplementedError("SQL is not supported for this connection type")
|
||||
raise NotImplementedError(
|
||||
_append_doc_url("SQL is not supported for this connection type", "sql")
|
||||
)
|
||||
|
||||
|
||||
class LanceDBConnection(DBConnection):
|
||||
|
||||
@@ -13,7 +13,7 @@ import pyarrow as pa
|
||||
from lancedb.background_loop import LOOP
|
||||
|
||||
from . import _lancedb
|
||||
from ._lancedb import JobDescription, JobFailureInfo, JobInfo
|
||||
from ._lancedb import JobDescription, JobFailureInfo, JobInfo, _append_doc_url
|
||||
from .remote.errors import HttpError, RetryError
|
||||
|
||||
T = TypeVar("T")
|
||||
@@ -266,7 +266,10 @@ class AsyncJob(Generic[T]):
|
||||
"""
|
||||
if self._inner is None:
|
||||
raise NotImplementedError(
|
||||
"job event history is only available for server-side jobs"
|
||||
_append_doc_url(
|
||||
"job event history is only available for server-side jobs",
|
||||
"jobs",
|
||||
)
|
||||
)
|
||||
return await self._inner.events(limit=limit, filter=filter)
|
||||
|
||||
@@ -431,7 +434,10 @@ class Job(Generic[T]):
|
||||
"""
|
||||
if self._inner is None:
|
||||
raise NotImplementedError(
|
||||
"job event history is only available for server-side jobs"
|
||||
_append_doc_url(
|
||||
"job event history is only available for server-side jobs",
|
||||
"jobs",
|
||||
)
|
||||
)
|
||||
return LOOP.run(self._inner.events(limit=limit, filter=filter))
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
import lancedb
|
||||
from lancedb._lancedb import __version__ as _core_version, _append_doc_url, _doc_url
|
||||
from lancedb.db import DBConnection
|
||||
from lancedb.job import AsyncJob, Job
|
||||
import pytest
|
||||
|
||||
|
||||
def test_python_sdk_utm_content_and_doc_links(tmp_path):
|
||||
compaction_url = _doc_url("compaction")
|
||||
expected_content = f"utm_content=python-{_core_version}"
|
||||
assert (
|
||||
compaction_url
|
||||
== f"https://docs.lancedb.com/performance?utm_source=lancedb_sdk&utm_medium=warning&utm_campaign=compaction&{expected_content}#compaction-and-cleanup"
|
||||
)
|
||||
|
||||
db = lancedb.connect(tmp_path)
|
||||
with pytest.raises(NotImplementedError) as sql_exc:
|
||||
db.execute_query("SELECT 1")
|
||||
assert _doc_url("sql") in str(sql_exc.value)
|
||||
|
||||
with pytest.raises(NotImplementedError) as gpu_exc:
|
||||
db.list_gpu_classes()
|
||||
assert _doc_url("gpu_classes") in str(gpu_exc.value)
|
||||
|
||||
with pytest.raises(NotImplementedError) as job_exc:
|
||||
db.open_job("job-1")
|
||||
assert _doc_url("jobs") in str(job_exc.value)
|
||||
|
||||
with pytest.raises(NotImplementedError) as secret_exc:
|
||||
db.create_secret("s", "v")
|
||||
assert _doc_url("secrets") in str(secret_exc.value)
|
||||
|
||||
with pytest.raises(NotImplementedError) as view_exc:
|
||||
db.create_view("v", "SELECT 1")
|
||||
assert _doc_url("views") in str(view_exc.value)
|
||||
|
||||
with pytest.raises(NotImplementedError) as mv_exc:
|
||||
DBConnection.open_materialized_view(db, "mv")
|
||||
assert _doc_url("materialized_views") in str(mv_exc.value)
|
||||
|
||||
with pytest.raises(NotImplementedError) as create_mv_exc:
|
||||
DBConnection.create_materialized_view(db, "mv", "SELECT 1")
|
||||
assert _doc_url("materialized_views") in str(create_mv_exc.value)
|
||||
|
||||
local_job = Job(AsyncJob(None))
|
||||
with pytest.raises(NotImplementedError) as events_exc:
|
||||
local_job.events()
|
||||
assert _doc_url("jobs") in str(events_exc.value)
|
||||
|
||||
appended = _append_doc_url("Something failed.", "sql")
|
||||
assert appended.endswith(f"See {_doc_url('sql')}")
|
||||
@@ -58,8 +58,25 @@ fn shutdown_runtime(py: Python<'_>) {
|
||||
py.detach(|| runtime::shutdown(std::time::Duration::from_secs(5)));
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _doc_url(campaign: &str) -> PyResult<String> {
|
||||
let link = lancedb::docs::DocLink::from_campaign(campaign).ok_or_else(|| {
|
||||
pyo3::exceptions::PyValueError::new_err(format!("unknown doc link campaign: {campaign}"))
|
||||
})?;
|
||||
Ok(link.url())
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _append_doc_url(message: &str, campaign: &str) -> PyResult<String> {
|
||||
let link = lancedb::docs::DocLink::from_campaign(campaign).ok_or_else(|| {
|
||||
pyo3::exceptions::PyValueError::new_err(format!("unknown doc link campaign: {campaign}"))
|
||||
})?;
|
||||
Ok(link.append_to(message))
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
pub fn _lancedb(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
lancedb::docs::set_sdk_ident("python", env!("CARGO_PKG_VERSION"));
|
||||
let env = Env::new()
|
||||
.filter_or("LANCEDB_LOG", "warn")
|
||||
.write_style("LANCEDB_LOG_STYLE");
|
||||
@@ -133,6 +150,8 @@ pub fn _lancedb(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(expr_col, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(expr_lit, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(expr_func, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(_doc_url, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(_append_doc_url, m)?)?;
|
||||
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
|
||||
m.add(
|
||||
"_experimental_functions_enabled",
|
||||
|
||||
Reference in New Issue
Block a user