chore: update lance dependency to v15.0.0-beta.6 (#4742)

Updates Rust and Java Lance dependencies to
[v15.0.0-beta.6](https://github.com/lance-format/lance/releases/tag/v15.0.0-beta.6),
pins Python tests to published pylance 15.0.0b6, and refreshes both
lockfiles.

Aligns Arrow 59, DataFusion 55, and PyO3 0.29; adapts execution plans
and MemWAL directory handling, updates the ABI metadata test, and
removes the obsolete Lance Arrow patch now fixed upstream.

Validation: workspace clippy with all features/tests and warnings
denied, cargo fmt, Python/CI Ruff checks, uv lock check, 16 CI-script
tests, and targeted Python regressions (54 passed, 28 skipped);
repository-wide Ruff retains existing documentation-example failures,
and the upstream macOS wheel build failed while Linux and Windows wheels
are published.

---------

Co-authored-by: Daniel Rammer <hamersaw@protonmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
LanceDB Robot
2026-10-29 16:14:09 -05:00
committed by GitHub
co-authored by Daniel Rammer Claude Opus 5.5
parent 03c7d1f199
commit d5013459eb
29 changed files with 1913 additions and 1366 deletions
+1 -1
View File
@@ -381,7 +381,7 @@ jobs:
runs-on: ubuntu-24.04
strategy:
matrix:
msrv: ["1.94.0"] # This should match up with rust-version in Cargo.toml
msrv: ["1.95.0"] # This should match up with rust-version in Cargo.toml
env:
# Need up-to-date compilers for kernels
CC: clang-18
Generated
+1421 -1245
View File
File diff suppressed because it is too large Load Diff
+35 -39
View File
@@ -16,52 +16,52 @@ repository = "https://github.com/lancedb/lancedb"
description = "Serverless, low-latency vector database for AI applications"
keywords = ["lancedb", "lance", "database", "vector", "search"]
categories = ["database-implementations"]
rust-version = "1.94.0"
rust-version = "1.95.0"
[workspace.dependencies]
lance = { "version" = "=15.0.0-beta.3", default-features = false, "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=15.0.0-beta.3", default-features = false, "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-jsonb = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=15.0.0-beta.3", default-features = false, "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { version = "=15.0.0-beta.3", path = "python/vendor/lance-arrow" }
lance = { "version" = "=15.0.0-beta.6", default-features = false, "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=15.0.0-beta.6", default-features = false, "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-jsonb = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=15.0.0-beta.6", default-features = false, "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=15.0.0-beta.6", "tag" = "v15.0.0-beta.6", "git" = "https://github.com/lance-format/lance.git" }
lancedb = { path = "rust/lancedb", default-features = false }
ahash = "0.8"
anyhow = "1"
# Note that this one does not include pyarrow
arrow = { version = "58.0.0", optional = false }
arrow-array = "58.0.0"
arrow-buffer = "58.0.0"
arrow-data = "58.0.0"
arrow-ipc = "58.0.0"
arrow-ord = "58.0.0"
arrow-schema = "58.0.0"
arrow-select = "58.0.0"
arrow-cast = "58.0.0"
arrow-flight = { version = "58.0.0", features = ["flight-sql-experimental"] }
arrow = { version = "59.2.0", optional = false }
arrow-array = "59.2.0"
arrow-buffer = "59.2.0"
arrow-data = "59.2.0"
arrow-ipc = "59.2.0"
arrow-ord = "59.2.0"
arrow-schema = "59.2.0"
arrow-select = "59.2.0"
arrow-cast = "59.2.0"
arrow-flight = { version = "59.2.0", features = ["flight-sql-experimental"] }
async-trait = "0"
# Smithy JSON 0.63 requires the pre-1.7 Document representation; allow the MSRV pin.
aws-smithy-types = ">=1.3.6, <1.7"
base64 = "0.22"
bytes = "1"
datafusion = { version = "54.0.0", default-features = false }
datafusion-catalog = "54.0.0"
datafusion-common = { version = "54.0.0", default-features = false }
datafusion-execution = "54.0.0"
datafusion-expr = "54.0.0"
datafusion-functions = "54.0.0"
datafusion-physical-plan = "54.0.0"
datafusion-physical-expr = "54.0.0"
datafusion-sql = "54.0.0"
datafusion = { version = "55.0.0", default-features = false }
datafusion-catalog = "55.0.0"
datafusion-common = { version = "55.0.0", default-features = false }
datafusion-execution = "55.0.0"
datafusion-expr = "55.0.0"
datafusion-functions = "55.0.0"
datafusion-physical-plan = "55.0.0"
datafusion-physical-expr = "55.0.0"
datafusion-sql = "55.0.0"
env_logger = "0.11"
half = { "version" = "2.7.1", default-features = false, features = [
"num-traits",
@@ -94,10 +94,6 @@ tracing = "0.1"
uuid = { version = "1.7.0", features = ["v4", "v7"] }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
[patch."https://github.com/lance-format/lance.git"]
# Carry the offset-safe validity fix until it is available in a Lance release.
lance-arrow = { path = "python/vendor/lance-arrow" }
[patch.crates-io]
# Lance v14.0.0-beta.18 requires multipart write modes from this upstream revision.
object_store = { git = "https://github.com/apache/arrow-rs-object-store.git", rev = "c10758c207c0103355f940f0c736fb8ecb157b04" }
+1 -1
View File
@@ -28,7 +28,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<arrow.version>15.0.0</arrow.version>
<lance-core.version>15.0.0-beta.3</lance-core.version>
<lance-core.version>15.0.0-beta.6</lance-core.version>
<jackson.version>2.18.11</jackson.version>
<spotless.skip>false</spotless.skip>
<spotless.version>2.30.0</spotless.version>
+2 -1
View File
@@ -1731,12 +1731,13 @@ describe("When creating an index", () => {
config: Index.ivfFlat({ numPartitions: 2, targetPartitionSize: 100 }),
});
// Lance records the target size the partition count implies: 300 rows / 2.
const indices = await tbl.listIndices();
expect(
(indices[0].indexDetails as Record<string, unknown>)[
"target_partition_size"
],
).toBeUndefined();
).toBe(150);
});
it("should allow me to replace (or not) an existing index", async () => {
+4 -4
View File
@@ -8,7 +8,7 @@ license.workspace = true
repository.workspace = true
keywords.workspace = true
categories.workspace = true
rust-version = "1.94.0"
rust-version = "1.95.0"
[lib]
name = "_lancedb"
@@ -30,9 +30,9 @@ env_logger.workspace = true
log.workspace = true
# Maturin enables extension-module mode for Python builds. Keeping it out of
# Cargo features lets Rust unit tests link against libpython.
pyo3 = { version = "0.28", features = ["abi3-py310", "chrono", "uuid"] }
pyo3 = { version = "0.29", features = ["abi3-py310", "chrono", "uuid"] }
chrono.workspace = true
pyo3-async-runtimes = { version = "0.28", features = [
pyo3-async-runtimes = { version = "0.29", features = [
"attributes",
"tokio-runtime",
] }
@@ -46,7 +46,7 @@ uuid.workspace = true
libc = "0.2"
[build-dependencies]
pyo3-build-config = { version = "0.28", features = ["abi3-py310"] }
pyo3-build-config = "0.29"
[features]
default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface", "lancedb/cos", "lancedb/goosefs", "lancedb/metrics-otel"]
+1 -1
View File
@@ -64,7 +64,7 @@ tests = [
"polars>=0.19, <=1.32.3",
"pyarrow<25",
"pyarrow-stubs>=16.0",
"pylance==15.0.0b3",
"pylance==15.0.0b6",
"requests>=2.31.0",
"datafusion>=54,<55",
"opentelemetry-sdk>=1.30.0",
+2 -1
View File
@@ -5921,7 +5921,8 @@ class AsyncTable:
"""Remove the RTEWriteSpec from this table.
Reverts to the standard `merge_insert` write path. Errors if no spec
is currently set.
is currently set, or if the RTE still holds rows that have not been
merged into the base table, since removing it would drop them.
"""
await self._inner.disable_rte()
+1 -1
View File
@@ -386,7 +386,7 @@ def test_custom_stop_words_list(table):
assert [token.text for token in tokens] == ["the", "data"]
empty_tokens = ldb.tokenize("the lance data", stem=False, custom_stop_words=[])
assert [token.text for token in empty_tokens] == ["the", "lance", "data"]
with pytest.raises(TypeError, match=r"custom_stop_words.*int"):
with pytest.raises(TypeError, match=r"'int' object is not an instance of 'str'"):
ldb.tokenize(
"the lance data",
custom_stop_words=["lance", 42],
+4 -2
View File
@@ -32,8 +32,10 @@ def test_pyo3_abi_matches_minimum_supported_python():
expected_abi = f"abi3-py{major}{minor}"
configured_abis = re.findall(r'"(abi3-py\d+)"', cargo_manifest)
assert configured_abis == [expected_abi, expected_abi], (
"the pyo3 runtime and build ABI features must both match requires-python"
# PyO3 0.29 configures the ABI through the runtime dependency; its
# build-config crate no longer exposes ABI features.
assert configured_abis == [expected_abi], (
"the pyo3 runtime ABI feature must match requires-python"
)
+1
View File
@@ -1417,6 +1417,7 @@ def test_branches_preserve_namespace(tmp_path):
"lance"
) # namespace_path routes through lance's DirectoryNamespace
db = lancedb.connect(tmp_path)
db.create_namespace(["ns1"])
table = db.create_table("t", [{"id": 1}], namespace_path=["ns1"])
assert table.namespace == ["ns1"]
+6 -8
View File
@@ -2007,7 +2007,7 @@ requires-dist = [
{ name = "pyarrow-stubs", marker = "extra == 'tests'", specifier = ">=16.0" },
{ name = "pydantic", specifier = ">=2.7.4,<3" },
{ name = "pylance", marker = "extra == 'pylance'", specifier = ">=15.0.0b3" },
{ name = "pylance", marker = "extra == 'tests'", specifier = "==15.0.0b3" },
{ name = "pylance", marker = "extra == 'tests'", specifier = "==15.0.0b6" },
{ name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.350" },
{ name = "pytest", marker = "extra == 'tests'", specifier = ">=7.0" },
{ name = "pytest-asyncio", marker = "extra == 'tests'", specifier = ">=0.21" },
@@ -3938,7 +3938,7 @@ crypto = [
[[package]]
name = "pylance"
version = "15.0.0b3"
version = "15.0.0b6"
source = { registry = "https://pypi.fury.io/lance-format/" }
dependencies = [
{ name = "lance-namespace" },
@@ -3948,12 +3948,10 @@ dependencies = [
{ name = "pyarrow" },
]
wheels = [
{ url = "https://pypi.fury.io/lance-format/-/ver_1ttHFr/pylance-15.0.0b3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:aaca475121cef79ada6fc19e2e2b54860e411cc8291f9ffa5643500264513086" },
{ url = "https://pypi.fury.io/lance-format/-/ver_19DzNz/pylance-15.0.0b3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5632f269b9e138b9ed3e5c7b788dfe27e04debab9d0f4c72be468f56d4a0d45a" },
{ url = "https://pypi.fury.io/lance-format/-/ver_20XdL/pylance-15.0.0b3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bdc92d7dc4d38323452543f71e61b493c3cfb5129dade33175f478e394b1959" },
{ url = "https://pypi.fury.io/lance-format/-/ver_YBklr/pylance-15.0.0b3-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6fcaced6f675f027979e1928284e104ae45eb8282d7c1eb0e51e4a5b92b93392" },
{ url = "https://pypi.fury.io/lance-format/-/ver_1HLHhZ/pylance-15.0.0b3-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f19240f3ccd621b746062fd2825e9340b99131c2cbda7ed5b226b6f7871b73f4" },
{ url = "https://pypi.fury.io/lance-format/-/ver_24P9Ax/pylance-15.0.0b3-cp310-abi3-win_amd64.whl", hash = "sha256:3f964e4ed093b6f170601d40772fcd3525fd73d15fd56eb30d884a446668d602" },
{ url = "https://pypi.fury.io/lance-format/-/ver_2jARvj/pylance-15.0.0b6-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88bd0646118c57e1c0a3118d04162323a74ebea28578f362977c927cdcb0f0c9" },
{ url = "https://pypi.fury.io/lance-format/-/ver_28yD6d/pylance-15.0.0b6-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:66613ecb88dcb8e02f0cd21d241987018d924b9f587cef4015bf7ea1815ab44f" },
{ url = "https://pypi.fury.io/lance-format/-/ver_1Us94M/pylance-15.0.0b6-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:d0765efab1f21b4697207ab75b9d391f1962813afb08e78db794eeea280c0bcc" },
{ url = "https://pypi.fury.io/lance-format/-/ver_ccoue/pylance-15.0.0b6-cp310-abi3-win_amd64.whl", hash = "sha256:f21eb48c6ec5d2269bb1817b2c0bb036956383d05033fb54278c4df64251ead4" },
]
[[package]]
+1 -1
View File
@@ -47,7 +47,7 @@ lance-index = { workspace = true, features = ["tokenizer-jieba", "tokenizer-lind
lance-table = { workspace = true }
lance-linalg = { workspace = true }
lance-encoding = { workspace = true }
lance-arrow = { "version" = "=15.0.0-beta.3", "tag" = "v15.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { workspace = true }
lance-namespace = { workspace = true }
lance-namespace-impls = { workspace = true }
metrics = { workspace = true, optional = true }
+1 -1
View File
@@ -17,7 +17,7 @@ anyhow.workspace = true
arrow-array.workspace = true
arrow-buffer.workspace = true
arrow-cast.workspace = true
arrow-json = "58.0.0"
arrow-json = "59.2.0"
arrow-schema.workspace = true
async-trait.workspace = true
clap = { version = "4.5.13", features = ["derive", "env"] }
@@ -254,6 +254,7 @@ async fn replace_fragments(dataset: Dataset, stream: SendableRecordBatchStream)
update_mode: None,
inserted_rows_filter: Some(inserted_rows_filter),
updated_fragment_offsets: None,
mem_wal_dir_id: None,
},
None,
))
@@ -663,6 +664,7 @@ mod tests {
update_mode: None,
inserted_rows_filter: Some(filter),
updated_fragment_offsets: None,
mem_wal_dir_id: None,
},
None,
))
+10
View File
@@ -1862,6 +1862,16 @@ impl DisplayAs for TakeRestoreExec {
}
impl ExecutionPlan for TakeRestoreExec {
fn apply_expressions(
&self,
_f: &mut dyn FnMut(
&Arc<dyn datafusion_physical_expr::PhysicalExpr>,
) -> datafusion_common::Result<
datafusion_common::tree_node::TreeNodeRecursion,
>,
) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion> {
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
}
fn name(&self) -> &str {
"TakeRestoreExec"
}
+20
View File
@@ -589,6 +589,16 @@ impl<S: HttpSend + 'static> DisplayAs for RemoteWriteExec<S> {
}
impl<S: HttpSend + 'static> ExecutionPlan for RemoteWriteExec<S> {
fn apply_expressions(
&self,
_f: &mut dyn FnMut(
&Arc<dyn datafusion_physical_expr::PhysicalExpr>,
) -> datafusion_common::Result<
datafusion_common::tree_node::TreeNodeRecursion,
>,
) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion> {
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
}
fn name(&self) -> &str {
Self::static_name()
}
@@ -1149,6 +1159,16 @@ mod tests {
}
impl ExecutionPlan for ErroringExec {
fn apply_expressions(
&self,
_f: &mut dyn FnMut(
&Arc<dyn datafusion_physical_expr::PhysicalExpr>,
) -> datafusion_common::Result<
datafusion_common::tree_node::TreeNodeRecursion,
>,
) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion> {
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
}
fn name(&self) -> &str {
"ErroringExec"
}
+3 -1
View File
@@ -2075,7 +2075,9 @@ impl Table {
/// Remove the [`RTEWriteSpec`] from this table, reverting to the standard
/// `merge_insert` write path.
///
/// Errors if no spec is currently set.
/// Errors if no spec is currently set, or if the RTE still holds rows that
/// have not been merged into the base table, since removing it would drop
/// them.
pub async fn disable_rte(&self) -> Result<()> {
self.inner.disable_rte().await
}
+4 -4
View File
@@ -3576,9 +3576,8 @@ mod tests {
assert!(matches!(err, Error::NotSupported { .. }), "declare won");
}
/// The gate's reproducer: after catch-up activation, an LSM write, and
/// unset, retained SSTable rows survive without a live spec. The catch-up
/// flag is the durable marker; declaration refuses on it.
/// Rows written through the RTE keep the spec installed, since
/// `disable_rte` refuses while they are unmerged, so declaration refuses.
#[tokio::test]
#[cfg_attr(
not(feature = "experimental-functions"),
@@ -3622,7 +3621,8 @@ mod tests {
.execute(Box::new(RecordBatchIterator::new(vec![Ok(batch)], schema)))
.await
.unwrap();
table.disable_rte().await.unwrap();
// The RTE holds the written rows, so unsetting it is refused.
table.disable_rte().await.unwrap_err();
let err = add_computed(&table, &[("doubled".into(), "value * 2".into())])
.await
+31 -4
View File
@@ -104,6 +104,16 @@ impl DisplayAs for MetadataEraserExec {
}
impl ExecutionPlan for MetadataEraserExec {
fn apply_expressions(
&self,
_f: &mut dyn FnMut(
&Arc<dyn datafusion_physical_expr::PhysicalExpr>,
) -> datafusion_common::Result<
datafusion_common::tree_node::TreeNodeRecursion,
>,
) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion> {
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
}
fn name(&self) -> &str {
"MetadataEraserExec"
}
@@ -157,8 +167,21 @@ impl ExecutionPlan for MetadataEraserExec {
)
}
fn partition_statistics(&self, partition: Option<usize>) -> DataFusionResult<Arc<Statistics>> {
self.input.partition_statistics(partition)
fn child_stats_requests(
&self,
partition: Option<usize>,
) -> Vec<datafusion_physical_plan::statistics::ChildStats> {
vec![datafusion_physical_plan::statistics::ChildStats::At(
partition,
)]
}
fn statistics_from_inputs(
&self,
input_stats: &[Arc<Statistics>],
_args: &datafusion_physical_plan::statistics::StatisticsArgs,
) -> DataFusionResult<Arc<Statistics>> {
Ok(input_stats[0].clone())
}
fn supports_limit_pushdown(&self) -> bool {
@@ -1398,7 +1421,9 @@ pub mod tests {
assert_eq!(physical_plan.name(), "MetadataEraserExec");
let partition_stats = physical_plan.partition_statistics(None).unwrap();
let partition_stats = datafusion_physical_plan::statistics::StatisticsContext::new()
.compute(physical_plan.as_ref(), &Default::default())
.unwrap();
assert!(matches!(partition_stats.num_rows, Precision::Exact(10)));
}
@@ -1492,7 +1517,9 @@ pub mod tests {
.await
.unwrap();
let rewired = foo.with_new_children(vec![tbl2.clone()]).unwrap();
let rewired =
datafusion_physical_plan::replace_children_if_necessary(foo, vec![tbl2.clone()])
.unwrap();
assert_eq!(rewired.schema(), tbl2.schema());
}
}
@@ -141,6 +141,16 @@ impl DisplayAs for InsertExec {
}
impl ExecutionPlan for InsertExec {
fn apply_expressions(
&self,
_f: &mut dyn FnMut(
&Arc<dyn datafusion_physical_expr::PhysicalExpr>,
) -> datafusion_common::Result<
datafusion_common::tree_node::TreeNodeRecursion,
>,
) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion> {
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
}
fn name(&self) -> &str {
Self::static_name()
}
@@ -62,6 +62,16 @@ impl DisplayAs for ScannableExec {
}
impl ExecutionPlan for ScannableExec {
fn apply_expressions(
&self,
_f: &mut dyn FnMut(
&Arc<dyn datafusion_physical_expr::PhysicalExpr>,
) -> datafusion_common::Result<
datafusion_common::tree_node::TreeNodeRecursion,
>,
) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion> {
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
}
fn name(&self) -> &str {
"ScannableExec"
}
@@ -507,6 +507,16 @@ impl DisplayAs for DuplicatePairsExec {
}
impl ExecutionPlan for DuplicatePairsExec {
fn apply_expressions(
&self,
_f: &mut dyn FnMut(
&Arc<dyn datafusion_physical_expr::PhysicalExpr>,
) -> datafusion_common::Result<
datafusion_common::tree_node::TreeNodeRecursion,
>,
) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion> {
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
}
fn name(&self) -> &str {
"DuplicatePairsExec"
}
@@ -373,6 +373,16 @@ impl DisplayAs for VectorDedupExec {
}
impl ExecutionPlan for VectorDedupExec {
fn apply_expressions(
&self,
_f: &mut dyn FnMut(
&Arc<dyn datafusion_physical_expr::PhysicalExpr>,
) -> datafusion_common::Result<
datafusion_common::tree_node::TreeNodeRecursion,
>,
) -> datafusion_common::Result<datafusion_common::tree_node::TreeNodeRecursion> {
Ok(datafusion_common::tree_node::TreeNodeRecursion::Continue)
}
fn name(&self) -> &str {
"VectorDedupExec"
}
+151 -10
View File
@@ -1453,6 +1453,132 @@ mod lsm_tests {
);
}
/// Another process re-initializing the MemWAL retires this process's
/// cached writer: its rows are not read, and the next write opens a
/// writer for the new MemWAL.
///
/// `disable_rte` refuses while the writer holds rows, so the MemWAL index
/// is dropped through Lance directly, standing in for a write that lands
/// after that check. The writer's rows are lost to the base table either
/// way; what this pins is that they are not served from a directory the
/// index no longer names.
#[tokio::test]
async fn lsm_writer_of_a_retired_mem_wal_is_replaced() {
use lance::index::DatasetIndexExt;
let dir = tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let conn = connect(uri)
.read_consistency_interval(std::time::Duration::ZERO)
.execute()
.await
.unwrap();
let table = conn
.create_table("t", id_value_reader(vec![1, 2, 3]))
.execute()
.await
.unwrap();
table.set_unenforced_primary_key(["id"]).await.unwrap();
table.enable_rte(RTEWriteSpec::unsharded()).await.unwrap();
lsm_upsert(&table, vec![4, 5]).await;
let retired_dir_id = mem_wal_dir_id(&table).await;
let mut dataset = lance::Dataset::open(&table.uri().await.unwrap())
.await
.unwrap();
dataset
.drop_index(lance_index::mem_wal::MEM_WAL_INDEX_NAME)
.await
.unwrap();
let other = connect(uri)
.execute()
.await
.unwrap()
.open_table("t")
.execute()
.await
.unwrap();
other.enable_rte(RTEWriteSpec::unsharded()).await.unwrap();
let dir_id = mem_wal_dir_id(&table).await;
assert_ne!(dir_id, retired_dir_id);
let ids = || async {
let rows = collect_id_value(table.query().execute().await.unwrap()).await;
rows.into_iter().map(|(id, _)| id).collect::<Vec<_>>()
};
assert_eq!(ids().await, vec![1, 2, 3]);
lsm_upsert(&table, vec![6]).await;
assert_eq!(ids().await, vec![1, 2, 3, 6]);
let native = table.as_native().unwrap();
assert!(
native
.dataset
.shard_writer()
.read_snapshot(&dir_id)
.await
.unwrap()
.is_some(),
"the write opened a writer for the new MemWAL"
);
}
/// `disable_rte` refuses while the MemWAL holds rows the base table does
/// not, since a MemWAL installed later would not read them.
#[tokio::test]
async fn lsm_disable_is_refused_while_rows_are_unmerged() {
let dir = tempdir().unwrap();
let table = id_value_table(&dir).await;
table.enable_rte(RTEWriteSpec::unsharded()).await.unwrap();
lsm_upsert(&table, vec![4, 5]).await;
let err = table.disable_rte().await.unwrap_err();
assert!(
err.to_string()
.contains("not been merged into the base table"),
"unexpected error: {err}"
);
assert_eq!(
table.get_rte_spec().await.unwrap(),
Some(RTEWriteSpec::unsharded())
);
let rows = collect_id_value(table.query().execute().await.unwrap()).await;
assert_eq!(
rows.into_iter().map(|(id, _)| id).collect::<Vec<_>>(),
vec![1, 2, 3, 4, 5]
);
}
/// Rows another process's writer has acknowledged but not flushed are in
/// its WAL only, and still refuse `disable_rte`.
#[tokio::test]
async fn lsm_disable_is_refused_while_another_writer_holds_rows() {
let dir = tempdir().unwrap();
let table = id_value_table(&dir).await;
table.enable_rte(RTEWriteSpec::unsharded()).await.unwrap();
lsm_upsert(&table, vec![4, 5]).await;
let other = connect(dir.path().to_str().unwrap())
.execute()
.await
.unwrap()
.open_table("t")
.execute()
.await
.unwrap();
let err = other.disable_rte().await.unwrap_err();
assert!(
err.to_string().contains("0 SSTable(s) not merged")
&& !err.to_string().contains(" 0 WAL entry(ies) not flushed"),
"unexpected error: {err}"
);
let rows = collect_id_value(table.query().execute().await.unwrap()).await;
assert_eq!(
rows.into_iter().map(|(id, _)| id).collect::<Vec<_>>(),
vec![1, 2, 3, 4, 5]
);
}
#[tokio::test]
async fn base_table_adapter_use_rte_false_scans_the_base_only() {
use crate::table::datafusion::BaseTableAdapter;
@@ -1728,9 +1854,10 @@ mod lsm_tests {
/// A spec is replaced by unsetting it and setting the new one.
///
/// `disable_rte` drains the open writer, so the rows it held
/// survive the replacement, and the writer the next write opens builds its
/// MemTables from the new set.
/// The writer the next write opens builds its MemTables from the new set.
/// Unsetting is refused while the MemWAL holds unmerged rows (see
/// `lsm_disable_is_refused_while_rows_are_unmerged`), so nothing is
/// written before the replacement.
#[tokio::test]
async fn lsm_spec_is_replaced_by_unsetting_it_first() {
use crate::index::Index;
@@ -1745,13 +1872,11 @@ mod lsm_tests {
.unwrap();
let fts_index = table.list_indices().await.unwrap()[0].name.clone();
// Installed maintaining nothing, then written to: the writer opens with
// no FTS index.
// Installed maintaining nothing.
table
.enable_rte(RTEWriteSpec::unsharded().with_maintained_indexes(Vec::new()))
.await
.unwrap();
upsert_text(&table, vec![(99, "zebra")]).await;
// Setting over an installed spec is refused; unset, then set.
let err = table
@@ -2269,7 +2394,7 @@ mod lsm_tests {
.unwrap()
.dataset
.shard_writer()
.read_snapshot()
.read_snapshot(&mem_wal_dir_id(&table).await)
.await
.unwrap()
.expect("the merge opened a writer");
@@ -2288,6 +2413,20 @@ mod lsm_tests {
assert_sstables_answer_through_region_bitmap(&table, vec![10, 11]).await;
}
/// The `dir_id` of the table's current MemWAL, read from storage.
async fn mem_wal_dir_id(table: &Table) -> String {
use lance::dataset::mem_wal::DatasetMemWalExt;
lance::Dataset::open(&table.uri().await.unwrap())
.await
.unwrap()
.mem_wal_index_details()
.await
.unwrap()
.expect("the table has a MemWAL")
.dir_id
}
/// The SSTables each shard's manifest publishes.
async fn published_sstables(table: &Table) -> Vec<lance::Dataset> {
use lance::dataset::mem_wal::ShardManifestStore;
@@ -2296,13 +2435,15 @@ mod lsm_tests {
let (store, base) = lance_io::object_store::ObjectStore::from_uri(&uri)
.await
.unwrap();
let mem_wal = std::path::Path::new(&uri).join("_mem_wal");
let dir_id = mem_wal_dir_id(table).await;
let mem_wal = std::path::Path::new(&uri)
.join(lance::dataset::mem_wal::util::mem_wal_dir_name(&dir_id));
let mut sstables = Vec::new();
for shard in std::fs::read_dir(&mem_wal).unwrap() {
let Ok(shard_id) = shard.unwrap().file_name().to_string_lossy().parse() else {
continue;
};
let manifest = ShardManifestStore::new(store.clone(), &base, shard_id, 2)
let manifest = ShardManifestStore::new(store.clone(), &base, &dir_id, shard_id, 2)
.latest()
.await
.unwrap()
@@ -2417,7 +2558,7 @@ mod lsm_tests {
.unwrap()
.dataset
.shard_writer()
.read_snapshot()
.read_snapshot(&mem_wal_dir_id(&table).await)
.await
.unwrap()
.expect("the merge opened a writer");
+117 -17
View File
@@ -30,8 +30,8 @@ use datafusion_physical_plan::SendableRecordBatchStream;
use futures::TryStreamExt;
use lance::Dataset;
use lance::dataset::mem_wal::{
DatasetMemWalExt, ShardWriter, ShardWriterConfig, evaluate_sharding_spec,
validate_maintained_indexes_with,
DatasetMemWalExt, ShardManifestStore, ShardWriter, ShardWriterConfig, WalTailer,
evaluate_sharding_spec, validate_maintained_indexes_with,
};
use lance::index::DatasetIndexExt;
use lance_core::datatypes::Schema as LanceSchema;
@@ -197,7 +197,9 @@ fn index_name_list(indices: &[IndexConfig]) -> String {
/// Remove the [`RTEWriteSpec`] from the table by dropping the MemWAL index.
///
/// Any cached shard writers are drained and closed first. Errors if no spec is
/// currently set.
/// currently set, or if the MemWAL still holds rows that have not been merged
/// into the base table: dropping the index would orphan them, since a MemWAL
/// installed later gets a new directory.
#[allow(clippy::redundant_pub_crate)]
pub(crate) async fn disable_rte(table: &NativeTable) -> Result<()> {
table.dataset.ensure_mutable()?;
@@ -214,6 +216,9 @@ pub(crate) async fn disable_rte(table: &NativeTable) -> Result<()> {
table.dataset.shard_writer().drain_and_close().await?;
let mut dataset = (*table.dataset.get().await?).clone();
if let Some(details) = dataset.mem_wal_index_details().await? {
ensure_mem_wal_merged(&dataset, &details).await?;
}
dataset
.drop_index(lance_index::mem_wal::MEM_WAL_INDEX_NAME)
.await?;
@@ -221,6 +226,67 @@ pub(crate) async fn disable_rte(table: &NativeTable) -> Result<()> {
Ok(())
}
/// Error if any shard of the MemWAL holds rows the base table does not: an
/// SSTable past the shard's compaction watermark, or a WAL entry past the
/// position its flushed SSTables cover.
///
/// Reading the WAL tip catches rows a live writer in another process has
/// acknowledged but not flushed. A write that lands after this check is not
/// caught.
async fn ensure_mem_wal_merged(dataset: &Dataset, details: &MemWalIndexDetails) -> Result<()> {
let store = dataset.object_store(None).await?;
let base_path = dataset.branch_location().path;
let scan_batch_size = ShardWriterConfig::default().manifest_scan_batch_size;
let mut pending = Vec::new();
for shard_id in dataset.list_mem_wal_latest_shard_ids().await? {
// First occurrence per shard, as Lance's `compacted_generation_for_shard`.
let compacted = details
.compacted_sstables
.iter()
.find(|entry| entry.shard_id == shard_id)
.map_or(0, |entry| entry.generation);
let manifest = ShardManifestStore::new(
store.clone(),
&base_path,
&details.dir_id,
shard_id,
scan_batch_size,
)
.latest()
.await?;
let (unmerged, flushed_through) = manifest.map_or((0, 0), |manifest| {
let unmerged = manifest
.sstables
.iter()
.filter(|sstable| sstable.generation > compacted)
.count();
(unmerged, manifest.replay_after_wal_entry_position)
});
let wal_tip = WalTailer::new(store.clone(), base_path.clone(), &details.dir_id, shard_id)
.next_position()
.await?
.saturating_sub(1);
let unflushed = wal_tip.saturating_sub(flushed_through);
if unmerged > 0 || unflushed > 0 {
pending.push(format!(
"shard {shard_id}: {unmerged} SSTable(s) not merged, {unflushed} WAL entry(ies) not flushed"
));
}
}
if pending.is_empty() {
return Ok(());
}
Err(Error::InvalidInput {
message: format!(
"disable_rte: the RTE holds rows that have not been merged into the base \
table and would be lost if it were removed ({}); disable_rte is only \
allowed before any rows have been written through the RTE",
pending.join("; ")
),
})
}
// =============================================================================
// get_rte_spec
// =============================================================================
@@ -351,12 +417,17 @@ impl std::fmt::Debug for ShardWriterCache {
}
struct ShardWriterEntry {
/// The `dir_id` of the MemWAL the writer was opened for. Another process
/// can drop and re-initialize the MemWAL under this one, leaving the cached
/// writer bound to a directory the index no longer names.
dir_id: String,
inner: RwLock<Option<ShardWriter>>,
}
impl ShardWriterEntry {
fn new(writer: ShardWriter) -> Self {
Self {
dir_id: writer.config().dir_id.clone(),
inner: RwLock::new(Some(writer)),
}
}
@@ -420,37 +491,57 @@ impl ShardWriterCache {
/// Return the cached writer, opening one for `shard_id` with `config` if
/// the slot is empty. Errors if a writer is already open for a *different*
/// shard — the caller must close it first.
///
/// A cached writer opened for a MemWAL other than `dir_id` is retired and
/// replaced: the MemWAL it wrote to was dropped by another process, so its
/// rows are no longer part of the table and its commits would be fenced.
async fn writer_for_shard(
&self,
dataset: &Dataset,
dir_id: &str,
shard_id: Uuid,
config: ShardWriterConfig,
) -> Result<Arc<ShardWriterEntry>> {
{
let guard = self.slot.read().await;
if let Some((cached, entry)) = guard.as_ref() {
if let Some((cached, entry)) = guard.as_ref()
&& entry.dir_id == dir_id
{
check_shard_match(*cached, shard_id)?;
return Ok(entry.clone());
}
}
let mut guard = self.slot.write().await;
// Re-check: another caller may have opened the writer meanwhile.
if let Some((cached, entry)) = guard.as_ref() {
if let Some((cached, entry)) = guard.as_ref()
&& entry.dir_id == dir_id
{
check_shard_match(*cached, shard_id)?;
return Ok(entry.clone());
}
let writer = dataset
let retired = guard.take();
let opened = dataset
.mem_wal_writer(shard_id, config)
.await
.map_err(|e| Error::Runtime {
message: format!(
"merge_insert: failed to open RTE shard writer for shard {}: {}",
shard_id, e
),
})?;
let entry = Arc::new(ShardWriterEntry::new(writer));
*guard = Some((shard_id, entry.clone()));
Ok(entry)
.map(|writer| Arc::new(ShardWriterEntry::new(writer)));
if let Ok(entry) = &opened {
*guard = Some((shard_id, entry.clone()));
}
drop(guard);
if let Some((retired_shard, retired)) = retired
&& let Err(error) = retired.close().await
{
log::warn!(
"the MemWAL was re-initialized, and the writer this process held for \
shard {retired_shard} of the retired MemWAL could not be closed: {error}"
);
}
opened.map_err(|e| Error::Runtime {
message: format!(
"merge_insert: failed to open RTE shard writer for shard {}: {}",
shard_id, e
),
})
}
/// Install `dataset`'s maintained index set on the cached writer, if one is
@@ -505,10 +596,12 @@ impl ShardWriterCache {
/// Snapshot the cached writer's shard for the RTE read path: its shard id,
/// authoritative in-memory manifest, and active + frozen memtable refs.
/// Returns `None` when no writer is currently cached (e.g. nothing has been
/// written this session, or the writer was closed).
/// written this session, or the writer was closed), or when the cached
/// writer belongs to a MemWAL other than `dir_id`.
#[allow(clippy::redundant_pub_crate)]
pub(crate) async fn read_snapshot(
&self,
dir_id: &str,
) -> Result<
Option<(
Uuid,
@@ -523,6 +616,10 @@ impl ShardWriterCache {
let Some((shard_id, entry)) = cached else {
return Ok(None);
};
// A writer for a retired MemWAL holds no rows of the current one.
if entry.dir_id != dir_id {
return Ok(None);
}
// Capture memtables before the manifest. If a flush interleaves, dedup
// tolerates the same rows appearing in both a memtable and a freshly
// flushed generation, but would drop rows present in neither. Manifest
@@ -576,6 +673,8 @@ enum LsmMode {
#[allow(clippy::redundant_pub_crate)]
pub(crate) struct LsmPlan {
mode: LsmMode,
/// The `dir_id` of the MemWAL the write targets.
dir_id: String,
writer_config_defaults: HashMap<String, String>,
}
@@ -644,6 +743,7 @@ pub(crate) async fn lsm_dispatch_decision(
let mode = resolve_lsm_mode(&details)?;
Ok(LsmDispatch::Lsm(LsmPlan {
mode,
dir_id: details.dir_id,
writer_config_defaults: details.writer_config_defaults,
}))
}
@@ -745,7 +845,7 @@ pub(crate) async fn execute_lsm_merge_insert(
let writer = table
.dataset
.shard_writer()
.writer_for_shard(dataset.as_ref(), shard_id, config)
.writer_for_shard(dataset.as_ref(), &plan.dir_id, shard_id, config)
.await?;
writer.put(batches).await?;
+2 -2
View File
@@ -27,7 +27,7 @@ use datafusion_physical_plan::PhysicalExpr;
use datafusion_physical_plan::projection::ProjectionExec;
use datafusion_physical_plan::repartition::RepartitionExec;
use datafusion_physical_plan::union::UnionExec;
use datafusion_physical_plan::{ExecutionPlan, with_new_children_if_necessary};
use datafusion_physical_plan::{ExecutionPlan, replace_children_if_necessary};
use lance::dataset::mem_wal::DatasetMemWalExt;
use lance::dataset::scanner::DatasetRecordBatchStream;
use lance::dataset::scanner::Scanner;
@@ -740,7 +740,7 @@ fn replace_ann_branches(
.cloned()
.map(|child| replace_ann_branches(child, internal_ann_plans, normalized_l2_indices))
.collect::<Result<Vec<_>>>()?;
Ok(with_new_children_if_necessary(public_plan, children)?)
Ok(replace_children_if_necessary(public_plan, children)?)
}
fn scale_distance_column(
+48 -19
View File
@@ -120,12 +120,11 @@ pub(super) async fn create_lsm_plan(
} else {
plain_plan(
&ds_ref,
&details,
&query,
pk_columns.clone(),
snapshots,
in_memory,
limit,
offset,
)
.await?
};
@@ -304,8 +303,13 @@ async fn build_read_context(
let mut snapshots: Vec<ShardSnapshot> = Vec::new();
for shard_id in shard_ids {
let manifest_store =
ShardManifestStore::new(store.clone(), &base_path, shard_id, scan_batch_size);
let manifest_store = ShardManifestStore::new(
store.clone(),
&base_path,
&details.dir_id,
shard_id,
scan_batch_size,
);
if let Some(manifest) = manifest_store.latest().await? {
snapshots.push(snapshot_from_manifest(shard_id, &manifest, &exclude));
}
@@ -324,8 +328,11 @@ async fn build_read_context(
// Override the active shard with the cached writer's in-memory view.
let mut in_memory: HashMap<Uuid, InMemoryMemTables> = HashMap::new();
if !wal_only
&& let Some((shard_id, manifest, memtables)) =
table.dataset.shard_writer().read_snapshot().await?
&& let Some((shard_id, manifest, memtables)) = table
.dataset
.shard_writer()
.read_snapshot(&details.dir_id)
.await?
{
if let Some(manifest) = manifest {
let snapshot = snapshot_from_manifest(shard_id, &manifest, &exclude);
@@ -384,12 +391,14 @@ fn as_i64(value: Option<usize>) -> Option<i64> {
/// scanner's `create_plan`.
fn base_scanner(
dataset: &Dataset,
dir_id: &str,
query: &VectorQueryRequest,
pk_columns: Vec<String>,
snapshots: Vec<ShardSnapshot>,
in_memory: HashMap<Uuid, InMemoryMemTables>,
) -> Result<LsmScanner> {
let mut scanner = LsmScanner::new(Arc::new(dataset.clone()), snapshots, pk_columns);
let mut scanner = LsmScanner::new(Arc::new(dataset.clone()), snapshots, pk_columns)
.with_mem_wal_dir_id(dir_id);
for (shard_id, memtables) in in_memory {
scanner = scanner.with_in_memory_memtables(shard_id, memtables);
}
@@ -435,15 +444,21 @@ fn enrich_filter_error(error: DataFusionError, schema: &ArrowSchema, context: &s
/// The plain scan applies limit and offset inside the planner.
async fn plain_plan(
dataset: &Dataset,
details: &MemWalIndexDetails,
query: &VectorQueryRequest,
pk_columns: Vec<String>,
snapshots: Vec<ShardSnapshot>,
in_memory: HashMap<Uuid, InMemoryMemTables>,
limit: Option<usize>,
offset: Option<usize>,
) -> Result<Arc<dyn ExecutionPlan>> {
let scanner = base_scanner(dataset, query, pk_columns, snapshots, in_memory)?
.limit(as_i64(limit), as_i64(offset))?;
let scanner = base_scanner(
dataset,
&details.dir_id,
query,
pk_columns,
snapshots,
in_memory,
)?
.limit(as_i64(query.base.limit), as_i64(query.base.offset))?;
Ok(scanner.create_plan().await?)
}
@@ -491,10 +506,17 @@ async fn fts_plan(
});
}
let scanner = base_scanner(dataset, query, pk_columns, snapshots, in_memory)?
.with_overfetch_factor(LSM_OVERFETCH_FACTOR)
.full_text_search(fts)?
.limit(as_i64(limit), as_i64(offset))?;
let scanner = base_scanner(
dataset,
&details.dir_id,
query,
pk_columns,
snapshots,
in_memory,
)?
.with_overfetch_factor(LSM_OVERFETCH_FACTOR)
.full_text_search(fts)?
.limit(as_i64(limit), as_i64(offset))?;
Ok(scanner.create_plan().await?)
}
@@ -740,10 +762,17 @@ async fn vector_plan(
// Guard `k` to at least 1 so a degenerate `limit(0)` is trimmed by `limit`
// below rather than rejected by `nearest`.
let k = limit.unwrap_or(DEFAULT_TOP_K).max(1);
let mut scanner = base_scanner(dataset, query, pk_columns, snapshots, in_memory)?
.with_overfetch_factor(LSM_OVERFETCH_FACTOR)
.nearest(&column, query_vector.as_ref(), k)?
.distance_metric(distance_type.into());
let mut scanner = base_scanner(
dataset,
&details.dir_id,
query,
pk_columns,
snapshots,
in_memory,
)?
.with_overfetch_factor(LSM_OVERFETCH_FACTOR)
.nearest(&column, query_vector.as_ref(), k)?
.distance_metric(distance_type.into());
if let Some(nprobes) = query.nprobes {
scanner = if query.maximum_nprobes == Some(0) {
scanner.minimum_nprobes(nprobes)
+4 -3
View File
@@ -1805,8 +1805,8 @@ mod tests {
assert!(matches!(err, Error::NotSupported { .. }));
}
/// After catch-up activation and unset, no spec remains but the catch-up
/// flag still marks retained SSTable rows; refresh refuses on the flag.
/// Rows written through the RTE keep the spec installed, since
/// `disable_rte` refuses while they are unmerged, so refresh refuses.
#[tokio::test]
#[cfg_attr(
not(feature = "experimental-functions"),
@@ -1849,7 +1849,8 @@ mod tests {
)))
.await
.unwrap();
table.disable_rte().await.unwrap();
// The RTE holds the written rows, so unsetting it is refused.
table.disable_rte().await.unwrap_err();
super::super::computed_columns::add_foreign_kind(&table, "doubled", "sql").await;
let err = table.refresh_column("doubled").await.unwrap_err();