Compare commits

..

1 Commits

Author SHA1 Message Date
Gatefixer 3f9ff474c9 test(rust): guard parallel compaction fragment reservation 2026-08-06 00:15:11 +00:00
5 changed files with 69 additions and 50 deletions
-31
View File
@@ -656,7 +656,6 @@ pub struct ConnectRequest {
/// - `/path/to/database` - local database on file system.
/// - `s3://bucket/path/to/database` or `gs://bucket/path/to/database` - database on cloud object store
/// - `db://dbname` - LanceDB Cloud
/// - `db://` with a host override - remote tables in the storage root
pub uri: String,
#[cfg(feature = "remote")]
@@ -769,8 +768,6 @@ impl ConnectBuilder {
///
/// This option is only used when connecting to LanceDB Cloud (db:// URIs)
/// and will be ignored for other URIs.
/// Use the URI `db://` together with a host override to connect to remote
/// tables stored directly in the storage root.
///
/// # Arguments
///
@@ -1357,34 +1354,6 @@ mod tests {
}
}
#[cfg(feature = "remote")]
#[tokio::test]
async fn test_connect_remote_storage_root() {
let conn = ConnectBuilder::new("db://")
.region("us-east-1")
.api_key("my-api-key")
.host_override("https://example.com")
.execute()
.await
.unwrap();
let (impl_name, properties) = conn.namespace_client_config().await.unwrap();
assert_eq!(impl_name, "rest");
assert_eq!(properties["uri"], "https://example.com");
assert_eq!(properties["header.x-lancedb-database"], "");
let result = ConnectBuilder::new("db://")
.region("us-east-1")
.api_key("my-api-key")
.execute()
.await;
assert!(matches!(
result,
Err(Error::InvalidInput { message })
if message.contains("A host override is required")
));
}
#[cfg(feature = "remote")]
#[tokio::test]
async fn test_connect_rejects_header_provider_with_oauth_config() {
-1
View File
@@ -54,7 +54,6 @@
//! - `/path/to/database` - local database on file system.
//! - `s3://bucket/path/to/database` or `gs://bucket/path/to/database` - database on cloud object store
//! - `db://dbname` - Lance Cloud
//! - `db://` with a host override - remote tables in the storage root
//!
//! You can also use [`ConnectBuilder`] to configure the connection to the database.
//!
+7 -11
View File
@@ -349,22 +349,18 @@ pub struct ParsedDbUrl {
/// Parse a database URL and extract the database name and optional prefix.
///
/// Expected format: `db://db_name`, `db://db_name/prefix`, or `db://` when
/// connecting to the storage root through a host override.
/// Expected format: `db://db_name` or `db://db_name/prefix`
pub fn parse_db_url(db_url: &str) -> Result<ParsedDbUrl> {
let parsed_url = url::Url::parse(db_url).map_err(|err| Error::InvalidInput {
message: format!("db_url is not a valid URL. '{db_url}'. Error: {err}"),
})?;
debug_assert_eq!(parsed_url.scheme(), "db");
let db_name = match parsed_url.host_str() {
Some(db_name) => db_name.to_string(),
None if matches!(parsed_url.path(), "" | "/") => String::new(),
None => {
return Err(Error::InvalidInput {
message: format!("Invalid database URL (missing host) '{}'", db_url),
});
}
};
if !parsed_url.has_host() {
return Err(Error::InvalidInput {
message: format!("Invalid database URL (missing host) '{}'", db_url),
});
}
let db_name = parsed_url.host_str().unwrap().to_string();
let db_prefix = {
let prefix = parsed_url.path().trim_start_matches('/');
if prefix.is_empty() {
-7
View File
@@ -272,13 +272,6 @@ impl RemoteDatabase {
read_consistency_interval: Option<std::time::Duration>,
) -> Result<Self> {
let parsed = super::client::parse_db_url(uri)?;
if parsed.db_name.is_empty() && host_override.is_none() {
return Err(Error::InvalidInput {
message:
"A host override is required when connecting to the storage root with 'db://'"
.to_string(),
});
}
let header_map = RestfulLanceDbClient::<Sender>::default_headers(
api_key,
region,
+62
View File
@@ -304,6 +304,68 @@ mod tests {
assert_eq!(all_values, expected);
}
#[tokio::test]
async fn test_parallel_compaction_reserves_fragment_ids_once() {
let conn = connect("memory://").execute().await.unwrap();
let schema = Arc::new(Schema::new(vec![Field::new("i", DataType::Int32, false)]));
let batch =
RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from_iter_values(0..10))])
.unwrap();
let table = conn
.create_table("test_parallel_compaction", batch.clone())
.execute()
.await
.unwrap();
// Create 64 fragments. With a 20-row target, compaction plans 32 tasks,
// which is more than the commit retry limit that used to be exhausted
// when each parallel task reserved fragment IDs independently.
for _ in 1..64 {
table.add(batch.clone()).execute().await.unwrap();
}
// Legacy row IDs require fragment IDs before an index can be remapped.
assert!(
!table
.as_native()
.unwrap()
.manifest()
.await
.unwrap()
.uses_stable_row_ids()
);
table
.create_index(&["i"], Index::BTree(BTreeIndexBuilder::default()))
.execute()
.await
.unwrap();
let version_before = table.version().await.unwrap();
let stats = table
.optimize(OptimizeAction::Compact {
options: CompactionOptions {
target_rows_per_fragment: 20,
num_threads: Some(64),
..Default::default()
},
remap_options: None,
})
.await
.unwrap()
.compaction
.unwrap();
assert_eq!(stats.fragments_removed, 64);
assert_eq!(stats.fragments_added, 32);
assert_eq!(table.count_rows(None).await.unwrap(), 640);
assert_eq!(
table.version().await.unwrap(),
version_before + 2,
"parallel compaction should use one fragment reservation commit and one rewrite commit"
);
}
#[tokio::test]
async fn test_optimize_prune_versions() {
let conn = connect("memory://").execute().await.unwrap();