Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,34 @@ ds.scan()
// consume stream...
```

### Share metadata and index caches

Use a session to share Lance's metadata and index caches across dataset
handles. Cache limits are bytes; zero requests zero capacity for the
corresponding cache.

```c
LanceSession* session = lance_session_new(
6ULL * 1024 * 1024 * 1024,
1ULL * 1024 * 1024 * 1024);
LanceDataset* ds =
lance_dataset_open_with_session("data.lance", NULL, 0, session);

LanceSessionCacheStats stats;
lance_session_get_cache_stats(session, &stats);

lance_session_close(session); /* ds remains valid */
lance_dataset_close(ds);
```

```cpp
lance::Session session(
6ULL * 1024 * 1024 * 1024,
1ULL * 1024 * 1024 * 1024);
auto ds = lance::Dataset::open_with_session(session, "data.lance");
auto stats = session.cache_stats();
```

### Open at a specific version

`lance_dataset_open` takes a `version` argument — `0` means the latest, any
Expand Down
65 changes: 65 additions & 0 deletions include/lance/lance.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,58 @@ void lance_free_string(const char* s);
typedef struct LanceDataset LanceDataset;
typedef struct LanceScanner LanceScanner;
typedef struct LanceBatch LanceBatch;
typedef struct LanceSession LanceSession;
typedef struct LanceVersions LanceVersions;
typedef struct LanceDataStatistics LanceDataStatistics;
typedef struct LanceIndexSegmentBuilder LanceIndexSegmentBuilder;
typedef struct LanceIndexSegmentMetadata LanceIndexSegmentMetadata;
typedef struct LanceFtsQueryContext LanceFtsQueryContext;

/* ─── Shared session ─── */

/**
* Snapshot of a shared session's cache statistics.
*
* Cache sizes are the bytes currently retained, not their configured limits.
*/
typedef struct LanceSessionCacheStats {
uint64_t index_cache_hits;
uint64_t index_cache_misses;
Comment thread
Jay-ju marked this conversation as resolved.
uint64_t index_cache_entries;
uint64_t index_cache_size_bytes;
uint64_t metadata_cache_hits;
uint64_t metadata_cache_misses;
uint64_t metadata_cache_entries;
uint64_t metadata_cache_size_bytes;
} LanceSessionCacheStats;

/**
* Create a session that can share metadata and index caches across datasets.
*
* Cache limits are specified in bytes. Pass 0 to request zero capacity.
* @return Session handle, or NULL on error
*/
LanceSession* lance_session_new(
uint64_t index_cache_size_bytes,
uint64_t metadata_cache_size_bytes
);

/**
* Close a session handle. Safe to call with NULL. Datasets previously opened
* with the session remain valid and retain the shared cache state.
*/
void lance_session_close(LanceSession* session);

/**
* Copy current cache statistics to `out_stats`.
*
* @return 0 on success, -1 on error
*/
int32_t lance_session_get_cache_stats(
const LanceSession* session,
LanceSessionCacheStats* out_stats
Comment thread
Jay-ju marked this conversation as resolved.
);

/* ─── Dataset lifecycle ─── */

/**
Expand All @@ -208,6 +254,25 @@ LanceDataset* lance_dataset_open(
uint64_t version
);

/**
* Open a Lance dataset using a shared session.
*
* The dataset retains the shared session state and remains valid if the caller
* subsequently closes `session`.
*
* @param uri Dataset path (file://, s3://, memory://, etc.)
* @param storage_opts NULL-terminated key-value pairs ["k1","v1",NULL], or NULL
* @param version Version to open (0 = latest)
* @param session Shared session; must not be NULL
* @return Dataset handle, or NULL on error
*/
LanceDataset* lance_dataset_open_with_session(
const char* uri,
const char* const* storage_opts,
uint64_t version,
const LanceSession* session
);

/** Close and free a dataset handle. Safe to call with NULL. */
void lance_dataset_close(LanceDataset* dataset);

Expand Down
59 changes: 51 additions & 8 deletions include/lance/lance.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,27 @@ struct SqlColumn {
std::string expression;
};

// ─── Shared Session ──────────────────────────────────────────────────────────

class Session {
Handle<LanceSession, lance_session_close> handle_;

public:
Session(uint64_t index_cache_size_bytes, uint64_t metadata_cache_size_bytes)
: handle_(lance_session_new(index_cache_size_bytes, metadata_cache_size_bytes)) {
if (!handle_) check_error();
}

LanceSessionCacheStats cache_stats() const {
LanceSessionCacheStats stats{};
if (lance_session_get_cache_stats(handle_.get(), &stats) != 0)
check_error();
return stats;
}

const LanceSession* c_handle() const { return handle_.get(); }
};

// ─── Process-local FTS query context ────────────────────────────────────────

/// Immutable, query-specific global BM25 scorer plus pinned FTS segment list.
Expand All @@ -192,6 +213,17 @@ class FtsQueryContext {
class Dataset {
Handle<LanceDataset, lance_dataset_close> handle_;

static std::vector<const char*> to_c_storage_options(
const std::vector<std::pair<std::string, std::string>>& storage_opts) {
std::vector<const char*> kv;
for (auto& [k, v] : storage_opts) {
kv.push_back(k.c_str());
kv.push_back(v.c_str());
}
kv.push_back(nullptr);
return kv;
}

public:
/// Open a dataset at the given URI. Pass `version` = 0 (the default) for
/// the latest, or a specific version id from `versions()` to check out
Expand All @@ -201,14 +233,7 @@ class Dataset {
const std::vector<std::pair<std::string, std::string>>& storage_opts = {},
uint64_t version = 0) {

// Build NULL-terminated key-value array for storage options.
std::vector<const char*> kv;
for (auto& [k, v] : storage_opts) {
kv.push_back(k.c_str());
kv.push_back(v.c_str());
}
kv.push_back(nullptr);

auto kv = to_c_storage_options(storage_opts);
const char* const* opts_ptr =
storage_opts.empty() ? nullptr : kv.data();

Expand All @@ -217,6 +242,24 @@ class Dataset {
return Dataset(ds);
}

/// Open a dataset with caches owned by `session`. The returned dataset
/// remains valid if the Session wrapper is destroyed first.
static Dataset open_with_session(
Comment thread
Jay-ju marked this conversation as resolved.
const Session& session,
const std::string& uri,
const std::vector<std::pair<std::string, std::string>>& storage_opts = {},
uint64_t version = 0) {

auto kv = to_c_storage_options(storage_opts);
const char* const* opts_ptr =
storage_opts.empty() ? nullptr : kv.data();

auto* ds = lance_dataset_open_with_session(
uri.c_str(), opts_ptr, version, session.c_handle());
if (!ds) check_error();
return Dataset(ds);
}

/// Write an Arrow record batch stream to a Lance dataset and return the
/// open dataset at the committed version.
///
Expand Down
39 changes: 38 additions & 1 deletion src/dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use lance_core::Result;
use crate::error::{ffi_try, swallow_unwind};
use crate::helpers;
use crate::runtime::block_on;
use crate::session::LanceSession;
use crate::stream_guard::guarded_ffi_stream_from_reader;

/// Opaque handle representing an opened Lance dataset.
Expand Down Expand Up @@ -121,15 +122,48 @@ pub unsafe extern "C" fn lance_dataset_open(
version: u64,
) -> *mut LanceDataset {
ffi_try!(
unsafe { open_dataset_inner(uri, storage_options, version) },
unsafe { open_dataset_inner(uri, storage_options, version, None) },
null
)
}

/// Open a Lance dataset using a shared session.
///
/// The dataset retains shared ownership of the session state, so the caller
/// may close the session handle after this function returns successfully.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lance_dataset_open_with_session(
uri: *const c_char,
storage_options: *const *const c_char,
version: u64,
session: *const LanceSession,
) -> *mut LanceDataset {
ffi_try!(
unsafe { open_dataset_with_session_inner(uri, storage_options, version, session) },
null
)
}

unsafe fn open_dataset_with_session_inner(
uri: *const c_char,
storage_options: *const *const c_char,
version: u64,
session: *const LanceSession,
) -> Result<*mut LanceDataset> {
if session.is_null() {
return Err(lance_core::Error::invalid_input_source(
"session must not be NULL".into(),
));
}
let session = unsafe { &*session };
unsafe { open_dataset_inner(uri, storage_options, version, Some(session)) }
}

unsafe fn open_dataset_inner(
uri: *const c_char,
storage_options: *const *const c_char,
version: u64,
session: Option<&LanceSession>,
) -> Result<*mut LanceDataset> {
let uri_str = unsafe { helpers::parse_c_string(uri)? }
.ok_or_else(|| lance_core::Error::invalid_input_source("uri must not be NULL".into()))?;
Expand All @@ -143,6 +177,9 @@ unsafe fn open_dataset_inner(
if version != 0 {
builder = builder.with_version(version);
}
if let Some(session) = session {
builder = builder.with_session(session.inner.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This passes the same session into a builder that has just accepted per-open storage options, but the pinned Lance caches do not incorporate those options or effective store identity. In e934cc2, the metadata namespace and index namespace are both just the raw URI, and datasets construct both from that URI here. A second open of the same URI through a different account or endpoint can therefore hit entries populated by the first binding.

This is the same limitation previously noted as a risk, but upstream #7721 supplies the concrete same-URI/cross-account failure path: one dataset observed the other account's index UUID or foreign metadata. Its store-aware namespace change closed unmerged, and the pinned source still retains the URI-only keys.

Please establish isolation before exposing this combination: reject same-session/same-URI reuse when the effective storage binding differs, or move to an upstream revision that namespaces every store-bound cache by binding. Using separate sessions is safe for callers, but leaving this as documentation would still make the public API permit cross-binding cache reuse.

}

let dataset = block_on(builder.load())?;
let handle = LanceDataset {
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ mod merge_insert;
mod restore;
pub mod runtime;
mod scanner;
mod session;
pub mod stream_guard;
mod update;
mod versions;
Expand All @@ -65,6 +66,7 @@ pub use index_segment::*;
pub use merge_insert::*;
pub use restore::*;
pub use scanner::*;
pub use session::*;
pub use update::*;
pub use versions::*;
pub use writer::*;
Loading
Loading