feat: expose exact Function column freshness (#4958)

## Problem

Function-backed columns need an exact `fresh`, `stale`,
`never_computed`, and
`failed` partition for the table version a caller is reading. The
existing
error listing is capped, and composing it with job and schema listings
cannot
prove which rows currently failed.

## Behavior

- Adds a strict freshness reader beside the existing permissive planner.
It
  distinguishes an absent certificate from legacy evidence that omits
  failures, and from a current certificate carrying failure coverage.
- Extends the immutable freshness sidecar with per-fragment known and
failed
  row bitmaps. Partial or `WHERE` publications cover only observed rows;
  untouched rows stay unknown. Deleted rows do not block classification.
- Adds failure-aware whole-fragment and row-repair writers. A rejected
older
attempt cannot overwrite failure evidence already recorded for a newer
input
  signature.
- Adds `column_status`, `column_statuses`, and `wait_fresh` to Python
and the
  corresponding Node methods. The client calls the server's exact status
endpoint, validates the four-way partition and one-version guarantee,
and
preserves the complete Function version, last-job, and automatic-refresh
  metadata.
- Adds an MDX-commented draft usage section to the Enterprise
computed-column
refresh guide, covering Python, Rust, and TypeScript and remaining
hidden
  until the server integration ships.
- Sends `column`, `branch`, and exact `version` selectors to the status
route.
A pinned handle rejects a nonempty response from any other table
version;
  latest handles keep the existing read-freshness headers.
- Retries `wait_fresh` only for HTTP 503 with
`function_column_status_finalization_pending`. Existing failed rows
remain
  visible and do not keep the wait pending; other errors propagate.
- Models the shared scheduler projection, including `up_to_date`, real
`blocked` states, their reason, failure streak, and effective cooldown.

## Validation

This is the SDK side of ENT-3563. Sophon's failure-coverage publication
and
status endpoint are a separate integration; deployed end-to-end and
whole-ticket
acceptance remain pending.

- Rust: 9 exact freshness/status tests passed, plus 1 exact
latest-handle
  freshness-header test. The pinned transport test was extended with a
wrong-version response and rerun at `ebd6649a` (1 passed). Coverage
includes untouched partial rows,
  selected-row failure clearing, cross-generation evidence, typed and
non-retried errors, `up_to_date`, blocked scheduler fields, and
exact-version
  requests.
- Python: 2 native PyO3 remote-transport tests passed together,
including
typed retry; one local facade test reaches the existing unsupported
boundary.
- Node: the NAPI addon built successfully, TypeScript and Biome passed,
and the
selected native remote-transport test passed 1/0, including
exact-version
  selection and blocked scheduler metadata.
- `cargo fmt`, `git diff --check`, Ruff format/lint, Python compile,
Biome, and
  TypeScript checks passed.

Validation is mapped by the relevant unchanged source rather than
treated as one
final-head run: the nine Rust tests ran at `e4d51fc8`, the latest-header
and Python
remote tests at `31e68b7d`, the local Python test and Node build at
`6f213584`, and
the final TypeScript/Jest delta at `4f2d5c27`. The final product tree
excludes those five private EC2 runner files and adds
six generated TypeDoc files plus one hidden draft usage section in the
computed-column refresh guide. The Node delta corrected a private
TypeScript
command and reused the unchanged native addon.
This commit is contained in:
Xuanwo
2026-11-11 19:12:06 +08:00
committed by GitHub
parent 34e8b8087b
commit 7cc3904a7e
25 changed files with 3055 additions and 41 deletions
+8 -3
View File
@@ -16,9 +16,10 @@ use query::{FTSQuery, HybridQuery, Query, VectorQuery};
use session::Session;
use table::{
AddColumnsResult, AddResult, AlterColumnsResult, DeleteResult, DropColumnsResult, FtsToken,
FunctionErrorFragment, FunctionErrorRecord, FunctionErrors, MergeResult, PyBlobFile,
RTEWriteSpec, RefreshColumnResult, RefreshMaterializedViewResult, Table,
UpdateFieldMetadataResult, UpdateResult,
FunctionColumnAutoRefreshStatus, FunctionColumnLastRefreshJob, FunctionColumnStatus,
FunctionColumnVersionStatus, FunctionErrorFragment, FunctionErrorRecord, FunctionErrors,
MergeResult, PyBlobFile, RTEWriteSpec, RefreshColumnResult, RefreshMaterializedViewResult,
Table, UpdateFieldMetadataResult, UpdateResult,
};
pub mod arrow;
@@ -117,6 +118,10 @@ pub fn _lancedb(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<FunctionErrors>()?;
m.add_class::<FunctionErrorRecord>()?;
m.add_class::<FunctionErrorFragment>()?;
m.add_class::<FunctionColumnStatus>()?;
m.add_class::<FunctionColumnVersionStatus>()?;
m.add_class::<FunctionColumnLastRefreshJob>()?;
m.add_class::<FunctionColumnAutoRefreshStatus>()?;
m.add_class::<RefreshMaterializedViewResult>()?;
m.add_class::<AlterColumnsResult>()?;
m.add_class::<UpdateFieldMetadataResult>()?;
+175
View File
@@ -892,6 +892,136 @@ impl From<lancedb::function::FunctionErrors> for FunctionErrors {
}
}
/// Exact immutable Function revision attached to a table output.
#[pyclass(module = "lancedb._lancedb", get_all, from_py_object)]
#[derive(Clone, Debug)]
pub struct FunctionColumnVersionStatus {
pub name: String,
pub namespace_path: Vec<String>,
pub object_id: String,
pub location: String,
pub version: String,
pub manifest_digest: String,
}
impl From<lancedb::function::FunctionVersionRef> for FunctionColumnVersionStatus {
fn from(function: lancedb::function::FunctionVersionRef) -> Self {
Self {
name: function.name,
namespace_path: function.namespace_path,
object_id: function.object_id,
location: function.location,
version: function.version,
manifest_digest: function.manifest_digest,
}
}
}
/// Latest matching refresh Job projected for a Function output.
#[pyclass(module = "lancedb._lancedb", get_all, from_py_object)]
#[derive(Clone, Debug)]
pub struct FunctionColumnLastRefreshJob {
pub job_id: String,
pub state: String,
pub automatic: Option<bool>,
pub completed_at_millis: Option<i64>,
}
impl From<lancedb::table::FunctionColumnLastRefreshJob> for FunctionColumnLastRefreshJob {
fn from(job: lancedb::table::FunctionColumnLastRefreshJob) -> Self {
Self {
job_id: job.job_id,
state: job.state,
automatic: job.automatic,
completed_at_millis: job.completed_at_millis,
}
}
}
/// Automatic-refresh projection for a Function output.
#[pyclass(module = "lancedb._lancedb", get_all, from_py_object)]
#[derive(Clone, Debug)]
pub struct FunctionColumnAutoRefreshStatus {
pub enabled: bool,
pub interval_seconds: Option<u64>,
pub active_job_id: Option<String>,
pub state: String,
pub failure_streak: u32,
pub effective_cooldown_millis: Option<u64>,
pub blocked_reason: Option<String>,
pub next_run_at_millis: Option<i64>,
pub next_run_at_reason: Option<String>,
}
impl From<lancedb::table::FunctionColumnAutoRefreshStatus> for FunctionColumnAutoRefreshStatus {
fn from(status: lancedb::table::FunctionColumnAutoRefreshStatus) -> Self {
Self {
enabled: status.enabled,
interval_seconds: status.interval_seconds,
active_job_id: status.active_job_id,
state: status.state.as_str().to_string(),
failure_streak: status.failure_streak,
effective_cooldown_millis: status.effective_cooldown_millis,
blocked_reason: status
.blocked_reason
.map(|reason| reason.as_str().to_string()),
next_run_at_millis: status.next_run_at_millis,
next_run_at_reason: status.next_run_at_reason,
}
}
}
/// Exact four-way status of one Function output at one table version.
#[pyclass(module = "lancedb._lancedb", get_all, from_py_object)]
#[derive(Clone, Debug)]
pub struct FunctionColumnStatus {
pub column: String,
pub binding_id: String,
pub function: FunctionColumnVersionStatus,
pub table_version: u64,
pub rows: u64,
pub fresh: u64,
pub stale: u64,
pub never_computed: u64,
pub failed: u64,
pub last_refresh_job: Option<FunctionColumnLastRefreshJob>,
pub auto_refresh: FunctionColumnAutoRefreshStatus,
}
#[pymethods]
impl FunctionColumnStatus {
pub fn __repr__(&self) -> String {
format!(
"FunctionColumnStatus(column={:?}, table_version={}, rows={}, fresh={}, stale={}, never_computed={}, failed={})",
self.column,
self.table_version,
self.rows,
self.fresh,
self.stale,
self.never_computed,
self.failed
)
}
}
impl From<lancedb::table::FunctionColumnStatus> for FunctionColumnStatus {
fn from(status: lancedb::table::FunctionColumnStatus) -> Self {
Self {
column: status.column,
binding_id: status.binding_id,
function: status.function.into(),
table_version: status.table_version,
rows: status.rows,
fresh: status.fresh,
stale: status.stale,
never_computed: status.never_computed,
failed: status.failed,
last_refresh_job: status.last_refresh_job.map(Into::into),
auto_refresh: status.auto_refresh.into(),
}
}
}
#[pyclass(get_all, from_py_object)]
#[derive(Clone, Debug)]
pub struct RefreshMaterializedViewResult {
@@ -2425,6 +2555,51 @@ impl Table {
})
}
#[pyo3(signature = (column=None))]
pub fn column_statuses(
self_: PyRef<'_, Self>,
column: Option<String>,
) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.inner_ref()?.clone();
future_into_py(self_.py(), async move {
let statuses = inner
.function_column_statuses(column.as_deref())
.await
.infer_error()?;
Ok(statuses
.into_iter()
.map(FunctionColumnStatus::from)
.collect::<Vec<_>>())
})
}
pub fn column_status(self_: PyRef<'_, Self>, column: String) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.inner_ref()?.clone();
future_into_py(self_.py(), async move {
let status = inner.function_column_status(&column).await.infer_error()?;
Ok(FunctionColumnStatus::from(status))
})
}
#[pyo3(signature = (column, timeout=None))]
pub fn wait_column_fresh<'py>(
self_: PyRef<'py, Self>,
column: String,
timeout: Option<Bound<'py, PyAny>>,
) -> PyResult<Bound<'py, PyAny>> {
let timeout = timeout
.map(|timeout| timeout.extract::<std::time::Duration>())
.transpose()?;
let inner = self_.inner_ref()?.clone();
future_into_py(self_.py(), async move {
let status = inner
.wait_for_function_column(&column, timeout)
.await
.infer_error()?;
Ok(FunctionColumnStatus::from(status))
})
}
#[pyo3(signature = (source_version=None, *, full=false, cpu_millis=None, memory_bytes=None, gpu_count=None, gpu_class=None))]
#[allow(clippy::too_many_arguments)]
pub fn refresh_materialized_view(