1use std::collections::{HashMap, HashSet};
17use std::sync::Arc;
18
19use lance_core::deepsize::DeepSizeOf;
20use lance_core::{Error, Result};
21use serde::{Deserialize, Serialize};
22use uuid::Uuid;
23
24use crate::format::{IndexMetadata, pb};
25
26pub const MEM_WAL_INDEX_NAME: &str = "__lance_mem_wal";
27
28pub type ShardId = Uuid;
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeepSizeOf)]
33pub struct SsTable {
34 pub generation: u64,
35 pub path: String,
36 pub in_memory_bytes: Option<u64>,
46 pub physical_rows: Option<u64>,
49 pub primary_key_bytes: Option<u64>,
55}
56
57impl SsTable {
58 pub fn unmeasured(generation: u64, path: String) -> Self {
60 Self {
61 generation,
62 path,
63 in_memory_bytes: None,
64 physical_rows: None,
65 primary_key_bytes: None,
66 }
67 }
68}
69
70impl From<&SsTable> for pb::SsTable {
71 fn from(sstable: &SsTable) -> Self {
72 Self {
73 generation: sstable.generation,
74 path: sstable.path.clone(),
75 in_memory_bytes: sstable.in_memory_bytes,
76 physical_rows: sstable.physical_rows,
77 primary_key_bytes: sstable.primary_key_bytes,
78 }
79 }
80}
81
82impl From<pb::SsTable> for SsTable {
83 fn from(sstable: pb::SsTable) -> Self {
84 Self {
85 generation: sstable.generation,
86 path: sstable.path,
87 in_memory_bytes: sstable.in_memory_bytes,
88 physical_rows: sstable.physical_rows,
89 primary_key_bytes: sstable.primary_key_bytes,
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash, Serialize, Deserialize)]
96pub struct CompactedSsTable {
97 pub shard_id: Uuid,
98 pub generation: u64,
99}
100
101impl DeepSizeOf for CompactedSsTable {
102 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
103 0 }
105}
106
107impl CompactedSsTable {
108 pub fn new(shard_id: Uuid, generation: u64) -> Self {
109 Self {
110 shard_id,
111 generation,
112 }
113 }
114}
115
116impl From<&CompactedSsTable> for pb::CompactedSsTable {
117 fn from(sstable: &CompactedSsTable) -> Self {
118 Self {
119 shard_id: Some((&sstable.shard_id).into()),
120 generation: sstable.generation,
121 }
122 }
123}
124
125impl TryFrom<pb::CompactedSsTable> for CompactedSsTable {
126 type Error = Error;
127
128 fn try_from(sstable: pb::CompactedSsTable) -> lance_core::Result<Self> {
129 let shard_id = sstable
130 .shard_id
131 .as_ref()
132 .map(Uuid::try_from)
133 .ok_or_else(|| Error::invalid_input("Missing shard_id in CompactedSsTable"))??;
134 Ok(Self {
135 shard_id,
136 generation: sstable.generation,
137 })
138 }
139}
140
141#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, DeepSizeOf)]
144pub struct IndexCatchupProgress {
145 pub index_name: String,
146 pub caught_up_generations: Vec<CompactedSsTable>,
147}
148
149impl IndexCatchupProgress {
150 pub fn new(index_name: String, caught_up_generations: Vec<CompactedSsTable>) -> Self {
151 Self {
152 index_name,
153 caught_up_generations,
154 }
155 }
156
157 pub fn caught_up_generation_for_shard(&self, shard_id: &Uuid) -> Option<u64> {
160 self.caught_up_generations
161 .iter()
162 .find(|sstable| &sstable.shard_id == shard_id)
163 .map(|sstable| sstable.generation)
164 }
165}
166
167impl From<&IndexCatchupProgress> for pb::IndexCatchupProgress {
168 fn from(icp: &IndexCatchupProgress) -> Self {
169 Self {
170 index_name: icp.index_name.clone(),
171 caught_up_generations: icp
172 .caught_up_generations
173 .iter()
174 .map(|sstable| sstable.into())
175 .collect(),
176 }
177 }
178}
179
180impl TryFrom<pb::IndexCatchupProgress> for IndexCatchupProgress {
181 type Error = Error;
182
183 fn try_from(icp: pb::IndexCatchupProgress) -> lance_core::Result<Self> {
184 Ok(Self {
185 index_name: icp.index_name,
186 caught_up_generations: icp
187 .caught_up_generations
188 .into_iter()
189 .map(CompactedSsTable::try_from)
190 .collect::<lance_core::Result<_>>()?,
191 })
192 }
193}
194
195#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
201pub enum ShardStatus {
202 #[default]
204 Active,
205 Sealed,
207}
208
209impl ShardStatus {
210 fn to_i32(self) -> i32 {
212 match self {
213 Self::Active => 0,
214 Self::Sealed => 1,
215 }
216 }
217
218 fn from_i32(v: i32) -> Self {
221 match v {
222 1 => Self::Sealed,
223 _ => Self::Active,
224 }
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231pub struct ShardManifest {
232 pub shard_id: Uuid,
233 pub version: u64,
234 pub shard_spec_id: u32,
235 pub shard_field_values: HashMap<String, Vec<u8>>,
240 pub writer_epoch: u64,
241 pub replay_after_wal_entry_position: u64,
247 pub wal_entry_position_last_seen: u64,
251 pub current_generation: u64,
252 pub sstables: Vec<SsTable>,
253 pub status: ShardStatus,
256}
257
258impl ShardManifest {
259 pub fn next_version(&self) -> u64 {
265 self.version + 1
266 }
267}
268
269impl DeepSizeOf for ShardManifest {
270 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
271 self.shard_field_values.deep_size_of_children(context)
272 + self.sstables.deep_size_of_children(context)
273 }
274}
275
276impl From<&ShardManifest> for pb::ShardManifest {
277 fn from(rm: &ShardManifest) -> Self {
278 Self {
279 shard_id: Some((&rm.shard_id).into()),
280 version: rm.version,
281 shard_spec_id: rm.shard_spec_id,
282 shard_field_entries: rm
283 .shard_field_values
284 .iter()
285 .map(|(k, v)| pb::ShardFieldEntry {
286 field_id: k.clone(),
287 value: v.clone(),
288 })
289 .collect(),
290 writer_epoch: rm.writer_epoch,
291 replay_after_wal_entry_position: rm.replay_after_wal_entry_position,
292 wal_entry_position_last_seen: rm.wal_entry_position_last_seen,
293 current_generation: rm.current_generation,
294 sstables: rm.sstables.iter().map(|sstable| sstable.into()).collect(),
295 status: rm.status.to_i32(),
296 }
297 }
298}
299
300impl TryFrom<pb::ShardManifest> for ShardManifest {
301 type Error = Error;
302
303 fn try_from(rm: pb::ShardManifest) -> lance_core::Result<Self> {
304 let shard_id = rm
305 .shard_id
306 .as_ref()
307 .map(Uuid::try_from)
308 .ok_or_else(|| Error::invalid_input("Missing shard_id in ShardManifest"))??;
309 let shard_field_values = rm
310 .shard_field_entries
311 .into_iter()
312 .map(|e| (e.field_id, e.value))
313 .collect();
314 Ok(Self {
315 shard_id,
316 version: rm.version,
317 shard_spec_id: rm.shard_spec_id,
318 shard_field_values,
319 writer_epoch: rm.writer_epoch,
320 replay_after_wal_entry_position: rm.replay_after_wal_entry_position,
321 wal_entry_position_last_seen: rm.wal_entry_position_last_seen,
322 current_generation: rm.current_generation,
323 sstables: rm.sstables.into_iter().map(SsTable::from).collect(),
324 status: ShardStatus::from_i32(rm.status),
325 })
326 }
327}
328
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeepSizeOf)]
331pub struct ShardingField {
332 pub field_id: String,
333 pub source_ids: Vec<i32>,
334 pub transform: Option<String>,
335 pub expression: Option<String>,
336 pub result_type: String,
337 pub parameters: HashMap<String, String>,
338}
339
340impl From<&ShardingField> for pb::ShardingField {
341 fn from(rf: &ShardingField) -> Self {
342 Self {
343 field_id: rf.field_id.clone(),
344 source_ids: rf.source_ids.clone(),
345 transform: rf.transform.clone(),
346 expression: rf.expression.clone(),
347 result_type: rf.result_type.clone(),
348 parameters: rf.parameters.clone(),
349 }
350 }
351}
352
353impl From<pb::ShardingField> for ShardingField {
354 fn from(rf: pb::ShardingField) -> Self {
355 Self {
356 field_id: rf.field_id,
357 source_ids: rf.source_ids,
358 transform: rf.transform,
359 expression: rf.expression,
360 result_type: rf.result_type,
361 parameters: rf.parameters,
362 }
363 }
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeepSizeOf)]
368pub struct ShardingSpec {
369 pub spec_id: u32,
370 pub fields: Vec<ShardingField>,
371}
372
373impl From<&ShardingSpec> for pb::ShardingSpec {
374 fn from(rs: &ShardingSpec) -> Self {
375 Self {
376 spec_id: rs.spec_id,
377 fields: rs.fields.iter().map(|f| f.into()).collect(),
378 }
379 }
380}
381
382impl From<pb::ShardingSpec> for ShardingSpec {
383 fn from(rs: pb::ShardingSpec) -> Self {
384 Self {
385 spec_id: rs.spec_id,
386 fields: rs.fields.into_iter().map(ShardingField::from).collect(),
387 }
388 }
389}
390
391#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, DeepSizeOf)]
393pub struct MemWalIndexDetails {
394 pub snapshot_ts_millis: i64,
395 pub num_shards: u32,
396 pub inline_snapshots: Option<Vec<u8>>,
397 pub sharding_specs: Vec<ShardingSpec>,
398 pub maintained_indexes: Vec<String>,
399 pub maintain_all_indexes: bool,
400 pub compacted_sstables: Vec<CompactedSsTable>,
401 pub index_catchup: Vec<IndexCatchupProgress>,
402 pub writer_config_defaults: HashMap<String, String>,
409}
410
411impl From<&MemWalIndexDetails> for pb::MemWalIndexDetails {
412 fn from(details: &MemWalIndexDetails) -> Self {
413 Self {
414 snapshot_ts_millis: details.snapshot_ts_millis,
415 num_shards: details.num_shards,
416 inline_snapshots: details.inline_snapshots.clone(),
417 sharding_specs: details.sharding_specs.iter().map(|rs| rs.into()).collect(),
418 maintained_indexes: details.maintained_indexes.clone(),
419 maintain_all_indexes: Some(details.maintain_all_indexes),
420 compacted_sstables: details
421 .compacted_sstables
422 .iter()
423 .map(|sstable| sstable.into())
424 .collect(),
425 index_catchup: details.index_catchup.iter().map(|icp| icp.into()).collect(),
426 writer_config_defaults: details.writer_config_defaults.clone(),
427 }
428 }
429}
430
431impl TryFrom<pb::MemWalIndexDetails> for MemWalIndexDetails {
432 type Error = Error;
433
434 fn try_from(details: pb::MemWalIndexDetails) -> lance_core::Result<Self> {
435 Ok(Self {
436 snapshot_ts_millis: details.snapshot_ts_millis,
437 num_shards: details.num_shards,
438 inline_snapshots: details.inline_snapshots,
439 sharding_specs: details
440 .sharding_specs
441 .into_iter()
442 .map(ShardingSpec::from)
443 .collect(),
444 maintained_indexes: details.maintained_indexes,
445 maintain_all_indexes: details.maintain_all_indexes.unwrap_or(false),
446 compacted_sstables: details
447 .compacted_sstables
448 .into_iter()
449 .map(CompactedSsTable::try_from)
450 .collect::<lance_core::Result<_>>()?,
451 index_catchup: details
452 .index_catchup
453 .into_iter()
454 .map(IndexCatchupProgress::try_from)
455 .collect::<lance_core::Result<_>>()?,
456 writer_config_defaults: details.writer_config_defaults,
457 })
458 }
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, DeepSizeOf)]
463pub struct MemWalIndex {
464 pub details: MemWalIndexDetails,
465}
466
467impl MemWalIndex {
468 pub fn new(details: MemWalIndexDetails) -> Self {
469 Self { details }
470 }
471
472 pub fn compacted_generation_for_shard(&self, shard_id: &Uuid) -> Option<u64> {
473 self.details
474 .compacted_sstables
475 .iter()
476 .find(|sstable| &sstable.shard_id == shard_id)
477 .map(|sstable| sstable.generation)
478 }
479
480 pub fn index_caught_up_generation(&self, index_name: &str, shard_id: &Uuid) -> Option<u64> {
483 self.details
484 .index_catchup
485 .iter()
486 .find(|icp| icp.index_name == index_name)
487 .and_then(|icp| icp.caught_up_generation_for_shard(shard_id))
488 }
489}
490
491pub fn load_mem_wal_index_details(index: IndexMetadata) -> Result<MemWalIndexDetails> {
495 if let Some(details_any) = index.index_details.as_ref() {
496 if !details_any.type_url.ends_with("MemWalIndexDetails") {
497 return Err(Error::index(format!(
498 "Index details is not for the MemWAL index, but {}",
499 details_any.type_url
500 )));
501 }
502
503 Ok(MemWalIndexDetails::try_from(
504 details_any.to_msg::<pb::MemWalIndexDetails>()?,
505 )?)
506 } else {
507 Err(Error::index("Index details not found for the MemWAL index"))
508 }
509}
510
511pub fn open_mem_wal_index(index: IndexMetadata) -> Result<Arc<MemWalIndex>> {
513 Ok(Arc::new(MemWalIndex::new(load_mem_wal_index_details(
514 index,
515 )?)))
516}
517
518pub fn update_mem_wal_index_compacted_sstables(
532 indices: &mut [IndexMetadata],
533 dataset_version: u64,
534 new_compacted_sstables: Vec<CompactedSsTable>,
535) -> Result<()> {
536 if new_compacted_sstables.is_empty() {
537 return Ok(());
538 }
539
540 let mut seen_shards = HashSet::with_capacity(new_compacted_sstables.len());
541 for sstable in &new_compacted_sstables {
542 if !seen_shards.insert(sstable.shard_id) {
543 return Err(Error::invalid_input(format!(
544 "Duplicate shard {} in one SSTable compaction update; each shard \
545 may advance at most once per transaction",
546 sstable.shard_id
547 )));
548 }
549 }
550
551 let pos = indices
555 .iter()
556 .position(|idx| idx.name == MEM_WAL_INDEX_NAME)
557 .ok_or_else(|| {
558 Error::invalid_input(format!(
559 "Cannot record SSTable compaction progress: the {} system index \
560 does not exist on this table",
561 MEM_WAL_INDEX_NAME
562 ))
563 })?;
564
565 let mut details = load_mem_wal_index_details(indices[pos].clone())?;
568
569 for new_sstable in new_compacted_sstables {
570 match details
571 .compacted_sstables
572 .iter_mut()
573 .find(|sstable| sstable.shard_id == new_sstable.shard_id)
574 {
575 Some(existing) if new_sstable.generation <= existing.generation => {
576 return Err(Error::invalid_input(format!(
577 "Stale SSTable compaction for shard {}: proposed generation {} \
578 is not greater than the recorded generation {}",
579 new_sstable.shard_id, new_sstable.generation, existing.generation
580 )));
581 }
582 Some(existing) => existing.generation = new_sstable.generation,
583 None => details.compacted_sstables.push(new_sstable),
584 }
585 }
586
587 indices[pos] = new_mem_wal_index_meta(dataset_version, details)?;
589 Ok(())
590}
591
592pub fn new_mem_wal_index_meta(
599 dataset_version: u64,
600 details: MemWalIndexDetails,
601) -> Result<IndexMetadata> {
602 Ok(IndexMetadata {
603 uuid: Uuid::new_v4(),
604 name: MEM_WAL_INDEX_NAME.to_string(),
605 fields: vec![],
606 covering_fields: vec![],
607 dataset_version,
608 fragment_bitmap: None,
609 index_details: Some(Arc::new(prost_types::Any::from_msg(
610 &pb::MemWalIndexDetails::from(&details),
611 )?)),
612 index_version: 0,
613 created_at: Some(chrono::Utc::now()),
614 base_id: None,
615 files: None,
617 })
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
628 fn an_sstable_without_the_size_fields_decodes_as_unmeasured() {
629 let legacy = pb::SsTable {
630 generation: 7,
631 path: "aaa_gen_7".to_string(),
632 in_memory_bytes: None,
633 physical_rows: None,
634 primary_key_bytes: None,
635 };
636 let decoded = SsTable::from(legacy);
637 assert_eq!(decoded.generation, 7);
638 assert_eq!(decoded.in_memory_bytes, None);
639 assert_eq!(decoded.physical_rows, None);
640 assert_eq!(decoded.primary_key_bytes, None);
641 }
642
643 #[test]
646 fn the_recorded_size_survives_the_round_trip() {
647 let recorded = SsTable {
648 generation: 7,
649 path: "aaa_gen_7".to_string(),
650 in_memory_bytes: Some(4_096),
651 physical_rows: Some(10),
652 primary_key_bytes: Some(80),
653 };
654 let encoded = pb::SsTable::from(&recorded);
655 assert_eq!(encoded.in_memory_bytes, Some(4_096));
656 assert_eq!(encoded.physical_rows, Some(10));
657 assert_eq!(encoded.primary_key_bytes, Some(80));
658 assert_eq!(SsTable::from(encoded), recorded);
659 }
660}