Skip to main content

lance_table/transaction/
operation.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! The vocabulary of changes a transaction can describe.
5//!
6//! Each [`Operation`] variant names one kind of change and carries exactly the
7//! inputs needed to apply it: the fragments to add, the fields that were
8//! rewritten, the indices that were rebuilt. Applying them is
9//! [`super::manifest_build`]; deciding whether two of them collide is
10//! [`super::conflicts`].
11
12use crate::format::key_existence::KeyExistenceFilter;
13use crate::format::overlay::DataOverlayFile;
14use crate::format::{BasePath, DataFile, Fragment, IndexFile, IndexMetadata};
15use crate::system_index::mem_wal::CompactedSsTable;
16use crate::transaction::UpdateMap;
17use lance_core::datatypes::Schema;
18use lance_core::deepsize::DeepSizeOf;
19use roaring::RoaringBitmap;
20use std::collections::HashMap;
21use uuid::Uuid;
22
23#[derive(Debug, Clone, DeepSizeOf, PartialEq)]
24pub struct DataReplacementGroup(pub u64, pub DataFile);
25
26/// Overlay files to append to a single fragment, in order (the last entry is
27/// newest). The overlays are appended to the fragment's existing `overlays`
28/// list rather than replacing it, so overlays written by concurrent commits are
29/// preserved. Each overlay's `committed_version` is stamped to the new dataset
30/// version at commit time (re-stamped on retry).
31#[derive(Debug, Clone, DeepSizeOf, PartialEq)]
32pub struct DataOverlayGroup {
33    pub fragment_id: u64,
34    pub overlays: Vec<DataOverlayFile>,
35}
36
37/// An operation on a dataset.
38#[derive(Debug, Clone, DeepSizeOf)]
39pub enum Operation {
40    /// Adding new fragments to the dataset. The fragments contained within
41    /// haven't yet been assigned a final ID.
42    Append { fragments: Vec<Fragment> },
43    /// Updated fragments contain those that have been modified with new deletion
44    /// files. The deleted fragment IDs are those that should be removed from
45    /// the manifest.
46    Delete {
47        updated_fragments: Vec<Fragment>,
48        deleted_fragment_ids: Vec<u64>,
49        predicate: String,
50    },
51    /// Overwrite the entire dataset with the given fragments. This is also
52    /// used when initially creating a table.
53    ///
54    /// The fragments are newly written ones and are assigned fresh ids at commit
55    /// time, continuing from the dataset's highest id ever used; the ids they
56    /// arrive with are ignored.
57    ///
58    /// A fragment carrying a deletion file is rejected. A deletion file's path
59    /// embeds the fragment id, so it cannot follow its fragment to the new id:
60    /// minting a fragment and giving it a deletion file are mutually exclusive in
61    /// one transaction. Use [`Self::Delete`] to commit deletions against existing
62    /// fragments, or [`Self::Merge`] to change their schema.
63    Overwrite {
64        fragments: Vec<Fragment>,
65        schema: Schema,
66        config_upsert_values: Option<HashMap<String, String>>,
67        initial_bases: Option<Vec<BasePath>>,
68    },
69    /// A new index has been created.
70    CreateIndex {
71        /// The new secondary indices,
72        /// any existing indices with the same name will be replaced.
73        new_indices: Vec<IndexMetadata>,
74        /// The indices that have been modified.
75        removed_indices: Vec<IndexMetadata>,
76    },
77    /// Data is rewritten but *not* modified. This is used for things like
78    /// compaction or re-ordering. Contains the old fragments and the new
79    /// ones that have been replaced.
80    ///
81    /// This operation will modify the row addresses of existing rows and
82    /// so any existing index covering a rewritten fragment will need to be
83    /// remapped.
84    Rewrite {
85        /// Groups of fragments that have been modified
86        groups: Vec<RewriteGroup>,
87        /// Indices that have been updated with the new row addresses
88        rewritten_indices: Vec<RewrittenIndex>,
89        /// The fragment reuse index entry to be created or updated to: the
90        /// complete entry the caller wants installed, that is a base entry
91        /// plus this rewrite's own records.
92        ///
93        /// On a v0 history (`index_version` 0) it must be the entry of the dataset
94        /// the rewrite is committed through plus one version; a latest entry that
95        /// is neither that entry nor a trim of it is a retryable conflict.
96        ///
97        /// On a tagged history (`index_version` 1) the commit path does not
98        /// splice this entry as it is. It works out which records the entry
99        /// adds relative to the entry at the transaction's read version, so
100        /// the entry must be built from that same snapshot (its
101        /// `dataset_version` is the read version; an entry built from a
102        /// later snapshot is refused, since it would fold other writers'
103        /// records in between into this rewrite's additions). It then
104        /// validates them against `groups` (binding, row conservation,
105        /// deletion folding, the ledger), and merges them onto the entry
106        /// current at commit, so a rewrite never overwrites history another
107        /// writer recorded concurrently. A rewrite only appends: an entry
108        /// that drops a recorded transition, or carries a recorded transition
109        /// with different content, is refused; history is retired through
110        /// the reuse index cleanup instead.
111        ///
112        /// In-memory only, never serialized into the transaction file: other
113        /// writers' conflict decisions only need the fragment sets in
114        /// `groups`, and the manifest is the durable record of the history.
115        frag_reuse_index: Option<IndexMetadata>,
116    },
117    /// Replace data in a column in the dataset with new data. This is used for
118    /// null column population where we replace an entirely null column with a
119    /// new column that has data.
120    ///
121    /// This operation will only allow replacing files that contain the same schema
122    /// e.g. if the original files contain columns A, B, C and the new files contain
123    /// only columns A, B then the operation is not allowed. As we would need to split
124    /// the original files into two files, one with column A, B and the other with column C.
125    ///
126    /// Corollary to the above: the operation will also not allow replacing files unless the
127    /// affected columns all have the same datafile layout across the fragments being replaced.
128    ///
129    /// e.g. if fragments being replaced contain files with different schema layouts on
130    /// the column being replaced, the operation is not allowed.
131    /// say `frag_1: [A] [B, C]` and `frag_2: [A, B] [C]` and we are trying to replace column A
132    /// with a new column A, the operation is not allowed.
133    DataReplacement {
134        replacements: Vec<DataReplacementGroup>,
135    },
136    /// Attach overlay files to fragments, supplying new values for a subset of
137    /// `(physical offset, field)` cells without rewriting the fragments' base
138    /// data files. See [`DataOverlayFile`] and the Data Overlay Files
139    /// specification for resolution, coverage, and versioning rules.
140    DataOverlay { groups: Vec<DataOverlayGroup> },
141    /// Merge a new column in
142    /// 'fragments' is the final fragment list: the merged version of every existing
143    /// fragment (aligned with the old one at rows) and, optionally, brand-new fragments
144    /// listed after them. New fragments use id 0 (assigned a fresh id at commit time) or
145    /// a pre-reserved id; either way, on stable row id datasets they are also assigned
146    /// row ids at commit time, like Append. New fragments must not carry row id metadata.
147    /// 'schema' is not forced to include existed columns, which means we could use Merge to drop column data
148    Merge {
149        fragments: Vec<Fragment>,
150        schema: Schema,
151        /// Set when this merge makes no nullability-affecting schema change:
152        /// it introduces no field that data staged against an earlier schema
153        /// could not safely omit. Without the assertion the merge conflicts
154        /// with concurrent appends in either commit order, since a stale
155        /// append omits new columns entirely and its rows read as null.
156        preserves_nullability: bool,
157    },
158    /// Restore an old version of the database
159    Restore { version: u64 },
160    /// Reserves fragment ids for future use
161    /// This can be used when row ids need to be known before a transaction
162    /// has been committed.  It is used during a rewrite operation to allow
163    /// indices to be remapped to the new row ids as part of the operation.
164    ReserveFragments { num_fragments: u32 },
165
166    /// Update values in the dataset.
167    ///
168    /// Updates are generally vertical or horizontal.
169    ///
170    /// A vertical update adds new rows.  In this case, the updated_fragments
171    /// will only have existing rows deleted and will not have any new fields added.
172    /// All new data will be contained in new_fragments.
173    /// This is what is used by a merge_insert that matches the whole schema and what
174    /// is used by the dataset updater.
175    ///
176    /// A horizontal update adds new columns.  In this case, the updated fragments
177    /// may have fields removed or added.  It is even possible for a field to be tombstoned
178    /// and then added back in the same update. (which is a field modification).  If any
179    /// fields are modified in this way then they need to be added to the fields_modified list.
180    /// This way we can correctly update the indices.
181    /// This is what is used by a merge insert that does not match the whole schema.
182    Update {
183        /// Ids of fragments that have been moved
184        removed_fragment_ids: Vec<u64>,
185        /// Fragments that have been updated
186        updated_fragments: Vec<Fragment>,
187        /// Fragments that have been added
188        new_fragments: Vec<Fragment>,
189        /// The fields that have been modified
190        fields_modified: Vec<u32>,
191        /// MemWAL SSTables to mark as compacted after this transaction.
192        compacted_sstables: Vec<CompactedSsTable>,
193        /// The fields that used to judge whether to preserve the new frag's id into
194        /// the frag bitmap of the specified indices.
195        fields_for_preserving_frag_bitmap: Vec<u32>,
196        /// The mode of update
197        update_mode: Option<UpdateMode>,
198        /// Optional filter for detecting conflicts on inserted row keys.
199        /// Only tracks keys from INSERT operations during merge insert, not updates.
200        inserted_rows_filter: Option<KeyExistenceFilter>,
201        /// Physical row offsets (per fragment) that matched `update_columns` for RewriteColumns.
202        /// `None` means callers did not supply offsets; `build_manifest` skips partial refresh then.
203        updated_fragment_offsets: Option<UpdatedFragmentOffsets>,
204    },
205
206    /// Project to a new schema.
207    Project {
208        schema: Schema,
209        /// Set when this projection makes no nullability-affecting schema
210        /// change, as a rename or a drop does not. A nullability tightening
211        /// must not set this: its producer proved the claim by scanning at its
212        /// read version, so a concurrent write can falsify it and the
213        /// projection conflicts with value-writes in either commit order.
214        preserves_nullability: bool,
215    },
216
217    /// Update the dataset configuration and metadata.
218    ///
219    /// Schema or field metadata updates conflict with a concurrent
220    /// [`Self::Merge`] in either commit order. A merge carries complete schema
221    /// state from its read version, so rebasing the operations could discard
222    /// metadata installed by the other transaction.
223    UpdateConfig {
224        config_updates: Option<UpdateMap>,
225        table_metadata_updates: Option<UpdateMap>,
226        schema_metadata_updates: Option<UpdateMap>,
227        field_metadata_updates: HashMap<i32, UpdateMap>,
228    },
229    /// Update SSTable compaction progress in the MemWAL index.
230    ///
231    /// This is used during merge-insert to atomically record which
232    /// SSTables have been compacted into the base table.
233    UpdateMemWalState {
234        compacted_sstables: Vec<CompactedSsTable>,
235    },
236
237    /// Clone a dataset.
238    ///
239    /// Only a shallow clone can be committed through `CommitBuilder`. A deep
240    /// clone (`is_shallow = false`) must copy the source files first, so
241    /// committing one returns an invalid input error; use
242    /// `Dataset::deep_clone` instead.
243    Clone {
244        is_shallow: bool,
245        ref_name: Option<String>,
246        ref_version: u64,
247        ref_path: String,
248        branch_name: Option<String>,
249    },
250
251    // Update base paths in the dataset (currently only supports adding new bases).
252    UpdateBases {
253        /// The new base paths to add to the manifest.
254        new_bases: Vec<BasePath>,
255    },
256
257    /// An operation written by a newer version of Lance that this version does
258    /// not recognize. It can be read but never re-encoded or committed, and it
259    /// is assumed to conflict with everything.
260    #[non_exhaustive]
261    Unknown {},
262}
263
264#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
265pub enum UpdateMode {
266    /// rows are deleted in current fragments and rewritten in new fragments.
267    /// This is most optimal when the majority of columns are being rewritten
268    /// or only a few rows are being updated.
269    RewriteRows,
270
271    /// within each fragment, columns are fully rewritten and inserted as new data files.
272    /// Old versions of columns are tombstoned. This is most optimal when most rows are affected
273    /// but a small subset of columns are affected.
274    RewriteColumns,
275}
276
277/// Matched physical row offsets per fragment for a partial [`UpdateMode::RewriteColumns`] update.
278///
279/// Used with stable row IDs so `build_manifest` can refresh row-level version
280/// metadata only for rows that were rewritten.
281#[derive(Debug, Clone, PartialEq, Eq, Default)]
282pub struct UpdatedFragmentOffsets(pub HashMap<u64, RoaringBitmap>);
283
284impl DeepSizeOf for UpdatedFragmentOffsets {
285    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
286        self.0.iter().fold(0_usize, |acc, (frag_id, bitmap)| {
287            acc + frag_id.deep_size_of_children(context)
288                + (bitmap.len() as usize).saturating_mul(std::mem::size_of::<u32>())
289        })
290    }
291}
292
293impl std::fmt::Display for Operation {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        match self {
296            Self::Append { .. } => write!(f, "Append"),
297            Self::Delete { .. } => write!(f, "Delete"),
298            Self::Overwrite { .. } => write!(f, "Overwrite"),
299            Self::CreateIndex { .. } => write!(f, "CreateIndex"),
300            Self::Rewrite { .. } => write!(f, "Rewrite"),
301            Self::Merge { .. } => write!(f, "Merge"),
302            Self::Restore { .. } => write!(f, "Restore"),
303            Self::ReserveFragments { .. } => write!(f, "ReserveFragments"),
304            Self::Update { .. } => write!(f, "Update"),
305            Self::Project { .. } => write!(f, "Project"),
306            Self::UpdateConfig { .. } => write!(f, "UpdateConfig"),
307            Self::DataReplacement { .. } => write!(f, "DataReplacement"),
308            Self::DataOverlay { .. } => write!(f, "DataOverlay"),
309            Self::Unknown { .. } => write!(f, "Unknown"),
310            Self::Clone { .. } => write!(f, "Clone"),
311            Self::UpdateMemWalState { .. } => write!(f, "UpdateMemWalState"),
312            Self::UpdateBases { .. } => write!(f, "UpdateBases"),
313        }
314    }
315}
316
317/// What the commit path assembled for a rewrite on a tagged fragment reuse
318/// history: the complete entry to install, merged against the manifest the
319/// commit attempt builds on. Supplied through
320/// [`FragReuseUpdate::Rewrite`](crate::transaction::FragReuseUpdate) on the
321/// prepared index list once per attempt, while the operation's
322/// `frag_reuse_index` stays what the caller supplied, so every retry works
323/// from the same intent.
324#[derive(Debug, Clone)]
325pub struct TaggedRewriteAssembly {
326    /// The entry to install, assembled against the manifest being built.
327    pub entry: IndexMetadata,
328    /// `dataset_version` of the entry the assembly appended onto (`None`
329    /// when this rewrite creates the entry). The manifest build refuses the
330    /// splice when the manifest's entry is a different one.
331    pub base_entry_version: Option<u64>,
332    /// The source fragments of the transitions this rewrite appended. Their
333    /// rewrite groups redistribute rows, so index bitmaps keep the retired
334    /// source ids as provenance instead of following the rewrite.
335    pub reordered_sources: RoaringBitmap,
336}
337
338impl TaggedRewriteAssembly {
339    /// Bundle an assembled entry with the transitions it appended.
340    pub fn new(
341        entry: IndexMetadata,
342        base_entry_version: Option<u64>,
343        transitions: &[crate::format::pb::fragment_reuse_index_details::Transition],
344    ) -> lance_core::Result<Self> {
345        Ok(Self {
346            entry,
347            base_entry_version,
348            reordered_sources: reordered_sources(transitions)?,
349        })
350    }
351}
352
353/// The union of `transitions`' source fragment ids. Rewrite groups covered
354/// by this set skip index-bitmap maintenance: their bitmaps keep the retired
355/// source ids as provenance.
356///
357/// Fragment ids in the reuse domain are bounded by the row-address fragment
358/// space (u32); the ledger's digest validation is the authoritative
359/// enforcement, but it only runs during entry assembly, so an out-of-range
360/// id is rejected here too rather than silently truncated into an alias of
361/// another fragment.
362pub fn reordered_sources(
363    transitions: &[crate::format::pb::fragment_reuse_index_details::Transition],
364) -> lance_core::Result<RoaringBitmap> {
365    transitions
366        .iter()
367        .flat_map(|transition| transition.sources.iter().map(|source| source.id))
368        .map(|id| {
369            u32::try_from(id).map_err(|_| {
370                lance_core::Error::invalid_input(format!(
371                    "transition source fragment id {id} is outside the row-address range"
372                ))
373            })
374        })
375        .collect()
376}
377
378#[derive(Debug, Clone, PartialEq)]
379pub struct RewrittenIndex {
380    pub old_id: Uuid,
381    pub new_id: Uuid,
382    pub new_index_details: prost_types::Any,
383    pub new_index_version: u32,
384    /// Files in the new index with their sizes.
385    /// Empty list from older writers that didn't persist this field.
386    pub new_index_files: Option<Vec<IndexFile>>,
387}
388
389impl DeepSizeOf for RewrittenIndex {
390    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
391        self.new_index_details
392            .type_url
393            .deep_size_of_children(context)
394            + self.new_index_details.value.deep_size_of_children(context)
395    }
396}
397
398#[derive(Debug, Clone, DeepSizeOf)]
399pub struct RewriteGroup {
400    pub old_fragments: Vec<Fragment>,
401    pub new_fragments: Vec<Fragment>,
402}
403
404impl PartialEq for RewriteGroup {
405    fn eq(&self, other: &Self) -> bool {
406        fn compare_vec<T: PartialEq>(a: &[T], b: &[T]) -> bool {
407            a.len() == b.len() && a.iter().all(|f| b.contains(f))
408        }
409        compare_vec(&self.old_fragments, &other.old_fragments)
410            && compare_vec(&self.new_fragments, &other.new_fragments)
411    }
412}
413
414impl Operation {
415    pub fn name(&self) -> &str {
416        match self {
417            Self::Append { .. } => "Append",
418            Self::Delete { .. } => "Delete",
419            Self::Overwrite { .. } => "Overwrite",
420            Self::CreateIndex { .. } => "CreateIndex",
421            Self::Rewrite { .. } => "Rewrite",
422            Self::Merge { .. } => "Merge",
423            Self::ReserveFragments { .. } => "ReserveFragments",
424            Self::Restore { .. } => "Restore",
425            Self::Update { .. } => "Update",
426            Self::Project { .. } => "Project",
427            Self::UpdateConfig { .. } => "UpdateConfig",
428            Self::DataReplacement { .. } => "DataReplacement",
429            Self::DataOverlay { .. } => "DataOverlay",
430            Self::Unknown { .. } => "Unknown",
431            Self::UpdateMemWalState { .. } => "UpdateMemWalState",
432            Self::Clone { .. } => "Clone",
433            Self::UpdateBases { .. } => "UpdateBases",
434        }
435    }
436}