Skip to content

Commit 5d2bf09

Browse files
committed
feat: expose exact V2 versions in bindings
1 parent e9be694 commit 5d2bf09

23 files changed

Lines changed: 363 additions & 22 deletions

docs/src/format/table/versioning.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,64 @@ they should return an "unsupported" error on any read or write operation.
3636
</div>
3737

3838
Flags with bit values 512 and above are unknown and will cause implementations to reject the dataset with an "unsupported" error. The paired mixed-version reader and writer bits must either both be set or both be clear; a half-set manifest is invalid.
39+
40+
## Mixed V2 Data File Versions
41+
42+
The manifest data storage version is the default for operations that do not
43+
select an exact output version. It is a fallback, not a summary, minimum,
44+
maximum, or profile of the data files referenced by the snapshot. Once
45+
`FLAG_MIXED_DATA_FILE_VERSIONS` is enabled, each base data file and data overlay
46+
file is decoded according to its own normalized version identity.
47+
48+
Mixed snapshots have the following invariants:
49+
50+
- Only exact V2.0, V2.1, V2.2, and V2.3 data file versions may be mixed.
51+
- V1 and V2 data files may not appear in the same snapshot.
52+
- A commit that first produces a mixed snapshot derives and sets both the reader
53+
and writer capability bits from its final manifest. The bits remain set on all
54+
later snapshots, even if a later compaction makes the files homogeneous again.
55+
- A snapshot without the capability may only reference files matching its
56+
manifest fallback. The only repair exception is an unambiguous, homogeneous
57+
historical V2 snapshot whose legacy manifest metadata is stale.
58+
- An operation-level `data_storage_version` selects the exact output version
59+
for that operation. Omitting it uses the manifest fallback. Neither case
60+
changes the fallback.
61+
62+
For example, a dataset whose fallback is V2.1 can append V2.2 files by setting
63+
`data_storage_version="2.2"`. The same commit adds both mixed-version capability
64+
bits. Reads then dispatch V2.1 files to the V2.1 decoder and V2.2 files to the
65+
V2.2 decoder. Compaction can deliberately rewrite selected fragments to any
66+
supported exact V2 target; binary copy is only valid when every selected input
67+
file already has that exact target version.
68+
69+
### Compatibility Matrix
70+
71+
| Dataset state | Mixed-aware client | Client without bit 256 support |
72+
| --- | --- | --- |
73+
| Historical homogeneous V1 | Reads and writes through legacy paths | Unchanged |
74+
| Historical homogeneous V2 | Reads and writes; legacy metadata repair remains uniform-only | Unchanged |
75+
| New homogeneous V2 without bit 256 | Reads and writes using the manifest fallback | Unchanged |
76+
| Mixed V2.0-V2.3 with both bits set | Reads and writes by exact per-file identity | Rejects before reading or writing |
77+
| Mixed V2 without both bits | Rejects as a per-file capability mismatch | Not a valid dataset state |
78+
| V1/V2 mixture | Rejects | Not a valid dataset state |
79+
80+
### Error Categories
81+
82+
Implementations distinguish these failures in their error messages so operators
83+
can identify the violated boundary:
84+
85+
- unsupported reader or writer feature bit;
86+
- half-set mixed-version capability corruption;
87+
- unknown or malformed data file version identity;
88+
- V1/V2 mixture;
89+
- a non-fallback file without mixed-version capability; and
90+
- binary-copy target mismatch, including the target, actual version, and path.
91+
92+
### Rollout Gate
93+
94+
Before the first mixed-version commit, deploy mixed-aware readers and writers
95+
everywhere that can access the dataset. Then drain or fence writers that opened
96+
the dataset with an older client. Only after both steps may a writer select a
97+
different exact V2 output version. The capability bit makes clients that open
98+
the resulting snapshot fail closed, but it cannot retroactively fence an old
99+
writer that read an earlier manifest.

java/lance-jni/src/blocking_dataset.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3340,6 +3340,14 @@ fn convert_java_compaction_options_to_rust(
33403340
&[],
33413341
)?
33423342
.l()?;
3343+
let data_storage_version = env
3344+
.call_method(
3345+
&java_options,
3346+
"getDataStorageVersion",
3347+
"()Ljava/util/Optional;",
3348+
&[],
3349+
)?
3350+
.l()?;
33433351

33443352
build_compaction_options(
33453353
env,
@@ -3357,6 +3365,7 @@ fn convert_java_compaction_options_to_rust(
33573365
&max_source_rows,
33583366
&max_source_bytes,
33593367
&excluded_fragment_ids,
3368+
&data_storage_version,
33603369
config,
33613370
)
33623371
}

java/lance-jni/src/merge_insert.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ use lance::dataset::{
1515
MergeInsertBuilder, MergeStats, WhenMatched, WhenNotMatched, WhenNotMatchedBySource,
1616
};
1717
use lance_core::datatypes::Schema;
18+
use lance_file::version::LanceFileVersion;
1819
use lance_index::mem_wal::CompactedSsTable;
20+
use std::str::FromStr;
1921
use std::sync::Arc;
2022
use std::time::Duration;
2123
use uuid::Uuid;
@@ -53,6 +55,7 @@ fn inner_merge_insert<'local>(
5355
let skip_auto_cleanup = extract_skip_auto_cleanup(env, &jparam)?;
5456
let use_index = extract_use_index(env, &jparam)?;
5557
let compacted_sstables = extract_compacted_sstables(env, &jparam)?;
58+
let data_storage_version = extract_data_storage_version(env, &jparam)?;
5659

5760
let (new_ds, merge_stats) = unsafe {
5861
let dataset = env.get_rust_field::<_, _, BlockingDataset>(jdataset, NATIVE_DATASET)?;
@@ -63,7 +66,11 @@ fn inner_merge_insert<'local>(
6366
when_not_matched_by_source_delete_expr,
6467
)?;
6568

66-
let merge_insert_job = MergeInsertBuilder::try_new(Arc::new(dataset.clone().inner), on)?
69+
let mut builder = MergeInsertBuilder::try_new(Arc::new(dataset.clone().inner), on)?;
70+
if let Some(version) = data_storage_version {
71+
builder.data_storage_version(LanceFileVersion::from_str(&version)?);
72+
}
73+
let merge_insert_job = builder
6774
.when_matched(when_matched)
6875
.when_not_matched(when_not_matched)
6976
.when_not_matched_by_source(when_not_matched_by_source)
@@ -241,6 +248,16 @@ fn extract_use_index<'local>(env: &mut JNIEnv<'local>, jparam: &JObject) -> Resu
241248
Ok(use_index)
242249
}
243250

251+
fn extract_data_storage_version<'local>(
252+
env: &mut JNIEnv<'local>,
253+
jparam: &JObject,
254+
) -> Result<Option<String>> {
255+
let version = env
256+
.call_method(jparam, "dataStorageVersion", "()Ljava/util/Optional;", &[])?
257+
.l()?;
258+
env.get_string_opt(&version)
259+
}
260+
244261
fn extract_compacted_sstables<'local>(
245262
env: &mut JNIEnv<'local>,
246263
jparam: &JObject,

java/lance-jni/src/optimize.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use lance::dataset::{
1717
};
1818

1919
use crate::{
20-
block_on,
20+
JNIEnvExt, block_on,
2121
blocking_dataset::{BlockingDataset, NATIVE_DATASET},
2222
traits::{
2323
FromJObjectWithEnv, IntoJava, export_vec, import_vec_from_method, import_vec_to_rust,
@@ -49,6 +49,7 @@ pub extern "system" fn Java_org_lance_compaction_Compaction_nativePlanCompaction
4949
max_source_rows: JObject, // Optional<Long>
5050
max_source_bytes: JObject, // Optional<Long>
5151
excluded_fragment_ids: JObject, // List<Long>
52+
data_storage_version: JObject, // Optional<String>
5253
) -> JObject<'local> {
5354
ok_or_throw_with_return!(
5455
env,
@@ -68,7 +69,8 @@ pub extern "system" fn Java_org_lance_compaction_Compaction_nativePlanCompaction
6869
max_source_fragments,
6970
max_source_rows,
7071
max_source_bytes,
71-
excluded_fragment_ids
72+
excluded_fragment_ids,
73+
data_storage_version
7274
),
7375
JObject::null()
7476
)
@@ -92,6 +94,7 @@ fn inner_plan_compaction<'local>(
9294
max_source_rows: JObject, // Optional<Long>
9395
max_source_bytes: JObject, // Optional<Long>
9496
excluded_fragment_ids: JObject, // List<Long>
97+
data_storage_version: JObject, // Optional<String>
9598
) -> Result<JObject<'local>> {
9699
let config = {
97100
let dataset =
@@ -114,6 +117,7 @@ fn inner_plan_compaction<'local>(
114117
&max_source_rows,
115118
&max_source_bytes,
116119
&excluded_fragment_ids,
120+
&data_storage_version,
117121
&config,
118122
)?;
119123

@@ -145,6 +149,7 @@ pub extern "system" fn Java_org_lance_compaction_Compaction_nativeCommitCompacti
145149
max_source_rows: JObject, // Optional<Long>
146150
max_source_bytes: JObject, // Optional<Long>
147151
excluded_fragment_ids: JObject, // List<Long>
152+
data_storage_version: JObject, // Optional<String>
148153
) -> JObject<'local> {
149154
ok_or_throw_with_return!(
150155
env,
@@ -166,6 +171,7 @@ pub extern "system" fn Java_org_lance_compaction_Compaction_nativeCommitCompacti
166171
max_source_rows,
167172
max_source_bytes,
168173
excluded_fragment_ids,
174+
data_storage_version,
169175
),
170176
JObject::null()
171177
)
@@ -190,6 +196,7 @@ fn inner_commit_compaction<'local>(
190196
max_source_rows: JObject, // Optional<Long>
191197
max_source_bytes: JObject, // Optional<Long>
192198
excluded_fragment_ids: JObject, // List<Long>
199+
data_storage_version: JObject, // Optional<String>
193200
) -> Result<JObject<'local>> {
194201
let config = {
195202
let dataset =
@@ -212,6 +219,7 @@ fn inner_commit_compaction<'local>(
212219
&max_source_rows,
213220
&max_source_bytes,
214221
&excluded_fragment_ids,
222+
&data_storage_version,
215223
&config,
216224
)?;
217225
let completed_tasks = import_vec_to_rust(env, &rewrite_results, |env, rewrite_result| {
@@ -252,6 +260,7 @@ pub extern "system" fn Java_org_lance_compaction_CompactionTask_nativeExecute<'l
252260
max_source_rows: JObject, // Optional<Long>
253261
max_source_bytes: JObject, // Optional<Long>
254262
excluded_fragment_ids: JObject, // List<Long>
263+
data_storage_version: JObject, // Optional<String>
255264
) -> JObject<'local> {
256265
ok_or_throw_with_return!(
257266
env,
@@ -273,7 +282,8 @@ pub extern "system" fn Java_org_lance_compaction_CompactionTask_nativeExecute<'l
273282
max_source_fragments,
274283
max_source_rows,
275284
max_source_bytes,
276-
excluded_fragment_ids
285+
excluded_fragment_ids,
286+
data_storage_version
277287
),
278288
JObject::null()
279289
)
@@ -299,6 +309,7 @@ fn inner_execute_task<'local>(
299309
max_source_rows: JObject, // Optional<Long>
300310
max_source_bytes: JObject, // Optional<Long>
301311
excluded_fragment_ids: JObject, // List<Long>
312+
data_storage_version: JObject, // Optional<String>
302313
) -> Result<JObject<'local>> {
303314
let task_data: TaskData = task_data.extract_object(env)?;
304315
let config = {
@@ -322,6 +333,7 @@ fn inner_execute_task<'local>(
322333
&max_source_rows,
323334
&max_source_bytes,
324335
&excluded_fragment_ids,
336+
&data_storage_version,
325337
&config,
326338
)?;
327339
let compaction_task = CompactionTask {
@@ -345,11 +357,10 @@ const COMPACTION_PLAN_CLASS: &str = "org/lance/compaction/CompactionPlan";
345357
const COMPACTION_PLAN_CONSTRUCTOR_SIG: &str =
346358
"(Ljava/util/List;JLorg/lance/compaction/CompactionOptions;)V";
347359
const REWRITE_RESULT_CLASS: &str = "org/lance/compaction/RewriteResult";
348-
const REWRITE_RESULT_CONSTRUCTOR_SIG: &str =
349-
"(Lorg/lance/compaction/CompactionMetrics;Ljava/util/List;Ljava/util/List;J[B)V";
360+
const REWRITE_RESULT_CONSTRUCTOR_SIG: &str = "(Lorg/lance/compaction/CompactionMetrics;Ljava/util/List;Ljava/util/List;J[BLjava/lang/String;)V";
350361
const COMPACTION_OPTIONS_CLASS: &str = "org/lance/compaction/CompactionOptions";
351362
const COMPACTION_MODE_CLASS: &str = "org/lance/compaction/CompactionMode";
352-
const COMPACTION_OPTIONS_CONSTRUCTOR_SIG: &str = "(Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/List;)V";
363+
const COMPACTION_OPTIONS_CONSTRUCTOR_SIG: &str = "(Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/List;Ljava/util/Optional;)V";
353364

354365
impl IntoJava for &TaskData {
355366
fn into_java<'a>(self, env: &mut JNIEnv<'a>) -> Result<JObject<'a>> {
@@ -431,6 +442,11 @@ impl IntoJava for &CompactionOptions {
431442
.map(|fragment_id| to_java_long_obj(env, Some(*fragment_id as i64)))
432443
.collect::<Result<Vec<_>>>()?;
433444
let excluded_fragment_ids = to_java_list(env, &excluded_fragment_ids)?;
445+
let data_storage_version = match self.data_storage_version {
446+
Some(version) => env.new_string(version.to_string())?.into(),
447+
None => JObject::null(),
448+
};
449+
let data_storage_version_opt = to_java_optional(env, data_storage_version)?;
434450

435451
Ok(env.new_object(
436452
COMPACTION_OPTIONS_CLASS,
@@ -450,6 +466,7 @@ impl IntoJava for &CompactionOptions {
450466
JValueGen::Object(&max_source_rows_opt),
451467
JValueGen::Object(&max_source_bytes_opt),
452468
JValueGen::Object(&excluded_fragment_ids),
469+
JValueGen::Object(&data_storage_version_opt),
453470
],
454471
)?)
455472
}
@@ -481,6 +498,7 @@ impl IntoJava for &RewriteResult {
481498
} else {
482499
JObject::null()
483500
};
501+
let write_version: JObject<'_> = env.new_string(&self.write_version)?.into();
484502
Ok(env.new_object(
485503
REWRITE_RESULT_CLASS,
486504
REWRITE_RESULT_CONSTRUCTOR_SIG,
@@ -490,6 +508,7 @@ impl IntoJava for &RewriteResult {
490508
JValueGen::Object(&original_fragments),
491509
JValueGen::Long(self.read_version as i64),
492510
JValueGen::Object(&row_addrs),
511+
JValueGen::Object(&write_version),
493512
],
494513
)?)
495514
}
@@ -554,12 +573,17 @@ impl FromJObjectWithEnv<RewriteResult> for JObject<'_> {
554573
} else {
555574
Some(env.convert_byte_array(row_addrs_obj)?)
556575
};
576+
let write_version_obj = env
577+
.call_method(self, "getWriteVersion", "()Ljava/util/Optional;", &[])?
578+
.l()?;
579+
let write_version = env.get_string_opt(&write_version_obj)?.unwrap_or_default();
557580
Ok(RewriteResult {
558581
metrics,
559582
new_fragments,
560583
read_version,
561584
original_fragments,
562585
row_addrs,
586+
write_version,
563587
})
564588
}
565589
}

java/lance-jni/src/update.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use crate::{JNIEnvExt, block_on};
99
use jni::JNIEnv;
1010
use jni::objects::{JMap, JObject, JValueGen};
1111
use lance::dataset::UpdateBuilder;
12+
use lance_file::version::LanceFileVersion;
13+
use std::str::FromStr;
1214
use std::sync::Arc;
1315
use std::time::Duration;
1416

@@ -30,6 +32,7 @@ fn inner_update<'local>(
3032
let where_clause = extract_where(env, &jparam)?;
3133
let conflict_retries = extract_conflict_retries(env, &jparam)?;
3234
let retry_timeout_ms = extract_retry_timeout_ms(env, &jparam)?;
35+
let data_storage_version = extract_data_storage_version(env, &jparam)?;
3336

3437
// Clone the inner Dataset out of the `get_rust_field` guard and drop the
3538
// guard before running the long-lived async update. Otherwise the guard
@@ -44,6 +47,10 @@ fn inner_update<'local>(
4447
.conflict_retries(conflict_retries)
4548
.retry_timeout(Duration::from_millis(retry_timeout_ms));
4649

50+
if let Some(version) = data_storage_version {
51+
builder = builder.data_storage_version(LanceFileVersion::from_str(&version)?);
52+
}
53+
4754
if let Some(predicate) = where_clause {
4855
builder = builder.update_where(&predicate)?;
4956
}
@@ -96,6 +103,16 @@ fn extract_retry_timeout_ms<'local>(env: &mut JNIEnv<'local>, jparam: &JObject)
96103
Ok(timeout_ms)
97104
}
98105

106+
fn extract_data_storage_version<'local>(
107+
env: &mut JNIEnv<'local>,
108+
jparam: &JObject,
109+
) -> Result<Option<String>> {
110+
let version = env
111+
.call_method(jparam, "dataStorageVersion", "()Ljava/util/Optional;", &[])?
112+
.l()?;
113+
env.get_string_opt(&version)
114+
}
115+
99116
const UPDATE_RESULT_CLASS: &str = "org/lance/update/UpdateResult";
100117
const UPDATE_RESULT_CONSTRUCTOR_SIG: &str = "(Lorg/lance/Dataset;J)V";
101118

java/lance-jni/src/utils.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ pub fn build_compaction_options(
194194
max_source_rows: &JObject, // Optional<Long>
195195
max_source_bytes: &JObject, // Optional<Long>
196196
excluded_fragment_ids: &JObject, // List<Long>
197+
data_storage_version: &JObject, // Optional<String>
197198
config: &std::collections::HashMap<String, String>,
198199
) -> Result<CompactionOptions> {
199200
let mut compaction_options = CompactionOptions::from_dataset_config(config)?;
@@ -256,6 +257,9 @@ pub fn build_compaction_options(
256257
})
257258
})
258259
.collect::<Result<Vec<_>>>()?;
260+
if let Some(version) = env.get_string_opt(data_storage_version)? {
261+
compaction_options.data_storage_version = Some(LanceFileVersion::from_str(&version)?);
262+
}
259263

260264
Ok(compaction_options)
261265
}

0 commit comments

Comments
 (0)