mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-23 12:12:27 +00:00
fix: validate Python read consistency intervals (#4454)
Negative `read_consistency_interval` values currently reach Rust's
panicking `Duration::from_secs_f64` conversion. For example:
```python
from datetime import timedelta
import lancedb
lancedb.connect("./db", read_consistency_interval=timedelta(seconds=-1))
```
Validate the interval with `Duration::try_from_secs_f64` at the three
native connection entry points and return `ValueError` before
constructing the connection or async future. `None`, zero and positive
intervals retain their behavior. The raw native binding also rejects
NaN, infinity and overflow; the public Python API still accepts
`timedelta`, not floats.
Tests cover public sync/async connections, native namespace connections,
real Python namespace clients, the three raw binding entry points and
valid-value controls.
Validation on Python 3.12 / Rust 1.97:
- Same 26 regression cases against separately built native baseline and
candidate: baseline 22 failed / 4 passed; candidate 26 passed.
- Existing basic connection and background-loop regression tests pass.
- `cargo fmt --all -- --check`, Ruff check/format and `git diff --check`
pass.
The native builds used the unchanged repository `ci` profile and
Cargo.lock with `--no-default-features --features
remote,lancedb/metrics-otel,pyo3/extension-module`. Python runtime and
focused test dependency versions came from `python/uv.lock`. Source-tree
tests used a minimal distribution-version metadata fixture derived from
the unchanged Cargo/pyproject manifests; both imported module origins
and native versions were checked in fresh processes. This is not a
complete Maturin packaging, cloud backend or full Python suite result.
No GPU was used.
AI-assisted diagnosis, implementation and validation.
Signed-off-by: Xucheng Zhou <aden1350@outlook.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import lancedb
|
||||
import pytest
|
||||
from lance_namespace import connect as connect_namespace_client
|
||||
from lancedb import _lancedb
|
||||
from lancedb.namespace import LanceNamespaceDBConnection
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seconds", [-1.0, -0.000001])
|
||||
@pytest.mark.parametrize("kind", ["sync", "namespace", "namespace_async", "client"])
|
||||
def test_negative_read_consistency_interval(tmp_path, seconds, kind):
|
||||
interval = timedelta(seconds=seconds)
|
||||
with pytest.raises(ValueError, match="read_consistency_interval"):
|
||||
if kind == "sync":
|
||||
lancedb.connect(tmp_path, read_consistency_interval=interval)
|
||||
elif kind == "client":
|
||||
client = connect_namespace_client("dir", {"root": str(tmp_path)})
|
||||
LanceNamespaceDBConnection(client, read_consistency_interval=interval)
|
||||
else:
|
||||
connect = (
|
||||
lancedb.connect_namespace
|
||||
if kind == "namespace"
|
||||
else lancedb.connect_namespace_async
|
||||
)
|
||||
connect("dir", {"root": str(tmp_path)}, read_consistency_interval=interval)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("seconds", [-1.0, -0.000001])
|
||||
async def test_negative_async_read_consistency_interval(tmp_path, seconds):
|
||||
with pytest.raises(ValueError, match="read_consistency_interval"):
|
||||
await lancedb.connect_async(
|
||||
tmp_path, read_consistency_interval=timedelta(seconds=seconds)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("seconds", [float("nan"), float("inf"), -float("inf"), 1e30])
|
||||
@pytest.mark.parametrize("kind", ["connect", "namespace", "client"])
|
||||
async def test_binding_rejects_invalid_read_consistency_interval(
|
||||
tmp_path, seconds, kind
|
||||
):
|
||||
with pytest.raises(ValueError, match="read_consistency_interval"):
|
||||
if kind == "connect":
|
||||
await _lancedb.connect(str(tmp_path), read_consistency_interval=seconds)
|
||||
elif kind == "namespace":
|
||||
_lancedb.connect_namespace(
|
||||
"dir", {"root": str(tmp_path)}, read_consistency_interval=seconds
|
||||
)
|
||||
else:
|
||||
client = connect_namespace_client("dir", {"root": str(tmp_path)})
|
||||
_lancedb.connect_namespace_client(client, read_consistency_interval=seconds)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"interval", [None, timedelta(0), timedelta(microseconds=1), timedelta(seconds=5)]
|
||||
)
|
||||
async def test_valid_read_consistency_intervals(tmp_path, interval):
|
||||
db = lancedb.connect(tmp_path / "sync", read_consistency_interval=interval)
|
||||
assert db.read_consistency_interval == interval
|
||||
db.close()
|
||||
|
||||
db = await lancedb.connect_async(
|
||||
tmp_path / "async", read_consistency_interval=interval
|
||||
)
|
||||
assert await db.get_read_consistency_interval() == interval
|
||||
db.close()
|
||||
|
||||
db = lancedb.connect_namespace(
|
||||
"dir", {"root": str(tmp_path / "namespace")}, read_consistency_interval=interval
|
||||
)
|
||||
assert db.read_consistency_interval == interval
|
||||
db.close()
|
||||
|
||||
client = connect_namespace_client("dir", {"root": str(tmp_path / "client")})
|
||||
db = LanceNamespaceDBConnection(client, read_consistency_interval=interval)
|
||||
assert db.read_consistency_interval == interval
|
||||
db.close()
|
||||
@@ -49,6 +49,18 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_read_consistency_interval(interval: Option<f64>) -> PyResult<Option<Duration>> {
|
||||
interval
|
||||
.map(|seconds| {
|
||||
Duration::try_from_secs_f64(seconds).map_err(|_| {
|
||||
PyValueError::new_err(
|
||||
"read_consistency_interval must be finite, non-negative, and within the supported range",
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn parse_namespace_client_pushdown_operations(
|
||||
operations: Option<Vec<String>>,
|
||||
) -> PyResult<HashSet<NamespaceClientPushdownOperation>> {
|
||||
@@ -1077,6 +1089,7 @@ pub fn connect(
|
||||
namespace_client_properties: Option<HashMap<String, String>>,
|
||||
oauth_config: Option<crate::oauth::PyOAuthConfig>,
|
||||
) -> PyResult<Bound<'_, PyAny>> {
|
||||
let read_consistency_interval = parse_read_consistency_interval(read_consistency_interval)?;
|
||||
future_into_py(py, async move {
|
||||
let mut builder = lancedb::connect(&uri);
|
||||
if let Some(api_key) = api_key {
|
||||
@@ -1095,7 +1108,6 @@ pub fn connect(
|
||||
#[cfg(not(feature = "remote"))]
|
||||
let _ = sql_host_override;
|
||||
if let Some(read_consistency_interval) = read_consistency_interval {
|
||||
let read_consistency_interval = Duration::from_secs_f64(read_consistency_interval);
|
||||
builder = builder.read_consistency_interval(read_consistency_interval);
|
||||
}
|
||||
if let Some(storage_options) = storage_options {
|
||||
@@ -1144,7 +1156,7 @@ pub fn connect_namespace_client(
|
||||
namespace_client_impl: Option<String>,
|
||||
namespace_client_properties: Option<HashMap<String, String>>,
|
||||
) -> PyResult<Connection> {
|
||||
let read_consistency_interval = read_consistency_interval.map(Duration::from_secs_f64);
|
||||
let read_consistency_interval = parse_read_consistency_interval(read_consistency_interval)?;
|
||||
let namespace_client_pushdown_operations =
|
||||
parse_namespace_client_pushdown_operations(namespace_client_pushdown_operations)?;
|
||||
let ns_properties = namespace_client_properties.unwrap_or_default();
|
||||
@@ -1201,7 +1213,7 @@ pub fn connect_namespace(
|
||||
session: Option<crate::session::Session>,
|
||||
namespace_client_pushdown_operations: Option<Vec<String>>,
|
||||
) -> PyResult<Connection> {
|
||||
let read_consistency_interval = read_consistency_interval.map(Duration::from_secs_f64);
|
||||
let read_consistency_interval = parse_read_consistency_interval(read_consistency_interval)?;
|
||||
let namespace_client_pushdown_operations =
|
||||
parse_namespace_client_pushdown_operations(namespace_client_pushdown_operations)?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user