Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion java/lance-jni/src/blocking_dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -785,6 +785,32 @@ impl IntoJava for Version {
}
}

impl IntoJava for WriterVersion {
fn into_java<'a>(self, env: &mut JNIEnv<'a>) -> Result<JObject<'a>> {
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,
Expand Down Expand Up @@ -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<JObject<'local>> {
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<u8> {
let dataset_guard =
unsafe { env.get_rust_field::<_, _, BlockingDataset>(java_dataset, NATIVE_DATASET) }?;
Expand Down
16 changes: 16 additions & 0 deletions java/src/main/java/org/lance/Dataset.java
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,22 @@ public boolean hasStableRowIds() {

private native boolean nativeHasStableRowIds();

/**
* Get the library version that wrote the current manifest.
*
* <p>Older manifests may not contain writer version metadata.
*
* @return the current manifest writer version, or empty if unavailable
*/
public Optional<WriterVersion> 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.
*
Expand Down
57 changes: 57 additions & 0 deletions java/src/main/java/org/lance/WriterVersion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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;
}

/**
* Version string reported by the writer library.
*
* <p>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, when supplied by the writer. */
public Optional<String> getPrerelease() {
return Optional.ofNullable(prerelease);
}

/** Optional semantic-version build metadata component, when supplied by the writer. */
public Optional<String> getBuildMetadata() {
return Optional.ofNullable(buildMetadata);
}
}
40 changes: 40 additions & 0 deletions java/src/test/java/org/lance/DatasetTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new API also promises Optional.empty() for legacy manifests and preserves prerelease/build qualifiers, but this coverage only exercises a newly written manifest’s present library/core version. Please add focused assertions for those boundary paths—existing legacy and prerelease fixtures can cover two of them—so JNI null/optional mapping regressions are caught.

@lance-gatekeeper lance-gatekeeper Bot Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially addressed on 942f3fd: the historical v0.7.5 assertion now covers the JNI null → Optional.empty() path. The qualifier test constructs WriterVersion directly, so it would still pass if JNI swapped or dropped prerelease / buildMetadata; qualifier transport remains uncovered. This non-blocking thread remains open.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed on 7ae0f8e: the 2.0.0-beta.1 fixture now exercises prerelease presence and build-metadata absence through Dataset.getWriterVersion(). The focused suite passed 54 Java tests and 17 JNI Rust tests, closing the qualifier-mapping risk.

}

// Test LEGACY version
Expand All @@ -144,9 +147,46 @@ 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());
}

// 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());
}
}
}

@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();
Expand Down
Loading