From 415144e8ea300c3cbac92a5d3fd20bfa1f6585ab Mon Sep 17 00:00:00 2001 From: wangzheyan Date: Mon, 10 Aug 2026 23:08:15 +0800 Subject: [PATCH 1/3] feat(java): expose manifest writer version --- java/lance-jni/src/blocking_dataset.rs | 51 ++++++++++++++++++- java/src/main/java/org/lance/Dataset.java | 16 ++++++ .../main/java/org/lance/WriterVersion.java | 51 +++++++++++++++++++ java/src/test/java/org/lance/DatasetTest.java | 3 ++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 java/src/main/java/org/lance/WriterVersion.java diff --git a/java/lance-jni/src/blocking_dataset.rs b/java/lance-jni/src/blocking_dataset.rs index 9d93afc95ba..5417260588d 100644 --- a/java/lance-jni/src/blocking_dataset.rs +++ b/java/lance-jni/src/blocking_dataset.rs @@ -46,7 +46,7 @@ use lance::io::commit::namespace_manifest::LanceNamespaceExternalManifestStore; use lance::io::{ObjectStore, ObjectStoreParams}; use lance::session::Session as LanceSession; use lance::table::format::IndexMetadata; -use lance::table::format::{BasePath, Fragment}; +use lance::table::format::{BasePath, Fragment, WriterVersion}; use lance_core::datatypes::Schema as LanceSchema; use lance_file::version::LanceFileVersion; use lance_index::IndexCriteria as RustIndexCriteria; @@ -785,6 +785,32 @@ impl IntoJava for Version { } } +impl IntoJava for WriterVersion { + fn into_java<'a>(self, env: &mut JNIEnv<'a>) -> Result> { + let library = env.new_string(self.library)?; + let version = env.new_string(self.version)?; + let prerelease = match self.prerelease { + Some(value) => JObject::from(env.new_string(value)?), + None => JObject::null(), + }; + let build_metadata = match self.build_metadata { + Some(value) => JObject::from(env.new_string(value)?), + None => JObject::null(), + }; + + Ok(env.new_object( + "org/lance/WriterVersion", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + &[ + JValue::Object(&library), + JValue::Object(&version), + JValue::Object(&prerelease), + JValue::Object(&build_metadata), + ], + )?) + } +} + fn attach_native_dataset<'local>( env: &mut JNIEnv<'local>, dataset: BlockingDataset, @@ -2049,6 +2075,29 @@ pub extern "system" fn Java_org_lance_Dataset_nativeHasStableRowIds( ok_or_throw_with_return!(env, inner_has_stable_row_ids(&mut env, java_dataset), 0u8) } +#[unsafe(no_mangle)] +pub extern "system" fn Java_org_lance_Dataset_nativeGetWriterVersion<'local>( + mut env: JNIEnv<'local>, + java_dataset: JObject, +) -> JObject<'local> { + ok_or_throw!(env, inner_get_writer_version(&mut env, java_dataset)) +} + +fn inner_get_writer_version<'local>( + env: &mut JNIEnv<'local>, + java_dataset: JObject, +) -> Result> { + let writer_version = { + let dataset_guard = + unsafe { env.get_rust_field::<_, _, BlockingDataset>(java_dataset, NATIVE_DATASET) }?; + dataset_guard.inner.manifest().writer_version.clone() + }; + match writer_version { + Some(writer_version) => writer_version.into_java(env), + None => Ok(JObject::null()), + } +} + fn inner_has_stable_row_ids(env: &mut JNIEnv, java_dataset: JObject) -> Result { let dataset_guard = unsafe { env.get_rust_field::<_, _, BlockingDataset>(java_dataset, NATIVE_DATASET) }?; diff --git a/java/src/main/java/org/lance/Dataset.java b/java/src/main/java/org/lance/Dataset.java index 02b7c81ce39..71e6293bd6d 100644 --- a/java/src/main/java/org/lance/Dataset.java +++ b/java/src/main/java/org/lance/Dataset.java @@ -1567,6 +1567,22 @@ public boolean hasStableRowIds() { private native boolean nativeHasStableRowIds(); + /** + * Get the library version that wrote the current manifest. + * + *

Older manifests may not contain writer version metadata. + * + * @return the current manifest writer version, or empty if unavailable + */ + public Optional getWriterVersion() { + try (LockManager.ReadLock readLock = lockManager.acquireReadLock()) { + Preconditions.checkArgument(nativeDatasetHandle != 0, "Dataset is closed"); + return Optional.ofNullable(nativeGetWriterVersion()); + } + } + + private native WriterVersion nativeGetWriterVersion(); + /** * Get the Lance file format version of this dataset. * diff --git a/java/src/main/java/org/lance/WriterVersion.java b/java/src/main/java/org/lance/WriterVersion.java new file mode 100644 index 00000000000..c35de589c94 --- /dev/null +++ b/java/src/main/java/org/lance/WriterVersion.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lance; + +import java.util.Optional; + +/** Version metadata for the library that wrote a dataset manifest. */ +public final class WriterVersion { + private final String library; + private final String version; + private final String prerelease; + private final String buildMetadata; + + WriterVersion(String library, String version, String prerelease, String buildMetadata) { + this.library = library; + this.version = version; + this.prerelease = prerelease; + this.buildMetadata = buildMetadata; + } + + /** Name of the writer library, such as {@code lance}. */ + public String getLibrary() { + return library; + } + + /** Core semantic version without prerelease or build metadata. */ + public String getVersion() { + return version; + } + + /** Optional semantic-version prerelease component. */ + public Optional getPrerelease() { + return Optional.ofNullable(prerelease); + } + + /** Optional semantic-version build metadata component. */ + public Optional getBuildMetadata() { + return Optional.ofNullable(buildMetadata); + } +} diff --git a/java/src/test/java/org/lance/DatasetTest.java b/java/src/test/java/org/lance/DatasetTest.java index 25610a5c96c..453507508d0 100644 --- a/java/src/test/java/org/lance/DatasetTest.java +++ b/java/src/test/java/org/lance/DatasetTest.java @@ -129,6 +129,9 @@ void testGetLanceFileFormatVersion(@TempDir Path tempDir) { new TestUtils.SimpleTestDataset(allocator, defaultPath); try (Dataset dataset = testDataset.createEmptyDataset()) { assertEquals(LanceConstants.FILE_FORMAT_VERSION_2_1, dataset.getLanceFileFormatVersion()); + WriterVersion writerVersion = dataset.getWriterVersion().orElseThrow(AssertionError::new); + assertEquals("lance", writerVersion.getLibrary()); + assertFalse(writerVersion.getVersion().isEmpty()); } // Test LEGACY version From 942f3fd0467cde95f13c686656ddf4ee35d1ddd2 Mon Sep 17 00:00:00 2001 From: wangzheyan Date: Tue, 11 Aug 2026 01:10:46 +0800 Subject: [PATCH 2/3] test(java): cover writer version compatibility --- .../main/java/org/lance/WriterVersion.java | 12 ++++++++--- java/src/test/java/org/lance/DatasetTest.java | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/java/src/main/java/org/lance/WriterVersion.java b/java/src/main/java/org/lance/WriterVersion.java index c35de589c94..7e4fda48cf1 100644 --- a/java/src/main/java/org/lance/WriterVersion.java +++ b/java/src/main/java/org/lance/WriterVersion.java @@ -34,17 +34,23 @@ public String getLibrary() { return library; } - /** Core semantic version without prerelease or build metadata. */ + /** + * Version string reported by the writer library. + * + *

This value is opaque because writer libraries are not required to use semantic versioning. + * When a writer does use semantic versioning, newer writers store the core version here and + * expose prerelease and build metadata separately. + */ public String getVersion() { return version; } - /** Optional semantic-version prerelease component. */ + /** Optional semantic-version prerelease component, when supplied by the writer. */ public Optional getPrerelease() { return Optional.ofNullable(prerelease); } - /** Optional semantic-version build metadata component. */ + /** Optional semantic-version build metadata component, when supplied by the writer. */ public Optional getBuildMetadata() { return Optional.ofNullable(buildMetadata); } diff --git a/java/src/test/java/org/lance/DatasetTest.java b/java/src/test/java/org/lance/DatasetTest.java index 453507508d0..1ca7fa38bc8 100644 --- a/java/src/test/java/org/lance/DatasetTest.java +++ b/java/src/test/java/org/lance/DatasetTest.java @@ -147,9 +147,30 @@ void testGetLanceFileFormatVersion(@TempDir Path tempDir) { assertEquals( LanceConstants.FILE_FORMAT_VERSION_0_1, legacyDataset.getLanceFileFormatVersion()); } + + // This dataset was written before writer_version was added to the manifest. + String historicalPath = + Path.of("..", "test_data", "v0.7.5", "with_deletions") + .toAbsolutePath() + .normalize() + .toString(); + try (Dataset historicalDataset = Dataset.open(historicalPath, allocator)) { + assertTrue(historicalDataset.getWriterVersion().isEmpty()); + } } } + @Test + void testWriterVersionPreservesOpaqueAndOptionalFields() { + WriterVersion writerVersion = + new WriterVersion("custom-writer", "release-2026", "preview.1", "build.42"); + + assertEquals("custom-writer", writerVersion.getLibrary()); + assertEquals("release-2026", writerVersion.getVersion()); + assertEquals("preview.1", writerVersion.getPrerelease().orElseThrow(AssertionError::new)); + assertEquals("build.42", writerVersion.getBuildMetadata().orElseThrow(AssertionError::new)); + } + @Test void testCreateDirNotExist(@TempDir Path tempDir) throws IOException, URISyntaxException { String testMethodName = new Object() {}.getClass().getEnclosingMethod().getName(); From 7ae0f8ecd706a8fbb456d91541a963f07163350e Mon Sep 17 00:00:00 2001 From: wangzheyan Date: Tue, 11 Aug 2026 11:08:50 +0800 Subject: [PATCH 3/3] test(java): cover prerelease writer version over JNI --- java/src/test/java/org/lance/DatasetTest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/java/src/test/java/org/lance/DatasetTest.java b/java/src/test/java/org/lance/DatasetTest.java index 1ca7fa38bc8..dfba61f7d19 100644 --- a/java/src/test/java/org/lance/DatasetTest.java +++ b/java/src/test/java/org/lance/DatasetTest.java @@ -157,6 +157,22 @@ void testGetLanceFileFormatVersion(@TempDir Path tempDir) { try (Dataset historicalDataset = Dataset.open(historicalPath, allocator)) { assertTrue(historicalDataset.getWriterVersion().isEmpty()); } + + // This fixture was written by lance 2.0.0-beta.1. Reading it through Dataset verifies + // that the manifest's prerelease qualifier survives the Rust-to-Java JNI mapping. + String prereleasePath = + Path.of("..", "test_data", "pre_file_sizes", "index_without_file_sizes") + .toAbsolutePath() + .normalize() + .toString(); + try (Dataset prereleaseDataset = Dataset.open(prereleasePath, allocator)) { + WriterVersion writerVersion = + prereleaseDataset.getWriterVersion().orElseThrow(AssertionError::new); + assertEquals("lance", writerVersion.getLibrary()); + assertEquals("2.0.0", writerVersion.getVersion()); + assertEquals("beta.1", writerVersion.getPrerelease().orElseThrow(AssertionError::new)); + assertTrue(writerVersion.getBuildMetadata().isEmpty()); + } } }