fix: preserve all function revisions through SQL history (#4786)

`list_functions()` promises every published immutable revision. Request
`SHOW FUNCTIONS HISTORY` so the SQL server's latest-only default does
not hide older revisions, and preserve the server's numeric revision
ordering when constructing page cursors (`1`, `2`, `10`).

```python
[(fn.name, fn.version) for fn in db.list_functions(page_limit=2)]
# [("embed", "1"), ("embed", "2"), ("embed", "10")]
```

```sql
SHOW FUNCTIONS; -- latest visible revision per name
SHOW FUNCTIONS HISTORY WITH (limit = 3, include_definition = true);
SHOW FUNCTIONS HISTORY WITH (after_name = 'embed', after_version = '2', limit = 3);
```

Each listing page uses the existing SQL query and polling transport; the
continuation token stores the last returned name/revision and remains
bound to its database and namespace. Function mutations keep their
query-backed job handles: `Job.id` is the query's ID and `job_type` is
`query`, so waiting observes that same SQL execution. No registry job or
second query ID is added.

Update the SQL and Python documentation for exact-revision drops:
sibling revisions and grants survive, and only the last visible
revision's removal releases the name and schedules object cleanup.
Extend the existing Flight SQL regression to paginate revisions `1`,
`2`, and `10` and verify the explicit history statement. Formatting and
static Python checks pass; local builds and tests were intentionally not
run.
This commit is contained in:
Jack Ye
2026-11-02 01:15:39 +08:00
committed by GitHub
parent cbc24fa84f
commit eeb244fa57
6 changed files with 92 additions and 52 deletions
+8 -6
View File
@@ -564,13 +564,15 @@ itself is: as source, snapshotted at registration.
## Drop it {#drop}
`db.drop_function(name, version=...)` drops the function's name and the object
it is bound to; `version` must be a version that exists. It returns `False` if
the name was already gone. Deleting the object's content can finish after the
call returns, as a [background job](/maintenance/background-jobs).
`db.drop_function(name, version=...)` removes only that published revision from
lookup and listing, preserving other revisions and their grants. It returns
`False` for a repeated drop or a name that is already gone. A revision never
published under an existing name is an error. The last visible revision's drop
releases the name and schedules object cleanup, which can finish after the call
returns as a [background job](/maintenance/background-jobs).
`drop_function_async` returns `(dropped, job)`, where `job` is a query-backed
handle. Waiting confirms catalog
removal; physical cleanup can continue afterward.
handle. Waiting confirms the revision's catalog removal; physical cleanup can
continue afterward.
A deployment refuses the drop unless it also allows changes to a function's
lifecycle, which is a separate setting, off by default even where Functions are
+13 -7
View File
@@ -107,12 +107,15 @@ from it.
```sql
SHOW FUNCTIONS
SHOW FUNCTIONS IN "analytics" WITH (name = 'embed', limit = 100)
SHOW FUNCTIONS WITH (after_name = 'embed', after_version = '1', limit = 100)
SHOW FUNCTIONS HISTORY IN "analytics" WITH (name = 'embed', limit = 100)
SHOW FUNCTIONS HISTORY WITH (after_name = 'embed', after_version = '1', limit = 100)
```
Rows are ordered lexicographically by name and version. `after_name` and
`after_version` must be supplied together for keyset pagination. `limit` bounds
`SHOW FUNCTIONS` returns the highest published numeric revision for each name.
Add `HISTORY` to list every published revision, as Python's `list_functions()`
does. Rows are ordered by name and numeric revision, so `2` comes before `10`.
`after_name` and `after_version` must be supplied together for keyset pagination.
`limit` bounds
the response; `include_definition = false` omits the optional definition JSON.
## DROP FUNCTION {#drop}
@@ -122,9 +125,12 @@ DROP FUNCTION embed WITH (version = '1')
DROP FUNCTION IF EXISTS embed WITH (version = '1')
```
`version` must identify an existing revision. DROP releases the function name
and its object binding; physical cleanup continues separately. The result
reports `dropped` or `not_found`. `IF EXISTS` allows a missing name.
DROP removes only the requested revision from lookup and listing. Other
published revisions and their grants remain available. Dropping the last visible
revision releases the name and object binding; physical cleanup continues
separately. The result reports `dropped` or `not_found`. `IF EXISTS` allows a
missing name or a repeated drop of a previously published revision. A revision
that was never published under an existing name is an error.
All function object commands retain shared results even when general query-result
copying is disabled, so asynchronous polling can resume on another query node.
+30 -16
View File
@@ -1018,7 +1018,9 @@ class DBConnection(EnforceOverrides):
"""List every published immutable Function version in a namespace.
Functions in child namespaces are not included. Results are ordered by
Function name then version. LanceDB Embedded connections raise
Function name then numeric version. Remote connections use
``SHOW FUNCTIONS HISTORY`` to include every revision.
LanceDB Embedded connections raise
``NotImplementedError``.
Examples
@@ -1044,13 +1046,15 @@ class DBConnection(EnforceOverrides):
version: str,
namespace_path: Optional[List[str]] = None,
) -> bool:
"""Drop a Function name and the object it was bound to.
"""Drop one published Function revision, preserving its siblings.
The requested version must exist in the currently named object. Returns
True when the catalog binding was removed and False when it was absent.
Physical cleanup continues in the background; use
:meth:`drop_function_async` to access the query job. Local connections
raise NotImplementedError.
Returns True when the revision was removed and False for a repeated drop
or a missing name. A revision never published under an existing name is
an error. Dropping the last visible revision releases the name and
schedules physical cleanup in the background. Use
:meth:`drop_function_async` to
access the query job, whose completion confirms catalog removal of the
revision. Local connections raise NotImplementedError.
"""
raise NotImplementedError(
"Function catalog operations are not supported for this connection type"
@@ -1063,11 +1067,13 @@ class DBConnection(EnforceOverrides):
version: str,
namespace_path: Optional[List[str]] = None,
) -> Tuple[bool, Job]:
"""Drop a Function binding and return its SQL query job.
"""Drop one Function revision and return its SQL query job.
The boolean reports catalog removal. The job has type ``query`` and its
ID is the SQL query UUID. Repeated waits confirm that completion;
physical cleanup continues in the background. LanceDB Embedded
The boolean reports removal of the requested revision from the catalog;
sibling revisions and their grants remain available. The job has type
``query`` and its ID is the SQL query UUID. Repeated waits confirm that
revision's removal. Only the last visible revision's drop releases the
name and schedules background object cleanup. LanceDB Embedded
connections raise NotImplementedError.
"""
raise NotImplementedError(
@@ -3515,7 +3521,13 @@ class AsyncConnection(object):
version: str,
namespace_path: Optional[List[str]] = None,
) -> bool:
"""Drop a Function name and the object it was bound to."""
"""Drop one published Function revision, preserving its siblings.
Returns True when the requested revision was removed and False for a
repeated drop or a missing name. A revision never published under an
existing name is an error. Only the last visible revision's removal
releases the name and schedules background object cleanup.
"""
return await self._inner.drop_function(name, version, namespace_path)
async def drop_function_async(
@@ -3525,11 +3537,13 @@ class AsyncConnection(object):
version: str,
namespace_path: Optional[List[str]] = None,
) -> Tuple[bool, AsyncJob]:
"""Drop a Function binding and return its SQL query job.
"""Drop one Function revision and return its SQL query job.
The boolean reports catalog removal. The job has type ``query`` and its
ID is the SQL query UUID. Repeated waits confirm that completion;
physical cleanup continues in the background.
The boolean reports removal of the requested revision from the catalog;
sibling revisions and their grants remain available. The job has type
``query`` and its ID is the SQL query UUID. Repeated waits confirm that
revision's removal. Only the last visible revision's drop releases the
name and schedules background object cleanup.
"""
dropped, job = await self._inner.drop_function_async(
name, version, namespace_path
+14 -10
View File
@@ -684,8 +684,8 @@ impl Connection {
/// List every published immutable Function version in `namespace_path` of
/// the remote catalog. Functions in child namespaces are not included.
///
/// Results are ordered by Function name then version. LanceDB Embedded databases return
/// [`Error::NotSupported`].
/// Results are ordered by Function name then numeric version, using
/// `SHOW FUNCTIONS HISTORY`. LanceDB Embedded databases return [`Error::NotSupported`].
///
/// # Example
///
@@ -719,11 +719,13 @@ impl Connection {
})
}
/// Remove the current Function name binding, retaining the object history.
/// Remove one published Function revision, preserving sibling revisions and their grants.
///
/// Returns `true` when the server appended a Dropped transition and
/// `false` for an idempotent replay. LanceDB Embedded databases return
/// [`Error::NotSupported`].
/// Returns `true` when the requested revision was removed and `false` for
/// a repeated drop or a missing name. A revision never published under an
/// existing name is an error. Only the last visible revision's removal
/// releases the name and schedules object cleanup. LanceDB Embedded databases
/// return [`Error::NotSupported`].
pub async fn drop_function(
&self,
name: impl AsRef<str>,
@@ -735,11 +737,13 @@ impl Connection {
.await
}
/// Drop a Function binding and return its SQL query handle.
/// Drop one Function revision and return its SQL query handle.
///
/// The boolean reports catalog removal. The query job retains its query ID,
/// and repeated waits confirm completion. Physical cleanup continues in the
/// background. Local databases return [`Error::NotSupported`].
/// The boolean reports removal of the requested revision from the catalog.
/// Sibling revisions and their grants remain available. The query job retains
/// its query ID, and repeated waits confirm that revision's removal. Dropping
/// the last visible revision releases the name and schedules background object
/// cleanup. Local databases return [`Error::NotSupported`].
pub async fn drop_function_async(
&self,
name: impl AsRef<str>,
+1 -5
View File
@@ -108,7 +108,7 @@ impl SqlClient {
let mut items: Vec<FunctionVersion> = self
.object_job(
ExecuteQueryRequest {
query: format!("SHOW FUNCTIONS WITH ({})", sql_options.join(", ")),
query: format!("SHOW FUNCTIONS HISTORY WITH ({})", sql_options.join(", ")),
default_namespace_path: namespace.to_vec(),
parameters: None,
},
@@ -126,10 +126,6 @@ impl SqlClient {
.await?
.wait()
.await?;
items.sort_by(|a, b| (a.name(), a.version()).cmp(&(b.name(), b.version())));
if let Some((name, version)) = after {
items.retain(|item| (item.name(), item.version()) > (name.as_str(), version.as_str()));
}
let page_token = if items.len() > limit as usize {
items.truncate(limit as usize);
let last = items.last().unwrap();
+26 -8
View File
@@ -2637,9 +2637,16 @@ async fn function_listing_uses_bounded_sql_and_scoped_version_cursors() {
let mut second: serde_json::Value = serde_json::from_str(VERSION).unwrap();
second["version"] = serde_json::json!("2");
let second = second.to_string();
let mut tenth: serde_json::Value = serde_json::from_str(VERSION).unwrap();
tenth["version"] = serde_json::json!("10");
let tenth = tenth.to_string();
let result = RecordBatch::try_from_iter(vec![(
"definition",
Arc::new(StringArray::from(vec![VERSION, second.as_str()])) as ArrayRef,
Arc::new(StringArray::from(vec![
VERSION,
second.as_str(),
tenth.as_str(),
])) as ArrayRef,
)])
.unwrap();
let service = TestSqlService {
@@ -2648,6 +2655,10 @@ async fn function_listing_uses_bounded_sql_and_scoped_version_cursors() {
result,
..Default::default()
};
service.result_overrides.lock().unwrap().insert(
"SHOW FUNCTIONS HISTORY WITH (limit = 3, include_definition = true, after_name = 'embed', after_version = '2')".into(),
RecordBatch::try_from_iter(vec![("definition", Arc::new(StringArray::from(vec![tenth.as_str()])) as ArrayRef)]).unwrap(),
);
let statements = service.statements.clone();
let (client, shutdown) = start_sql_service(service, ClientConfig::default()).await;
let db = RemoteDatabase::new_mock(|_| -> http::Response<String> {
@@ -2657,29 +2668,36 @@ async fn function_listing_uses_bounded_sql_and_scoped_version_cursors() {
let first = db
.list_functions(
&["team".into()],
crate::listing::ListingOptions::default().page_limit(1),
crate::listing::ListingOptions::default().page_limit(2),
)
.await
.unwrap();
assert_eq!(first.items.len(), 1);
assert_eq!(
first
.items
.iter()
.map(|item| item.version())
.collect::<Vec<_>>(),
["1", "2"]
);
let token = first.page_token.unwrap();
let second = db
.list_functions(
&["team".into()],
crate::listing::ListingOptions::default()
.page_limit(1)
.page_limit(2)
.page_token(token.clone()),
)
.await
.unwrap();
assert_eq!(second.items.len(), 1);
assert_eq!(second.items[0].version(), "2");
assert_eq!(second.items[0].version(), "10");
assert!(second.page_token.is_none());
assert!(
db.list_functions(
&["other".into()],
crate::listing::ListingOptions::default()
.page_limit(1)
.page_limit(2)
.page_token(token)
)
.await
@@ -2687,8 +2705,8 @@ async fn function_listing_uses_bounded_sql_and_scoped_version_cursors() {
);
let seen = statements.lock().unwrap();
assert_eq!(seen.len(), 2);
assert!(seen[0].contains("limit = 2"));
assert!(seen[1].contains("after_version = '1'"));
assert!(seen[0].starts_with("SHOW FUNCTIONS HISTORY WITH (limit = 3,"));
assert!(seen[1].contains("after_version = '2'"));
shutdown.send(()).unwrap();
}