Skip to main content

lance_nd/
maintenance.rs

1//! Delete rows, compact the table and clean up old versions.
2//!
3//! A delete marks rows in deletion files and keeps the data files. A
4//! compaction rewrites small fragments and fragments with many deleted rows.
5//! Both make a new table version. The files of old versions stay on disk
6//! until [`cleanup_old_versions`] removes them.
7
8use std::collections::HashSet;
9use std::ops::Range;
10use std::sync::Arc;
11use std::time::Duration;
12
13use arrow::array::{RecordBatch, RecordBatchIterator};
14use arrow::compute::concat_batches;
15use arrow::datatypes::{Schema, SchemaRef};
16use arrow::error::ArrowError;
17use futures::TryStreamExt;
18use lance::Dataset;
19use lance::dataset::fragment::write::FragmentCreateBuilder;
20use lance::dataset::transaction::{Operation, RewriteGroup, Transaction};
21use lance::dataset::write::delete::DeleteBuilder;
22use lance::dataset::{CommitBuilder, WriteMode, WriteParams};
23use lance::deps::datafusion::logical_expr::Expr;
24use lance::table::format::Fragment;
25use lance_core::datatypes::Schema as LanceSchema;
26
27use crate::error::Result;
28use crate::format::check_format;
29use crate::schema::{conform_batch, fill_hidden_nulls};
30use crate::write::FILE_VERSION;
31
32/// Delete the rows that match `predicate`, a Lance SQL filter.
33///
34/// Lance gets `predicate` with no change, so it must be exact for a row,
35/// for example `__global_attrs__.id = 'a'` or `sst.attrs.units = 'K'`.
36/// Returns the number of deleted rows.
37#[tracing::instrument(skip_all, fields(uri = %dataset.uri(), predicate = %predicate), err)]
38pub async fn delete(dataset: &mut Dataset, predicate: &str) -> Result<u64> {
39    check_format(dataset)?;
40    let builder = DeleteBuilder::new(Arc::new(dataset.clone()), predicate);
41    run_delete(dataset, builder).await
42}
43
44/// Delete the rows that match `expr`, a DataFusion filter on the Lance
45/// table columns.
46///
47/// Lance gets `expr` with no change. Unlike [`crate::NdScanner::filter_expr`],
48/// a delete does not change value parts into stats filters, because a stats
49/// filter keeps rows that do not match. Returns the number of deleted rows.
50#[tracing::instrument(skip_all, fields(uri = %dataset.uri(), predicate = %expr), err)]
51pub async fn delete_expr(dataset: &mut Dataset, expr: Expr) -> Result<u64> {
52    check_format(dataset)?;
53    let builder = DeleteBuilder::from_expr(Arc::new(dataset.clone()), expr);
54    run_delete(dataset, builder).await
55}
56
57async fn run_delete(dataset: &mut Dataset, builder: DeleteBuilder) -> Result<u64> {
58    let result = builder.execute().await?;
59    *dataset = Arc::unwrap_or_clone(result.new_dataset);
60    Ok(result.num_deleted_rows)
61}
62
63/// What a [`compact`] changed.
64#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
65pub struct CompactionStats {
66    /// Fragments that the compaction replaced.
67    pub fragments_removed: usize,
68    /// New fragments.
69    pub fragments_added: usize,
70    /// Data and deletion files that the new version no longer uses.
71    pub files_removed: usize,
72    /// New data files.
73    pub files_added: usize,
74}
75
76/// What a [`cleanup_old_versions`] removed.
77#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
78pub struct CleanupStats {
79    /// The bytes of the removed files.
80    pub bytes_removed: u64,
81    /// The number of removed versions.
82    pub old_versions: u64,
83}
84
85/// Options for [`compact`].
86#[derive(Debug, Clone)]
87pub struct CompactOptions {
88    /// The size of a fragment on disk that a compaction aims for. A fragment
89    /// below half this size is merged with its small neighbors. A fragment
90    /// above twice this size is split. The default is 128 MiB.
91    pub target_fragment_bytes: u64,
92    /// A fragment with at least this fraction of deleted rows is rewritten
93    /// without them. The default is 0.1. A value above 1 never rewrites a
94    /// fragment for its deleted rows.
95    pub deleted_fraction: f32,
96}
97
98impl Default for CompactOptions {
99    fn default() -> Self {
100        Self {
101            target_fragment_bytes: 128 << 20,
102            deleted_fraction: 0.1,
103        }
104    }
105}
106
107/// Merge small fragments, split large fragments and remove deleted rows.
108/// Keeps the row order.
109///
110/// The plan uses the size of each fragment on disk. Each run of
111/// neighboring fragments that need work becomes fragments of about the
112/// same size, near the target. A row is never split, so a row larger than
113/// the target is a fragment on its own. lance-nd holds about one new
114/// fragment in memory at a time. Without work to do, the table keeps its
115/// version.
116#[tracing::instrument(skip_all, fields(uri = %dataset.uri()), err)]
117pub async fn compact(dataset: &mut Dataset, options: &CompactOptions) -> Result<CompactionStats> {
118    check_format(dataset)?;
119    let fragments: Vec<Fragment> = dataset.fragments().as_ref().clone();
120    let mut pieces = Vec::with_capacity(fragments.len());
121    for fragment in &fragments {
122        pieces.push(piece_of(dataset, fragment).await?);
123    }
124    let target = options.target_fragment_bytes.max(1);
125    let tasks = plan(&pieces, target, options.deleted_fraction);
126    let mut metrics = CompactionStats::default();
127    if tasks.is_empty() {
128        return Ok(metrics);
129    }
130    let read_version = dataset.version().version;
131    let mut groups = Vec::with_capacity(tasks.len());
132    for task in tasks {
133        let old = &fragments[task.clone()];
134        let new_fragments = rewrite(dataset, old, &pieces[task], target).await?;
135        metrics.fragments_removed += old.len();
136        metrics.files_removed += old
137            .iter()
138            .map(|f| f.files.len() + usize::from(f.deletion_file.is_some()))
139            .sum::<usize>();
140        metrics.fragments_added += new_fragments.len();
141        metrics.files_added += new_fragments.len();
142        groups.push(RewriteGroup {
143            old_fragments: old.to_vec(),
144            new_fragments,
145        });
146    }
147    // New fragments need ids that no other writer takes.
148    let count: usize = groups.iter().map(|g| g.new_fragments.len()).sum();
149    let reserve = Operation::ReserveFragments {
150        num_fragments: count as u32,
151    };
152    let reserved = CommitBuilder::new(Arc::new(dataset.clone()))
153        .execute(Transaction::new(read_version, reserve, None))
154        .await?;
155    let end = u64::from(reserved.manifest().max_fragment_id.unwrap_or(0)) + 1;
156    let ids = end - count as u64..end;
157    for (fragment, id) in groups
158        .iter_mut()
159        .flat_map(|g| g.new_fragments.iter_mut())
160        .zip(ids)
161    {
162        fragment.id = id;
163    }
164    let rewrite = Operation::Rewrite {
165        groups,
166        rewritten_indices: Vec::new(),
167        frag_reuse_index: None,
168    };
169    *dataset = CommitBuilder::new(Arc::new(reserved))
170        .execute(Transaction::new(read_version, rewrite, None))
171        .await?;
172    tracing::info!(
173        version = dataset.version().version,
174        fragments_removed = metrics.fragments_removed,
175        fragments_added = metrics.fragments_added,
176        "compacted the table"
177    );
178    Ok(metrics)
179}
180
181/// The size of a fragment on disk and its row counts.
182#[derive(Debug, Clone, Copy)]
183struct Piece {
184    bytes: u64,
185    rows: u64,
186    deleted: u64,
187}
188
189impl Piece {
190    /// The estimated bytes of one row: the rows of a fragment count the same.
191    fn row_bytes(&self) -> f64 {
192        self.bytes as f64 / self.rows.max(1) as f64
193    }
194
195    fn live_rows(&self) -> u64 {
196        self.rows.saturating_sub(self.deleted)
197    }
198}
199
200async fn piece_of(dataset: &Dataset, fragment: &Fragment) -> Result<Piece> {
201    let mut bytes = 0;
202    for file in &fragment.files {
203        bytes += match file.file_size_bytes.get() {
204            Some(size) => size.get(),
205            None => {
206                let path = dataset.data_dir().join(file.path.as_str());
207                dataset
208                    .object_store(file.base_id)
209                    .await?
210                    .size(&path)
211                    .await?
212            }
213        };
214    }
215    let deleted = fragment
216        .deletion_file
217        .as_ref()
218        .and_then(|file| file.num_deleted_rows)
219        .unwrap_or(0);
220    Ok(Piece {
221        bytes,
222        rows: fragment.physical_rows.unwrap_or(0) as u64,
223        deleted: deleted as u64,
224    })
225}
226
227/// The runs of neighboring fragments to rewrite, as index ranges.
228///
229/// A fragment needs work when it is below half of `target`, above twice
230/// `target` with more than one row, or has at least `deleted_fraction` of
231/// its rows deleted. A run of one small fragment alone has nothing to
232/// merge with, so it stays.
233fn plan(pieces: &[Piece], target: u64, deleted_fraction: f32) -> Vec<Range<usize>> {
234    let small = |p: &Piece| p.bytes < target / 2;
235    let rewrite = |p: &Piece| {
236        let large = p.bytes > target.saturating_mul(2) && p.rows > 1;
237        let deleted = deleted_fraction <= 1.0
238            && p.deleted > 0
239            && p.deleted as f32 >= deleted_fraction * p.rows as f32;
240        large || deleted
241    };
242    let marked = |p: &Piece| small(p) || rewrite(p);
243    let mut tasks = Vec::new();
244    let mut start = 0;
245    while start < pieces.len() {
246        if !marked(&pieces[start]) {
247            start += 1;
248            continue;
249        }
250        let mut end = start;
251        while end < pieces.len() && marked(&pieces[end]) {
252            end += 1;
253        }
254        if end - start > 1 || rewrite(&pieces[start]) {
255            tasks.push(start..end);
256        }
257        start = end;
258    }
259    tasks
260}
261
262/// The number of new fragments for `bytes` in `rows` rows: the nearest to
263/// `bytes / target`, at least one, at most one per row.
264fn outputs(bytes: u64, rows: u64, target: u64) -> u64 {
265    let count = (bytes as f64 / target as f64).round() as u64;
266    count.clamp(1, rows.max(1))
267}
268
269/// The live rows of `fragments`, written again as new fragments without
270/// ids, each of about the same size on disk.
271///
272/// The new fragments hold only the top-level columns that `fragments` hold,
273/// as a writer writes them. The others read as null.
274async fn rewrite(
275    dataset: &Dataset,
276    fragments: &[Fragment],
277    pieces: &[Piece],
278    target: u64,
279) -> Result<Vec<Fragment>> {
280    let held: HashSet<i32> = fragments
281        .iter()
282        .flat_map(|fragment| fragment.files.iter())
283        .flat_map(|file| file.fields.iter().copied())
284        .collect();
285    let names: Vec<&str> = dataset
286        .schema()
287        .fields
288        .iter()
289        .filter(|field| holds(&held, field))
290        .map(|field| field.name.as_str())
291        .collect();
292    let lance_schema = dataset.schema().project(&names)?;
293    let writer = PartWriter {
294        dataset,
295        arrow_schema: Arc::new(Schema::from(&lance_schema)),
296        lance_schema,
297    };
298
299    let bytes: f64 = pieces
300        .iter()
301        .map(|p| p.row_bytes() * p.live_rows() as f64)
302        .sum();
303    let rows: u64 = pieces.iter().map(Piece::live_rows).sum();
304    let count = outputs(bytes as u64, rows, target);
305    // Cut at each multiple of the mean part size, so the parts come out even.
306    let part_bytes = bytes / count as f64;
307    // Float sums miss an exact cut by a tiny amount, which would add a row.
308    let slack = part_bytes * 1e-9;
309    let mut cuts = 1;
310    let mut written = 0.0;
311    let mut pending: Vec<RecordBatch> = Vec::new();
312    let mut new_fragments = Vec::new();
313    for (fragment, piece) in fragments.iter().zip(pieces) {
314        let row_bytes = piece.row_bytes().max(f64::MIN_POSITIVE);
315        let mut scanner = dataset.scan();
316        scanner.project(&names)?;
317        scanner.with_fragments(vec![fragment.clone()]);
318        // Small batches, so that a cut can fall between them.
319        scanner.batch_size(64);
320        let mut stream = scanner.try_into_stream().await?;
321        while let Some(batch) = stream.try_next().await? {
322            let mut batch = writer.conform(&batch)?;
323            while batch.num_rows() > 0 {
324                let take = if cuts < count {
325                    let left = part_bytes * cuts as f64 - written - slack;
326                    ((left / row_bytes).ceil() as usize).clamp(1, batch.num_rows())
327                } else {
328                    batch.num_rows()
329                };
330                pending.push(batch.slice(0, take));
331                written += row_bytes * take as f64;
332                batch = batch.slice(take, batch.num_rows() - take);
333                if cuts < count && written + slack >= part_bytes * cuts as f64 {
334                    new_fragments.extend(writer.write(&std::mem::take(&mut pending)).await?);
335                    cuts += 1;
336                }
337            }
338        }
339    }
340    new_fragments.extend(writer.write(&pending).await?);
341    Ok(new_fragments)
342}
343
344/// Writes rows as new fragments of one schema.
345struct PartWriter<'a> {
346    dataset: &'a Dataset,
347    lance_schema: LanceSchema,
348    arrow_schema: SchemaRef,
349}
350
351impl PartWriter<'_> {
352    /// `batch` with the columns and the null rules of the new fragments.
353    fn conform(&self, batch: &RecordBatch) -> Result<RecordBatch> {
354        let batch = conform_batch(batch, &self.arrow_schema)?;
355        let columns = self
356            .arrow_schema
357            .fields()
358            .iter()
359            .zip(batch.columns())
360            .map(|(field, column)| fill_hidden_nulls(column, field))
361            .collect::<Result<Vec<_>>>()?;
362        Ok(RecordBatch::try_new(self.arrow_schema.clone(), columns)?)
363    }
364
365    /// `batches` as fragments: one fragment, or more when the rows do not
366    /// fit in one Arrow array.
367    async fn write(&self, batches: &[RecordBatch]) -> Result<Vec<Fragment>> {
368        let params = WriteParams {
369            mode: WriteMode::Append,
370            data_storage_version: Some(FILE_VERSION),
371            store_params: self.dataset.store_params().cloned(),
372            ..Default::default()
373        };
374        let concat = |part: &[RecordBatch]| concat_batches(&self.arrow_schema, part);
375        let mut new_fragments = Vec::new();
376        // One batch per fragment: Lance 11 fails to read back a fragment of
377        // several batches when a struct column has nulls and large lists.
378        for batch in split_concat(batches, &concat)? {
379            if batch.num_rows() == 0 {
380                continue;
381            }
382            let reader = RecordBatchIterator::new([Ok(batch)], self.arrow_schema.clone());
383            new_fragments.extend(
384                FragmentCreateBuilder::new(self.dataset.uri())
385                    .schema(&self.lance_schema)
386                    .write_params(&params)
387                    .write_fragments(reader)
388                    .await?,
389            );
390        }
391        Ok(new_fragments)
392    }
393}
394
395/// `batches` joined by `concat` into as few batches as possible, in order.
396///
397/// A list or a string array holds at most `i32::MAX` values. When the join
398/// overflows that limit, each half of `batches` is joined on its own.
399fn split_concat(
400    batches: &[RecordBatch],
401    concat: &impl Fn(&[RecordBatch]) -> Result<RecordBatch, ArrowError>,
402) -> Result<Vec<RecordBatch>> {
403    match concat(batches) {
404        Ok(batch) => Ok(vec![batch]),
405        Err(ArrowError::OffsetOverflowError(_)) if batches.len() > 1 => {
406            let (first, second) = batches.split_at(batches.len() / 2);
407            let mut parts = split_concat(first, concat)?;
408            parts.extend(split_concat(second, concat)?);
409            Ok(parts)
410        }
411        Err(error) => Err(error.into()),
412    }
413}
414
415/// `true` when `held` has the id of `field` or of a field in it. A data file
416/// lists the ids of leaf fields, not always of their parents.
417fn holds(held: &HashSet<i32>, field: &lance_core::datatypes::Field) -> bool {
418    held.contains(&field.id) || field.children.iter().any(|child| holds(held, child))
419}
420
421/// Remove the files of the table versions older than `older_than`. The
422/// current version always stays.
423///
424/// A reader that still uses a removed version fails, so keep a margin for
425/// long scans.
426#[tracing::instrument(skip_all, fields(uri = %dataset.uri(), older_than = ?older_than), err)]
427pub async fn cleanup_old_versions(dataset: &Dataset, older_than: Duration) -> Result<CleanupStats> {
428    let older_than = chrono::Duration::from_std(older_than).unwrap_or(chrono::Duration::MAX);
429    let removed = dataset.cleanup_old_versions(older_than, None, None).await?;
430    Ok(CleanupStats {
431        bytes_removed: removed.bytes_removed,
432        old_versions: removed.old_versions,
433    })
434}
435
436#[cfg(test)]
437mod tests {
438    use arrow::array::{AsArray, Int64Array};
439    use arrow::datatypes::{DataType, Field, Int64Type};
440
441    use super::*;
442
443    fn batch(id: i64) -> RecordBatch {
444        let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)]));
445        RecordBatch::try_new(schema, vec![Arc::new(Int64Array::from(vec![id]))]).unwrap()
446    }
447
448    fn ids(batch: &RecordBatch) -> Vec<i64> {
449        batch
450            .column(0)
451            .as_primitive::<Int64Type>()
452            .values()
453            .to_vec()
454    }
455
456    fn piece(bytes: u64, rows: u64, deleted: u64) -> Piece {
457        Piece {
458            bytes,
459            rows,
460            deleted,
461        }
462    }
463
464    #[test]
465    fn plan_merges_small_neighbors_and_keeps_others() {
466        let pieces = [
467            piece(10, 1, 0),
468            piece(20, 2, 0),
469            piece(100, 5, 0),
470            piece(30, 3, 0),
471            piece(100, 5, 0),
472            piece(10, 1, 0),
473            piece(10, 1, 0),
474        ];
475        assert_eq!(plan(&pieces, 100, 0.1), [0..2, 5..7]);
476    }
477
478    #[test]
479    fn plan_skips_a_lone_small_fragment() {
480        let pieces = [piece(100, 5, 0), piece(10, 1, 0), piece(100, 5, 0)];
481        assert!(plan(&pieces, 100, 0.1).is_empty());
482    }
483
484    #[test]
485    fn plan_splits_a_large_fragment_of_several_rows() {
486        let pieces = [piece(100, 5, 0), piece(500, 5, 0), piece(500, 1, 0)];
487        let tasks = plan(&pieces, 100, 0.1);
488        assert_eq!(tasks.len(), 1);
489        assert_eq!(tasks[0], 1..2);
490    }
491
492    #[test]
493    fn plan_rewrites_deleted_rows() {
494        let pieces = [piece(100, 10, 1), piece(100, 10, 0), piece(100, 10, 5)];
495        assert_eq!(plan(&pieces, 100, 0.1), [0..1, 2..3]);
496        assert!(plan(&pieces, 100, 1.5).is_empty());
497    }
498
499    #[test]
500    fn outputs_follow_the_target() {
501        assert_eq!(outputs(1000, 100, 100), 10);
502        assert_eq!(outputs(1040, 100, 100), 10);
503        assert_eq!(outputs(10, 100, 100), 1);
504        assert_eq!(outputs(1000, 3, 100), 3);
505        assert_eq!(outputs(0, 0, 100), 1);
506    }
507
508    #[test]
509    fn concat_splits_on_offset_overflow() {
510        let batches: Vec<RecordBatch> = (0..5).map(batch).collect();
511        let schema = batches[0].schema();
512        // More than two batches overflow, as a large list column does.
513        let concat = |part: &[RecordBatch]| {
514            if part.len() > 2 {
515                return Err(ArrowError::OffsetOverflowError(part.len()));
516            }
517            concat_batches(&schema, part)
518        };
519        let parts = split_concat(&batches, &concat).unwrap();
520        let parts: Vec<Vec<i64>> = parts.iter().map(ids).collect();
521        assert_eq!(parts, [vec![0, 1], vec![2], vec![3, 4]]);
522    }
523
524    #[test]
525    fn concat_keeps_other_errors() {
526        let batches: Vec<RecordBatch> = (0..2).map(batch).collect();
527        let concat = |_: &[RecordBatch]| Err(ArrowError::ComputeError("no".into()));
528        assert!(split_concat(&batches, &concat).is_err());
529    }
530}