1use 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#[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#[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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
65pub struct CompactionStats {
66 pub fragments_removed: usize,
68 pub fragments_added: usize,
70 pub files_removed: usize,
72 pub files_added: usize,
74}
75
76#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
78pub struct CleanupStats {
79 pub bytes_removed: u64,
81 pub old_versions: u64,
83}
84
85#[derive(Debug, Clone)]
87pub struct CompactOptions {
88 pub target_fragment_bytes: u64,
92 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#[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 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#[derive(Debug, Clone, Copy)]
183struct Piece {
184 bytes: u64,
185 rows: u64,
186 deleted: u64,
187}
188
189impl Piece {
190 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
227fn 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
262fn 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
269async 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 let part_bytes = bytes / count as f64;
307 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 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
344struct PartWriter<'a> {
346 dataset: &'a Dataset,
347 lance_schema: LanceSchema,
348 arrow_schema: SchemaRef,
349}
350
351impl PartWriter<'_> {
352 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 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 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(¶ms)
387 .write_fragments(reader)
388 .await?,
389 );
390 }
391 Ok(new_fragments)
392 }
393}
394
395fn 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
415fn 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#[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 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}