mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 14:16:54 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user