Skip to main content

lance/
blob.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Builders and file-level writer helpers for Lance blob v2 columns.
5//!
6//! Logical blob input uses either `Struct<data: LargeBinary?, uri: Utf8?>` or the complete
7//! `Struct<data: LargeBinary?, uri: Utf8?, position: UInt64?, size: UInt64?>` shape. In the
8//! complete shape, `position` and `size` select a non-empty range within an external `uri` and
9//! must be set together. Every non-null row must set exactly one of `data` and `uri`. File-level
10//! blob preparation produces a kind-aware writer intermediate with `blob_id` and range fields.
11
12use 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
49/// Construct the Arrow field for a blob v2 column.
50///
51/// This helper constructs the minimal logical shape
52/// `Struct<data: LargeBinary?, uri: Utf8?>`, tagged with
53/// `ARROW:extension:name = "lance.blob.v2"`. Writers also accept the complete logical shape
54/// with trailing `position: UInt64?` and `size: UInt64?` fields for external URI ranges.
55pub fn blob_field(name: &str, nullable: bool) -> Field {
56    blob_field_with_options(name, nullable, BlobFieldOptions::default())
57}
58
59/// Options for constructing a blob v2 field.
60#[derive(Clone, Debug, Default)]
61pub struct BlobFieldOptions {
62    /// Maximum payload size to keep inline in the data file before using packed blob storage.
63    pub inline_size_threshold: Option<usize>,
64    /// Maximum payload size to store in packed blob storage before using dedicated blob storage.
65    ///
66    /// A zero threshold is invalid because dedicated blob storage is selected when
67    /// the payload size is greater than this value.
68    pub dedicated_size_threshold: Option<NonZeroUsize>,
69}
70
71impl BlobFieldOptions {
72    /// Set the maximum payload size to keep inline in the data file.
73    pub fn with_inline_size_threshold(mut self, threshold: usize) -> Self {
74        self.inline_size_threshold = Some(threshold);
75        self
76    }
77
78    /// Set the maximum payload size to store in packed blob storage.
79    pub fn with_dedicated_size_threshold(mut self, threshold: NonZeroUsize) -> Self {
80        self.dedicated_size_threshold = Some(threshold);
81        self
82    }
83}
84
85/// Construct the Arrow field for a blob v2 column with storage layout options.
86///
87/// This helper constructs the minimal logical shape
88/// `Struct<data: LargeBinary?, uri: Utf8?>`, tagged with
89/// `ARROW:extension:name = "lance.blob.v2"`. Writers also accept the complete logical shape
90/// with trailing `position: UInt64?` and `size: UInt64?` fields for external URI ranges.
91///
92/// ```
93/// # use lance::{BlobFieldOptions, blob_field_with_options};
94/// let field = blob_field_with_options(
95///     "blob",
96///     true,
97///     BlobFieldOptions::default().with_inline_size_threshold(16 * 1024),
98/// );
99/// assert_eq!(
100///     field
101///         .metadata()
102///         .get("lance-encoding:blob-inline-size-threshold")
103///         .map(String::as_str),
104///     Some("16384"),
105/// );
106/// ```
107pub 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
588/// Validate a writer-side prepared blob v2 array before it reaches the encoder.
589pub(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/// Byte range inside a packed or external blob object.
609#[derive(Clone, Copy, Debug, PartialEq, Eq)]
610pub struct BlobRange {
611    /// Byte offset relative to the beginning of the blob object.
612    pub offset: u64,
613    /// Number of bytes in this range.
614    pub size: u64,
615}
616
617/// A kind-aware row used to build the writer-prepared blob representation.
618#[derive(Clone, Debug, PartialEq, Eq)]
619pub enum BlobDescriptor {
620    /// A null blob row.
621    Null,
622    /// Payload bytes embedded into the data file by the blob encoder.
623    Inline { data: Bytes },
624    /// Payload bytes stored as a range in a packed sidecar blob.
625    Packed {
626        blob_id: u32,
627        offset: u64,
628        size: u64,
629    },
630    /// Payload bytes stored as the full contents of a dedicated sidecar blob.
631    Dedicated { blob_id: u32, size: u64 },
632    /// Payload bytes referenced from an external object or registered base.
633    External {
634        base_id: u32,
635        uri: String,
636        offset: u64,
637        size: u64,
638    },
639}
640
641/// A writer-prepared blob column ready to be included in a [`RecordBatch`](arrow_array::RecordBatch).
642///
643/// Despite the legacy type name, its Arrow representation is
644/// [`BlobV2Layout::Prepared`], not the descriptor stored in a Lance file.
645pub struct BlobDescriptorColumn {
646    field: Field,
647    array: ArrayRef,
648}
649
650impl BlobDescriptorColumn {
651    /// Return the Arrow field for the prepared column.
652    pub fn field(&self) -> &Field {
653        &self.field
654    }
655
656    /// Return the Arrow array for the prepared column.
657    pub fn array(&self) -> &ArrayRef {
658        &self.array
659    }
660
661    /// Consume this column into `(field, array)` parts.
662    pub fn into_parts(self) -> (Field, ArrayRef) {
663        (self.field, self.array)
664    }
665}
666
667/// Builds the writer-prepared representation for one blob v2 column.
668///
669/// This builder only produces the writer-prepared struct array. It does not allocate blob ids,
670/// choose sidecar paths, write blob objects, or commit data files.
671pub struct BlobDescriptorArrayBuilder {
672    field: Field,
673    values: Vec<BlobDescriptor>,
674}
675
676impl BlobDescriptorArrayBuilder {
677    /// Create a descriptor array builder for one blob column.
678    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    /// Append one packed blob descriptor.
696    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    /// Append multiple packed blob descriptors for the same blob object.
705    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    /// Append one dedicated blob descriptor.
717    pub fn push_dedicated(&mut self, blob_id: u32, size: u64) -> Result<()> {
718        self.push(BlobDescriptor::Dedicated { blob_id, size })
719    }
720
721    /// Append one blob descriptor to this column.
722    pub fn push(&mut self, value: BlobDescriptor) -> Result<()> {
723        validate_blob_descriptor(&value)?;
724        self.values.push(value);
725        Ok(())
726    }
727
728    /// Append multiple blob descriptors to this column.
729    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    /// Append an inline blob value.
737    pub fn push_inline(&mut self, data: impl Into<Bytes>) -> Result<()> {
738        self.push(BlobDescriptor::Inline { data: data.into() })
739    }
740
741    /// Append an external blob reference.
742    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    /// Append a null blob row.
757    pub fn push_null(&mut self) -> Result<()> {
758        self.push(BlobDescriptor::Null)
759    }
760
761    /// Return the prepared Arrow field for this blob column.
762    pub fn field(&self) -> &Field {
763        &self.field
764    }
765
766    /// Finish this column and return the writer-prepared struct array.
767    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
907/// Writes a Lance-owned packed sidecar blob for one data file and returns descriptors.
908pub 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    /// Create a packed blob writer for `data_file_path` and `blob_id`.
919    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    /// Return the blob id for this packed sidecar.
937    pub fn blob_id(&self) -> u32 {
938        self.blob_id
939    }
940
941    /// Return the derived sidecar path for this packed blob.
942    pub fn path(&self) -> &Path {
943        &self.path
944    }
945
946    /// Append one logical blob payload to the packed sidecar.
947    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    /// Append multiple logical blobs, one per iterator item.
954    ///
955    /// Each `Some(bytes)` is appended to the sidecar and records a packed
956    /// descriptor; an empty slice records a valid zero-length blob. Each `None`
957    /// records a [`BlobDescriptor::Null`] without writing any bytes, so the
958    /// descriptors returned by [`Self::finish`] stay row-aligned with the input.
959    ///
960    /// If writing fails or the future is cancelled after a partial write, no
961    /// descriptors from this call are recorded, the active writer is dropped,
962    /// and this instance cannot be reused.
963    ///
964    /// ```
965    /// # use lance::{PackedBlobWriter, Result};
966    /// # async fn write(mut writer: PackedBlobWriter) -> Result<()> {
967    /// writer
968    ///     .write_packed_blobs([Some(b"first".as_slice()), None, Some(b"second".as_slice())])
969    ///     .await?;
970    /// let descriptors = writer.finish().await?;
971    /// assert_eq!(descriptors.len(), 3);
972    /// # Ok(())
973    /// # }
974    /// ```
975    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    /// Finish the packed sidecar and return descriptors in write order.
1040    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
1049/// Writes a Lance-owned dedicated sidecar blob for one data file and returns its descriptor.
1050pub 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    /// Create a dedicated blob writer for `data_file_path` and `blob_id`.
1060    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    /// Return the blob id for this dedicated sidecar.
1077    pub fn blob_id(&self) -> u32 {
1078        self.blob_id
1079    }
1080
1081    /// Return the derived sidecar path for this dedicated blob.
1082    pub fn path(&self) -> &Path {
1083        &self.path
1084    }
1085
1086    /// Append bytes to the dedicated sidecar.
1087    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    /// Finish the dedicated sidecar and return its descriptor.
1115    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
1131/// Builder for blob v2 input struct columns.
1132///
1133/// The builder enforces that each row contains exactly one of `data` or `uri` (or is null).
1134pub 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    /// Create a new builder with the given row capacity.
1144    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    /// Append a blob backed by raw bytes.
1155    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    /// Append a blob referenced by URI.
1165    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    /// Append an empty blob (inline, zero-length payload).
1179    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    /// Append a null row.
1189    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    /// Finish building and return an Arrow struct array.
1199    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}