lance 9.0.0

A columnar data format that is 100x faster than Parquet for random access.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The Lance Authors

//! Utility functions for MemWAL operations.

use lance_io::object_store::ObjectStoreParams;
use object_store::path::Path;
use uuid::Uuid;

// ============================================================================
// Watchable Cell
// ============================================================================

/// A cell that can be written to once and read by multiple readers.
///
/// Used for durability notifications where multiple callers may need to await the same result.
#[derive(Clone, Debug)]
pub struct WatchableOnceCell<T: Clone + std::fmt::Debug> {
    rx: tokio::sync::watch::Receiver<Option<T>>,
    tx: tokio::sync::watch::Sender<Option<T>>,
}

/// Reader handle for a WatchableOnceCell.
///
/// Can be cloned and shared across tasks to await the same value.
#[derive(Clone, Debug)]
pub struct WatchableOnceCellReader<T: Clone + std::fmt::Debug> {
    rx: tokio::sync::watch::Receiver<Option<T>>,
}

impl<T: Clone + std::fmt::Debug> WatchableOnceCell<T> {
    /// Create a new empty cell.
    pub fn new() -> Self {
        let (tx, rx) = tokio::sync::watch::channel(None);
        Self { rx, tx }
    }

    /// Write a value to the cell.
    ///
    /// Only the first write takes effect; subsequent writes are ignored.
    pub fn write(&self, val: T) {
        self.tx.send_if_modified(|v| {
            if v.is_some() {
                return false;
            }
            v.replace(val);
            true
        });
    }

    /// Get a reader handle for this cell.
    pub fn reader(&self) -> WatchableOnceCellReader<T> {
        WatchableOnceCellReader {
            rx: self.rx.clone(),
        }
    }
}

impl<T: Clone + std::fmt::Debug> Default for WatchableOnceCell<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: Clone + std::fmt::Debug> WatchableOnceCellReader<T> {
    /// Read the current value without waiting.
    ///
    /// Returns `None` if no value has been written yet.
    pub fn read(&self) -> Option<T> {
        self.rx.borrow().clone()
    }

    /// Wait for a value to be written.
    ///
    /// Returns immediately if a value is already present. Returns
    /// `None` if the underlying watch channel was closed before a
    /// value was published — typically because the producing task
    /// died (panicked or returned `Err`). Earlier this case used to
    /// panic with `expect("watch channel closed")`, which turned a
    /// dead-flusher consequence into a worker panic and made the
    /// merge_insert call fail with a `RustPanic` instead of a
    /// recoverable error.
    pub async fn await_value(&mut self) -> Option<T> {
        self.rx.wait_for(|v| v.is_some()).await.ok()?;
        self.rx.borrow().clone()
    }
}

/// Bit-reverse a 64-bit integer.
///
/// Used for file naming to distribute files evenly across object store keyspace,
/// optimizing S3 throughput by spreading sequential writes across internal partitions.
///
/// # Example
/// ```ignore
/// // 5 in binary: 000...101
/// // Reversed:    101...000
/// assert_eq!(bit_reverse_u64(5), 0xa000000000000000);
/// ```
pub fn bit_reverse_u64(n: u64) -> u64 {
    n.reverse_bits()
}

/// Generate a bit-reversed filename for a given ID.
///
/// # Arguments
/// * `id` - The sequential ID to convert
/// * `ext` - File extension (e.g., "binpb", "lance")
///
/// # Returns
/// A string like "1010000000000000000000000000000000000000000000000000000000000000.binpb"
/// for id=5, ext="binpb"
pub fn bit_reversed_filename(id: u64, ext: &str) -> String {
    format!("{:064b}.{}", bit_reverse_u64(id), ext)
}

/// Parse a bit-reversed filename back to the original ID.
///
/// # Arguments
/// * `filename` - The filename without path (e.g., "1010...0000.binpb")
///
/// # Returns
/// The original ID, or None if parsing fails
pub fn parse_bit_reversed_filename(filename: &str) -> Option<u64> {
    let stem = filename.split('.').next()?;
    if stem.len() != 64 || !stem.chars().all(|c| c == '0' || c == '1') {
        return None;
    }
    let reversed = u64::from_str_radix(stem, 2).ok()?;
    Some(bit_reverse_u64(reversed))
}

/// Adapt the store params a base dataset was opened with for use on a URI
/// *derived* from it (a flushed generation under `_mem_wal/`).
///
/// The deprecated `object_store` binding pins a store to one location: given
/// `Some((store, url))`, both `ObjectStore::from_uri_and_params` and
/// `DatasetBuilder::build_object_store` take the path from `url` and ignore the
/// URI they were asked to open. Carried onto a generation URI it would silently
/// redirect the open — and, on the flush path, the write — at the base table
/// itself. Drop it so the generation URI resolves its own store; everything
/// else (storage options, wrapper, credentials, block size) still carries over.
///
/// Only the base's *own* URI may reuse the params verbatim.
pub(crate) fn derived_store_params(params: &ObjectStoreParams) -> ObjectStoreParams {
    #[allow(deprecated)]
    ObjectStoreParams {
        object_store: None,
        ..params.clone()
    }
}

/// Path to the MemWAL root directory.
///
/// Returns: `{base_path}/_mem_wal/`
pub fn mem_wal_path(base_path: &Path) -> Path {
    base_path.clone().join("_mem_wal")
}

/// Base path for a shard within the MemWAL directory.
///
/// Returns: `{base_path}/_mem_wal/{shard_id}/`
pub fn shard_base_path(base_path: &Path, shard_id: &Uuid) -> Path {
    mem_wal_path(base_path).join(shard_id.as_hyphenated().to_string())
}

/// Path to the WAL directory for a shard.
///
/// Returns: `{base_path}/_mem_wal/{shard_id}/wal/`
pub fn shard_wal_path(base_path: &Path, shard_id: &Uuid) -> Path {
    shard_base_path(base_path, shard_id).join("wal")
}

/// Path to the manifest directory for a shard.
///
/// Returns: `{base_path}/_mem_wal/{shard_id}/manifest/`
pub fn shard_manifest_path(base_path: &Path, shard_id: &Uuid) -> Path {
    shard_base_path(base_path, shard_id).join("manifest")
}

/// Path to a flushed MemTable directory.
///
/// Returns: `{base_path}/_mem_wal/{shard_id}/{random_hash}_gen_{generation}/`
pub fn flushed_memtable_path(
    base_path: &Path,
    shard_id: &Uuid,
    random_hash: &str,
    generation: u64,
) -> Path {
    shard_base_path(base_path, shard_id).join(format!("{}_gen_{}", random_hash, generation))
}

/// Subdirectory of a flushed generation holding its standalone primary-key
/// dedup index (a sidecar BTree, not registered in the manifest). Both the
/// flush writer and the block-list probe join this onto the generation path.
pub const PK_INDEX_DIR: &str = "_pk_index";

/// Path to a flushed generation's standalone primary-key dedup index.
pub fn pk_index_path(gen_path: &Path) -> Path {
    gen_path.clone().join(PK_INDEX_DIR)
}

/// Generate an 8-character random hex string for flushed MemTable directories.
pub fn generate_random_hash() -> String {
    let bytes: [u8; 4] = rand::random();
    format!(
        "{:02x}{:02x}{:02x}{:02x}",
        bytes[0], bytes[1], bytes[2], bytes[3]
    )
}

/// WAL entry filename.
///
/// Returns bit-reversed filename with .arrow extension (Arrow IPC format).
pub fn wal_entry_filename(wal_entry_position: u64) -> String {
    bit_reversed_filename(wal_entry_position, "arrow")
}

/// Shard manifest filename.
///
/// Returns bit-reversed filename with .binpb extension.
pub fn manifest_filename(version: u64) -> String {
    bit_reversed_filename(version, "binpb")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_bit_reverse_u64() {
        // 0 should remain 0
        assert_eq!(bit_reverse_u64(0), 0);

        // 1 (least significant bit) becomes most significant
        assert_eq!(bit_reverse_u64(1), 0x8000000000000000);

        // 5 = 101 in binary, reversed = 101 followed by 61 zeros
        assert_eq!(bit_reverse_u64(5), 0xa000000000000000);

        // Double reversal should give original
        for i in [0u64, 1, 2, 5, 100, 1000, u64::MAX / 2, u64::MAX] {
            assert_eq!(bit_reverse_u64(bit_reverse_u64(i)), i);
        }
    }

    #[test]
    fn test_bit_reversed_filename() {
        let filename = bit_reversed_filename(1, "binpb");
        assert_eq!(
            filename,
            "1000000000000000000000000000000000000000000000000000000000000000.binpb"
        );

        let filename = bit_reversed_filename(5, "lance");
        assert_eq!(
            filename,
            "1010000000000000000000000000000000000000000000000000000000000000.lance"
        );
    }

    #[test]
    fn test_parse_bit_reversed_filename() {
        // Round-trip test
        for id in [1u64, 5, 100, 1000, u64::MAX / 2] {
            let filename = bit_reversed_filename(id, "binpb");
            let parsed = parse_bit_reversed_filename(&filename);
            assert_eq!(parsed, Some(id), "Failed round-trip for id={}", id);
        }

        // Invalid inputs
        assert_eq!(parse_bit_reversed_filename("invalid"), None);
        assert_eq!(parse_bit_reversed_filename("123.binpb"), None);
        assert_eq!(
            parse_bit_reversed_filename(
                "10100000000000000000000000000000000000000000000000000000000000002.binpb"
            ),
            None
        );
    }

    #[test]
    fn test_mem_wal_path() {
        let base_path = Path::from("my/dataset");
        assert_eq!(mem_wal_path(&base_path).as_ref(), "my/dataset/_mem_wal");

        let empty_base = Path::from("");
        assert_eq!(mem_wal_path(&empty_base).as_ref(), "_mem_wal");
    }

    #[test]
    fn test_shard_paths() {
        let base_path = Path::from("my/dataset");
        let shard_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();

        assert_eq!(
            shard_base_path(&base_path, &shard_id).as_ref(),
            "my/dataset/_mem_wal/550e8400-e29b-41d4-a716-446655440000"
        );

        assert_eq!(
            shard_wal_path(&base_path, &shard_id).as_ref(),
            "my/dataset/_mem_wal/550e8400-e29b-41d4-a716-446655440000/wal"
        );

        assert_eq!(
            shard_manifest_path(&base_path, &shard_id).as_ref(),
            "my/dataset/_mem_wal/550e8400-e29b-41d4-a716-446655440000/manifest"
        );

        assert_eq!(
            flushed_memtable_path(&base_path, &shard_id, "a1b2c3d4", 5).as_ref(),
            "my/dataset/_mem_wal/550e8400-e29b-41d4-a716-446655440000/a1b2c3d4_gen_5"
        );

        // Test with empty base path
        let empty_base = Path::from("");
        assert_eq!(
            shard_wal_path(&empty_base, &shard_id).as_ref(),
            "_mem_wal/550e8400-e29b-41d4-a716-446655440000/wal"
        );
    }

    #[test]
    fn test_generate_random_hash() {
        let hash = generate_random_hash();
        assert_eq!(hash.len(), 8);
        assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));

        // Should generate different values (with very high probability)
        let hash2 = generate_random_hash();
        assert_ne!(hash, hash2);
    }

    #[tokio::test]
    async fn test_watchable_once_cell_write_once() {
        let cell = WatchableOnceCell::new();
        let reader = cell.reader();

        assert_eq!(reader.read(), None);

        cell.write(42);
        assert_eq!(reader.read(), Some(42));

        // Second write is ignored
        cell.write(100);
        assert_eq!(reader.read(), Some(42));
    }

    #[tokio::test]
    async fn test_watchable_once_cell_await() {
        let cell = WatchableOnceCell::new();
        let mut reader = cell.reader();

        let handle = tokio::spawn(async move { reader.await_value().await });

        // Brief delay to ensure the task is waiting
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;

        cell.write(123);

        let result = handle.await.unwrap();
        assert_eq!(result, Some(123));
    }

    #[tokio::test]
    async fn test_watchable_once_cell_multiple_readers() {
        let cell = WatchableOnceCell::new();
        let mut reader1 = cell.reader();
        let mut reader2 = cell.reader();

        let h1 = tokio::spawn(async move { reader1.await_value().await });
        let h2 = tokio::spawn(async move { reader2.await_value().await });

        tokio::time::sleep(std::time::Duration::from_millis(10)).await;

        cell.write(456);

        assert_eq!(h1.await.unwrap(), Some(456));
        assert_eq!(h2.await.unwrap(), Some(456));
    }

    #[tokio::test]
    async fn test_watchable_once_cell_returns_none_on_close() {
        let cell: WatchableOnceCell<i32> = WatchableOnceCell::new();
        let mut reader = cell.reader();

        // Drop the cell (and thereby the sender) without ever writing.
        // Earlier this caused `await_value` to panic with
        // "watch channel closed"; now it returns None so callers can
        // surface the dead-producer condition as a recoverable error.
        let handle = tokio::spawn(async move { reader.await_value().await });
        drop(cell);
        assert_eq!(handle.await.unwrap(), None);
    }

    /// The path-bound store binding is the only thing dropped — credentials and
    /// storage options must still reach the generation's store.
    #[test]
    fn test_derived_store_params_drops_only_the_path_bound_store() {
        let accessor = lance_io::object_store::StorageOptionsAccessor::with_static_options(
            std::collections::HashMap::from([("access_key_id".to_string(), "key".to_string())]),
        );
        #[allow(deprecated)]
        let params = ObjectStoreParams {
            object_store: Some((
                std::sync::Arc::new(object_store::memory::InMemory::new()),
                url::Url::parse("memory:///base").unwrap(),
            )),
            block_size: Some(1234),
            storage_options_accessor: Some(std::sync::Arc::new(accessor)),
            ..Default::default()
        };

        let derived = derived_store_params(&params);

        #[allow(deprecated)]
        {
            assert!(
                derived.object_store.is_none(),
                "a store pinned to the base path must not be reused for a generation URI"
            );
        }
        assert_eq!(derived.block_size, Some(1234));
        assert_eq!(
            derived
                .storage_options()
                .and_then(|o| o.get("access_key_id")),
            Some(&"key".to_string()),
        );
    }
}