1use std::collections::{HashMap, HashSet};
13use std::num::NonZeroUsize;
14use std::ops::Range;
15use std::sync::{
16 Arc, Mutex,
17 atomic::{AtomicU32, Ordering},
18};
19
20use arrow_array::{
21 Array, ArrayRef, StructArray,
22 builder::{LargeBinaryBuilder, PrimitiveBuilder, StringBuilder},
23 cast::AsArray,
24 types::{UInt8Type, UInt32Type, UInt64Type},
25};
26use arrow_buffer::NullBufferBuilder;
27use arrow_schema::{DataType, Field, Schema as ArrowSchema};
28use bytes::Bytes;
29use lance_arrow::{
30 ARROW_EXT_NAME_KEY, BLOB_DEDICATED_SIZE_THRESHOLD_META_KEY,
31 BLOB_INLINE_SIZE_THRESHOLD_META_KEY, BLOB_V2_EXT_NAME, FieldExt,
32};
33use lance_core::{
34 datatypes::{
35 BLOB_V2_DESC_TYPE, BLOB_V2_LOGICAL_MINIMAL_FIELDS, BLOB_V2_PREPARED_FIELDS,
36 BLOB_V2_PREPARED_TYPE, BlobKind, BlobV2Layout, Field as LanceField, Schema as LanceSchema,
37 },
38 utils::blob::blob_path,
39};
40use lance_io::{
41 object_store::ObjectStore,
42 traits::{Reader, WriteExt, Writer},
43};
44use object_store::path::Path;
45use tokio::io::AsyncWriteExt;
46
47use crate::{Error, Result};
48
49pub fn blob_field(name: &str, nullable: bool) -> Field {
56 blob_field_with_options(name, nullable, BlobFieldOptions::default())
57}
58
59#[derive(Clone, Debug, Default)]
61pub struct BlobFieldOptions {
62 pub inline_size_threshold: Option<usize>,
64 pub dedicated_size_threshold: Option<NonZeroUsize>,
69}
70
71impl BlobFieldOptions {
72 pub fn with_inline_size_threshold(mut self, threshold: usize) -> Self {
74 self.inline_size_threshold = Some(threshold);
75 self
76 }
77
78 pub fn with_dedicated_size_threshold(mut self, threshold: NonZeroUsize) -> Self {
80 self.dedicated_size_threshold = Some(threshold);
81 self
82 }
83}
84
85pub fn blob_field_with_options(name: &str, nullable: bool, options: BlobFieldOptions) -> Field {
108 let mut metadata = [(ARROW_EXT_NAME_KEY.to_string(), BLOB_V2_EXT_NAME.to_string())]
109 .into_iter()
110 .collect::<std::collections::HashMap<_, _>>();
111 if let Some(threshold) = options.inline_size_threshold {
112 metadata.insert(
113 BLOB_INLINE_SIZE_THRESHOLD_META_KEY.to_string(),
114 threshold.to_string(),
115 );
116 }
117 if let Some(threshold) = options.dedicated_size_threshold {
118 metadata.insert(
119 BLOB_DEDICATED_SIZE_THRESHOLD_META_KEY.to_string(),
120 threshold.get().to_string(),
121 );
122 }
123 Field::new(
124 name,
125 DataType::Struct(BLOB_V2_LOGICAL_MINIMAL_FIELDS.clone()),
126 nullable,
127 )
128 .with_metadata(metadata)
129}
130
131pub(crate) fn prepared_blob_field_with_metadata(
132 name: &str,
133 nullable: bool,
134 metadata: HashMap<String, String>,
135) -> Field {
136 Field::new(name, BLOB_V2_PREPARED_TYPE.clone(), nullable).with_metadata(metadata)
137}
138
139fn logical_to_prepared_blob_field(field: &Field) -> Result<Field> {
140 if field.is_blob_v2() {
141 return match blob_v2_layout(field) {
142 Some(BlobV2Layout::Logical) | Some(BlobV2Layout::Prepared) => {
143 Ok(prepared_blob_field_with_metadata(
144 field.name(),
145 field.is_nullable(),
146 field.metadata().clone(),
147 ))
148 }
149 _ => Err(blob_v2_shape_error(
150 field,
151 &[BlobV2Layout::Logical, BlobV2Layout::Prepared],
152 )),
153 };
154 }
155
156 let data_type = match field.data_type() {
157 DataType::Struct(children) => DataType::Struct(
158 children
159 .iter()
160 .map(|child| logical_to_prepared_blob_field(child.as_ref()).map(Arc::new))
161 .collect::<Result<Vec<_>>>()?
162 .into(),
163 ),
164 DataType::List(child) => DataType::List(Arc::new(logical_to_prepared_blob_field(child)?)),
165 DataType::LargeList(child) => {
166 DataType::LargeList(Arc::new(logical_to_prepared_blob_field(child)?))
167 }
168 _ => field.data_type().clone(),
169 };
170 Ok(field.clone().with_data_type(data_type))
171}
172
173pub(crate) fn logical_to_prepared_blob_schema(schema: &ArrowSchema) -> Result<ArrowSchema> {
174 let fields = schema
175 .fields()
176 .iter()
177 .map(|field| logical_to_prepared_blob_field(field.as_ref()).map(Arc::new))
178 .collect::<Result<Vec<_>>>()?;
179 Ok(ArrowSchema::new_with_metadata(
180 fields,
181 schema.metadata().clone(),
182 ))
183}
184
185fn prepared_to_descriptor_blob_field(field: &Field) -> Result<Field> {
186 if field.is_blob_v2() {
187 return match blob_v2_layout(field) {
188 Some(BlobV2Layout::Prepared) | Some(BlobV2Layout::Descriptor) => {
189 Ok(
190 Field::new(field.name(), BLOB_V2_DESC_TYPE.clone(), field.is_nullable())
191 .with_metadata(field.metadata().clone()),
192 )
193 }
194 Some(BlobV2Layout::Logical) => Ok(field.clone()),
195 _ => Err(blob_v2_shape_error(
196 field,
197 &[BlobV2Layout::Prepared, BlobV2Layout::Descriptor],
198 )),
199 };
200 }
201
202 let data_type = match field.data_type() {
203 DataType::Struct(children) => DataType::Struct(
204 children
205 .iter()
206 .map(|child| prepared_to_descriptor_blob_field(child.as_ref()).map(Arc::new))
207 .collect::<Result<Vec<_>>>()?
208 .into(),
209 ),
210 DataType::List(child) => {
211 DataType::List(Arc::new(prepared_to_descriptor_blob_field(child)?))
212 }
213 DataType::LargeList(child) => {
214 DataType::LargeList(Arc::new(prepared_to_descriptor_blob_field(child)?))
215 }
216 _ => field.data_type().clone(),
217 };
218 Ok(field.clone().with_data_type(data_type))
219}
220
221pub(crate) fn prepared_to_descriptor_blob_schema(schema: &ArrowSchema) -> Result<ArrowSchema> {
222 let fields = schema
223 .fields()
224 .iter()
225 .map(|field| prepared_to_descriptor_blob_field(field.as_ref()).map(Arc::new))
226 .collect::<Result<Vec<_>>>()?;
227 Ok(ArrowSchema::new_with_metadata(
228 fields,
229 schema.metadata().clone(),
230 ))
231}
232
233fn logical_blob_lance_children() -> Result<Vec<LanceField>> {
234 BLOB_V2_LOGICAL_MINIMAL_FIELDS
235 .iter()
236 .map(|field| LanceField::try_from(field.as_ref()))
237 .collect()
238}
239
240pub(crate) fn blob_v2_layout(field: &Field) -> Option<BlobV2Layout> {
241 if !field.is_blob_v2() {
242 return None;
243 }
244 let DataType::Struct(fields) = field.data_type() else {
245 return None;
246 };
247 BlobV2Layout::classify(fields)
248}
249
250pub(crate) fn blob_v2_shape_error(field: &Field, expected: &[BlobV2Layout]) -> Error {
251 let expected = expected
252 .iter()
253 .map(ToString::to_string)
254 .collect::<Vec<_>>()
255 .join(" or ");
256 let actual = match field.data_type() {
257 DataType::Struct(fields) => BlobV2Layout::classify(fields)
258 .map(|layout| format!("{layout} layout"))
259 .unwrap_or_else(|| format!("unrecognized layout {fields:?}")),
260 data_type => format!("non-struct type {data_type}"),
261 };
262 Error::invalid_input(format!(
263 "Blob v2 field '{}' has {actual}; expected {expected} layout",
264 field.name(),
265 ))
266}
267
268fn prepared_to_logical_blob_lance_field(field: &LanceField) -> Result<LanceField> {
269 if field.is_blob_v2() {
270 let arrow_field = Field::from(field);
271 match blob_v2_layout(&arrow_field) {
272 Some(BlobV2Layout::Prepared) => {
273 let mut normalized = field.clone();
274 let mut logical_children = logical_blob_lance_children()?;
275 for logical_child in &mut logical_children {
276 let prepared_child = field
277 .children
278 .iter()
279 .find(|prepared_child| prepared_child.name == logical_child.name)
280 .ok_or_else(|| {
281 Error::internal(format!(
282 "Prepared blob v2 field '{}' is missing logical child '{}'",
283 field.name, logical_child.name
284 ))
285 })?;
286 logical_child.id = prepared_child.id;
287 logical_child.parent_id = field.id;
288 }
289 normalized.children = logical_children;
290 return Ok(normalized);
291 }
292 Some(BlobV2Layout::Logical) => return Ok(field.clone()),
293 _ => {
294 return Err(blob_v2_shape_error(
295 &arrow_field,
296 &[BlobV2Layout::Logical, BlobV2Layout::Prepared],
297 ));
298 }
299 }
300 }
301
302 if field.children.is_empty() {
303 return Ok(field.clone());
304 }
305
306 let normalized_children = field
307 .children
308 .iter()
309 .map(prepared_to_logical_blob_lance_field)
310 .collect::<Result<Vec<_>>>()?;
311
312 Ok(LanceField {
313 children: normalized_children,
314 ..field.clone()
315 })
316}
317
318pub(crate) fn prepared_to_logical_blob_schema(schema: &LanceSchema) -> Result<LanceSchema> {
319 let fields = schema
320 .fields
321 .iter()
322 .map(prepared_to_logical_blob_lance_field)
323 .collect::<Result<Vec<_>>>()?;
324 Ok(LanceSchema {
325 fields,
326 metadata: schema.metadata.clone(),
327 })
328}
329
330#[derive(Clone, Debug)]
331pub(crate) struct BlobIdAllocator {
332 inner: Arc<BlobIdAllocatorInner>,
333}
334
335#[derive(Debug)]
336struct BlobIdAllocatorInner {
337 start_inclusive: u32,
338 next: AtomicU32,
339 end_exclusive: Option<u32>,
340 state: Mutex<BlobIdAllocatorState>,
341}
342
343#[derive(Debug, Default)]
344struct BlobIdAllocatorState {
345 used: HashSet<u32>,
346 allocated: HashSet<u32>,
347}
348
349impl BlobIdAllocator {
350 pub(crate) fn new(start: u32) -> Self {
351 Self {
352 inner: Arc::new(BlobIdAllocatorInner {
353 start_inclusive: start,
354 next: AtomicU32::new(start),
355 end_exclusive: None,
356 state: Mutex::new(BlobIdAllocatorState::default()),
357 }),
358 }
359 }
360
361 pub(crate) fn from_range(range: Range<u32>) -> Result<Self> {
362 if range.start == 0 || range.start >= range.end {
363 return Err(Error::invalid_input(format!(
364 "Blob ID range must be non-empty and start at 1 or greater, got {}..{}",
365 range.start, range.end
366 )));
367 }
368 Ok(Self {
369 inner: Arc::new(BlobIdAllocatorInner {
370 start_inclusive: range.start,
371 next: AtomicU32::new(range.start),
372 end_exclusive: Some(range.end),
373 state: Mutex::new(BlobIdAllocatorState::default()),
374 }),
375 })
376 }
377
378 pub(crate) fn next(&self) -> Result<u32> {
379 loop {
380 let id = self.inner.next.load(Ordering::Relaxed);
381 if id == u32::MAX || self.inner.end_exclusive.is_some_and(|end| id >= end) {
382 return Err(Error::invalid_input(match self.inner.end_exclusive {
383 Some(end) => format!(
384 "Blob ID range exhausted before allocating another sidecar; range ends at {end}"
385 ),
386 None => "Blob id allocator exhausted u32 id space".to_string(),
387 }));
388 }
389 if self
390 .inner
391 .next
392 .compare_exchange(id, id + 1, Ordering::Relaxed, Ordering::Relaxed)
393 .is_err()
394 {
395 continue;
396 }
397 let mut state =
398 self.inner.state.lock().map_err(|_| {
399 Error::internal("Blob id allocator mutex was poisoned".to_string())
400 })?;
401 if state.used.insert(id) {
402 state.allocated.insert(id);
403 return Ok(id);
404 }
405 }
406 }
407
408 pub(crate) fn reserve(&self, id: u32) -> Result<()> {
409 if id < self.inner.start_inclusive || self.inner.end_exclusive.is_some_and(|end| id >= end)
410 {
411 return Err(Error::invalid_input(match self.inner.end_exclusive {
412 Some(end) => format!(
413 "Blob ID {id} is outside allocator range {}..{end}",
414 self.inner.start_inclusive
415 ),
416 None => format!(
417 "Blob ID {id} is below allocator start {}",
418 self.inner.start_inclusive
419 ),
420 }));
421 }
422 let mut state = self
423 .inner
424 .state
425 .lock()
426 .map_err(|_| Error::internal("Blob id allocator mutex was poisoned".to_string()))?;
427 if state.allocated.contains(&id) {
428 return Err(Error::invalid_input(format!(
429 "Blob ID {id} was already allocated for a generated sidecar"
430 )));
431 }
432 state.used.insert(id);
433 Ok(())
434 }
435
436 #[cfg(test)]
437 pub(crate) fn allocated_ids(&self) -> Result<Vec<u32>> {
438 let mut ids = self
439 .inner
440 .state
441 .lock()
442 .map_err(|_| Error::internal("Blob id allocator mutex was poisoned".to_string()))?
443 .allocated
444 .iter()
445 .copied()
446 .collect::<Vec<_>>();
447 ids.sort_unstable();
448 Ok(ids)
449 }
450}
451
452fn validate_blob_id(blob_id: u32) -> Result<()> {
453 if blob_id == 0 {
454 return Err(Error::invalid_input("Blob id 0 is reserved"));
455 }
456 Ok(())
457}
458
459fn validate_range(offset: u64, size: u64, object_size: u64, label: &str) -> Result<()> {
460 let end = offset.checked_add(size).ok_or_else(|| {
461 Error::invalid_input(format!(
462 "{label} range overflows u64: offset={offset}, size={size}"
463 ))
464 })?;
465 if end > object_size {
466 return Err(Error::invalid_input(format!(
467 "{label} range [{offset}, {end}) exceeds blob object size {object_size}"
468 )));
469 }
470 Ok(())
471}
472
473fn validate_prepared_blob_value_array(field: &Field, array: &ArrayRef) -> Result<()> {
474 if blob_v2_layout(field) != Some(BlobV2Layout::Prepared) {
475 return Err(blob_v2_shape_error(field, &[BlobV2Layout::Prepared]));
476 }
477
478 let struct_arr = array
479 .as_any()
480 .downcast_ref::<StructArray>()
481 .ok_or_else(|| Error::invalid_input("Prepared blob column was not a struct array"))?;
482 if BlobV2Layout::classify(struct_arr.fields()) != Some(BlobV2Layout::Prepared) {
483 let actual = BlobV2Layout::classify(struct_arr.fields())
484 .map(|layout| layout.to_string())
485 .unwrap_or_else(|| format!("unrecognized ({:?})", struct_arr.fields()));
486 return Err(Error::invalid_input(format!(
487 "Prepared blob column '{}' has {actual} array layout; expected prepared layout",
488 field.name()
489 )));
490 }
491 let kind_col = struct_arr
492 .column_by_name("kind")
493 .ok_or_else(|| Error::invalid_input("Prepared blob struct missing `kind` field"))?
494 .as_primitive::<UInt8Type>();
495 let data_col = struct_arr
496 .column_by_name("data")
497 .ok_or_else(|| Error::invalid_input("Prepared blob struct missing `data` field"))?
498 .as_binary::<i64>();
499 let uri_col = struct_arr
500 .column_by_name("uri")
501 .ok_or_else(|| Error::invalid_input("Prepared blob struct missing `uri` field"))?
502 .as_string::<i32>();
503 let blob_id_col = struct_arr
504 .column_by_name("blob_id")
505 .ok_or_else(|| Error::invalid_input("Prepared blob struct missing `blob_id` field"))?
506 .as_primitive::<UInt32Type>();
507 let blob_size_col = struct_arr
508 .column_by_name("blob_size")
509 .ok_or_else(|| Error::invalid_input("Prepared blob struct missing `blob_size` field"))?
510 .as_primitive::<UInt64Type>();
511 let position_col = struct_arr
512 .column_by_name("position")
513 .ok_or_else(|| Error::invalid_input("Prepared blob struct missing `position` field"))?
514 .as_primitive::<UInt64Type>();
515
516 for row in 0..struct_arr.len() {
517 if struct_arr.is_null(row) {
518 continue;
519 }
520 if kind_col.is_null(row) {
521 return Err(Error::invalid_input(format!(
522 "Prepared blob row {row} is non-null but `kind` is null"
523 )));
524 }
525
526 match BlobKind::try_from(kind_col.value(row))? {
527 BlobKind::Inline => {
528 if data_col.is_null(row) {
529 return Err(Error::invalid_input(format!(
530 "Prepared inline blob row {row} must set `data`"
531 )));
532 }
533 }
534 BlobKind::Packed => {
535 if blob_id_col.is_null(row)
536 || blob_size_col.is_null(row)
537 || position_col.is_null(row)
538 {
539 return Err(Error::invalid_input(format!(
540 "Prepared packed blob row {row} must set `blob_id`, `blob_size`, and `position`"
541 )));
542 }
543 validate_blob_id(blob_id_col.value(row))?;
544 let offset = position_col.value(row);
545 let size = blob_size_col.value(row);
546 offset.checked_add(size).ok_or_else(|| {
547 Error::invalid_input(format!(
548 "Prepared packed blob row {row} range overflows u64: offset={offset}, size={size}"
549 ))
550 })?;
551 }
552 BlobKind::Dedicated => {
553 if blob_id_col.is_null(row) || blob_size_col.is_null(row) {
554 return Err(Error::invalid_input(format!(
555 "Prepared dedicated blob row {row} must set `blob_id` and `blob_size`"
556 )));
557 }
558 validate_blob_id(blob_id_col.value(row))?;
559 }
560 BlobKind::External => {
561 if uri_col.is_null(row) || uri_col.value(row).is_empty() {
562 return Err(Error::invalid_input(format!(
563 "Prepared external blob row {row} must set a non-empty `uri`"
564 )));
565 }
566 let offset = if position_col.is_null(row) {
567 0
568 } else {
569 position_col.value(row)
570 };
571 let size = if blob_size_col.is_null(row) {
572 0
573 } else {
574 blob_size_col.value(row)
575 };
576 offset.checked_add(size).ok_or_else(|| {
577 Error::invalid_input(format!(
578 "Prepared external blob row {row} range overflows u64: offset={offset}, size={size}"
579 ))
580 })?;
581 }
582 }
583 }
584
585 Ok(())
586}
587
588pub(crate) fn validate_prepared_blob_array(field: &Field, array: &ArrayRef) -> Result<()> {
590 validate_prepared_blob_value_array(field, array)
591}
592
593fn sidecar_path_for_data_file(data_file_path: &Path, blob_id: u32) -> Result<Path> {
594 validate_blob_id(blob_id)?;
595 let file_name = data_file_path.filename().ok_or_else(|| {
596 Error::invalid_input("Data file path must include a file name".to_string())
597 })?;
598 let data_file_key = file_name.strip_suffix(".lance").ok_or_else(|| {
599 Error::invalid_input(format!(
600 "Data file path '{}' must end with '.lance'",
601 data_file_path
602 ))
603 })?;
604 let data_dir = data_file_path.parent().unwrap_or_default();
605 Ok(blob_path(&data_dir, data_file_key, blob_id))
606}
607
608#[derive(Clone, Copy, Debug, PartialEq, Eq)]
610pub struct BlobRange {
611 pub offset: u64,
613 pub size: u64,
615}
616
617#[derive(Clone, Debug, PartialEq, Eq)]
619pub enum BlobDescriptor {
620 Null,
622 Inline { data: Bytes },
624 Packed {
626 blob_id: u32,
627 offset: u64,
628 size: u64,
629 },
630 Dedicated { blob_id: u32, size: u64 },
632 External {
634 base_id: u32,
635 uri: String,
636 offset: u64,
637 size: u64,
638 },
639}
640
641pub struct BlobDescriptorColumn {
646 field: Field,
647 array: ArrayRef,
648}
649
650impl BlobDescriptorColumn {
651 pub fn field(&self) -> &Field {
653 &self.field
654 }
655
656 pub fn array(&self) -> &ArrayRef {
658 &self.array
659 }
660
661 pub fn into_parts(self) -> (Field, ArrayRef) {
663 (self.field, self.array)
664 }
665}
666
667pub struct BlobDescriptorArrayBuilder {
672 field: Field,
673 values: Vec<BlobDescriptor>,
674}
675
676impl BlobDescriptorArrayBuilder {
677 pub fn new(column: impl Into<String>) -> Self {
679 let mut metadata = HashMap::with_capacity(1);
680 metadata.insert(ARROW_EXT_NAME_KEY.to_string(), BLOB_V2_EXT_NAME.to_string());
681 Self::new_with_metadata(column, true, metadata)
682 }
683
684 pub(crate) fn new_with_metadata(
685 column: impl Into<String>,
686 nullable: bool,
687 metadata: HashMap<String, String>,
688 ) -> Self {
689 Self {
690 field: prepared_blob_field_with_metadata(&column.into(), nullable, metadata),
691 values: Vec::new(),
692 }
693 }
694
695 pub fn push_packed(&mut self, blob_id: u32, range: BlobRange) -> Result<()> {
697 self.push(BlobDescriptor::Packed {
698 blob_id,
699 offset: range.offset,
700 size: range.size,
701 })
702 }
703
704 pub fn extend_packed(
706 &mut self,
707 blob_id: u32,
708 ranges: impl IntoIterator<Item = BlobRange>,
709 ) -> Result<()> {
710 for range in ranges {
711 self.push_packed(blob_id, range)?;
712 }
713 Ok(())
714 }
715
716 pub fn push_dedicated(&mut self, blob_id: u32, size: u64) -> Result<()> {
718 self.push(BlobDescriptor::Dedicated { blob_id, size })
719 }
720
721 pub fn push(&mut self, value: BlobDescriptor) -> Result<()> {
723 validate_blob_descriptor(&value)?;
724 self.values.push(value);
725 Ok(())
726 }
727
728 pub fn extend(&mut self, values: impl IntoIterator<Item = BlobDescriptor>) -> Result<()> {
730 for value in values {
731 self.push(value)?;
732 }
733 Ok(())
734 }
735
736 pub fn push_inline(&mut self, data: impl Into<Bytes>) -> Result<()> {
738 self.push(BlobDescriptor::Inline { data: data.into() })
739 }
740
741 pub fn push_external(
743 &mut self,
744 uri: impl Into<String>,
745 range: Option<BlobRange>,
746 ) -> Result<()> {
747 let range = range.unwrap_or(BlobRange { offset: 0, size: 0 });
748 self.push(BlobDescriptor::External {
749 base_id: 0,
750 uri: uri.into(),
751 offset: range.offset,
752 size: range.size,
753 })
754 }
755
756 pub fn push_null(&mut self) -> Result<()> {
758 self.push(BlobDescriptor::Null)
759 }
760
761 pub fn field(&self) -> &Field {
763 &self.field
764 }
765
766 pub fn finish(self) -> Result<BlobDescriptorColumn> {
768 let mut kind_builder = PrimitiveBuilder::<UInt8Type>::with_capacity(self.values.len());
769 let mut data_builder = LargeBinaryBuilder::with_capacity(self.values.len(), 0);
770 let mut uri_builder = StringBuilder::with_capacity(self.values.len(), 0);
771 let mut blob_id_builder = PrimitiveBuilder::<UInt32Type>::with_capacity(self.values.len());
772 let mut blob_size_builder =
773 PrimitiveBuilder::<UInt64Type>::with_capacity(self.values.len());
774 let mut position_builder = PrimitiveBuilder::<UInt64Type>::with_capacity(self.values.len());
775 let mut validity = NullBufferBuilder::new(self.values.len());
776
777 for value in self.values {
778 match value {
779 BlobDescriptor::Null => {
780 validity.append_null();
781 kind_builder.append_null();
782 data_builder.append_null();
783 uri_builder.append_null();
784 blob_id_builder.append_null();
785 blob_size_builder.append_null();
786 position_builder.append_null();
787 }
788 BlobDescriptor::Inline { data } => {
789 validity.append_non_null();
790 kind_builder.append_value(BlobKind::Inline as u8);
791 data_builder.append_value(data.as_ref());
792 uri_builder.append_null();
793 blob_id_builder.append_null();
794 blob_size_builder.append_null();
795 position_builder.append_null();
796 }
797 BlobDescriptor::Packed {
798 blob_id,
799 offset,
800 size,
801 } => {
802 validity.append_non_null();
803 kind_builder.append_value(BlobKind::Packed as u8);
804 data_builder.append_null();
805 uri_builder.append_null();
806 blob_id_builder.append_value(blob_id);
807 blob_size_builder.append_value(size);
808 position_builder.append_value(offset);
809 }
810 BlobDescriptor::Dedicated { blob_id, size } => {
811 validity.append_non_null();
812 kind_builder.append_value(BlobKind::Dedicated as u8);
813 data_builder.append_null();
814 uri_builder.append_null();
815 blob_id_builder.append_value(blob_id);
816 blob_size_builder.append_value(size);
817 position_builder.append_null();
818 }
819 BlobDescriptor::External {
820 base_id,
821 uri,
822 offset,
823 size,
824 } => {
825 validity.append_non_null();
826 kind_builder.append_value(BlobKind::External as u8);
827 data_builder.append_null();
828 uri_builder.append_value(uri);
829 blob_id_builder.append_value(base_id);
830 blob_size_builder.append_value(size);
831 position_builder.append_value(offset);
832 }
833 }
834 }
835
836 let array = Arc::new(StructArray::try_new(
837 BLOB_V2_PREPARED_FIELDS.clone(),
838 vec![
839 Arc::new(kind_builder.finish()),
840 Arc::new(data_builder.finish()),
841 Arc::new(uri_builder.finish()),
842 Arc::new(blob_id_builder.finish()),
843 Arc::new(blob_size_builder.finish()),
844 Arc::new(position_builder.finish()),
845 ],
846 validity.finish(),
847 )?) as ArrayRef;
848 validate_prepared_blob_array(&self.field, &array)?;
849
850 Ok(BlobDescriptorColumn {
851 field: self.field,
852 array,
853 })
854 }
855}
856
857fn validate_blob_descriptor(value: &BlobDescriptor) -> Result<()> {
858 match value {
859 BlobDescriptor::Null => Ok(()),
860 BlobDescriptor::Inline { .. } => Ok(()),
861 BlobDescriptor::Packed {
862 blob_id,
863 offset,
864 size,
865 } => {
866 validate_blob_id(*blob_id)?;
867 offset.checked_add(*size).ok_or_else(|| {
868 Error::invalid_input(format!(
869 "Packed blob range overflows u64: offset={offset}, size={size}"
870 ))
871 })?;
872 Ok(())
873 }
874 BlobDescriptor::Dedicated { blob_id, .. } => validate_blob_id(*blob_id),
875 BlobDescriptor::External {
876 uri, offset, size, ..
877 } => {
878 if uri.is_empty() {
879 return Err(Error::invalid_input("External blob URI cannot be empty"));
880 }
881 offset.checked_add(*size).ok_or_else(|| {
882 Error::invalid_input(format!(
883 "External blob range overflows u64: offset={offset}, size={size}"
884 ))
885 })?;
886 Ok(())
887 }
888 }
889}
890
891fn packed_descriptor(blob_id: u32, offset: u64, size: u64) -> Result<(BlobDescriptor, u64)> {
892 let next_offset = offset.checked_add(size).ok_or_else(|| {
893 Error::invalid_input(format!(
894 "Packed blob writer offset overflowed: offset={offset}, size={size}"
895 ))
896 })?;
897 Ok((
898 BlobDescriptor::Packed {
899 blob_id,
900 offset,
901 size,
902 },
903 next_offset,
904 ))
905}
906
907pub struct PackedBlobWriter {
909 object_store: ObjectStore,
910 path: Path,
911 blob_id: u32,
912 writer: Option<Box<dyn Writer>>,
913 offset: u64,
914 values: Vec<BlobDescriptor>,
915}
916
917impl PackedBlobWriter {
918 pub async fn try_new(
920 object_store: ObjectStore,
921 data_file_path: Path,
922 blob_id: u32,
923 ) -> Result<Self> {
924 let path = sidecar_path_for_data_file(&data_file_path, blob_id)?;
925 let writer = object_store.create(&path).await?;
926 Ok(Self {
927 object_store,
928 path,
929 blob_id,
930 writer: Some(writer),
931 offset: 0,
932 values: Vec::new(),
933 })
934 }
935
936 pub fn blob_id(&self) -> u32 {
938 self.blob_id
939 }
940
941 pub fn path(&self) -> &Path {
943 &self.path
944 }
945
946 pub async fn write_blob(&mut self, bytes: impl AsRef<[u8]>) -> Result<()> {
948 let bytes = bytes.as_ref();
949 self.write_blob_bytes(bytes).await?;
950 Ok(())
951 }
952
953 pub async fn write_packed_blobs<'a>(
976 &mut self,
977 blobs: impl IntoIterator<Item = Option<&'a [u8]>>,
978 ) -> Result<()> {
979 let mut writer = self.take_writer()?;
980 let mut descriptors = Vec::new();
981 let mut next_offset = self.offset;
982 for blob in blobs {
983 let Some(blob) = blob else {
984 descriptors.push(BlobDescriptor::Null);
985 continue;
986 };
987 let (descriptor, following_offset) =
988 packed_descriptor(self.blob_id, next_offset, blob.len() as u64)?;
989 if !blob.is_empty() {
990 writer.write_all(blob).await?;
991 }
992 descriptors.push(descriptor);
993 next_offset = following_offset;
994 }
995 self.writer = Some(writer);
996 self.offset = next_offset;
997 self.values.extend(descriptors);
998 Ok(())
999 }
1000
1001 pub(crate) async fn write_blob_bytes(&mut self, bytes: &[u8]) -> Result<BlobDescriptor> {
1002 let size = bytes.len() as u64;
1003 let offset = self.offset;
1004 let mut writer = self.take_writer()?;
1005 writer.write_all(bytes).await?;
1006 self.writer = Some(writer);
1007 self.record_written_blob(offset, size)
1008 }
1009
1010 pub(crate) async fn write_blob_from_reader(
1011 &mut self,
1012 reader: &dyn Reader,
1013 range: Range<usize>,
1014 ) -> Result<BlobDescriptor> {
1015 let size = range.len() as u64;
1016 let offset = self.offset;
1017 let mut writer = self.take_writer()?;
1018 writer.copy_range_from_reader(reader, range).await?;
1019 self.writer = Some(writer);
1020 self.record_written_blob(offset, size)
1021 }
1022
1023 fn record_written_blob(&mut self, offset: u64, size: u64) -> Result<BlobDescriptor> {
1024 let (value, next_offset) = packed_descriptor(self.blob_id, offset, size)?;
1025 self.offset = next_offset;
1026 self.values.push(value.clone());
1027 Ok(value)
1028 }
1029
1030 fn take_writer(&mut self) -> Result<Box<dyn Writer>> {
1031 self.writer.take().ok_or_else(|| {
1032 Error::io(format!(
1033 "Packed blob writer for '{}' has no active upload",
1034 self.path
1035 ))
1036 })
1037 }
1038
1039 pub async fn finish(mut self) -> Result<Vec<BlobDescriptor>> {
1041 let mut writer = self.take_writer()?;
1042 Writer::shutdown(writer.as_mut()).await?;
1043 let object_size = self.object_store.size(&self.path).await?;
1044 validate_range(0, self.offset, object_size, "Packed blob")?;
1045 Ok(self.values)
1046 }
1047}
1048
1049pub struct DedicatedBlobWriter {
1051 object_store: ObjectStore,
1052 path: Path,
1053 blob_id: u32,
1054 writer: Box<dyn Writer>,
1055 size: u64,
1056}
1057
1058impl DedicatedBlobWriter {
1059 pub async fn try_new(
1061 object_store: ObjectStore,
1062 data_file_path: Path,
1063 blob_id: u32,
1064 ) -> Result<Self> {
1065 let path = sidecar_path_for_data_file(&data_file_path, blob_id)?;
1066 let writer = object_store.create(&path).await?;
1067 Ok(Self {
1068 object_store,
1069 path,
1070 blob_id,
1071 writer,
1072 size: 0,
1073 })
1074 }
1075
1076 pub fn blob_id(&self) -> u32 {
1078 self.blob_id
1079 }
1080
1081 pub fn path(&self) -> &Path {
1083 &self.path
1084 }
1085
1086 pub async fn write(&mut self, bytes: impl AsRef<[u8]>) -> Result<()> {
1088 let bytes = bytes.as_ref();
1089 let size = bytes.len() as u64;
1090 self.writer.write_all(bytes).await?;
1091 self.record_written_bytes(size)
1092 }
1093
1094 pub(crate) async fn write_from_reader(
1095 &mut self,
1096 reader: &dyn Reader,
1097 range: Range<usize>,
1098 ) -> Result<()> {
1099 let size = range.len() as u64;
1100 self.writer.copy_range_from_reader(reader, range).await?;
1101 self.record_written_bytes(size)
1102 }
1103
1104 fn record_written_bytes(&mut self, size: u64) -> Result<()> {
1105 self.size = self.size.checked_add(size).ok_or_else(|| {
1106 Error::invalid_input(format!(
1107 "Dedicated blob writer size overflowed: current={}, append={size}",
1108 self.size
1109 ))
1110 })?;
1111 Ok(())
1112 }
1113
1114 pub async fn finish(mut self) -> Result<BlobDescriptor> {
1116 Writer::shutdown(self.writer.as_mut()).await?;
1117 let object_size = self.object_store.size(&self.path).await?;
1118 if object_size != self.size {
1119 return Err(Error::io(format!(
1120 "Dedicated blob sidecar '{}' has size {}, expected {}",
1121 self.path, object_size, self.size
1122 )));
1123 }
1124 Ok(BlobDescriptor::Dedicated {
1125 blob_id: self.blob_id,
1126 size: self.size,
1127 })
1128 }
1129}
1130
1131pub struct BlobArrayBuilder {
1135 data_builder: LargeBinaryBuilder,
1136 uri_builder: StringBuilder,
1137 validity: NullBufferBuilder,
1138 expected_len: usize,
1139 len: usize,
1140}
1141
1142impl BlobArrayBuilder {
1143 pub fn new(capacity: usize) -> Self {
1145 Self {
1146 data_builder: LargeBinaryBuilder::with_capacity(capacity, 0),
1147 uri_builder: StringBuilder::with_capacity(capacity, 0),
1148 validity: NullBufferBuilder::new(capacity),
1149 expected_len: capacity,
1150 len: 0,
1151 }
1152 }
1153
1154 pub fn push_bytes(&mut self, bytes: impl AsRef<[u8]>) -> Result<()> {
1156 self.ensure_capacity()?;
1157 self.validity.append_non_null();
1158 self.data_builder.append_value(bytes);
1159 self.uri_builder.append_null();
1160 self.len += 1;
1161 Ok(())
1162 }
1163
1164 pub fn push_uri(&mut self, uri: impl Into<String>) -> Result<()> {
1166 self.ensure_capacity()?;
1167 let uri = uri.into();
1168 if uri.is_empty() {
1169 return Err(Error::invalid_input("URI cannot be empty"));
1170 }
1171 self.validity.append_non_null();
1172 self.data_builder.append_null();
1173 self.uri_builder.append_value(uri);
1174 self.len += 1;
1175 Ok(())
1176 }
1177
1178 pub fn push_empty(&mut self) -> Result<()> {
1180 self.ensure_capacity()?;
1181 self.validity.append_non_null();
1182 self.data_builder.append_value([]);
1183 self.uri_builder.append_null();
1184 self.len += 1;
1185 Ok(())
1186 }
1187
1188 pub fn push_null(&mut self) -> Result<()> {
1190 self.ensure_capacity()?;
1191 self.validity.append_null();
1192 self.data_builder.append_null();
1193 self.uri_builder.append_null();
1194 self.len += 1;
1195 Ok(())
1196 }
1197
1198 pub fn finish(mut self) -> Result<ArrayRef> {
1200 if self.len != self.expected_len {
1201 return Err(Error::invalid_input(format!(
1202 "Expected {} rows but received {}",
1203 self.expected_len, self.len
1204 )));
1205 }
1206
1207 let data = Arc::new(self.data_builder.finish());
1208 let uri = Arc::new(self.uri_builder.finish());
1209 let validity = self.validity.finish();
1210
1211 let struct_array = StructArray::try_new(
1212 BLOB_V2_LOGICAL_MINIMAL_FIELDS.clone(),
1213 vec![data as ArrayRef, uri as ArrayRef],
1214 validity,
1215 )?;
1216
1217 Ok(Arc::new(struct_array))
1218 }
1219
1220 fn ensure_capacity(&self) -> Result<()> {
1221 if self.len >= self.expected_len {
1222 Err(Error::invalid_input("BlobArrayBuilder capacity exceeded"))
1223 } else {
1224 Ok(())
1225 }
1226 }
1227}
1228
1229#[cfg(test)]
1230mod tests {
1231 use std::future::Future;
1232 use std::io;
1233 use std::num::NonZeroUsize;
1234 use std::pin::Pin;
1235 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1236 use std::task::{Context, Poll};
1237
1238 use super::*;
1239 use arrow_array::cast::AsArray;
1240 use arrow_array::{Array, StringArray};
1241 use arrow_schema::{Fields, Schema as ArrowSchema};
1242 use async_trait::async_trait;
1243 use futures::task::noop_waker;
1244 use lance_core::datatypes::{BLOB_V2_DESC_FIELDS, BLOB_V2_LOGICAL_FIELDS};
1245 use lance_core::utils::tempfile::TempDir;
1246 use lance_io::object_writer::WriteResult;
1247 use rstest::rstest;
1248 use tokio::io::AsyncWrite;
1249
1250 #[derive(Clone, Copy)]
1251 enum WriteTerminal {
1252 Error,
1253 Pending,
1254 }
1255
1256 struct PartialWriter {
1257 bytes_before_terminal: usize,
1258 terminal: WriteTerminal,
1259 bytes_written: Arc<AtomicUsize>,
1260 dropped: Arc<AtomicBool>,
1261 }
1262
1263 impl AsyncWrite for PartialWriter {
1264 fn poll_write(
1265 mut self: Pin<&mut Self>,
1266 _cx: &mut Context<'_>,
1267 bytes: &[u8],
1268 ) -> Poll<io::Result<usize>> {
1269 if self.bytes_before_terminal > 0 {
1270 let written = self.bytes_before_terminal.min(bytes.len());
1271 self.bytes_before_terminal -= written;
1272 self.bytes_written.fetch_add(written, Ordering::SeqCst);
1273 return Poll::Ready(Ok(written));
1274 }
1275 match self.terminal {
1276 WriteTerminal::Error => {
1277 Poll::Ready(Err(io::Error::other("injected write failure")))
1278 }
1279 WriteTerminal::Pending => Poll::Pending,
1280 }
1281 }
1282
1283 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1284 Poll::Ready(Ok(()))
1285 }
1286
1287 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1288 Poll::Ready(Ok(()))
1289 }
1290 }
1291
1292 #[async_trait]
1293 impl Writer for PartialWriter {
1294 async fn tell(&mut self) -> Result<usize> {
1295 Ok(self.bytes_written.load(Ordering::SeqCst))
1296 }
1297
1298 async fn shutdown(&mut self) -> Result<WriteResult> {
1299 Ok(WriteResult::default())
1300 }
1301 }
1302
1303 impl Drop for PartialWriter {
1304 fn drop(&mut self) {
1305 self.dropped.store(true, Ordering::SeqCst);
1306 }
1307 }
1308
1309 fn partial_writer(
1310 terminal: WriteTerminal,
1311 ) -> (Box<dyn Writer>, Arc<AtomicUsize>, Arc<AtomicBool>) {
1312 let bytes_written = Arc::new(AtomicUsize::new(0));
1313 let dropped = Arc::new(AtomicBool::new(false));
1314 (
1315 Box::new(PartialWriter {
1316 bytes_before_terminal: 2,
1317 terminal,
1318 bytes_written: bytes_written.clone(),
1319 dropped: dropped.clone(),
1320 }),
1321 bytes_written,
1322 dropped,
1323 )
1324 }
1325
1326 fn cancel_pending<F: Future>(future: F) {
1327 let mut future = Box::pin(future);
1328 let waker = noop_waker();
1329 let mut context = Context::from_waker(&waker);
1330 assert!(future.as_mut().poll(&mut context).is_pending());
1331 }
1332
1333 #[test]
1334 fn part_blob_id_allocator_stops_at_lease_end() {
1335 let allocator = BlobIdAllocator::from_range(7..8).unwrap();
1336 assert_eq!(allocator.next().unwrap(), 7);
1337 let error = allocator.next().unwrap_err();
1338 assert!(error.to_string().contains("range ends at 8"), "{error}");
1339 assert_eq!(allocator.allocated_ids().unwrap(), vec![7]);
1340
1341 let allocator = BlobIdAllocator::from_range(7..9).unwrap();
1342 allocator.reserve(7).unwrap();
1343 assert_eq!(allocator.next().unwrap(), 8);
1344 assert_eq!(allocator.allocated_ids().unwrap(), vec![8]);
1345 }
1346
1347 #[test]
1348 fn part_blob_id_allocator_rejects_generated_id_reservations() {
1349 let allocator = BlobIdAllocator::from_range(7..9).unwrap();
1350 assert_eq!(allocator.next().unwrap(), 7);
1351 let error = allocator.reserve(7).unwrap_err();
1352 assert!(error.to_string().contains("already allocated"), "{error}");
1353
1354 allocator.reserve(8).unwrap();
1355 allocator.reserve(8).unwrap();
1356 }
1357
1358 #[test]
1359 fn test_field_metadata() {
1360 let field = blob_field("blob", true);
1361 assert!(field.metadata().get(ARROW_EXT_NAME_KEY).is_some());
1362 assert_eq!(
1363 field.metadata().get(ARROW_EXT_NAME_KEY).unwrap(),
1364 BLOB_V2_EXT_NAME
1365 );
1366 }
1367
1368 #[test]
1369 fn test_field_metadata_with_options() {
1370 let field = blob_field_with_options(
1371 "blob",
1372 true,
1373 BlobFieldOptions::default()
1374 .with_inline_size_threshold(16 * 1024)
1375 .with_dedicated_size_threshold(NonZeroUsize::new(2 * 1024 * 1024).unwrap()),
1376 );
1377 assert_eq!(
1378 field
1379 .metadata()
1380 .get(BLOB_INLINE_SIZE_THRESHOLD_META_KEY)
1381 .unwrap(),
1382 "16384"
1383 );
1384 assert_eq!(
1385 field
1386 .metadata()
1387 .get(BLOB_DEDICATED_SIZE_THRESHOLD_META_KEY)
1388 .unwrap(),
1389 "2097152"
1390 );
1391 }
1392
1393 #[test]
1394 fn test_mem_wal_blob_schema_transforms_nested_fields() {
1395 let schema = ArrowSchema::new(vec![
1396 Field::new(
1397 "record",
1398 DataType::Struct(vec![blob_field("payload", true)].into()),
1399 true,
1400 ),
1401 Field::new(
1402 "items",
1403 DataType::List(Arc::new(blob_field("item", false))),
1404 true,
1405 ),
1406 ]);
1407
1408 let prepared = logical_to_prepared_blob_schema(&schema).unwrap();
1409 let DataType::Struct(record_fields) = prepared.field(0).data_type() else {
1410 panic!("record must remain a struct");
1411 };
1412 assert_eq!(
1413 blob_v2_layout(record_fields[0].as_ref()),
1414 Some(BlobV2Layout::Prepared)
1415 );
1416 let DataType::List(item) = prepared.field(1).data_type() else {
1417 panic!("items must remain a list");
1418 };
1419 assert_eq!(blob_v2_layout(item), Some(BlobV2Layout::Prepared));
1420
1421 let descriptor = prepared_to_descriptor_blob_schema(&prepared).unwrap();
1422 let DataType::Struct(record_fields) = descriptor.field(0).data_type() else {
1423 panic!("record must remain a struct");
1424 };
1425 assert_eq!(
1426 blob_v2_layout(record_fields[0].as_ref()),
1427 Some(BlobV2Layout::Descriptor)
1428 );
1429 let DataType::List(item) = descriptor.field(1).data_type() else {
1430 panic!("items must remain a list");
1431 };
1432 assert_eq!(blob_v2_layout(item), Some(BlobV2Layout::Descriptor));
1433 }
1434
1435 #[test]
1436 fn test_builder_basic() {
1437 let mut b = BlobArrayBuilder::new(4);
1438 b.push_bytes(b"hi").unwrap();
1439 b.push_uri("s3://bucket/key").unwrap();
1440 b.push_empty().unwrap();
1441 b.push_null().unwrap();
1442
1443 let arr = b.finish().unwrap();
1444 assert_eq!(arr.len(), 4);
1445 assert_eq!(arr.null_count(), 1);
1446
1447 let struct_arr = arr.as_struct();
1448 let data = struct_arr.column(0).as_binary::<i64>();
1449 let uri = struct_arr.column(1).as_string::<i32>();
1450
1451 assert_eq!(data.value(0), b"hi");
1452 assert!(uri.is_null(0));
1453 assert!(data.is_null(1));
1454 assert_eq!(uri.value(1), "s3://bucket/key");
1455 assert_eq!(data.value(2).len(), 0);
1456 assert!(uri.is_null(2));
1457 }
1458
1459 #[test]
1460 fn test_capacity_error() {
1461 let mut b = BlobArrayBuilder::new(1);
1462 b.push_bytes(b"a").unwrap();
1463 let err = b.push_bytes(b"b").unwrap_err();
1464 assert!(err.to_string().contains("capacity exceeded"));
1465 }
1466
1467 #[test]
1468 fn test_empty_uri_rejected() {
1469 let mut b = BlobArrayBuilder::new(1);
1470 let err = b.push_uri("").unwrap_err();
1471 assert!(err.to_string().contains("URI cannot be empty"));
1472 }
1473
1474 #[test]
1475 fn test_prepared_blob_column_finish() {
1476 let mut writer = BlobDescriptorArrayBuilder::new("blob");
1477 writer.push_inline(Bytes::from_static(b"hello")).unwrap();
1478 writer
1479 .push_packed(7, BlobRange { offset: 3, size: 5 })
1480 .unwrap();
1481 writer.push_dedicated(8, 9).unwrap();
1482 writer.push_null().unwrap();
1483
1484 let column = writer.finish().unwrap();
1485 assert_eq!(blob_v2_layout(column.field()), Some(BlobV2Layout::Prepared));
1486 let struct_arr = column.array().as_struct();
1487 let kinds = struct_arr
1488 .column_by_name("kind")
1489 .unwrap()
1490 .as_primitive::<UInt8Type>();
1491 let blob_ids = struct_arr
1492 .column_by_name("blob_id")
1493 .unwrap()
1494 .as_primitive::<UInt32Type>();
1495 let sizes = struct_arr
1496 .column_by_name("blob_size")
1497 .unwrap()
1498 .as_primitive::<UInt64Type>();
1499 let positions = struct_arr
1500 .column_by_name("position")
1501 .unwrap()
1502 .as_primitive::<UInt64Type>();
1503
1504 assert_eq!(kinds.value(0), BlobKind::Inline as u8);
1505 assert_eq!(kinds.value(1), BlobKind::Packed as u8);
1506 assert_eq!(blob_ids.value(1), 7);
1507 assert_eq!(positions.value(1), 3);
1508 assert_eq!(sizes.value(1), 5);
1509 assert_eq!(kinds.value(2), BlobKind::Dedicated as u8);
1510 assert_eq!(blob_ids.value(2), 8);
1511 assert_eq!(sizes.value(2), 9);
1512 assert!(struct_arr.is_null(3));
1513 }
1514
1515 #[test]
1516 fn test_prepared_blob_array_rejects_range_overflow() {
1517 let mut metadata = HashMap::new();
1518 metadata.insert(ARROW_EXT_NAME_KEY.to_string(), BLOB_V2_EXT_NAME.to_string());
1519 let field = prepared_blob_field_with_metadata("blob", true, metadata);
1520
1521 for (kind, uri) in [
1522 (BlobKind::Packed, None),
1523 (BlobKind::External, Some("file:///external.bin")),
1524 ] {
1525 let array = Arc::new(
1526 StructArray::try_new(
1527 BLOB_V2_PREPARED_FIELDS.clone(),
1528 vec![
1529 Arc::new(arrow_array::UInt8Array::from(vec![kind as u8])) as ArrayRef,
1530 Arc::new(arrow_array::LargeBinaryArray::from_iter([None::<&[u8]>])),
1531 Arc::new(arrow_array::StringArray::from_iter([uri])),
1532 Arc::new(arrow_array::UInt32Array::from(vec![1])),
1533 Arc::new(arrow_array::UInt64Array::from(vec![4])),
1534 Arc::new(arrow_array::UInt64Array::from(vec![u64::MAX - 1])),
1535 ],
1536 None,
1537 )
1538 .unwrap(),
1539 ) as ArrayRef;
1540
1541 let err = validate_prepared_blob_array(&field, &array).unwrap_err();
1542 assert!(err.to_string().contains("range overflows u64"));
1543 }
1544 }
1545
1546 #[derive(Clone, Copy)]
1547 enum LogicalBlobShape {
1548 Minimal,
1549 CompleteNullableRange,
1550 CompleteRequiredRange,
1551 }
1552
1553 fn blob_v2_field_with_children(name: &str, children: Fields, nullable: bool) -> Field {
1554 let mut metadata = HashMap::new();
1555 metadata.insert(ARROW_EXT_NAME_KEY.to_string(), BLOB_V2_EXT_NAME.to_string());
1556 metadata.insert("blob-root-metadata".to_string(), "preserved".to_string());
1557 Field::new(name, DataType::Struct(children), nullable).with_metadata(metadata)
1558 }
1559
1560 fn logical_blob_fields(shape: LogicalBlobShape) -> Fields {
1561 let source = match shape {
1562 LogicalBlobShape::Minimal => &*BLOB_V2_LOGICAL_MINIMAL_FIELDS,
1563 LogicalBlobShape::CompleteNullableRange | LogicalBlobShape::CompleteRequiredRange => {
1564 &*BLOB_V2_LOGICAL_FIELDS
1565 }
1566 };
1567 source
1568 .iter()
1569 .enumerate()
1570 .map(|(index, field)| {
1571 let nullable = !matches!(shape, LogicalBlobShape::CompleteRequiredRange)
1572 || index < BLOB_V2_LOGICAL_MINIMAL_FIELDS.len();
1573 Arc::new(
1574 field
1575 .as_ref()
1576 .clone()
1577 .with_nullable(nullable)
1578 .with_metadata(HashMap::from([(
1579 "blob-child-metadata".to_string(),
1580 field.name().to_string(),
1581 )])),
1582 )
1583 })
1584 .collect::<Vec<_>>()
1585 .into()
1586 }
1587
1588 fn lance_schema_with_metadata(fields: Vec<Field>) -> LanceSchema {
1589 let arrow_schema = ArrowSchema::new_with_metadata(
1590 fields,
1591 HashMap::from([("schema-metadata".to_string(), "preserved".to_string())]),
1592 );
1593 let mut schema = LanceSchema::try_from(&arrow_schema).unwrap();
1594 schema.set_field_id(None);
1595 schema
1596 }
1597
1598 #[rstest]
1599 #[case::minimal(LogicalBlobShape::Minimal)]
1600 #[case::complete_nullable_range(LogicalBlobShape::CompleteNullableRange)]
1601 #[case::complete_required_range(LogicalBlobShape::CompleteRequiredRange)]
1602 fn test_logical_blob_schema_normalization_is_identity(#[case] shape: LogicalBlobShape) {
1603 let blob_field = blob_v2_field_with_children("blob", logical_blob_fields(shape), false);
1604 let schema = lance_schema_with_metadata(vec![blob_field]);
1605
1606 let normalized = prepared_to_logical_blob_schema(&schema).unwrap();
1607
1608 assert_eq!(normalized.fields, schema.fields);
1609 assert_eq!(normalized.metadata, schema.metadata);
1610 }
1611
1612 #[test]
1613 fn test_nested_logical_blob_schema_normalization_is_identity() {
1614 let struct_blob = blob_v2_field_with_children(
1615 "struct_blob",
1616 logical_blob_fields(LogicalBlobShape::CompleteNullableRange),
1617 true,
1618 );
1619 let list_blob = blob_v2_field_with_children(
1620 "item",
1621 logical_blob_fields(LogicalBlobShape::CompleteRequiredRange),
1622 false,
1623 );
1624 let schema = lance_schema_with_metadata(vec![
1625 Field::new("payload", DataType::Struct(vec![struct_blob].into()), true),
1626 Field::new("items", DataType::List(Arc::new(list_blob)), false),
1627 ]);
1628
1629 let normalized = prepared_to_logical_blob_schema(&schema).unwrap();
1630
1631 assert_eq!(normalized.fields, schema.fields);
1632 assert_eq!(normalized.metadata, schema.metadata);
1633 }
1634
1635 #[test]
1636 fn test_prepared_to_logical_blob_schema_preserves_non_blob_fields() {
1637 let mut metadata = HashMap::new();
1638 metadata.insert(ARROW_EXT_NAME_KEY.to_string(), BLOB_V2_EXT_NAME.to_string());
1639 let prepared_field = prepared_blob_field_with_metadata("blob", true, metadata);
1640 let dict_field = Field::new(
1641 "dict",
1642 DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8)),
1643 true,
1644 );
1645 let mut schema =
1646 LanceSchema::try_from(&ArrowSchema::new(vec![dict_field, prepared_field])).unwrap();
1647 schema.fields[0].id = 42;
1648 schema.fields[1].id = 7;
1649
1650 let dictionary_values = Arc::new(StringArray::from(vec!["a", "b"])) as ArrayRef;
1651 schema.fields[0].set_dictionary_values(&dictionary_values);
1652
1653 let normalized = prepared_to_logical_blob_schema(&schema).unwrap();
1654
1655 assert_eq!(normalized.fields[0].id, 42);
1656 assert_eq!(
1657 normalized.fields[0]
1658 .dictionary
1659 .as_ref()
1660 .and_then(|dict| dict.values.as_ref())
1661 .map(|values| values.len()),
1662 Some(2)
1663 );
1664 assert_eq!(normalized.fields[1].id, 7);
1665 assert_eq!(normalized.fields[1].children.len(), 2);
1666 assert_eq!(normalized.fields[1].children[0].name, "data");
1667 assert_eq!(normalized.fields[1].children[1].name, "uri");
1668 assert!(normalized.fields[1].children[0].id >= 0);
1669 assert!(normalized.fields[1].children[1].id >= 0);
1670 }
1671
1672 #[test]
1673 fn test_prepared_blob_schema_normalizes_by_semantic_child_name() {
1674 let mut metadata = HashMap::new();
1675 metadata.insert(ARROW_EXT_NAME_KEY.to_string(), BLOB_V2_EXT_NAME.to_string());
1676 metadata.insert("blob-root-metadata".to_string(), "preserved".to_string());
1677 let prepared_field = prepared_blob_field_with_metadata("blob", false, metadata);
1678 let dict_field = Field::new(
1679 "dict",
1680 DataType::Dictionary(Box::new(DataType::UInt32), Box::new(DataType::Utf8)),
1681 true,
1682 );
1683 let mut schema = lance_schema_with_metadata(vec![dict_field, prepared_field]);
1684 schema.fields[0].id = 42;
1685 schema.fields[1].id = 7;
1686 for (child, id) in schema.fields[1].children.iter_mut().zip(70..76) {
1687 child.id = id;
1688 child.parent_id = 7;
1689 }
1690
1691 let dictionary_values = Arc::new(StringArray::from(vec!["a", "b"])) as ArrayRef;
1692 schema.fields[0].set_dictionary_values(&dictionary_values);
1693
1694 let normalized = prepared_to_logical_blob_schema(&schema).unwrap();
1695
1696 assert_eq!(normalized.metadata, schema.metadata);
1697 assert_eq!(normalized.fields[0], schema.fields[0]);
1698 assert_eq!(normalized.fields[1].id, 7);
1699 assert!(!normalized.fields[1].nullable);
1700 assert_eq!(normalized.fields[1].metadata, schema.fields[1].metadata);
1701 assert_eq!(normalized.fields[1].children.len(), 2);
1702 assert_eq!(normalized.fields[1].children[0].name, "data");
1703 assert_eq!(normalized.fields[1].children[0].id, 71);
1704 assert_eq!(normalized.fields[1].children[0].parent_id, 7);
1705 assert_eq!(normalized.fields[1].children[1].name, "uri");
1706 assert_eq!(normalized.fields[1].children[1].id, 72);
1707 assert_eq!(normalized.fields[1].children[1].parent_id, 7);
1708 }
1709
1710 #[rstest]
1711 #[case::descriptor(BLOB_V2_DESC_FIELDS.clone(), "descriptor layout")]
1712 #[case::malformed(
1713 vec![
1714 Field::new("data", DataType::LargeBinary, true),
1715 Field::new("uri", DataType::Utf8, true),
1716 Field::new("size", DataType::UInt64, true),
1717 ].into(),
1718 "unrecognized layout"
1719 )]
1720 fn test_non_logical_blob_schema_normalization_is_rejected(
1721 #[case] fields: Fields,
1722 #[case] actual_layout: &str,
1723 ) {
1724 let field = blob_v2_field_with_children("blob", fields, true);
1725 let schema = lance_schema_with_metadata(vec![field]);
1726
1727 let error = prepared_to_logical_blob_schema(&schema).unwrap_err();
1728
1729 assert!(matches!(error, Error::InvalidInput { .. }));
1730 assert!(error.to_string().contains(actual_layout));
1731 assert!(
1732 error
1733 .to_string()
1734 .contains("expected logical or prepared layout")
1735 );
1736 }
1737
1738 #[tokio::test]
1739 async fn test_sidecar_writers_return_prepared_values() {
1740 let temp_dir = TempDir::default();
1741 let data_dir = Path::from_absolute_path(temp_dir.std_path().join("data")).unwrap();
1742 let data_file_key = "data-file".to_string();
1743 let data_file_path = data_dir.clone().join(format!("{data_file_key}.lance"));
1744 let object_store = ObjectStore::local();
1745
1746 let packed_id = 7;
1747 let packed_path = blob_path(&data_dir, &data_file_key, packed_id);
1748 let mut packed =
1749 PackedBlobWriter::try_new(object_store.clone(), data_file_path.clone(), packed_id)
1750 .await
1751 .unwrap();
1752 assert_eq!(packed.path(), &packed_path);
1753 packed.write_blob(b"abc").await.unwrap();
1754 packed.write_blob(b"de").await.unwrap();
1755 let packed_values = packed.finish().await.unwrap();
1756 assert_eq!(
1757 packed_values,
1758 vec![
1759 BlobDescriptor::Packed {
1760 blob_id: packed_id,
1761 offset: 0,
1762 size: 3,
1763 },
1764 BlobDescriptor::Packed {
1765 blob_id: packed_id,
1766 offset: 3,
1767 size: 2,
1768 },
1769 ]
1770 );
1771
1772 let dedicated_id = 8;
1773 let dedicated_path = blob_path(&data_dir, &data_file_key, dedicated_id);
1774 let mut dedicated =
1775 DedicatedBlobWriter::try_new(object_store.clone(), data_file_path, dedicated_id)
1776 .await
1777 .unwrap();
1778 assert_eq!(dedicated.path(), &dedicated_path);
1779 dedicated.write(b"abcdef").await.unwrap();
1780 assert_eq!(
1781 dedicated.finish().await.unwrap(),
1782 BlobDescriptor::Dedicated {
1783 blob_id: dedicated_id,
1784 size: 6,
1785 }
1786 );
1787
1788 let mut builder = BlobDescriptorArrayBuilder::new("blob");
1789 builder
1790 .extend_packed(
1791 42,
1792 vec![
1793 BlobRange { offset: 1, size: 2 },
1794 BlobRange { offset: 4, size: 1 },
1795 ],
1796 )
1797 .unwrap();
1798 builder.push_dedicated(43, 3).unwrap();
1799 let column = builder.finish().unwrap();
1800 assert_eq!(column.array().len(), 3);
1801 }
1802
1803 #[tokio::test]
1804 async fn test_packed_blob_writer_bulk_bytes() {
1805 let temp_dir = TempDir::default();
1806 let data_dir = Path::from_absolute_path(temp_dir.std_path().join("data")).unwrap();
1807 let data_file_path = data_dir.join("data-file.lance");
1808 let mut writer = PackedBlobWriter::try_new(ObjectStore::local(), data_file_path, 7)
1809 .await
1810 .unwrap();
1811
1812 writer
1813 .write_packed_blobs([
1814 Some(b"a".as_slice()),
1815 Some(b"".as_slice()),
1816 None,
1817 Some(b"bc".as_slice()),
1818 ])
1819 .await
1820 .unwrap();
1821
1822 assert_eq!(
1823 writer.finish().await.unwrap(),
1824 vec![
1825 BlobDescriptor::Packed {
1826 blob_id: 7,
1827 offset: 0,
1828 size: 1,
1829 },
1830 BlobDescriptor::Packed {
1831 blob_id: 7,
1832 offset: 1,
1833 size: 0,
1834 },
1835 BlobDescriptor::Null,
1836 BlobDescriptor::Packed {
1837 blob_id: 7,
1838 offset: 1,
1839 size: 2,
1840 },
1841 ]
1842 );
1843 }
1844
1845 #[tokio::test]
1846 async fn test_packed_blob_writer_bulk_drops_after_partial_write_error() {
1847 let (partial_writer, bytes_written, dropped) = partial_writer(WriteTerminal::Error);
1848 let previous_descriptor = BlobDescriptor::Packed {
1849 blob_id: 7,
1850 offset: 0,
1851 size: 3,
1852 };
1853 let mut writer = PackedBlobWriter {
1854 object_store: ObjectStore::local(),
1855 path: Path::from("packed.blob"),
1856 blob_id: 7,
1857 writer: Some(partial_writer),
1858 offset: 3,
1859 values: vec![previous_descriptor.clone()],
1860 };
1861
1862 let error = writer
1863 .write_packed_blobs([Some(b"abcdef".as_slice())])
1864 .await
1865 .unwrap_err();
1866
1867 assert!(matches!(error, Error::IO { .. }));
1868 assert!(error.to_string().contains("injected write failure"));
1869 assert_eq!(bytes_written.load(Ordering::SeqCst), 2);
1870 assert!(dropped.load(Ordering::SeqCst));
1871 assert!(writer.writer.is_none());
1872 assert_eq!(writer.offset, 3);
1873 assert_eq!(writer.values, vec![previous_descriptor]);
1874 let retry_error = writer.write_blob(b"retry").await.unwrap_err();
1875 assert!(matches!(retry_error, Error::IO { .. }));
1876 assert!(retry_error.to_string().contains("no active upload"));
1877 }
1878
1879 #[test]
1880 fn test_packed_blob_writer_bulk_drops_if_cancelled() {
1881 let (partial_writer, bytes_written, dropped) = partial_writer(WriteTerminal::Pending);
1882 let previous_descriptor = BlobDescriptor::Packed {
1883 blob_id: 7,
1884 offset: 0,
1885 size: 3,
1886 };
1887 let mut writer = PackedBlobWriter {
1888 object_store: ObjectStore::local(),
1889 path: Path::from("packed.blob"),
1890 blob_id: 7,
1891 writer: Some(partial_writer),
1892 offset: 3,
1893 values: vec![previous_descriptor.clone()],
1894 };
1895
1896 cancel_pending(writer.write_packed_blobs([Some(b"abcdef".as_slice())]));
1897
1898 assert_eq!(bytes_written.load(Ordering::SeqCst), 2);
1899 assert!(dropped.load(Ordering::SeqCst));
1900 assert!(writer.writer.is_none());
1901 assert_eq!(writer.offset, 3);
1902 assert_eq!(writer.values, vec![previous_descriptor]);
1903 }
1904}